1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
54
55
56
57 class ApiCall {
58
59
60 private static final transient String AUTH_TOKEN_HEADER = "X-Rundeck-Auth-Token";
61
62
63 private static final transient String COOKIE_HEADER = "Cookie";
64
65
66 private final RundeckClient client;
67
68
69
70
71
72
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
82
83
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
100 }
101 }
102 }
103
104
105
106
107
108
109
110
111
112
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
126
127
128
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
142
143
144
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
158
159
160
161
162
163
164
165
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
179
180
181
182
183
184
185
186
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
199
200
201
202
203
204
205
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
216 if(parseXml) {
217 ParserHelper.loadDocument(response);
218 response.reset();
219 }
220
221 return response;
222 }
223
224
225
226
227
228
229
230
231
232
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
248
249
250
251
252
253
254
255
256
257
258
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
272
273
274
275
276
277
278
279
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
288
289
290
291
292
293
294
295
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
305
306
307
308
309
310
311
312
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
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
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
374
375
376
377
378
379
380
381
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
389
390
391
392
393
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
406
407
408
409
410
411
412
413
414
415 private <T> T execute(HttpRequestBase request, XmlNodeParser<T> parser) throws RundeckApiException,
416 RundeckApiLoginException, RundeckApiTokenException {
417
418 return new ParserHandler<T>(parser).handle(execute(request, new ResultHandler()));
419 }
420
421
422
423
424
425
426
427
428
429
430
431
432
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
453
454
455
456
457
458
459
460 private ByteArrayInputStream execute(HttpUriRequest request) throws RundeckApiException, RundeckApiLoginException,
461 RundeckApiTokenException {
462 return execute(request, new ResultHandler() );
463 }
464
465
466
467
468
469
470 private static interface Handler<T,V>{
471 public V handle(T response);
472 }
473
474
475
476
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
488 return parser.parseXmlNode(ParserHelper.loadDocument(response));
489 }
490 }
491
492
493
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
510
511
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
523 return parser.parseResponse(new ResultHandler().handle(response));
524 }
525 }
526
527
528
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
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
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
595
596 private static class ResultHandler implements Handler<HttpResponse,ByteArrayInputStream> {
597 @Override
598 public ByteArrayInputStream handle(final HttpResponse response) {
599
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
609
610
611
612
613
614
615
616 private <T> T execute(HttpUriRequest request, Handler<HttpResponse,T> handler) throws RundeckApiException,
617 RundeckApiLoginException,
618 RundeckApiTokenException {
619 try(CloseableHttpClient httpClient = instantiateHttpClient()) {
620
621
622 if (client.getToken() == null && client.getSessionID() == null) {
623 login(httpClient);
624 }
625
626
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
636
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
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
680
681
682
683
684
685 private String login(HttpClient httpClient) throws RundeckApiLoginException {
686 String sessionID = null;
687
688
689 String location = client.getUrl();
690
691 try {
692 HttpGet getRequest = new HttpGet(location);
693 HttpResponse response = httpClient.execute(getRequest);
694
695
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
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
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
776
777
778
779 private CloseableHttpClient instantiateHttpClient() {
780 HttpClientBuilder httpClientBuilder = HttpClientBuilder.create().useSystemProperties();
781
782
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
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
802 httpClientBuilder.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()));
803 }
804
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
813 } else if (client.getSessionID() != null) {
814 request.addHeader(COOKIE_HEADER, "JSESSIONID=" + client.getSessionID());
815
816 }
817 }
818 }
819 );
820
821 return httpClientBuilder.build();
822 }
823 }