org.rundeck.api.ApiCall Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rundeck-api-java-client Show documentation
Show all versions of rundeck-api-java-client Show documentation
Java client for the Rundeck REST API
The newest version!
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.*;
import org.apache.http.entity.*;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
import org.rundeck.api.parser.ParserHelper;
import org.rundeck.api.parser.ResponseParser;
import org.rundeck.api.parser.XmlNodeParser;
import org.rundeck.api.util.AssertUtil;
import org.rundeck.api.util.DocumentContentProducer;
import java.io.*;
import java.net.ProxySelector;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
/**
* Class responsible for making the HTTP API calls
*
* @author Vincent Behar
*/
class ApiCall {
/** Rundeck HTTP header for the auth-token (in case of token-based authentication) */
private static final transient String AUTH_TOKEN_HEADER = "X-Rundeck-Auth-Token";
/** Rundeck HTTP header for the setting session cookie (in case of session-based authentication) */
private static final transient String COOKIE_HEADER = "Cookie";
/** {@link RundeckClient} instance holding the Rundeck url and the credentials */
private final RundeckClient client;
/**
* Build a new instance, linked to the given Rundeck client
*
* @param client holding the Rundeck url and the credentials
* @throws IllegalArgumentException if client is null
*/
public ApiCall(RundeckClient client) throws IllegalArgumentException {
super();
this.client = client;
AssertUtil.notNull(client, "The Rundeck Client must not be null !");
}
/**
* Try to "ping" the Rundeck instance to see if it is alive
*
* @throws RundeckApiException if the ping fails
*/
public void ping() throws RundeckApiException {
CloseableHttpClient httpClient = instantiateHttpClient();
String UrlToPing = null;
if (client.getToken() != null || client.getSessionID() != null) {
// The preauthenticated mode always returns a HTTP 403 if we make a
// call to the root URL of the Rundeck instance. Hence, make a call to
// /api//system/info instead
UrlToPing = client.getUrl() + client.getApiEndpoint() + "/system/info" ;
} else {
UrlToPing = client.getUrl() ;
}
try {
HttpResponse response = httpClient.execute(new HttpGet(UrlToPing));
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' when pinging "
+ client.getUrl());
}
} catch (IOException e) {
throw new RundeckApiException("Failed to ping Rundeck instance at " + client.getUrl(), e);
} finally {
try {
httpClient.close();
} catch (IOException e) {
// ignore
}
}
}
/**
* Test the authentication on the Rundeck instance. Will delegate to either {@link #testLoginAuth()} (in case of
* login-based auth) or {@link #testTokenAuth()} (in case of token-based auth).
*
* @return the login session ID if using login-based auth, otherwise null
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
* @see #testLoginAuth()
* @see #testTokenAuth()
*/
public String testAuth() throws RundeckApiLoginException, RundeckApiTokenException {
String sessionID = null;
if (client.getToken() != null || client.getSessionID() != null) {
testTokenAuth();
} else {
sessionID = testLoginAuth();
}
return sessionID;
}
/**
* Test the login-based authentication on the Rundeck instance
*
* @throws RundeckApiLoginException if the login fails
* @see #testAuth()
*/
public String testLoginAuth() throws RundeckApiLoginException {
String sessionID = null;
try (CloseableHttpClient httpClient = instantiateHttpClient()){
sessionID = login(httpClient);
} catch (IOException e) {
e.printStackTrace();
}
return sessionID;
}
/**
* Test the token-based authentication on the Rundeck instance
*
* @throws RundeckApiTokenException if the token is invalid
* @see #testAuth()
*/
public void testTokenAuth() throws RundeckApiTokenException {
try {
execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + "/system/info"));
} catch (RundeckApiTokenException e) {
throw e;
} catch (RundeckApiException e) {
throw new RundeckApiTokenException("Failed to verify token", e);
}
}
/**
* Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
* API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T get(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
if (null != apiPath.getAccept()) {
request.setHeader("Accept", apiPath.getAccept());
}
return execute(request, parser);
}
/**
* Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
* API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T get(ApiPathBuilder apiPath, ResponseParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
if (null != apiPath.getAccept()) {
request.setHeader("Accept", apiPath.getAccept());
}
return execute(request, new ContentHandler(parser));
}
/**
* Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
* API call.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @return a new {@link InputStream} instance, not linked with network resources
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public InputStream get(ApiPathBuilder apiPath, boolean parseXml) throws RundeckApiException, RundeckApiLoginException,
RundeckApiTokenException {
HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
if (null != apiPath.getAccept()) {
request.setHeader("Accept", apiPath.getAccept());
}
ByteArrayInputStream response = execute(request);
// try to load the document, to throw an exception in case of error
if(parseXml) {
ParserHelper.loadDocument(response);
response.reset();
}
return response;
}
/**
* Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
* API call without appending the API_ENDPOINT to the URL.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @return a new {@link InputStream} instance, not linked with network resources
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public InputStream getNonApi(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException,
RundeckApiTokenException {
HttpGet request = new HttpGet(client.getUrl() + apiPath);
if (null != apiPath.getAccept()) {
request.setHeader("Accept", apiPath.getAccept());
}
ByteArrayInputStream response = execute(request);
response.reset();
return response;
}
/**
* Execute an HTTP POST or GET request to the Rundeck instance, on the given path, depend ing of the {@link
* ApiPathBuilder} contains POST content or not (attachments or Form data). We will login first, and then execute
* the API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
*
* @return the result of the call, as formatted by the parser
*
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T postOrGet(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException,
RundeckApiTokenException {
if (apiPath.hasPostContent()) {
return post(apiPath, parser);
} else {
return get(apiPath, parser);
}
}
/**
* Execute an HTTP POST request to the Rundeck instance, on the given path. We will login first, and then execute
* the API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
HttpPost httpPost = new HttpPost(client.getUrl() + client.getApiEndpoint() + apiPath);
return requestWithEntity(apiPath, parser, httpPost);
}
/**
* Execute an HTTP PUT request to the Rundeck instance, on the given path. We will login first, and then execute
* the API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T put(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
HttpPut httpPut = new HttpPut(client.getUrl() + client.getApiEndpoint() + apiPath);
return requestWithEntity(apiPath, parser, httpPut);
}
/**
* Execute an HTTP PUT request to the Rundeck instance, on the given path. We will login first, and then execute
* the API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T put(ApiPathBuilder apiPath, ResponseParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
HttpPut httpPut = new HttpPut(client.getUrl() + client.getApiEndpoint() + apiPath);
return requestWithEntity(apiPath, new ContentHandler(parser), httpPut);
}
private T requestWithEntity(ApiPathBuilder apiPath, XmlNodeParser parser, HttpEntityEnclosingRequestBase
httpPost) {
return new ParserHandler(parser).handle(requestWithEntity(apiPath, new ResultHandler(), httpPost));
}
private T requestWithEntity(ApiPathBuilder apiPath, Handler handler, HttpEntityEnclosingRequestBase
httpPost) {
if(null!= apiPath.getAccept()) {
httpPost.setHeader("Accept", apiPath.getAccept());
}
// POST a multi-part request, with all attachments
if (apiPath.getAttachments().size() > 0 || apiPath.getFileAttachments().size() > 0) {
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
ArrayList tempfiles = new ArrayList<>();
//attach streams
for (Entry attachment : apiPath.getAttachments().entrySet()) {
if(client.isUseIntermediateStreamFile()) {
//transfer to file
File f = copyToTempfile(attachment.getValue());
multipartEntityBuilder.addBinaryBody(attachment.getKey(), f);
tempfiles.add(f);
}else{
multipartEntityBuilder.addBinaryBody(attachment.getKey(), attachment.getValue());
}
}
if (tempfiles.size() > 0) {
handler = TempFileCleanupHandler.chain(handler, tempfiles);
}
//attach files
for (Entry attachment : apiPath.getFileAttachments().entrySet()) {
multipartEntityBuilder.addBinaryBody(attachment.getKey(), attachment.getValue());
}
httpPost.setEntity(multipartEntityBuilder.build());
} else if (apiPath.getForm().size() > 0) {
try {
httpPost.setEntity(new UrlEncodedFormEntity(apiPath.getForm(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RundeckApiException("Unsupported encoding: " + e.getMessage(), e);
}
} else if (apiPath.getContentStream() != null && apiPath.getContentType() != null) {
if(client.isUseIntermediateStreamFile()){
ArrayList tempfiles = new ArrayList<>();
File f = copyToTempfile(apiPath.getContentStream());
tempfiles.add(f);
httpPost.setEntity(new FileEntity(f, ContentType.create(apiPath.getContentType())));
handler = TempFileCleanupHandler.chain(handler, tempfiles);
}else{
InputStreamEntity entity = new InputStreamEntity(
apiPath.getContentStream(),
ContentType.create(apiPath.getContentType())
);
httpPost.setEntity(entity);
}
} else if (apiPath.getContents() != null && apiPath.getContentType() != null) {
ByteArrayEntity bae = new ByteArrayEntity(
apiPath.getContents(),
ContentType.create(apiPath.getContentType())
);
httpPost.setEntity(bae);
} else if (apiPath.getContentFile() != null && apiPath.getContentType() != null) {
httpPost.setEntity(new FileEntity(apiPath.getContentFile(), ContentType.create(apiPath.getContentType())));
} else if (apiPath.getXmlDocument() != null) {
httpPost.setHeader("Content-Type", "application/xml");
httpPost.setEntity(new EntityTemplate(new DocumentContentProducer(apiPath.getXmlDocument())));
} else if (apiPath.isEmptyContent()) {
//empty content
} else {
throw new IllegalArgumentException("No Form or Multipart entity for POST content-body");
}
return execute(httpPost, handler);
}
private File copyToTempfile(final InputStream stream) throws RundeckApiException {
try {
File f = File.createTempFile("post-data", ".tmp");
FileUtils.copyInputStreamToFile(stream, f);
f.deleteOnExit();
return f;
} catch (IOException e) {
throw new RundeckApiException("IO exception " + e.getMessage(), e);
}
}
/**
* Execute an HTTP DELETE request to the Rundeck instance, on the given path. We will login first, and then execute
* the API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public T delete(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
return execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath), parser);
}
/**
* Execute an HTTP DELETE request to the Rundeck instance, on the given path, and expect a 204 response.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public void delete(ApiPathBuilder apiPath) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
InputStream response = execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath));
if(null!=response){
throw new RundeckApiException("Unexpected Rundeck response content, expected no content!");
}
}
/**
* Execute an HTTP request to the Rundeck instance. We will login first, and then execute the API call. At the end,
* the given parser will be used to convert the response to a more useful result object.
*
* @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
private T execute(HttpRequestBase request, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
// execute the request
return new ParserHandler(parser).handle(execute(request, new ResultHandler()));
}
/**
* Execute an HTTP GET request to the Rundeck instance, on the given path. We will login first, and then execute the
* API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
* @param outputStream write output to this stream
*
* @return the result of the call, as formatted by the parser
*
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
public int get(ApiPathBuilder apiPath, OutputStream outputStream) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException, IOException {
HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
if (null != apiPath.getAccept()) {
request.setHeader("Accept", apiPath.getAccept());
}
final WriteOutHandler writeOutHandler = new WriteOutHandler(outputStream);
Handler handler = writeOutHandler;
if(null!=apiPath.getRequiredContentType()){
handler = new RequireContentTypeHandler(apiPath.getRequiredContentType(), handler);
}
final int wrote = execute(request, handler);
if(writeOutHandler.thrown!=null){
throw writeOutHandler.thrown;
}
return wrote;
}
/**
* Execute an HTTP request to the Rundeck instance. We will login first, and then execute the API call.
*
* @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
* @return a new {@link InputStream} instance, not linked with network resources
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
private ByteArrayInputStream execute(HttpUriRequest request) throws RundeckApiException, RundeckApiLoginException,
RundeckApiTokenException {
return execute(request, new ResultHandler() );
}
/**
* Handles one type into another
* @param
* @param
*/
private static interface Handler{
public V handle(T response);
}
/**
* Handles parsing inputstream via a parser
* @param
*/
private static class ParserHandler implements Handler {
XmlNodeParser parser;
private ParserHandler(XmlNodeParser parser) {
this.parser = parser;
}
@Override
public S handle(InputStream response) {
// read and parse the response
return parser.parseXmlNode(ParserHelper.loadDocument(response));
}
}
/**
* Converts to a string
*/
public static class PlainTextHandler implements ResponseParser{
@Override
public String parseResponse(final InputStream response) {
StringWriter output = new StringWriter();
try {
IOUtils.copy(response, output);
} catch (IOException e) {
throw new RundeckApiException("Failed to consume text/plain input to string", e);
}
return output.toString();
}
}
/**
* Handles parsing response via a {@link ResponseParser}
*
* @param
*/
private static class ContentHandler implements Handler {
ResponseParser parser;
private ContentHandler(ResponseParser parser) {
this.parser = parser;
}
@Override
public S handle(HttpResponse response) {
// read and parse the response
return parser.parseResponse(new ResultHandler().handle(response));
}
}
/**
* Handles writing response to an output stream
*/
private static class ChainHandler implements Handler {
Handler chain;
private ChainHandler(Handler chain) {
this.chain=chain;
}
@Override
public T handle(final HttpResponse response) {
return chain.handle(response);
}
}
/**
* Handles writing response to an output stream
*/
private static class RequireContentTypeHandler extends ChainHandler {
String contentType;
private RequireContentTypeHandler(final String contentType, final Handler chain) {
super(chain);
this.contentType = contentType;
}
@Override
public T handle(final HttpResponse response) {
final Header firstHeader = response.getFirstHeader("Content-Type");
final String[] split = firstHeader.getValue().split(";");
boolean matched=false;
for (int i = 0; i < split.length; i++) {
String s = split[i];
if (this.contentType.equalsIgnoreCase(s.trim())) {
matched=true;
break;
}
}
if(!matched) {
throw new RundeckApiException.RundeckApiHttpContentTypeException(firstHeader.getValue(),
this.contentType);
}
return super.handle(response);
}
}
/**
* Handles writing response to an output stream
*/
private static class WriteOutHandler implements Handler {
private WriteOutHandler(OutputStream writeOut) {
this.writeOut = writeOut;
}
OutputStream writeOut;
IOException thrown;
@Override
public Integer handle(final HttpResponse response) {
try {
return IOUtils.copy(response.getEntity().getContent(), writeOut);
} catch (IOException e) {
thrown=e;
}
return -1;
}
}
/**
* Handles reading response into a byte array stream
*/
private static class ResultHandler implements Handler {
@Override
public ByteArrayInputStream handle(final HttpResponse response) {
// return a new inputStream, so that we can close all network resources
try {
return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
} catch (IOException e) {
throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
}
}
}
/**
* Removes temp files after response
*/
private static class TempFileCleanupHandler extends ChainHandler {
List files;
public TempFileCleanupHandler(final Handler chain, final List files) {
super(chain);
this.files = files;
}
public static TempFileCleanupHandler chain(
final Handler chain,
final List files
)
{
return new TempFileCleanupHandler<>(chain, files);
}
@Override
public T handle(final HttpResponse response) {
try {
return super.handle(response);
}finally {
for (File file : files) {
file.delete();
}
}
}
}
/**
* Execute an HTTP request to the Rundeck instance. We will login first, and then execute the API call.
*
* @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
* @return a new {@link InputStream} instance, not linked with network resources
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
private T execute(HttpUriRequest request, Handler handler) throws RundeckApiException,
RundeckApiLoginException,
RundeckApiTokenException {
try(CloseableHttpClient httpClient = instantiateHttpClient()) {
// we only need to manually login in case of login-based authentication
// note that in case of token-based auth, the auth (via an HTTP header) is managed by an interceptor.
if (client.getToken() == null && client.getSessionID() == null) {
login(httpClient);
}
// execute the HTTP request
HttpResponse response = null;
try {
response = httpClient.execute(request);
} catch (IOException e) {
throw new RundeckApiException("Failed to execute an HTTP " + request.getMethod() + " on url : "
+ request.getURI(), e);
}
// in case of error, we get a redirect to /api/error
// that we need to follow manually for POST and DELETE requests (as GET)
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode / 100 == 3) {
String newLocation = response.getFirstHeader("Location").getValue();
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiException("Failed to consume entity (release connection)", e);
}
request = new HttpGet(newLocation);
try {
response = httpClient.execute(request);
statusCode = response.getStatusLine().getStatusCode();
} catch (IOException e) {
throw new RundeckApiException("Failed to execute an HTTP GET on url : " + request.getURI(), e);
}
}
// check the response code (should be 2xx, even in case of error : error message is in the XML result)
if (statusCode / 100 != 2) {
if (statusCode == 403 &&
(client.getToken() != null || client.getSessionID() != null)) {
throw new RundeckApiTokenException("Invalid Token or sessionID ! Got HTTP response '" + response.getStatusLine()
+ "' for " + request.getURI());
} else {
throw new RundeckApiException.RundeckApiHttpStatusException("Invalid HTTP response '" + response.getStatusLine() + "' for "
+ request.getURI(), statusCode);
}
}
if(statusCode==204){
return null;
}
if (response.getEntity() == null) {
throw new RundeckApiException("Empty Rundeck response ! HTTP status line is : "
+ response.getStatusLine());
}
return handler.handle(response);
} catch (IOException e) {
throw new RundeckApiException("failed closing http client", e);
}
}
/**
* Do the actual work of login, using the given {@link HttpClient} instance. You'll need to re-use this instance
* when making API calls (such as running a job). Only use this in case of login-based authentication.
*
* @param httpClient pre-instantiated
* @throws RundeckApiLoginException if the login failed
*/
private String login(HttpClient httpClient) throws RundeckApiLoginException {
String sessionID = null;
// 1. call expected GET request
String location = client.getUrl();
try {
HttpGet getRequest = new HttpGet(location);
HttpResponse response = httpClient.execute(getRequest);
// sessionID stored in case user wants to cache it for reuse
Header cookieHeader = response.getFirstHeader("Set-Cookie");
if (cookieHeader != null) {
String cookieStr = cookieHeader.getValue();
if (cookieStr != null) {
int i1 = cookieStr.indexOf("JSESSIONID=");
if (i1 >= 0) {
cookieStr = cookieStr.substring(i1 + "JSESSIONID=".length());
int i2 = cookieStr.indexOf(";");
if (i2 >= 0) {
sessionID = cookieStr.substring(0, i2);
}
}
}
}
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
}
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to get request on " + location, e);
}
// 2. then call POST login request
location += "/j_security_check";
while (true) {
try {
HttpPost postLogin = new HttpPost(location);
List params = new ArrayList();
params.add(new BasicNameValuePair("j_username", client.getLogin()));
params.add(new BasicNameValuePair("j_password", client.getPassword()));
params.add(new BasicNameValuePair("action", "login"));
postLogin.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8));
HttpResponse response = httpClient.execute(postLogin);
if (response.getStatusLine().getStatusCode() / 100 == 3) {
// HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually...
location = response.getFirstHeader("Location").getValue();
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
}
continue;
}
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for "
+ location);
}
try {
String content = EntityUtils.toString(response.getEntity(), Consts.UTF_8);
if (StringUtils.contains(content, "j_security_check")) {
throw new RundeckApiLoginException("Login failed for user " + client.getLogin());
}
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
}
break;
} catch (IOException io) {
throw new RundeckApiLoginException("Failed to read Rundeck result", io);
} catch (ParseException p) {
throw new RundeckApiLoginException("Failed to parse Rundeck response", p);
}
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to post login form on " + location, e);
}
}
return sessionID;
}
/**
* Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates
*
* @return an {@link HttpClient} instance - won't be null
*/
private CloseableHttpClient instantiateHttpClient() {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create().useSystemProperties();
// configure user-agent
httpClientBuilder.setUserAgent( "Rundeck API Java Client " + client.getApiVersion());
if (client.isSslHostnameVerifyAllowAll()) {
httpClientBuilder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
}
if (client.isSslCertificateTrustAllowSelfSigned()) {
// configure SSL
try {
httpClientBuilder.setSslcontext(
new SSLContextBuilder()
.loadTrustMaterial(null, new TrustSelfSignedStrategy())
.build());
} catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
if(client.isSystemProxyEnabled()) {
// configure proxy (use system env : http.proxyHost / http.proxyPort)
httpClientBuilder.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()));
}
// in case of token-based authentication, add the correct HTTP header to all requests via an interceptor
httpClientBuilder.addInterceptorFirst(
new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
if (client.getToken() != null) {
request.addHeader(AUTH_TOKEN_HEADER, client.getToken());
//System.out.println("httpClient adding token header");
} else if (client.getSessionID() != null) {
request.addHeader(COOKIE_HEADER, "JSESSIONID=" + client.getSessionID());
//System.out.println("httpClient adding session header, sessionID="+client.getSessionID());
}
}
}
);
return httpClientBuilder.build();
}
}