View Javadoc

1   /*
2    * Copyright 2011 Vincent Behar
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.rundeck.api;
17  
18  import org.apache.commons.io.IOUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.http.*;
21  import org.apache.http.client.HttpClient;
22  import org.apache.http.client.entity.UrlEncodedFormEntity;
23  import org.apache.http.client.methods.*;
24  import org.apache.http.conn.ssl.*;
25  import org.apache.http.entity.*;
26  import org.apache.http.entity.mime.HttpMultipartMode;
27  import org.apache.http.entity.mime.MultipartEntityBuilder;
28  import org.apache.http.entity.mime.content.InputStreamBody;
29  import org.apache.http.impl.client.CloseableHttpClient;
30  import org.apache.http.impl.client.HttpClientBuilder;
31  import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
32  import org.apache.http.message.BasicNameValuePair;
33  import org.apache.http.protocol.HttpContext;
34  import org.apache.http.util.EntityUtils;
35  import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
36  import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
37  import org.rundeck.api.parser.ParserHelper;
38  import org.rundeck.api.parser.ResponseParser;
39  import org.rundeck.api.parser.XmlNodeParser;
40  import org.rundeck.api.util.AssertUtil;
41  import org.rundeck.api.util.DocumentContentProducer;
42  
43  import java.io.*;
44  import java.net.ProxySelector;
45  import java.security.KeyManagementException;
46  import java.security.KeyStoreException;
47  import java.security.NoSuchAlgorithmException;
48  import java.util.ArrayList;
49  import java.util.List;
50  import java.util.Map.Entry;
51  
52  /**
53   * Class responsible for making the HTTP API calls
54   *
55   * @author Vincent Behar
56   */
57  class ApiCall {
58  
59      /** Rundeck HTTP header for the auth-token (in case of token-based authentication) */
60      private static final transient String AUTH_TOKEN_HEADER = "X-Rundeck-Auth-Token";
61  
62      /** Rundeck HTTP header for the setting session cookie (in case of session-based authentication) */
63      private static final transient String COOKIE_HEADER = "Cookie";
64  
65      /** {@link RundeckClient} instance holding the Rundeck url and the credentials */
66      private final RundeckClient client;
67  
68      /**
69       * Build a new instance, linked to the given Rundeck client
70       *
71       * @param client holding the Rundeck url and the credentials
72       * @throws IllegalArgumentException if client is null
73       */
74      public ApiCall(RundeckClient client) throws IllegalArgumentException {
75          super();
76          this.client = client;
77          AssertUtil.notNull(client, "The Rundeck Client must not be null !");
78      }
79  
80      /**
81       * Try to "ping" the Rundeck instance to see if it is alive
82       *
83       * @throws RundeckApiException if the ping fails
84       */
85      public void ping() throws RundeckApiException {
86          CloseableHttpClient httpClient = instantiateHttpClient();
87          try {
88              HttpResponse response = httpClient.execute(new HttpGet(client.getUrl()));
89              if (response.getStatusLine().getStatusCode() / 100 != 2) {
90                  throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' when pinging "
91                                                + client.getUrl());
92              }
93          } catch (IOException e) {
94              throw new RundeckApiException("Failed to ping Rundeck instance at " + client.getUrl(), e);
95          } finally {
96              try {
97                  httpClient.close();
98              } catch (IOException e) {
99                  // ignore
100             }
101         }
102     }
103 
104     /**
105      * Test the authentication on the Rundeck instance. Will delegate to either {@link #testLoginAuth()} (in case of
106      * login-based auth) or {@link #testTokenAuth()} (in case of token-based auth).
107      *
108      * @return the login session ID if using login-based auth, otherwise null
109      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
110      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
111      * @see #testLoginAuth()
112      * @see #testTokenAuth()
113      */
114     public String testAuth() throws RundeckApiLoginException, RundeckApiTokenException {
115         String sessionID = null;
116         if (client.getToken() != null || client.getSessionID() != null) {
117             testTokenAuth();
118         } else {
119             sessionID = testLoginAuth();
120         }
121         return sessionID;
122     }
123 
124     /**
125      * Test the login-based authentication on the Rundeck instance
126      *
127      * @throws RundeckApiLoginException if the login fails
128      * @see #testAuth()
129      */
130     public String testLoginAuth() throws RundeckApiLoginException {
131         String sessionID = null;
132         try (CloseableHttpClient httpClient = instantiateHttpClient()){
133             sessionID = login(httpClient);
134         } catch (IOException e) {
135             e.printStackTrace();
136         }
137         return sessionID;
138     }
139 
140     /**
141      * Test the token-based authentication on the Rundeck instance
142      *
143      * @throws RundeckApiTokenException if the token is invalid
144      * @see #testAuth()
145      */
146     public void testTokenAuth() throws RundeckApiTokenException {
147         try {
148             execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + "/system/info"));
149         } catch (RundeckApiTokenException e) {
150             throw e;
151         } catch (RundeckApiException e) {
152             throw new RundeckApiTokenException("Failed to verify token", e);
153         }
154     }
155 
156     /**
157      * Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
158      * API call. At the end, the given parser will be used to convert the response to a more useful result object.
159      *
160      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
161      * @param parser used to parse the response
162      * @return the result of the call, as formatted by the parser
163      * @throws RundeckApiException in case of error when calling the API
164      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
165      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
166      */
167     public <T> T get(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
168             RundeckApiLoginException, RundeckApiTokenException {
169         HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
170         if (null != apiPath.getAccept()) {
171             request.setHeader("Accept", apiPath.getAccept());
172         }
173         return execute(request, parser);
174     }
175 
176 
177     /**
178      * Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
179      * API call. At the end, the given parser will be used to convert the response to a more useful result object.
180      *
181      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
182      * @param parser used to parse the response
183      * @return the result of the call, as formatted by the parser
184      * @throws RundeckApiException in case of error when calling the API
185      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
186      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
187      */
188     public <T> T get(ApiPathBuilder apiPath, ResponseParser<T> parser) throws RundeckApiException,
189             RundeckApiLoginException, RundeckApiTokenException {
190         HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
191         if (null != apiPath.getAccept()) {
192             request.setHeader("Accept", apiPath.getAccept());
193         }
194         return execute(request, new ContentHandler<T>(parser));
195     }
196 
197     /**
198      * Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
199      * API call.
200      *
201      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
202      * @return a new {@link InputStream} instance, not linked with network resources
203      * @throws RundeckApiException in case of error when calling the API
204      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
205      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
206      */
207     public InputStream get(ApiPathBuilder apiPath, boolean parseXml) throws RundeckApiException, RundeckApiLoginException,
208             RundeckApiTokenException {
209         HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
210         if (null != apiPath.getAccept()) {
211             request.setHeader("Accept", apiPath.getAccept());
212         }
213         ByteArrayInputStream response = execute(request);
214 
215         // try to load the document, to throw an exception in case of error
216         if(parseXml) {
217             ParserHelper.loadDocument(response);
218             response.reset();
219         }
220 
221         return response;
222     }
223 
224     /**
225      * Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
226      * API call without appending the API_ENDPOINT to the URL.
227      *
228      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
229      * @return a new {@link InputStream} instance, not linked with network resources
230      * @throws RundeckApiException in case of error when calling the API
231      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
232      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
233      */
234     public InputStream getNonApi(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException,
235             RundeckApiTokenException {
236         HttpGet request = new HttpGet(client.getUrl() + apiPath);
237         if (null != apiPath.getAccept()) {
238             request.setHeader("Accept", apiPath.getAccept());
239         }
240         ByteArrayInputStream response = execute(request);
241         response.reset();
242 
243         return response;
244     }
245 
246     /**
247      * Execute an HTTP POST or GET request to the Rundeck instance, on the given path, depend ing of the {@link
248      * ApiPathBuilder} contains POST content or not (attachments or Form data). We will login first, and then execute
249      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
250      *
251      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
252      * @param parser  used to parse the response
253      *
254      * @return the result of the call, as formatted by the parser
255      *
256      * @throws RundeckApiException      in case of error when calling the API
257      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
258      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
259      */
260     public <T> T postOrGet(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
261                                                                                    RundeckApiLoginException,
262                                                                                    RundeckApiTokenException {
263         if (apiPath.hasPostContent()) {
264             return post(apiPath, parser);
265         } else {
266             return get(apiPath, parser);
267         }
268     }
269 
270     /**
271      * Execute an HTTP POST request to the Rundeck instance, on the given path. We will login first, and then execute
272      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
273      *
274      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
275      * @param parser used to parse the response
276      * @return the result of the call, as formatted by the parser
277      * @throws RundeckApiException in case of error when calling the API
278      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
279      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
280      */
281     public <T> T post(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
282             RundeckApiLoginException, RundeckApiTokenException {
283         HttpPost httpPost = new HttpPost(client.getUrl() + client.getApiEndpoint() + apiPath);
284         return requestWithEntity(apiPath, parser, httpPost);
285     }
286     /**
287      * Execute an HTTP PUT request to the Rundeck instance, on the given path. We will login first, and then execute
288      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
289      *
290      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
291      * @param parser used to parse the response
292      * @return the result of the call, as formatted by the parser
293      * @throws RundeckApiException in case of error when calling the API
294      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
295      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
296      */
297     public <T> T put(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
298             RundeckApiLoginException, RundeckApiTokenException {
299         HttpPut httpPut = new HttpPut(client.getUrl() + client.getApiEndpoint() + apiPath);
300         return requestWithEntity(apiPath, parser, httpPut);
301     }
302 
303     /**
304      * Execute an HTTP PUT request to the Rundeck instance, on the given path. We will login first, and then execute
305      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
306      *
307      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
308      * @param parser used to parse the response
309      * @return the result of the call, as formatted by the parser
310      * @throws RundeckApiException in case of error when calling the API
311      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
312      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
313      */
314     public <T> T put(ApiPathBuilder apiPath, ResponseParser<T> parser) throws RundeckApiException,
315             RundeckApiLoginException, RundeckApiTokenException {
316         HttpPut httpPut = new HttpPut(client.getUrl() + client.getApiEndpoint() + apiPath);
317         return requestWithEntity(apiPath, new ContentHandler<T>(parser), httpPut);
318     }
319     private <T> T requestWithEntity(ApiPathBuilder apiPath, XmlNodeParser<T> parser, HttpEntityEnclosingRequestBase
320             httpPost) {
321         return new ParserHandler<T>(parser).handle(requestWithEntity(apiPath, new ResultHandler(), httpPost));
322     }
323     private <T> T requestWithEntity(ApiPathBuilder apiPath, Handler<HttpResponse,T> handler, HttpEntityEnclosingRequestBase
324             httpPost) {
325         if(null!= apiPath.getAccept()) {
326             httpPost.setHeader("Accept", apiPath.getAccept());
327         }
328         // POST a multi-part request, with all attachments
329         if (apiPath.getAttachments().size() > 0) {
330             MultipartEntityBuilder multipartEntityBuilder =
331                     MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
332             for (Entry<String, InputStream> attachment : apiPath.getAttachments().entrySet()) {
333                 multipartEntityBuilder.addPart(
334                         attachment.getKey(),
335                         new InputStreamBody(attachment.getValue(), attachment.getKey())
336                 );
337             }
338             httpPost.setEntity(multipartEntityBuilder.build());
339         } else if (apiPath.getForm().size() > 0) {
340             try {
341                 httpPost.setEntity(new UrlEncodedFormEntity(apiPath.getForm(), "UTF-8"));
342             } catch (UnsupportedEncodingException e) {
343                 throw new RundeckApiException("Unsupported encoding: " + e.getMessage(), e);
344             }
345         } else if (apiPath.getContentStream() != null && apiPath.getContentType() != null) {
346             InputStreamEntity entity = new InputStreamEntity(
347                     apiPath.getContentStream(),
348                     ContentType.create(apiPath.getContentType())
349             );
350             httpPost.setEntity(entity);
351         } else if (apiPath.getContents() != null && apiPath.getContentType() != null) {
352             ByteArrayEntity bae = new ByteArrayEntity(
353                     apiPath.getContents(),
354                     ContentType.create(apiPath.getContentType())
355             );
356 
357             httpPost.setEntity(bae);
358         } else if (apiPath.getContentFile() != null && apiPath.getContentType() != null) {
359             httpPost.setEntity(new FileEntity(apiPath.getContentFile(), ContentType.create(apiPath.getContentType())));
360         } else if (apiPath.getXmlDocument() != null) {
361             httpPost.setHeader("Content-Type", "application/xml");
362             httpPost.setEntity(new EntityTemplate(new DocumentContentProducer(apiPath.getXmlDocument())));
363         } else if (apiPath.isEmptyContent()) {
364             //empty content
365         } else {
366             throw new IllegalArgumentException("No Form or Multipart entity for POST content-body");
367         }
368 
369         return execute(httpPost, handler);
370     }
371 
372     /**
373      * Execute an HTTP DELETE request to the Rundeck instance, on the given path. We will login first, and then execute
374      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
375      *
376      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
377      * @param parser used to parse the response
378      * @return the result of the call, as formatted by the parser
379      * @throws RundeckApiException in case of error when calling the API
380      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
381      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
382      */
383     public <T> T delete(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
384             RundeckApiLoginException, RundeckApiTokenException {
385         return execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath), parser);
386     }
387     /**
388      * Execute an HTTP DELETE request to the Rundeck instance, on the given path, and expect a 204 response.
389      *
390      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
391      * @throws RundeckApiException in case of error when calling the API
392      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
393      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
394      */
395     public void delete(ApiPathBuilder apiPath) throws RundeckApiException,
396             RundeckApiLoginException, RundeckApiTokenException {
397 
398         InputStream response = execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath));
399         if(null!=response){
400             throw new RundeckApiException("Unexpected Rundeck response content, expected no content!");
401         }
402     }
403 
404     /**
405      * Execute an HTTP request to the Rundeck instance. We will login first, and then execute the API call. At the end,
406      * the given parser will be used to convert the response to a more useful result object.
407      *
408      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
409      * @param parser used to parse the response
410      * @return the result of the call, as formatted by the parser
411      * @throws RundeckApiException in case of error when calling the API
412      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
413      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
414      */
415     private <T> T execute(HttpRequestBase request, XmlNodeParser<T> parser) throws RundeckApiException,
416             RundeckApiLoginException, RundeckApiTokenException {
417         // execute the request
418         return new ParserHandler<T>(parser).handle(execute(request, new ResultHandler()));
419     }
420 
421     /**
422      * Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
423      * API call. At the end, the given parser will be used to convert the response to a more useful result object.
424      *
425      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
426      * @param outputStream write output to this stream
427      *
428      * @return the result of the call, as formatted by the parser
429      *
430      * @throws RundeckApiException      in case of error when calling the API
431      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
432      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
433      */
434     public int get(ApiPathBuilder apiPath, OutputStream outputStream) throws RundeckApiException,
435             RundeckApiLoginException, RundeckApiTokenException, IOException {
436         HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
437         if (null != apiPath.getAccept()) {
438             request.setHeader("Accept", apiPath.getAccept());
439         }
440         final WriteOutHandler writeOutHandler = new WriteOutHandler(outputStream);
441         Handler<HttpResponse,Integer> handler = writeOutHandler;
442         if(null!=apiPath.getRequiredContentType()){
443             handler = new RequireContentTypeHandler<Integer>(apiPath.getRequiredContentType(), handler);
444         }
445         final int wrote = execute(request, handler);
446         if(writeOutHandler.thrown!=null){
447             throw writeOutHandler.thrown;
448         }
449         return wrote;
450     }
451     /**
452      * Execute an HTTP request to the Rundeck instance. We will login first, and then execute the API call.
453      *
454      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
455      * @return a new {@link InputStream} instance, not linked with network resources
456      * @throws RundeckApiException in case of error when calling the API
457      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
458      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
459      */
460     private ByteArrayInputStream execute(HttpUriRequest request) throws RundeckApiException, RundeckApiLoginException,
461             RundeckApiTokenException {
462         return execute(request, new ResultHandler() );
463     }
464 
465     /**
466      * Handles one type into another
467      * @param <T>
468      * @param <V>
469      */
470     private static interface Handler<T,V>{
471         public V handle(T response);
472     }
473 
474     /**
475      * Handles parsing inputstream via a parser
476      * @param <S>
477      */
478     private static class ParserHandler<S> implements Handler<InputStream,S> {
479         XmlNodeParser<S> parser;
480 
481         private ParserHandler(XmlNodeParser<S> parser) {
482             this.parser = parser;
483         }
484 
485         @Override
486         public S handle(InputStream response) {
487             // read and parse the response
488             return parser.parseXmlNode(ParserHelper.loadDocument(response));
489         }
490     }
491 
492     /**
493      * Converts to a string
494      */
495     public static class PlainTextHandler implements ResponseParser<String>{
496         @Override
497         public String parseResponse(final InputStream response) {
498             StringWriter output = new StringWriter();
499             try {
500                 IOUtils.copy(response, output);
501             } catch (IOException e) {
502                 throw new RundeckApiException("Failed to consume text/plain input to string", e);
503             }
504             return output.toString();
505         }
506     }
507 
508     /**
509      * Handles parsing response via a {@link ResponseParser}
510      *
511      * @param <S>
512      */
513     private static class ContentHandler<S> implements Handler<HttpResponse, S> {
514         ResponseParser<S> parser;
515 
516         private ContentHandler(ResponseParser<S> parser) {
517             this.parser = parser;
518         }
519 
520         @Override
521         public S handle(HttpResponse response) {
522             // read and parse the response
523             return parser.parseResponse(new ResultHandler().handle(response));
524         }
525     }
526 
527     /**
528      * Handles writing response to an output stream
529      */
530     private static class ChainHandler<T> implements Handler<HttpResponse,T> {
531         Handler<HttpResponse, T> chain;
532         private ChainHandler(Handler<HttpResponse,T> chain) {
533             this.chain=chain;
534         }
535         @Override
536         public T handle(final HttpResponse response) {
537             return chain.handle(response);
538         }
539     }
540 
541     /**
542      * Handles writing response to an output stream
543      */
544     private static class RequireContentTypeHandler<T> extends ChainHandler<T> {
545         String contentType;
546 
547         private RequireContentTypeHandler(final String contentType, final Handler<HttpResponse, T> chain) {
548             super(chain);
549             this.contentType = contentType;
550         }
551 
552         @Override
553         public T handle(final HttpResponse response) {
554             final Header firstHeader = response.getFirstHeader("Content-Type");
555             final String[] split = firstHeader.getValue().split(";");
556             boolean matched=false;
557             for (int i = 0; i < split.length; i++) {
558                 String s = split[i];
559                 if (this.contentType.equalsIgnoreCase(s.trim())) {
560                     matched=true;
561                     break;
562                 }
563             }
564             if(!matched) {
565                 throw new RundeckApiException.RundeckApiHttpContentTypeException(firstHeader.getValue(),
566                         this.contentType);
567             }
568             return super.handle(response);
569         }
570     }
571 
572     /**
573      * Handles writing response to an output stream
574      */
575     private static class WriteOutHandler implements Handler<HttpResponse,Integer> {
576         private WriteOutHandler(OutputStream writeOut) {
577             this.writeOut = writeOut;
578         }
579 
580         OutputStream writeOut;
581         IOException thrown;
582         @Override
583         public Integer handle(final HttpResponse response) {
584             try {
585                 return IOUtils.copy(response.getEntity().getContent(), writeOut);
586             } catch (IOException e) {
587                 thrown=e;
588             }
589             return -1;
590         }
591     }
592 
593     /**
594      * Handles reading response into a byte array stream
595      */
596     private static class ResultHandler implements Handler<HttpResponse,ByteArrayInputStream> {
597         @Override
598         public ByteArrayInputStream handle(final HttpResponse response) {
599             // return a new inputStream, so that we can close all network resources
600             try {
601                 return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
602             } catch (IOException e) {
603                 throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
604             }
605         }
606     }
607     /**
608      * Execute an HTTP request to the Rundeck instance. We will login first, and then execute the API call.
609      *
610      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
611      * @return a new {@link InputStream} instance, not linked with network resources
612      * @throws RundeckApiException in case of error when calling the API
613      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
614      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
615      */
616     private <T> T execute(HttpUriRequest request, Handler<HttpResponse,T> handler) throws RundeckApiException,
617             RundeckApiLoginException,
618             RundeckApiTokenException {
619         try(CloseableHttpClient httpClient = instantiateHttpClient()) {
620             // we only need to manually login in case of login-based authentication
621             // note that in case of token-based auth, the auth (via an HTTP header) is managed by an interceptor.
622             if (client.getToken() == null && client.getSessionID() == null) {
623                 login(httpClient);
624             }
625 
626             // execute the HTTP request
627             HttpResponse response = null;
628             try {
629                 response = httpClient.execute(request);
630             } catch (IOException e) {
631                 throw new RundeckApiException("Failed to execute an HTTP " + request.getMethod() + " on url : "
632                                               + request.getURI(), e);
633             }
634 
635             // in case of error, we get a redirect to /api/error
636             // that we need to follow manually for POST and DELETE requests (as GET)
637             int statusCode = response.getStatusLine().getStatusCode();
638             if (statusCode / 100 == 3) {
639                 String newLocation = response.getFirstHeader("Location").getValue();
640                 try {
641                     EntityUtils.consume(response.getEntity());
642                 } catch (IOException e) {
643                     throw new RundeckApiException("Failed to consume entity (release connection)", e);
644                 }
645                 request = new HttpGet(newLocation);
646                 try {
647                     response = httpClient.execute(request);
648                     statusCode = response.getStatusLine().getStatusCode();
649                 } catch (IOException e) {
650                     throw new RundeckApiException("Failed to execute an HTTP GET on url : " + request.getURI(), e);
651                 }
652             }
653 
654             // check the response code (should be 2xx, even in case of error : error message is in the XML result)
655             if (statusCode / 100 != 2) {
656                 if (statusCode == 403 &&
657                         (client.getToken() != null || client.getSessionID() != null)) {
658                     throw new RundeckApiTokenException("Invalid Token or sessionID ! Got HTTP response '" + response.getStatusLine()
659                                                        + "' for " + request.getURI());
660                 } else {
661                     throw new RundeckApiException.RundeckApiHttpStatusException("Invalid HTTP response '" + response.getStatusLine() + "' for "
662                                                   + request.getURI(), statusCode);
663                 }
664             }
665             if(statusCode==204){
666                 return null;
667             }
668             if (response.getEntity() == null) {
669                 throw new RundeckApiException("Empty Rundeck response ! HTTP status line is : "
670                                               + response.getStatusLine());
671             }
672             return handler.handle(response);
673         } catch (IOException e) {
674             throw new RundeckApiException("failed closing http client", e);
675         }
676     }
677 
678     /**
679      * Do the actual work of login, using the given {@link HttpClient} instance. You'll need to re-use this instance
680      * when making API calls (such as running a job). Only use this in case of login-based authentication.
681      *
682      * @param httpClient pre-instantiated
683      * @throws RundeckApiLoginException if the login failed
684      */
685     private String login(HttpClient httpClient) throws RundeckApiLoginException {
686         String sessionID = null;
687 
688         // 1. call expected GET request
689         String location = client.getUrl();
690 
691         try {
692             HttpGet getRequest = new HttpGet(location);
693             HttpResponse response = httpClient.execute(getRequest);
694 
695             // sessionID stored in case user wants to cache it for reuse
696             Header cookieHeader = response.getFirstHeader("Set-Cookie");
697             if (cookieHeader != null) {
698                 String cookieStr = cookieHeader.getValue();
699                 if (cookieStr != null) {
700                     int i1 = cookieStr.indexOf("JSESSIONID=");
701                     if (i1 >= 0) {
702                         cookieStr = cookieStr.substring(i1 + "JSESSIONID=".length());
703                         int i2 = cookieStr.indexOf(";");
704                         if (i2 >= 0) {
705                             sessionID = cookieStr.substring(0, i2);
706                         }
707                     }
708                 }
709             }
710 
711             try {
712                 EntityUtils.consume(response.getEntity());
713             } catch (IOException e) {
714                 throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
715             }
716         } catch (IOException e) {
717             throw new RundeckApiLoginException("Failed to get request on " + location, e);
718         }
719 
720         // 2. then call POST login request
721         location += "/j_security_check";
722 
723         while (true) {
724             try {
725                 HttpPost postLogin = new HttpPost(location);
726                 List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
727                 params.add(new BasicNameValuePair("j_username", client.getLogin()));
728                 params.add(new BasicNameValuePair("j_password", client.getPassword()));
729                 params.add(new BasicNameValuePair("action", "login"));
730                 postLogin.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8));
731                 HttpResponse response = httpClient.execute(postLogin);
732 
733                 if (response.getStatusLine().getStatusCode() / 100 == 3) {
734                     // HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually...
735                     location = response.getFirstHeader("Location").getValue();
736                     try {
737                         EntityUtils.consume(response.getEntity());
738                     } catch (IOException e) {
739                         throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
740                     }
741                     continue;
742                 }
743 
744                 if (response.getStatusLine().getStatusCode() / 100 != 2) {
745                     throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for "
746                             + location);
747                 }
748 
749                 try {
750                     String content = EntityUtils.toString(response.getEntity(), Consts.UTF_8);
751                     if (StringUtils.contains(content, "j_security_check")) {
752                         throw new RundeckApiLoginException("Login failed for user " + client.getLogin());
753                     }
754                     try {
755                         EntityUtils.consume(response.getEntity());
756                     } catch (IOException e) {
757                         throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
758                     }
759                     break;
760                 } catch (IOException io) {
761                     throw new RundeckApiLoginException("Failed to read Rundeck result", io);
762                 } catch (ParseException p) {
763                     throw new RundeckApiLoginException("Failed to parse Rundeck response", p);
764                 }
765             } catch (IOException e) {
766                 throw new RundeckApiLoginException("Failed to post login form on " + location, e);
767             }
768         }
769 
770         return sessionID;
771     }
772 
773 
774     /**
775      * Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates
776      *
777      * @return an {@link HttpClient} instance - won't be null
778      */
779     private CloseableHttpClient instantiateHttpClient() {
780         HttpClientBuilder httpClientBuilder = HttpClientBuilder.create().useSystemProperties();
781 
782         // configure user-agent
783         httpClientBuilder.setUserAgent( "Rundeck API Java Client " + client.getApiVersion());
784 
785         if (client.isSslHostnameVerifyAllowAll()) {
786             httpClientBuilder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
787         }
788         if (client.isSslCertificateTrustAllowSelfSigned()) {
789             // configure SSL
790             try {
791                 httpClientBuilder.setSslcontext(
792                         new SSLContextBuilder()
793                                 .loadTrustMaterial(null, new TrustSelfSignedStrategy())
794                                 .build());
795             } catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) {
796                 throw new RuntimeException(e);
797             }
798 
799         }
800         if(client.isSystemProxyEnabled()) {
801             // configure proxy (use system env : http.proxyHost / http.proxyPort)
802             httpClientBuilder.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()));
803         }
804         // in case of token-based authentication, add the correct HTTP header to all requests via an interceptor
805         httpClientBuilder.addInterceptorFirst(
806                 new HttpRequestInterceptor() {
807 
808                     @Override
809                     public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
810                         if (client.getToken() != null) {
811                             request.addHeader(AUTH_TOKEN_HEADER, client.getToken());
812                             //System.out.println("httpClient adding token header");
813                         } else if (client.getSessionID() != null) {
814                             request.addHeader(COOKIE_HEADER, "JSESSIONID=" + client.getSessionID());
815                             //System.out.println("httpClient adding session header, sessionID="+client.getSessionID());
816                         }
817                     }
818                 }
819         );
820 
821         return httpClientBuilder.build();
822     }
823 }