diff --git a/src/main/java/jenkins/plugins/http_request/HttpRequest.java b/src/main/java/jenkins/plugins/http_request/HttpRequest.java index 0893d983..88163bca 100644 --- a/src/main/java/jenkins/plugins/http_request/HttpRequest.java +++ b/src/main/java/jenkins/plugins/http_request/HttpRequest.java @@ -60,6 +60,7 @@ public class HttpRequest extends Builder { private Boolean ignoreSslErrors = DescriptorImpl.ignoreSslErrors; private HttpMode httpMode = DescriptorImpl.httpMode; private String httpProxy = DescriptorImpl.httpProxy; + private String proxyAuthentication = DescriptorImpl.proxyAuthentication; private Boolean passBuildParameters = DescriptorImpl.passBuildParameters; private String validResponseCodes = DescriptorImpl.validResponseCodes; private String validResponseContent = DescriptorImpl.validResponseContent; @@ -206,6 +207,15 @@ public void setAuthentication(String authentication) { this.authentication = authentication; } + public String getProxyAuthentication() { + return proxyAuthentication; + } + + @DataBoundSetter + public void setProxyAuthentication(String proxyAuthentication) { + this.proxyAuthentication = proxyAuthentication; + } + public String getRequestBody() { return requestBody; } @@ -419,6 +429,7 @@ public static final class DescriptorImpl extends BuildStepDescriptor { public static final boolean ignoreSslErrors = false; public static final HttpMode httpMode = HttpMode.GET; public static final String httpProxy = ""; + public static final String proxyAuthentication = ""; public static final Boolean passBuildParameters = false; public static final String validResponseCodes = "100:399"; public static final String validResponseContent = ""; @@ -469,6 +480,19 @@ public ListBoxModel doFillAuthenticationItems(@AncestorInPath Item project, return fillAuthenticationItems(project, url); } + public ListBoxModel doFillProxyAuthenticationItems(@AncestorInPath Item project, + @QueryParameter String url) { + if (project == null || !project.hasPermission(Item.CONFIGURE)) { + return new StandardListBoxModel(); + } else { + return new StandardListBoxModel() + .includeEmptyValue() + .includeAs(ACL.SYSTEM, + project, StandardUsernamePasswordCredentials.class, + URIRequirementBuilder.fromUri(url).build()); + } + } + public static ListBoxModel fillAuthenticationItems(Item project, String url) { if (project == null || !project.hasPermission(Item.CONFIGURE)) { return new StandardListBoxModel(); diff --git a/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java b/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java index 9d990b93..d294199c 100644 --- a/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java +++ b/src/main/java/jenkins/plugins/http_request/HttpRequestExecution.java @@ -84,6 +84,7 @@ public class HttpRequestExecution extends MasterToSlaveCallable headers; @@ -119,7 +120,8 @@ static HttpRequestExecution from(HttpRequest http, return new HttpRequestExecution( url, http.getHttpMode(), http.getIgnoreSslErrors(), - http.getHttpProxy(), body, headers, http.getTimeout(), + http.getHttpProxy(), http.getProxyAuthentication(), + body, headers, http.getTimeout(), uploadFile, http.getMultipartName(), http.getWrapAsMultipart(), http.getAuthentication(), http.isUseNtlm(), http.getUseSystemProperties(), @@ -141,7 +143,8 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener Item project = execution.getProject(); return new HttpRequestExecution( step.getUrl(), step.getHttpMode(), step.isIgnoreSslErrors(), - step.getHttpProxy(), step.getRequestBody(), headers, step.getTimeout(), + step.getHttpProxy(), step.getProxyAuthentication(), + step.getRequestBody(), headers, step.getTimeout(), uploadFile, step.getMultipartName(), step.isWrapAsMultipart(), step.getAuthentication(), step.isUseNtlm(), step.getUseSystemProperties(), @@ -153,7 +156,8 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener private HttpRequestExecution( String url, HttpMode httpMode, boolean ignoreSslErrors, - String httpProxy, String body, List headers, Integer timeout, + String httpProxy, String proxyAuthentication, String body, + List headers, Integer timeout, FilePath uploadFile, String multipartName, boolean wrapAsMultipart, String authentication, boolean useNtlm, boolean useSystemProperties, @@ -166,7 +170,30 @@ private HttpRequestExecution( this.url = url; this.httpMode = httpMode; this.ignoreSslErrors = ignoreSslErrors; - this.httpProxy = StringUtils.isNotBlank(httpProxy) ? HttpHost.create(httpProxy) : null; + + if (StringUtils.isNotBlank(httpProxy)) { + this.httpProxy = HttpHost.create(httpProxy); + if (StringUtils.isNotBlank(proxyAuthentication)) { + + StandardCredentials credential = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StandardCredentials.class, + project, ACL.SYSTEM, + URIRequirementBuilder.fromUri(url).build()), + CredentialsMatchers.withId(proxyAuthentication)); + if (credential instanceof StandardUsernamePasswordCredentials) { + this.proxyCredentials = (StandardUsernamePasswordCredentials) credential; + } else { + this.proxyCredentials = null; + throw new IllegalStateException("Proxy authentication '" + proxyAuthentication + "' doesn't exist anymore or is not a username/password credential type"); + } + } else { + this.proxyCredentials = null; + } + } else { + this.httpProxy = null; + this.proxyCredentials = null; + } this.body = body; this.headers = headers; @@ -338,6 +365,17 @@ private void configureTimeoutAndSsl(HttpClientBuilder clientBuilder) throws NoSu private CloseableHttpClient auth( HttpClientBuilder clientBuilder, HttpRequestBase httpRequestBase, HttpContext context) throws IOException, InterruptedException { + + if (proxyCredentials != null) { + logger().println("Using proxy authentication: " + proxyCredentials.getId()); + if (authenticator instanceof CredentialBasicAuthentication) { + ((CredentialBasicAuthentication) authenticator).addCredentials(httpProxy, proxyCredentials); + } else { + new CredentialBasicAuthentication((StandardUsernamePasswordCredentials) proxyCredentials) + .prepare(clientBuilder, context, httpProxy); + } + } + if (authenticator == null) { return clientBuilder.build(); } diff --git a/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java b/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java index 0ee2c9d5..059b17c1 100644 --- a/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java +++ b/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java @@ -39,6 +39,7 @@ public final class HttpRequestStep extends AbstractStepImpl { private boolean ignoreSslErrors = DescriptorImpl.ignoreSslErrors; private HttpMode httpMode = DescriptorImpl.httpMode; private String httpProxy = DescriptorImpl.httpProxy; + private String proxyAuthentication = DescriptorImpl.proxyAuthentication; private String validResponseCodes = DescriptorImpl.validResponseCodes; private String validResponseContent = DescriptorImpl.validResponseContent; private MimeType acceptType = DescriptorImpl.acceptType; @@ -165,7 +166,16 @@ public String getAuthentication() { return authentication; } - @DataBoundSetter + @DataBoundSetter + public void setProxyAuthentication(String proxyAuthentication) { + this.proxyAuthentication = proxyAuthentication; + } + + public String getProxyAuthentication() { + return proxyAuthentication; + } + + @DataBoundSetter public void setRequestBody(String requestBody) { this.requestBody = requestBody; } @@ -276,6 +286,7 @@ public static final class DescriptorImpl extends AbstractStepDescriptorImpl { public static final boolean ignoreSslErrors = HttpRequest.DescriptorImpl.ignoreSslErrors; public static final HttpMode httpMode = HttpRequest.DescriptorImpl.httpMode; public static final String httpProxy = HttpRequest.DescriptorImpl.httpProxy; + public static final String proxyAuthentication = HttpRequest.DescriptorImpl.proxyAuthentication; public static final String validResponseCodes = HttpRequest.DescriptorImpl.validResponseCodes; public static final String validResponseContent = HttpRequest.DescriptorImpl.validResponseContent; public static final MimeType acceptType = HttpRequest.DescriptorImpl.acceptType; @@ -333,7 +344,12 @@ public ListBoxModel doFillAuthenticationItems(@AncestorInPath Item project, return HttpRequest.DescriptorImpl.fillAuthenticationItems(project, url); } - public FormValidation doCheckValidResponseCodes(@QueryParameter String value) { + public ListBoxModel doFillProxyAuthenticationItems(@AncestorInPath Item project, + @QueryParameter String url) { + return HttpRequest.DescriptorImpl.fillAuthenticationItems(project, url); + } + + public FormValidation doCheckValidResponseCodes(@QueryParameter String value) { return HttpRequest.DescriptorImpl.checkValidResponseCodes(value); } diff --git a/src/main/java/jenkins/plugins/http_request/auth/BasicDigestAuthentication.java b/src/main/java/jenkins/plugins/http_request/auth/BasicDigestAuthentication.java index f1af3f17..c116218f 100644 --- a/src/main/java/jenkins/plugins/http_request/auth/BasicDigestAuthentication.java +++ b/src/main/java/jenkins/plugins/http_request/auth/BasicDigestAuthentication.java @@ -3,6 +3,7 @@ import java.io.PrintStream; import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.utils.URIUtils; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.protocol.HttpContext; @@ -52,7 +53,8 @@ public String getPassword() { @Override public CloseableHttpClient authenticate(HttpClientBuilder clientBuilder, HttpContext context, HttpRequestBase requestBase, PrintStream logger) { - return CredentialBasicAuthentication.auth(clientBuilder, context, requestBase, userName, password); + CredentialBasicAuthentication.auth(clientBuilder, context, URIUtils.extractHost(requestBase.getURI()), userName, password); + return clientBuilder.build(); } @Extension diff --git a/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java b/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java index d74b08d6..33719e48 100644 --- a/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java +++ b/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java @@ -2,7 +2,11 @@ import java.io.IOException; import java.io.PrintStream; +import java.net.URI; +import java.util.*; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.client.AuthCache; import org.apache.http.client.CredentialsProvider; @@ -22,38 +26,71 @@ * @author Janario Oliveira */ public class CredentialBasicAuthentication implements Authenticator { - private static final long serialVersionUID = 8034231374732499786L; - - private final StandardUsernamePasswordCredentials credential; - - public CredentialBasicAuthentication(StandardUsernamePasswordCredentials credential) { - this.credential = credential; - } - - @Override - public String getKeyName() { - return credential.getId(); - } - - @Override - public CloseableHttpClient authenticate(HttpClientBuilder clientBuilder, HttpContext context, HttpRequestBase requestBase, PrintStream logger) - throws IOException, InterruptedException { - return auth(clientBuilder, context, requestBase, - credential.getUsername(), credential.getPassword().getPlainText()); - } - - static CloseableHttpClient auth(HttpClientBuilder clientBuilder, HttpContext context, HttpRequestBase requestBase, - String username, String password) { - CredentialsProvider provider = new BasicCredentialsProvider(); - provider.setCredentials( - new AuthScope(requestBase.getURI().getHost(), requestBase.getURI().getPort()), - new org.apache.http.auth.UsernamePasswordCredentials(username, password)); - clientBuilder.setDefaultCredentialsProvider(provider); - - AuthCache authCache = new BasicAuthCache(); - authCache.put(URIUtils.extractHost(requestBase.getURI()), new BasicScheme()); - context.setAttribute(HttpClientContext.AUTH_CACHE, authCache); - - return clientBuilder.build(); - } + private static final long serialVersionUID = 8034231374732499786L; + + private final StandardUsernamePasswordCredentials credential; + private final Map extraCredentials = new Hashtable<>(); + + public CredentialBasicAuthentication(StandardUsernamePasswordCredentials credential) { + this.credential = credential; + } + + public void addCredentials(HttpHost host, StandardUsernamePasswordCredentials credentials) { + if (host == null || credentials == null) { + throw new IllegalArgumentException("Null target host or credentials"); + } + extraCredentials.put(host, credential); + } + + @Override + public String getKeyName() { + return credential.getId(); + } + + @Override + public CloseableHttpClient authenticate(HttpClientBuilder clientBuilder, HttpContext context, HttpRequestBase requestBase, PrintStream logger) + throws IOException, InterruptedException { + prepare(clientBuilder, context, requestBase); + return clientBuilder.build(); + } + + public void prepare(HttpClientBuilder clientBuilder, HttpContext context, HttpRequestBase requestBase) { + prepare(clientBuilder, context, URIUtils.extractHost(requestBase.getURI())); + } + + public void prepare(HttpClientBuilder clientBuilder, HttpContext context, HttpHost targetHost) { + auth(clientBuilder, context, targetHost, + credential.getUsername(), credential.getPassword().getPlainText(), extraCredentials); + } + + static void auth(HttpClientBuilder clientBuilder, HttpContext context, HttpHost targetHost, + String username, String password) { + auth(clientBuilder, context, targetHost, username, password, null); + } + + static void auth(HttpClientBuilder clientBuilder, HttpContext context, HttpHost targetHost, + String username, String password, Map extraCreds) { + CredentialsProvider provider = new BasicCredentialsProvider(); + AuthCache authCache = new BasicAuthCache(); + + provider.setCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()), + new org.apache.http.auth.UsernamePasswordCredentials(username, password)); + authCache.put(targetHost, new BasicScheme()); + + if (extraCreds != null && !extraCreds.isEmpty()) { + for (Map.Entry creds : extraCreds.entrySet()) { + provider.setCredentials( + new AuthScope(creds.getKey().getHostName(), creds.getKey().getPort()), + new org.apache.http.auth.UsernamePasswordCredentials( + creds.getValue().getUsername(), + creds.getValue().getPassword().getPlainText() + ) + ); + authCache.put(creds.getKey(), new BasicScheme()); + } + } + + clientBuilder.setDefaultCredentialsProvider(provider); + context.setAttribute(HttpClientContext.AUTH_CACHE, authCache); + } } diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly b/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly index 3d00bba7..abe91be6 100644 --- a/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly +++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestStep/config.jelly @@ -14,6 +14,9 @@ + + + diff --git a/src/main/webapp/help-proxyAuthentication.html b/src/main/webapp/help-proxyAuthentication.html new file mode 100644 index 00000000..eb839ec5 --- /dev/null +++ b/src/main/webapp/help-proxyAuthentication.html @@ -0,0 +1,4 @@ +
+ Proxy authentication that will be used before this request - only applicable if Http Proxy is set. + Authentications are created in global configuration under a key name that is selected here. +
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java index 67848c65..f8087e41 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java @@ -1,16 +1,6 @@ package jenkins.plugins.http_request; -import static jenkins.plugins.http_request.Registers.registerAcceptedTypeRequestChecker; -import static jenkins.plugins.http_request.Registers.registerBasicAuth; -import static jenkins.plugins.http_request.Registers.registerContentTypeRequestChecker; -import static jenkins.plugins.http_request.Registers.registerCustomHeaders; -import static jenkins.plugins.http_request.Registers.registerFormAuth; -import static jenkins.plugins.http_request.Registers.registerFormAuthBad; -import static jenkins.plugins.http_request.Registers.registerInvalidStatusCode; -import static jenkins.plugins.http_request.Registers.registerReqAction; -import static jenkins.plugins.http_request.Registers.registerRequestChecker; -import static jenkins.plugins.http_request.Registers.registerTimeout; -import static jenkins.plugins.http_request.Registers.registerFileUpload; +import static jenkins.plugins.http_request.Registers.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.File; @@ -670,4 +660,26 @@ public void testFileUpload() throws Exception { j.assertBuildStatusSuccess(run); j.assertLogContains(responseText, run); } + + @Test + public void nonExistentProxyAuthFailsTheBuild() throws Exception { + // Prepare the server + registerBasicAuth(); + + // Configure the build + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + proj.setDefinition(new CpsFlowDefinition( + "def response = httpRequest url:'"+baseURL()+"/proxyAuth',\n" + + " proxy: 'http://proxy.example.com:8080',\n" + + " proxyAuthentication: 'invalid'\n" + + "println('Status: '+response.getStatus())\n" + + "println('Response: '+response.getContent())\n", + true)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + + // Check expectations + j.assertBuildStatus(Result.FAILURE, run); + } } diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestTest.java index 20f10a02..47cf853e 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestTest.java @@ -1001,4 +1001,24 @@ public void testUnwrappedPutFileUpload() throws Exception { this.j.assertBuildStatusSuccess(build); this.j.assertLogContains(responseText, build); } + + @Test + public void nonExistentProxyAuthFailsTheBuild() throws Exception { + // Prepare the server + registerBasicAuth(); + + // Prepare HttpRequest + HttpRequest httpRequest = new HttpRequest(baseURL() + "/basicAuth"); + httpRequest.setHttpProxy("http://proxy.example.com:8888"); + httpRequest.setProxyAuthentication("non-existent-key"); + + // Run build + FreeStyleProject project = this.j.createFreeStyleProject(); + project.getBuildersList().add(httpRequest); + FreeStyleBuild build = project.scheduleBuild2(0).get(); + + // Check expectations + this.j.assertBuildStatus(Result.FAILURE, build); + } + }