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 java.io.ByteArrayInputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.security.KeyManagementException;
22  import java.security.KeyStoreException;
23  import java.security.NoSuchAlgorithmException;
24  import java.security.UnrecoverableKeyException;
25  import java.security.cert.CertificateException;
26  import java.security.cert.X509Certificate;
27  import java.util.ArrayList;
28  import java.util.List;
29  import org.apache.commons.lang.StringUtils;
30  import org.apache.http.HttpResponse;
31  import org.apache.http.NameValuePair;
32  import org.apache.http.ParseException;
33  import org.apache.http.client.HttpClient;
34  import org.apache.http.client.entity.UrlEncodedFormEntity;
35  import org.apache.http.client.methods.HttpDelete;
36  import org.apache.http.client.methods.HttpGet;
37  import org.apache.http.client.methods.HttpPost;
38  import org.apache.http.client.methods.HttpRequestBase;
39  import org.apache.http.conn.scheme.Scheme;
40  import org.apache.http.conn.ssl.SSLSocketFactory;
41  import org.apache.http.conn.ssl.TrustStrategy;
42  import org.apache.http.impl.client.DefaultHttpClient;
43  import org.apache.http.message.BasicNameValuePair;
44  import org.apache.http.protocol.HTTP;
45  import org.apache.http.util.EntityUtils;
46  import org.dom4j.Document;
47  import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
48  import org.rundeck.api.parser.ParserHelper;
49  import org.rundeck.api.parser.XmlNodeParser;
50  import org.rundeck.api.util.AssertUtil;
51  
52  /**
53   * Class responsible for making the HTTP API calls
54   * 
55   * @author Vincent Behar
56   */
57  class ApiCall {
58  
59      private final RundeckClient client;
60  
61      /**
62       * Build a new instance, linked to the given RunDeck client
63       * 
64       * @param client holding the RunDeck url and the credentials
65       * @throws IllegalArgumentException if client is null
66       */
67      public ApiCall(RundeckClient client) throws IllegalArgumentException {
68          super();
69          this.client = client;
70          AssertUtil.notNull(client, "The RunDeck Client must not be null !");
71      }
72  
73      /**
74       * Try to "ping" the RunDeck instance to see if it is alive
75       * 
76       * @throws RundeckApiException if the ping fails
77       */
78      public void ping() throws RundeckApiException {
79          HttpClient httpClient = instantiateHttpClient();
80          try {
81              HttpResponse response = httpClient.execute(new HttpGet(client.getUrl()));
82              if (response.getStatusLine().getStatusCode() / 100 != 2) {
83                  throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' when pinging "
84                                                + client.getUrl());
85              }
86          } catch (IOException e) {
87              throw new RundeckApiException("Failed to ping RunDeck instance at " + client.getUrl(), e);
88          } finally {
89              httpClient.getConnectionManager().shutdown();
90          }
91      }
92  
93      /**
94       * Test the credentials (login/password) on the RunDeck instance
95       * 
96       * @throws RundeckApiLoginException if the login fails
97       */
98      public void testCredentials() throws RundeckApiLoginException {
99          HttpClient httpClient = instantiateHttpClient();
100         try {
101             login(httpClient);
102         } finally {
103             httpClient.getConnectionManager().shutdown();
104         }
105     }
106 
107     /**
108      * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
109      * API call. At the end, the given parser will be used to convert the response to a more useful result object.
110      * 
111      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
112      * @param parser used to parse the response
113      * @return the result of the call, as formatted by the parser
114      * @throws RundeckApiException in case of error when calling the API
115      * @throws RundeckApiLoginException if the login fails
116      */
117     public <T> T get(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
118             RundeckApiLoginException {
119         return execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser);
120     }
121 
122     /**
123      * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
124      * API call.
125      * 
126      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
127      * @return a new {@link InputStream} instance, not linked with network resources
128      * @throws RundeckApiException in case of error when calling the API
129      * @throws RundeckApiLoginException if the login fails
130      */
131     public InputStream get(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException {
132         ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath));
133 
134         // try to load the document, to throw an exception in case of error
135         ParserHelper.loadDocument(response);
136         response.reset();
137 
138         return response;
139     }
140 
141     /**
142      * Execute an HTTP DELETE request to the RunDeck instance, on the given path. We will login first, and then execute
143      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
144      * 
145      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
146      * @param parser used to parse the response
147      * @return the result of the call, as formatted by the parser
148      * @throws RundeckApiException in case of error when calling the API
149      * @throws RundeckApiLoginException if the login fails
150      */
151     public <T> T delete(ApiPathBuilder apiPath, XmlNodeParser<T> parser) throws RundeckApiException,
152             RundeckApiLoginException {
153         return execute(new HttpDelete(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser);
154     }
155 
156     /**
157      * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call. At the end,
158      * the given parser will be used to convert the response to a more useful result object.
159      * 
160      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
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
165      */
166     private <T> T execute(HttpRequestBase request, XmlNodeParser<T> parser) throws RundeckApiException,
167             RundeckApiLoginException {
168         // execute the request
169         InputStream response = execute(request);
170 
171         // read and parse the response
172         Document xmlDocument = ParserHelper.loadDocument(response);
173         return parser.parseXmlNode(xmlDocument);
174     }
175 
176     /**
177      * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call.
178      * 
179      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
180      * @return a new {@link InputStream} instance, not linked with network resources
181      * @throws RundeckApiException in case of error when calling the API
182      * @throws RundeckApiLoginException if the login fails
183      */
184     private ByteArrayInputStream execute(HttpRequestBase request) throws RundeckApiException, RundeckApiLoginException {
185         HttpClient httpClient = instantiateHttpClient();
186         try {
187             login(httpClient);
188 
189             // execute the HTTP request
190             HttpResponse response = null;
191             try {
192                 response = httpClient.execute(request);
193             } catch (IOException e) {
194                 throw new RundeckApiException("Failed to execute an HTTP " + request.getMethod() + " on url : "
195                                               + request.getURI(), e);
196             }
197 
198             // HTTP client refuses to handle redirects (code 3xx) for DELETE, so we have to do it manually...
199             // See http://rundeck.lighthouseapp.com/projects/59277/tickets/248
200             if (response.getStatusLine().getStatusCode() / 100 == 3
201                 && HttpDelete.METHOD_NAME.equals(request.getMethod())) {
202                 String newLocation = response.getFirstHeader("Location").getValue();
203                 try {
204                     EntityUtils.consume(response.getEntity());
205                 } catch (IOException e) {
206                     throw new RundeckApiException("Failed to consume entity (release connection)", e);
207                 }
208                 request = new HttpDelete(newLocation);
209                 try {
210                     response = httpClient.execute(request);
211                 } catch (IOException e) {
212                     throw new RundeckApiException("Failed to execute an HTTP " + request.getMethod() + " on url : "
213                                                   + request.getURI(), e);
214                 }
215             }
216 
217             // check the response code (should be 2xx, even in case of error : error message is in the XML result)
218             if (response.getStatusLine().getStatusCode() / 100 != 2) {
219                 throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for "
220                                               + request.getURI());
221             }
222             if (response.getEntity() == null) {
223                 throw new RundeckApiException("Empty RunDeck response ! HTTP status line is : "
224                                               + response.getStatusLine());
225             }
226 
227             // return a new inputStream, so that we can close all network resources
228             try {
229                 return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
230             } catch (IOException e) {
231                 throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
232             }
233         } finally {
234             httpClient.getConnectionManager().shutdown();
235         }
236     }
237 
238     /**
239      * Do the actual work of login, using the given {@link HttpClient} instance. You'll need to re-use this instance
240      * when making API calls (such as running a job).
241      * 
242      * @param httpClient pre-instantiated
243      * @throws RundeckApiLoginException if the login failed
244      */
245     private void login(HttpClient httpClient) throws RundeckApiLoginException {
246         String location = client.getUrl() + "/j_security_check";
247 
248         while (true) {
249             HttpPost postLogin = new HttpPost(location);
250             List<NameValuePair> params = new ArrayList<NameValuePair>();
251             params.add(new BasicNameValuePair("j_username", client.getLogin()));
252             params.add(new BasicNameValuePair("j_password", client.getPassword()));
253             params.add(new BasicNameValuePair("action", "login"));
254 
255             HttpResponse response = null;
256             try {
257                 postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
258                 response = httpClient.execute(postLogin);
259             } catch (IOException e) {
260                 throw new RundeckApiLoginException("Failed to post login form on " + location, e);
261             }
262 
263             if (response.getStatusLine().getStatusCode() / 100 == 3) {
264                 // HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually...
265                 location = response.getFirstHeader("Location").getValue();
266                 try {
267                     EntityUtils.consume(response.getEntity());
268                 } catch (IOException e) {
269                     throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
270                 }
271                 continue;
272             }
273             if (response.getStatusLine().getStatusCode() / 100 != 2) {
274                 throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for "
275                                                    + location);
276             }
277             try {
278                 String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8);
279                 if (StringUtils.contains(content, "j_security_check")) {
280                     throw new RundeckApiLoginException("Login failed for user " + client.getLogin());
281                 }
282                 try {
283                     EntityUtils.consume(response.getEntity());
284                 } catch (IOException e) {
285                     throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
286                 }
287             } catch (IOException io) {
288                 throw new RundeckApiLoginException("Failed to read RunDeck result", io);
289             } catch (ParseException p) {
290                 throw new RundeckApiLoginException("Failed to parse RunDeck response", p);
291             }
292             break;
293         }
294     }
295 
296     /**
297      * Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates
298      * 
299      * @return an {@link HttpClient} instance - won't be null
300      */
301     private HttpClient instantiateHttpClient() {
302         SSLSocketFactory socketFactory = null;
303         try {
304             socketFactory = new SSLSocketFactory(new TrustStrategy() {
305 
306                 @Override
307                 public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
308                     return true;
309                 }
310             }, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
311         } catch (KeyManagementException e) {
312             throw new RuntimeException(e);
313         } catch (UnrecoverableKeyException e) {
314             throw new RuntimeException(e);
315         } catch (NoSuchAlgorithmException e) {
316             throw new RuntimeException(e);
317         } catch (KeyStoreException e) {
318             throw new RuntimeException(e);
319         }
320 
321         HttpClient httpClient = new DefaultHttpClient();
322         httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 443, socketFactory));
323         return httpClient;
324     }
325 }