Skip to content

Commit

Permalink
Provide an ability to specify proxy authentication (#46)
Browse files Browse the repository at this point in the history
* Source updates to support proxy authentication

* Updated pipeline syntax page

* Updated unit tests

Co-authored-by: Aleks Gekht <[email protected]>
  • Loading branch information
shchukax and shchukax authored Apr 26, 2021
1 parent c0c1565 commit e0a0639
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 52 deletions.
24 changes: 24 additions & 0 deletions src/main/java/jenkins/plugins/http_request/HttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -419,6 +429,7 @@ public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
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 = "";
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class HttpRequestExecution extends MasterToSlaveCallable<ResponseContentS
private final HttpMode httpMode;
private final boolean ignoreSslErrors;
private final HttpHost httpProxy;
private final StandardUsernamePasswordCredentials proxyCredentials;

private final String body;
private final List<HttpRequestNameValuePair> headers;
Expand Down Expand Up @@ -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(),

Expand All @@ -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(),

Expand All @@ -153,7 +156,8 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener

private HttpRequestExecution(
String url, HttpMode httpMode, boolean ignoreSslErrors,
String httpProxy, String body, List<HttpRequestNameValuePair> headers, Integer timeout,
String httpProxy, String proxyAuthentication, String body,
List<HttpRequestNameValuePair> headers, Integer timeout,
FilePath uploadFile, String multipartName, boolean wrapAsMultipart,
String authentication, boolean useNtlm, boolean useSystemProperties,

Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/jenkins/plugins/http_request/HttpRequestStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<HttpHost, StandardUsernamePasswordCredentials> 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<HttpHost, StandardUsernamePasswordCredentials> 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<HttpHost, StandardUsernamePasswordCredentials> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<f:entry field="httpProxy" title="Http Proxy" help="/plugin/http_request/help-httpProxy.html">
<f:textbox />
</f:entry>
<f:entry field="proxyAuthentication" title="Proxy authenticate" help="/plugin/http_request/help-proxyAuthentication.html">
<f:select />
</f:entry>
<f:entry field="validResponseCodes" title="Response codes expected" help="/plugin/http_request/help-validResponseCodes.html">
<f:textbox default="${descriptor.validResponseCodes}"/>
</f:entry>
Expand Down
4 changes: 4 additions & 0 deletions src/main/webapp/help-proxyAuthentication.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
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.
</div>
34 changes: 23 additions & 11 deletions src/test/java/jenkins/plugins/http_request/HttpRequestStepTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading

0 comments on commit e0a0639

Please sign in to comment.