params) throws IOException {
112 | try (UrlEncodedFormEntity entity = toUrlEncoded(params); InputStream is = entity.getContent()) {
113 | return IOUtils.toString(is, StandardCharsets.UTF_8);
114 | }
115 | }
116 |
117 | public HttpResponse execute(HttpClient client, HttpClientContext context, HttpUriRequestBase method,
118 | PrintStream logger) throws IOException {
119 | try {
120 | logger.println("Sending request to url: " + method.getUri());
121 |
122 | final HttpResponse httpResponse = client.executeOpen(URIUtils.extractHost(method.getUri()), method, context);
123 | logger.println("Response Code: " + httpResponse.getCode());
124 |
125 | return httpResponse;
126 | } catch (URISyntaxException ex) {
127 | throw new IllegalArgumentException(ex);
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/java/jenkins/plugins/http_request/ResponseContentSupplier.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.io.InputStreamReader;
7 | import java.io.Serial;
8 | import java.io.Serializable;
9 | import java.nio.charset.Charset;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.Map;
13 | import java.util.TreeMap;
14 |
15 | import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
16 | import org.apache.hc.core5.http.ClassicHttpResponse;
17 | import org.apache.hc.core5.http.ContentType;
18 | import org.apache.hc.core5.http.Header;
19 | import org.apache.hc.core5.http.HttpEntity;
20 | import org.apache.hc.core5.http.HttpHeaders;
21 | import org.apache.hc.core5.http.HttpResponse;
22 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
23 | import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
24 |
25 | import org.apache.commons.io.IOUtils;
26 |
27 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28 |
29 | /**
30 | * A container for the Http Response.
31 | *
32 | * The container is returned as is to the Pipeline.
33 | * For the normal plugin, the container is consumed internally (since it cannot be returned).
34 | *
35 | * @author Martin d'Anjou
36 | */
37 | public class ResponseContentSupplier implements Serializable, AutoCloseable {
38 |
39 | @Serial
40 | private static final long serialVersionUID = 1L;
41 |
42 | private final int status;
43 | private final Map> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
44 | private String charset;
45 |
46 | private ResponseHandle responseHandle;
47 | private String content;
48 | @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
49 | private transient InputStream contentStream;
50 | @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
51 | private transient CloseableHttpClient httpclient;
52 |
53 | public ResponseContentSupplier(String content, int status) {
54 | this.content = content;
55 | this.status = status;
56 | }
57 |
58 | public ResponseContentSupplier(ResponseHandle responseHandle, CloseableHttpResponse response) {
59 | this.status = response.getCode();
60 | this.responseHandle = responseHandle;
61 | readHeaders(response);
62 | readCharset(response);
63 |
64 | try {
65 | HttpEntity entity = response.getEntity();
66 | InputStream entityContent = entity != null ? entity.getContent() : null;
67 |
68 | if (responseHandle == ResponseHandle.STRING && entityContent != null) {
69 | byte[] bytes = IOUtils.toByteArray(entityContent);
70 | contentStream = new ByteArrayInputStream(bytes);
71 | content = new String(bytes, charset == null || charset.isEmpty() ?
72 | Charset.defaultCharset().name() : charset);
73 | } else {
74 | contentStream = entityContent;
75 | }
76 | } catch (IOException e) {
77 | throw new IllegalStateException(e);
78 | }
79 | }
80 |
81 | @Whitelisted
82 | public int getStatus() {
83 | return this.status;
84 | }
85 |
86 | @Whitelisted
87 | public Map> getHeaders() {
88 | return this.headers;
89 | }
90 |
91 | @Whitelisted
92 | public String getCharset() {
93 | return charset;
94 | }
95 |
96 | @Whitelisted
97 | public String getContent() {
98 | if (responseHandle == ResponseHandle.STRING) {
99 | return content;
100 | }
101 | if (content != null) {
102 | return content;
103 | }
104 | if (contentStream == null) {
105 | return null;
106 | }
107 |
108 | try (InputStreamReader in = new InputStreamReader(contentStream,
109 | charset == null || charset.isEmpty() ? Charset.defaultCharset().name() : charset)) {
110 | content = IOUtils.toString(in);
111 | return content;
112 | } catch (IOException e) {
113 | throw new IllegalStateException("Error reading response. " +
114 | "If you are reading the content in pipeline you should pass responseHandle: 'LEAVE_OPEN' and " +
115 | "close the response(response.close()) after consume it.", e);
116 | }
117 | }
118 |
119 | @Whitelisted
120 | public InputStream getContentStream() {
121 | return contentStream;
122 | }
123 |
124 | private void readCharset(ClassicHttpResponse response) {
125 | Charset charset = null;
126 |
127 | Header contentTypeHeader = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
128 | ContentType contentType = ContentType.parse(response.getEntity() != null ?
129 | response.getEntity().getContentType() :
130 | (contentTypeHeader != null ? contentTypeHeader.getValue() : null));
131 | if (contentType != null) {
132 | charset = contentType.getCharset();
133 | if (charset == null) {
134 | ContentType defaultContentType = ContentType.create(contentType.getMimeType());
135 | charset = defaultContentType.getCharset();
136 | }
137 | }
138 | if (charset != null) {
139 | this.charset = charset.name();
140 | }
141 | }
142 |
143 | private void readHeaders(HttpResponse response) {
144 | Header[] respHeaders = response.getHeaders();
145 | for (Header respHeader : respHeaders) {
146 | List hs = headers.computeIfAbsent(respHeader.getName(), k -> new ArrayList<>());
147 | hs.add(respHeader.getValue());
148 | }
149 | }
150 |
151 | @Override
152 | public String toString() {
153 | return "Status: " + this.status;
154 | }
155 |
156 | @Override
157 | public void close() throws IOException {
158 | if (httpclient != null) {
159 | httpclient.close();
160 | }
161 | if (contentStream != null) {
162 | contentStream.close();
163 | }
164 | }
165 |
166 | void setHttpClient(CloseableHttpClient httpclient) {
167 | this.httpclient = httpclient;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/test/java/jenkins/plugins/http_request/HttpRequestStepRoundTripTest.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | import java.net.URL;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | import jenkins.plugins.http_request.auth.FormAuthentication;
10 | import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
11 | import jenkins.plugins.http_request.util.RequestAction;
12 |
13 | import org.jenkinsci.plugins.workflow.steps.StepConfigTester;
14 | import org.junit.jupiter.api.Test;
15 |
16 | import org.jvnet.hudson.test.JenkinsRule;
17 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
18 |
19 | /**
20 | * @author Martin d'Anjou
21 | */
22 | @WithJenkins
23 | class HttpRequestStepRoundTripTest {
24 |
25 | private static final HttpRequestStep before = new HttpRequestStep("http://domain/");
26 |
27 | @Test
28 | void configRoundTripGroup1(JenkinsRule j) throws Exception {
29 | configRoundTrip(j);
30 | before.setHttpMode(HttpMode.GET);
31 | configRoundTrip(j);
32 | before.setValidResponseCodes("100:599");
33 | configRoundTrip(j);
34 | before.setValidResponseContent("some content we want to see");
35 | configRoundTrip(j);
36 | before.setAcceptType(MimeType.TEXT_HTML);
37 | configRoundTrip(j);
38 | before.setContentType(MimeType.TEXT_HTML);
39 | configRoundTrip(j);
40 | }
41 |
42 | @Test
43 | void configRoundtripGroup2(JenkinsRule j) throws Exception {
44 | before.setTimeout(12);
45 | configRoundTrip(j);
46 | before.setConsoleLogResponseBody(true);
47 | configRoundTrip(j);
48 | before.setConsoleLogResponseBody(false);
49 | configRoundTrip(j);
50 | }
51 |
52 | @Test
53 | void configRoundtripGroup3(JenkinsRule j) throws Exception {
54 | configRoundTrip(j);
55 |
56 | List params = new ArrayList<>();
57 | params.add(new HttpRequestNameValuePair("param1","value1"));
58 | params.add(new HttpRequestNameValuePair("param2","value2"));
59 |
60 | RequestAction action = new RequestAction(new URL("http://www.domain.com/"),HttpMode.GET,null,params);
61 | List actions = new ArrayList<>();
62 | actions.add(action);
63 |
64 | FormAuthentication formAuth = new FormAuthentication("keyname",actions);
65 | List formAuthList = new ArrayList<>();
66 | formAuthList.add(formAuth);
67 |
68 | HttpRequestGlobalConfig.get().setFormAuthentications(formAuthList);
69 | configRoundTrip(j);
70 |
71 | List customHeaders = new ArrayList<>();
72 | customHeaders.add(new HttpRequestNameValuePair("param1","value1"));
73 | before.setCustomHeaders(customHeaders);
74 | configRoundTrip(j);
75 | }
76 |
77 | @Test
78 | void configRoundtripGroup4(JenkinsRule j) throws Exception {
79 | before.setUploadFile("upload.txt");
80 | configRoundTrip(j);
81 | before.setMultipartName("filename");
82 | configRoundTrip(j);
83 | }
84 |
85 | private static void configRoundTrip(JenkinsRule j) throws Exception {
86 | HttpRequestStep after = new StepConfigTester(j).configRoundTrip(before);
87 | j.assertEqualBeans(before, after, "httpMode");
88 | j.assertEqualBeans(before, after, "url");
89 | j.assertEqualBeans(before, after, "validResponseCodes");
90 | j.assertEqualBeans(before, after, "validResponseContent");
91 | j.assertEqualBeans(before, after, "acceptType");
92 | j.assertEqualBeans(before, after, "contentType");
93 | j.assertEqualBeans(before, after, "uploadFile");
94 | j.assertEqualBeans(before, after, "multipartName");
95 | j.assertEqualBeans(before, after, "timeout");
96 | j.assertEqualBeans(before, after, "consoleLogResponseBody");
97 | j.assertEqualBeans(before, after, "authentication");
98 |
99 | // Custom header check
100 | assertEquals(before.getCustomHeaders().size(),after.getCustomHeaders().size());
101 | for (int idx = 0; idx < before.getCustomHeaders().size(); idx++) {
102 | HttpRequestNameValuePair bnvp = before.getCustomHeaders().get(idx);
103 | HttpRequestNameValuePair anvp = after.getCustomHeaders().get(idx);
104 | assertEquals(bnvp.getName(),anvp.getName());
105 | assertEquals(bnvp.getValue(),anvp.getValue());
106 | }
107 |
108 | // Form authentication check
109 | List beforeFas = HttpRequestGlobalConfig.get().getFormAuthentications();
110 | List afterFas = HttpRequestGlobalConfig.get().getFormAuthentications();
111 | assertEquals(beforeFas.size(), afterFas.size());
112 | for (int idx = 0; idx < beforeFas.size(); idx++) {
113 | FormAuthentication beforeFa = beforeFas.get(idx);
114 | FormAuthentication afterFa = afterFas.get(idx);
115 | assertEquals(beforeFa.getKeyName(), afterFa.getKeyName());
116 | List beforeActions = beforeFa.getActions();
117 | List afterActions = afterFa.getActions();
118 | assertEquals(beforeActions.size(), afterActions.size());
119 | for (int jdx = 0; jdx < beforeActions.size(); jdx ++) {
120 | RequestAction beforeAction = beforeActions.get(jdx);
121 | RequestAction afterAction = afterActions.get(jdx);
122 | assertEquals(beforeAction.getUrl(), afterAction.getUrl());
123 | assertEquals(beforeAction.getMode(), afterAction.getMode());
124 | List beforeParams = beforeAction.getParams();
125 | List afterParams = afterAction.getParams();
126 | assertEquals(beforeParams.size(), afterParams.size());
127 | for (int kdx = 0; kdx < beforeParams.size(); kdx++) {
128 | HttpRequestNameValuePair beforeNvp = beforeParams.get(kdx);
129 | HttpRequestNameValuePair afterNvp = afterParams.get(kdx);
130 | assertEquals(beforeNvp.getName(), afterNvp.getName());
131 | assertEquals(beforeNvp.getValue(), afterNvp.getValue());
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/test/java/jenkins/plugins/http_request/HttpRequestRoundTripTest.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import org.jvnet.hudson.test.JenkinsRule;
6 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
7 |
8 | import java.net.URL;
9 |
10 | import static org.junit.jupiter.api.Assertions.assertEquals;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | import jenkins.plugins.http_request.auth.FormAuthentication;
15 | import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
16 | import jenkins.plugins.http_request.util.RequestAction;
17 |
18 | /**
19 | * @author Martin d'Anjou
20 | */
21 | @WithJenkins
22 | class HttpRequestRoundTripTest {
23 |
24 | private static final HttpRequest before = new HttpRequest("http://domain/");
25 |
26 | @Test
27 | void configRoundtripGroup1(JenkinsRule j) throws Exception {
28 | configRoundTrip(j);
29 | before.setHttpMode(HttpMode.GET);
30 | configRoundTrip(j);
31 | before.setPassBuildParameters(true);
32 | configRoundTrip(j);
33 | before.setPassBuildParameters(false);
34 | configRoundTrip(j);
35 | }
36 |
37 | @Test
38 | void configRoundTripGroup1b(JenkinsRule j) throws Exception {
39 | before.setValidResponseCodes("100:599");
40 | configRoundTrip(j);
41 | before.setValidResponseContent("some content we want to see");
42 | configRoundTrip(j);
43 | before.setAcceptType(MimeType.TEXT_HTML);
44 | configRoundTrip(j);
45 | before.setContentType(MimeType.TEXT_HTML);
46 | configRoundTrip(j);
47 | }
48 |
49 | @Test
50 | void configRoundtripGroup2(JenkinsRule j) throws Exception {
51 | before.setOutputFile("myfile.txt");
52 | configRoundTrip(j);
53 | before.setTimeout(12);
54 | configRoundTrip(j);
55 | before.setConsoleLogResponseBody(true);
56 | configRoundTrip(j);
57 | before.setConsoleLogResponseBody(false);
58 | configRoundTrip(j);
59 | }
60 |
61 | @Test
62 | void configRoundtripGroup3(JenkinsRule j) throws Exception {
63 | configRoundTrip(j);
64 |
65 | List params = new ArrayList<>();
66 | params.add(new HttpRequestNameValuePair("param1","value1"));
67 | params.add(new HttpRequestNameValuePair("param2","value2"));
68 |
69 | RequestAction action = new RequestAction(new URL("http://www.domain.com/"),HttpMode.GET,null,params);
70 | List actions = new ArrayList<>();
71 | actions.add(action);
72 |
73 | FormAuthentication formAuth = new FormAuthentication("keyname",actions);
74 | List formAuthList = new ArrayList<>();
75 | formAuthList.add(formAuth);
76 |
77 | HttpRequestGlobalConfig.get().setFormAuthentications(formAuthList);
78 | configRoundTrip(j);
79 |
80 | List customHeaders = new ArrayList<>();
81 | customHeaders.add(new HttpRequestNameValuePair("param1","value1"));
82 | before.setCustomHeaders(customHeaders);
83 | configRoundTrip(j);
84 | }
85 |
86 | @Test
87 | void configRoundtripGroup4(JenkinsRule j) throws Exception {
88 | before.setUploadFile("upload.txt");
89 | configRoundTrip(j);
90 | before.setMultipartName("filename");
91 | configRoundTrip(j);
92 | }
93 |
94 | private static void configRoundTrip(JenkinsRule j) throws Exception {
95 | HttpRequest after = j.configRoundtrip(before);
96 | j.assertEqualBeans(before, after, "httpMode,passBuildParameters");
97 | j.assertEqualBeans(before, after, "url");
98 | j.assertEqualBeans(HttpRequestRoundTripTest.before, after, "validResponseCodes,validResponseContent");
99 | j.assertEqualBeans(before, after, "acceptType,contentType");
100 | j.assertEqualBeans(before, after, "uploadFile,multipartName");
101 | j.assertEqualBeans(before, after, "outputFile,timeout");
102 | j.assertEqualBeans(before, after, "consoleLogResponseBody");
103 | j.assertEqualBeans(before, after, "authentication");
104 |
105 | // Custom header check
106 | assertEquals(before.getCustomHeaders().size(),after.getCustomHeaders().size());
107 | for (int idx = 0; idx < before.getCustomHeaders().size(); idx++) {
108 | HttpRequestNameValuePair bnvp = before.getCustomHeaders().get(idx);
109 | HttpRequestNameValuePair anvp = after.getCustomHeaders().get(idx);
110 | assertEquals(bnvp.getName(),anvp.getName());
111 | assertEquals(bnvp.getValue(),anvp.getValue());
112 | }
113 |
114 | // Form authentication check
115 | List beforeFas = HttpRequestGlobalConfig.get().getFormAuthentications();
116 | List afterFas = HttpRequestGlobalConfig.get().getFormAuthentications();
117 | assertEquals(beforeFas.size(), afterFas.size());
118 | for (int idx = 0; idx < beforeFas.size(); idx++) {
119 | FormAuthentication beforeFa = beforeFas.get(idx);
120 | FormAuthentication afterFa = afterFas.get(idx);
121 | assertEquals(beforeFa.getKeyName(), afterFa.getKeyName());
122 | List beforeActions = beforeFa.getActions();
123 | List afterActions = afterFa.getActions();
124 | assertEquals(beforeActions.size(), afterActions.size());
125 | for (int jdx = 0; jdx < beforeActions.size(); jdx ++) {
126 | RequestAction beforeAction = beforeActions.get(jdx);
127 | RequestAction afterAction = afterActions.get(jdx);
128 | assertEquals(beforeAction.getUrl(), afterAction.getUrl());
129 | assertEquals(beforeAction.getMode(), afterAction.getMode());
130 | List beforeParams = beforeAction.getParams();
131 | List afterParams = afterAction.getParams();
132 | assertEquals(beforeParams.size(), afterParams.size());
133 | for (int kdx = 0; kdx < beforeParams.size(); kdx++) {
134 | HttpRequestNameValuePair beforeNvp = beforeParams.get(kdx);
135 | HttpRequestNameValuePair afterNvp = afterParams.get(kdx);
136 | assertEquals(beforeNvp.getName(), afterNvp.getName());
137 | assertEquals(beforeNvp.getValue(), afterNvp.getValue());
138 | }
139 | }
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/src/test/resources/jenkins/plugins/http_request/README.txt:
--------------------------------------------------------------------------------
1 | Test keystores:
2 |
3 | * test.p12 was picked from credentials-plugin
4 | It includes one entry aliased "1" with a private key,
5 | its cert, and issuing CA cert (as a simple chain).
6 | The keystore password is "password"
7 |
8 | * testTrusted.p12 is a modification of that, adding an
9 | entry aliased "ca" with that same CA cert imported as
10 | trusted:
11 | ````
12 | # Show certs in BASE64 format, last of these is CA:
13 | :; keytool -list -keystore test.p12 -storepass password -alias 1 -rfc
14 |
15 | # Save second block to "ca.pem"
16 |
17 | # Re-import:
18 | :; cp test.p12 testTrusted.p12
19 | :; keytool -importcert -trustcacerts -alias ca -file ca.pem -keystore testTrusted.p12 -storepass password
20 | ...
21 | Trust this certificate? [no]: yes
22 | Certificate was added to keystore
23 | ````
24 |
25 | Verification:
26 | ````
27 | :; keytool -list -keystore testTrusted.p12 -storepass password -rfc
28 | Keystore type: PKCS12
29 | Keystore provider: SUN
30 |
31 | Your keystore contains 2 entries
32 |
33 | Alias name: 1
34 | Creation date: 23.11.2022
35 | Entry type: PrivateKeyEntry
36 | Certificate chain length: 2
37 | Certificate[1]:
38 | -----BEGIN CERTIFICATE-----
39 | MIIDRzCCArCgAwIBAgIBATANBgkqhkiG9w0BAQQFADBmMQswCQYDVQQGEwJLRzEL
40 | MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t
41 | VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMB4XDTA1MDgw
42 | NDE4MTYyMFoXDTE1MDgwMjE4MTYyMFowfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
43 | AkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZvcnQtRnVuc3Rv
44 | bjEPMA0GA1UEAxMGcGtjczEyMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlk
45 | b21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMOT0PRbWiTEJTUjjiwW
46 | yPC7hR2ruxshzWcgWZUuNg5RARnnsQfGpBK+kKp4QsJSunVCo2fmUFkU/UGYVVXK
47 | nHMEcDtX2JqVY/bAPjxptn5k1bnvMFkKFnaAZl5Mi0K0s+D9U0ivpIaw1QXdQbw+
48 | w3STcv1kpy8rmyerH6KOXL1bAgMBAAGjge4wgeswCQYDVR0TBAIwADAsBglghkgB
49 | hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE
50 | FP9dcedV6TFtLIWOWXxIQ5h6JR45MIGQBgNVHSMEgYgwgYWAFImmYOO66j6v/GR/
51 | TL2M0kiN4MxGoWqkaDBmMQswCQYDVQQGEwJLRzELMAkGA1UECBMCTkExEDAOBgNV
52 | BAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4tVEVTVDEhMB8GCSqGSIb3DQEJ
53 | ARYSbWVAbXlob3N0Lm15ZG9tYWluggEAMA0GCSqGSIb3DQEBBAUAA4GBABP/5mXw
54 | ttXKG6dqQl5kPisFs/c+0j64xytp5/cdB/zMpEWRWTBXtyL3T5T16xs52kJS0VfT
55 | t+jezYbeu/dCdBL8Moz3RTYb1aY2/xymZ433kWjvgtrOzgGlaW3eKXcQpQEyK2v/
56 | J4q7+oDCElBRilZCm0mBcQsySKsZjGm8BMjh
57 | -----END CERTIFICATE-----
58 | Certificate[2]:
59 | -----BEGIN CERTIFICATE-----
60 | MIIDBjCCAm+gAwIBAgIBADANBgkqhkiG9w0BAQQFADBmMQswCQYDVQQGEwJLRzEL
61 | MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t
62 | VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMB4XDTA0MTEy
63 | NTE0NDA1NVoXDTE0MTEyMzE0NDA1NVowZjELMAkGA1UEBhMCS0cxCzAJBgNVBAgT
64 | Ak5BMRAwDgYDVQQHEwdCSVNIS0VLMRUwEwYDVQQKEwxPcGVuVlBOLVRFU1QxITAf
65 | BgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjCBnzANBgkqhkiG9w0BAQEF
66 | AAOBjQAwgYkCgYEAqPjWJnesPu6bR/iec4FMz3opVaPdBHxg+ORKNmrnVZPh0t8/
67 | ZT34KXkYoI9B82scurp8UlZVXG8JdUsz+yai8ti9+g7vcuyKUtcCIjn0HLgmdPu5
68 | gFX25lB0pXw+XIU031dOfPvtROdG5YZN5yCErgCy7TE7zntLnkEDuRmyU6cCAwEA
69 | AaOBwzCBwDAdBgNVHQ4EFgQUiaZg47rqPq/8ZH9MvYzSSI3gzEYwgZAGA1UdIwSB
70 | iDCBhYAUiaZg47rqPq/8ZH9MvYzSSI3gzEahaqRoMGYxCzAJBgNVBAYTAktHMQsw
71 | CQYDVQQIEwJOQTEQMA4GA1UEBxMHQklTSEtFSzEVMBMGA1UEChMMT3BlblZQTi1U
72 | RVNUMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21haW6CAQAwDAYDVR0T
73 | BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBfJoiWYrYdjM0mKPEzUQk0nLYTovBP
74 | I0es/2rfGrin1zbcFY+4dhVBd1E/StebnG+CP8r7QeEIwu7x8gYDdOLLsZn+2vBL
75 | e4jNU1ClI6Q0L7jrzhhunQ5mAaZztVyYwFB15odYcdN2iO0tP7jtEsvrRqxICNy3
76 | 8itzViPTf5W4sA==
77 | -----END CERTIFICATE-----
78 |
79 |
80 | *******************************************
81 | *******************************************
82 |
83 |
84 | Alias name: ca
85 | Creation date: 23.11.2022
86 | Entry type: trustedCertEntry
87 |
88 | -----BEGIN CERTIFICATE-----
89 | MIIDBjCCAm+gAwIBAgIBADANBgkqhkiG9w0BAQQFADBmMQswCQYDVQQGEwJLRzEL
90 | MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t
91 | VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMB4XDTA0MTEy
92 | NTE0NDA1NVoXDTE0MTEyMzE0NDA1NVowZjELMAkGA1UEBhMCS0cxCzAJBgNVBAgT
93 | Ak5BMRAwDgYDVQQHEwdCSVNIS0VLMRUwEwYDVQQKEwxPcGVuVlBOLVRFU1QxITAf
94 | BgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjCBnzANBgkqhkiG9w0BAQEF
95 | AAOBjQAwgYkCgYEAqPjWJnesPu6bR/iec4FMz3opVaPdBHxg+ORKNmrnVZPh0t8/
96 | ZT34KXkYoI9B82scurp8UlZVXG8JdUsz+yai8ti9+g7vcuyKUtcCIjn0HLgmdPu5
97 | gFX25lB0pXw+XIU031dOfPvtROdG5YZN5yCErgCy7TE7zntLnkEDuRmyU6cCAwEA
98 | AaOBwzCBwDAdBgNVHQ4EFgQUiaZg47rqPq/8ZH9MvYzSSI3gzEYwgZAGA1UdIwSB
99 | iDCBhYAUiaZg47rqPq/8ZH9MvYzSSI3gzEahaqRoMGYxCzAJBgNVBAYTAktHMQsw
100 | CQYDVQQIEwJOQTEQMA4GA1UEBxMHQklTSEtFSzEVMBMGA1UEChMMT3BlblZQTi1U
101 | RVNUMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21haW6CAQAwDAYDVR0T
102 | BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBfJoiWYrYdjM0mKPEzUQk0nLYTovBP
103 | I0es/2rfGrin1zbcFY+4dhVBd1E/StebnG+CP8r7QeEIwu7x8gYDdOLLsZn+2vBL
104 | e4jNU1ClI6Q0L7jrzhhunQ5mAaZztVyYwFB15odYcdN2iO0tP7jtEsvrRqxICNy3
105 | 8itzViPTf5W4sA==
106 | -----END CERTIFICATE-----
107 |
108 |
109 | *******************************************
110 | *******************************************
111 |
112 |
113 |
114 | Warning:
115 | <1> uses the MD5withRSA signature algorithm which is considered a security risk and is disabled.
116 | <1> uses a 1024-bit RSA key which is considered a security risk. This key size will be disabled in a future update.
117 | <1> uses a 1024-bit RSA key which is considered a security risk. This key size will be disabled in a future update.
118 | uses a 1024-bit RSA key which is considered a security risk. This key size will be disabled in a future update.
119 |
120 |
121 |
122 | # Keytool forbids to export private key; openssl can do it:
123 | :; openssl pkcs12 -in testTrusted.p12 -nodes -nocerts -password pass:password
124 | Bag Attributes
125 | friendlyName: 1
126 | localKeyID: 2E 39 A5 71 AE FD F1 64 40 83 69 72 3A B6 3D 64 05 18 58 B7
127 | Key Attributes:
128 | -----BEGIN PRIVATE KEY-----
129 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMOT0PRbWiTEJTUj
130 | jiwWyPC7hR2ruxshzWcgWZUuNg5RARnnsQfGpBK+kKp4QsJSunVCo2fmUFkU/UGY
131 | VVXKnHMEcDtX2JqVY/bAPjxptn5k1bnvMFkKFnaAZl5Mi0K0s+D9U0ivpIaw1QXd
132 | Qbw+w3STcv1kpy8rmyerH6KOXL1bAgMBAAECgYBommqsByATggUUgsvLsPQQLXto
133 | /yy3ukCN47OGIo0u4wxfupfovMmMbPga9O9f17d6eAXF0F0xCBTcPImHtTIvMLIt
134 | UY4U4xwtdlEM3G5ToBxNvCHvtkkDiUVW8AorZfLFY9Agnsc3cTarrvEkdtzyYN8k
135 | 246tqACTJZEW8b/QoQJBAP/cd5sEPACChyHx7jr44mMBUppfu/5QC3+0N39XhTx+
136 | gXNfpRe/V77Qn0CMcH8RqkQVWTaSzPrpzJcAXgc9cB8CQQDDrvois8c+5ZSGu8MG
137 | 1zNCEjxTU9BBjWEkGLgwMwsH+5BlA7QT8B9QGWYiCJ4pJVJQ37AyQrUqt4at19Yb
138 | vdtFAkEAgylFtxXInIpNM72N3nVPuGkpKzIAcTIfcuuzt3fqOUSwn7BcNXxFQvA3
139 | cyOLV9h6bER1Y2CF6+qGkrIBgbyhCQJBAKR14vRXdBWAjhvOolKVexcEjH7b6iOt
140 | 1v6nZ+XagGLtIqZDPo2jOi3vqs7fv02FeHFQDp2vQuPr6t0gkWovXqECQFEAAAj3
141 | TKXvRs1jL5gKNifOBJgeEqzZJXpLMkWDGTgImu+VyKcGE6+pie0okh4rmIoJqNx0
142 | EzBIslSYYUz8Q+A=
143 | -----END PRIVATE KEY-----
144 | ````
145 |
146 |
--------------------------------------------------------------------------------
/src/test/java/jenkins/plugins/http_request/HttpRequestTestBase.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import static org.hamcrest.MatcherAssert.assertThat;
4 | import static org.hamcrest.Matchers.both;
5 | import static org.hamcrest.Matchers.greaterThanOrEqualTo;
6 | import static org.hamcrest.Matchers.is;
7 | import static org.hamcrest.Matchers.lessThan;
8 |
9 | import java.io.IOException;
10 | import java.nio.charset.StandardCharsets;
11 | import java.util.ArrayList;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | import jakarta.servlet.ServletException;
17 |
18 | import org.apache.hc.core5.http.ContentType;
19 | import org.eclipse.jetty.http.HttpHeader;
20 | import org.eclipse.jetty.http.HttpStatus;
21 | import org.eclipse.jetty.io.Content;
22 | import org.eclipse.jetty.server.Connector;
23 | import org.eclipse.jetty.server.Handler;
24 | import org.eclipse.jetty.server.Request;
25 | import org.eclipse.jetty.server.Response;
26 | import org.eclipse.jetty.server.Server;
27 | import org.eclipse.jetty.server.ServerConnector;
28 | import org.eclipse.jetty.server.handler.ContextHandler;
29 | import org.eclipse.jetty.server.handler.DefaultHandler;
30 | import org.eclipse.jetty.util.Callback;
31 | import org.junit.jupiter.api.AfterAll;
32 | import org.junit.jupiter.api.AfterEach;
33 | import org.junit.jupiter.api.BeforeAll;
34 | import org.junit.jupiter.api.BeforeEach;
35 | import org.jvnet.hudson.test.JenkinsRule;
36 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
37 |
38 | import hudson.model.Descriptor.FormException;
39 | import com.cloudbees.plugins.credentials.Credentials;
40 | import com.cloudbees.plugins.credentials.CredentialsScope;
41 | import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
42 | import com.cloudbees.plugins.credentials.domains.Domain;
43 | import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
44 |
45 | /**
46 | * @author Martin d'Anjou
47 | */
48 | @WithJenkins
49 | class HttpRequestTestBase {
50 |
51 | private static ServerRunning SERVER;
52 | static final String ALL_IS_WELL = "All is well";
53 | protected JenkinsRule j;
54 |
55 | private Map> credentials;
56 |
57 | final String baseURL() {
58 | return SERVER.baseURL;
59 | }
60 |
61 | void registerBasicCredential(String id, String username, String password) throws FormException {
62 | credentials.get(Domain.global()).add(
63 | new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL,
64 | id, "", username, password));
65 | SystemCredentialsProvider.getInstance().setDomainCredentialsMap(credentials);
66 | }
67 |
68 | static void registerHandler(String target, HttpMode method, SimpleHandler handler) {
69 | Map handlerByMethod = SERVER.handlersByMethodByTarget.computeIfAbsent(target, k -> new HashMap<>());
70 | handlerByMethod.put(method, handler);
71 | }
72 |
73 | @BeforeAll
74 | static void beforeClass() throws Exception {
75 | if (SERVER != null) {
76 | return;
77 | }
78 | SERVER = new ServerRunning();
79 | }
80 |
81 | @AfterAll
82 | static void afterClass() throws Exception {
83 | if (SERVER != null) {
84 | SERVER.server.stop();
85 | SERVER = null;
86 | }
87 | }
88 |
89 | @BeforeEach
90 | void init(JenkinsRule j) {
91 | this.j = j;
92 | credentials = new HashMap<>();
93 | credentials.put(Domain.global(), new ArrayList<>());
94 | }
95 |
96 | @AfterEach
97 | void cleanHandlers() {
98 | if (SERVER != null) {
99 | SERVER.handlersByMethodByTarget.clear();
100 | }
101 | }
102 |
103 | protected abstract static class SimpleHandler extends Handler.Abstract {
104 | @Override
105 | public final boolean handle(Request request, Response response, Callback callback) throws IOException, ServletException {
106 | return doHandle(request, response, callback);
107 | }
108 |
109 | String requestBody(Request request) throws IOException {
110 | return Content.Source.asString(request, StandardCharsets.UTF_8);
111 | }
112 |
113 | boolean okAllIsWell(Response response, Callback callback) {
114 | return okText(response, ALL_IS_WELL, callback);
115 | }
116 |
117 | boolean okText(Response response, String body, Callback callback) {
118 | return body(response, HttpStatus.OK_200, ContentType.TEXT_PLAIN, body, callback);
119 | }
120 |
121 | boolean body(Response response, int status, ContentType contentType, String body, Callback callback) {
122 | assertThat(status, is(both(greaterThanOrEqualTo(200)).and(lessThan(300))));
123 | if (contentType != null) {
124 | response.getHeaders().add(HttpHeader.CONTENT_TYPE, contentType.toString());
125 | }
126 | response.setStatus(status);
127 | Content.Sink.write(response, true, body, callback);
128 | return true;
129 | }
130 |
131 | abstract boolean doHandle(Request request, Response response, Callback callback) throws IOException, ServletException;
132 | }
133 |
134 | private static final class ServerRunning {
135 | private final Server server;
136 | private final int port;
137 | private final String baseURL;
138 | private final Map> handlersByMethodByTarget = new HashMap<>();
139 |
140 | private ServerRunning() throws Exception {
141 | server = new Server();
142 | ServerConnector connector = new ServerConnector(server);
143 | server.setConnectors(new Connector[]{connector});
144 |
145 | ContextHandler context = new ContextHandler();
146 | context.setContextPath("/");
147 | context.setHandler(new DefaultHandler() {
148 | @Override
149 | public boolean handle(Request request, Response response, Callback callback) throws Exception {
150 | String target = request.getHttpURI().getPath();
151 | Map handlerByMethod = handlersByMethodByTarget.get(target);
152 | if (handlerByMethod != null) {
153 | Handler handler = handlerByMethod.get(HttpMode.valueOf(request.getMethod()));
154 | if (handler != null) {
155 | return handler.handle(request, response, callback);
156 | }
157 | }
158 |
159 | return super.handle(request, response, callback);
160 | }
161 | });
162 | server.setHandler(context);
163 |
164 | server.start();
165 | port = connector.getLocalPort();
166 | baseURL = "http://localhost:" + port;
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | [[http-request-plugin]]
2 | = Http Request Plugin
3 | :toc: macro
4 | :toc-title: HTTP Request Plugin ToC
5 | ifdef::env-github[]
6 | :tip-caption: :bulb:
7 | :note-caption: :information_source:
8 | :important-caption: :heavy_exclamation_mark:
9 | :caution-caption: :fire:
10 | :warning-caption: :warning:
11 | endif::[]
12 |
13 | link:https://ci.jenkins.io/job/Plugins/job/http-request-plugin/job/master/[image:https://ci.jenkins.io/job/Plugins/job/http-request-plugin/job/master/badge/icon[Build]]
14 | link:https://github.com/jenkinsci/http-request-plugin/graphs/contributors[image:https://img.shields.io/github/contributors/jenkinsci/http-request-plugin.svg?color=blue[Contributors]]
15 | link:https://plugins.jenkins.io/http_request/[image:https://img.shields.io/jenkins/plugin/i/http_request.svg?color=blue&label=installations[Jenkins Plugin Installs]]
16 | link:https://plugins.jenkins.io/http_request/[image:https://img.shields.io/jenkins/plugin/v/http_request.svg[Plugin]]
17 | link:https://github.com/jenkinsci/http-request-plugin/releases/latest[image:https://img.shields.io/github/release/jenkinsci/http-request-plugin.svg?label=changelog[GitHub release]]
18 |
19 | toc::[]
20 |
21 | [abstract]
22 | .Overview
23 | This plugin sends a HTTP/HTTPS request to a user specified URL. The request is made via job
24 | execution in Jenkins and depending on the HTTP response the job can be marked as failed
25 | (configurable). For example, responses such as 404 and 500 can make the job fail. When a
26 | job fails it will log the response to help identify the problem.
27 |
28 | According to the setting of HTTP mode the request will be performed either using HTTP GET or POST.
29 | If there is no such setting then it will use the default from global settings. Default there is POST.
30 |
31 | == Features
32 |
33 | The following powerful features are available in both Pipeline and traditional project types, giving you greater control and flexibility over your builds:
34 |
35 | * Programmable HTTP method:
36 | Choose from a variety of HTTP methods, including GET, POST, MKCOL, PUT, PATCH, DELETE, OPTIONS, or HEAD, to suit your project's specific needs.
37 |
38 | * Programmable range of expected response codes:
39 | Specify a range of expected response codes for your build, and if the response code falls outside the specified range, the build will fail, saving you time and hassle.
40 |
41 | * Supports Basic Authentication:
42 | Use Basic Authentication to ensure that only authorized users can access your project's resources, providing an additional layer of security.
43 |
44 | * Supports Form Authentication:
45 | Form Authentication enables users to authenticate themselves by submitting a username and password through a form, ensuring that only authorized users can access your resources.
46 |
47 | * Supports Certificate-based Authentication:
48 | Use a certificate from a Jenkins stored credential to authenticate your HTTPS requests to a remote server.
49 |
50 | * Specify a required string in the response:
51 | Ensure that a specific string is present in the response by specifying it beforehand. If the string is not present, the build will fail, alerting you to the issue.
52 |
53 | * Set a connection timeout limit:
54 | Prevent builds from taking too long by setting a connection timeout limit. If the limit is exceeded, the build will fail, saving you time and resources.
55 |
56 | * Set an "Accept" header directly:
57 | Set the "Accept" header directly, providing greater control over the type of data that the server returns in response to a request.
58 |
59 | * Set a "Content-Type" header directly:
60 | Set the "Content-Type" header directly, specifying the type of data that you are sending in your request, helping to ensure that the server can correctly process your request.
61 |
62 | * Set any custom header:
63 | Set any custom header that you require, enabling you to interact with APIs or web services that require specific headers or authentication protocols.
64 |
65 | === Basic plugin features
66 |
67 | [NOTE]
68 | .Feature Availability
69 | ====
70 | The following features are only present in the non-pipeline version of the plugin. For the Pipeline
71 | version, these features are available programmatically.
72 | ====
73 |
74 | * You can send the build parameters as URL query strings
75 | * You can store the response to a file, built-in to the plugin
76 |
77 | === Pipeline features
78 |
79 | In a Pipeline job, you have total control over how the url is formed. Suppose you have a build
80 | parameter called "`param1`", you can pass it to the HTTP request programmatically like so:
81 |
82 | [source,groovy]
83 | ----
84 | httpRequest "http://httpbin.org/response-headers?param1=${param1}"
85 | ----
86 |
87 | If you wish to save the response to a file, you need to grab a workspace. You can do this with a
88 | `node` Pipeline step. For example:
89 |
90 | [source,groovy]
91 | ----
92 | def response = httpRequest "http://httpbin.org/response-headers?param1=${param1}"
93 | node() {
94 | writeFile file: 'response.txt', text: response.content
95 | }
96 | ----
97 |
98 | You can access the response status code, content and headers programmatically:
99 |
100 | [source,groovy]
101 | ----
102 | def response = httpRequest "http://httpbin.org/response-headers?param1=${param1}"
103 | println("Status: ${response.status}")
104 | println("Response: ${response.content}")
105 | println("Headers: ${response.headers}")
106 | ----
107 |
108 | You may also send content in the body of the request, such as for a PATCH request:
109 |
110 | [source,groovy]
111 | ----
112 | // create payload
113 | def patchOrg = """
114 | {"description": "$description"}
115 | """
116 | def response = httpRequest acceptType: 'APPLICATION_JSON', contentType: 'APPLICATION_JSON',
117 | httpMode: 'PATCH', requestBody: patchOrg,
118 | url: "https://api.github.com/orgs/${orgName}"
119 | ----
120 |
121 | You may also send content in the body of the request, such as for a POST request:
122 |
123 | [source,groovy]
124 | ----
125 | httpRequest acceptType: 'APPLICATION_JSON', contentType: 'APPLICATION_JSON',
126 | httpMode: 'POST', quiet: true,
127 | requestBody: '''{
128 | "display-name" : "my_Username",
129 | "email" : "user@example.test",
130 | "password" : {
131 | "value" : "my_password"
132 | },
133 | }''',
134 | url: 'https://api.github.com/orgs/${orgName}'
135 | ----
136 |
137 |
138 | You can also set custom headers:
139 |
140 | [source,groovy]
141 | ----
142 | def response = httpRequest customHeaders: [[name: 'foo', value: 'bar']]
143 | ----
144 |
145 | You can also set custom headers with mask set true:
146 |
147 | [source,groovy]
148 | ----
149 | def response = httpRequest customHeaders: [[maskValue: true, name: 'foo', value: 'bar']],
150 | url: 'https://api.github.com/orgs/${orgName}'
151 | ----
152 |
153 | You can send ``multipart/form-data`` forms:
154 |
155 | [source,groovy]
156 | ----
157 | def response = httpRequest httpMode: 'POST', formData: [
158 | [contentType: 'application/json', name: 'model', body: '{"foo": "bar"}'],
159 | [contentType: 'text/plain', name: 'file', fileName: 'readme.txt',
160 | uploadFile: 'data/lipsum.txt']]
161 | ----
162 |
163 | You can send a request with form-data:
164 |
165 | [source,groovy]
166 | ----
167 | def response = httpRequest acceptType: 'APPLICATION_JSON', contentType: 'APPLICATION_FORM_DATA',
168 | formData: [[body: '''{
169 | "name" : "example",
170 | "type" : "bot"
171 | }''',
172 | contentType: 'text/plain', fileName: 'sample', name: 'data',
173 | uploadFile: './files/readme.txt']],
174 | httpMode: 'POST', quiet: true, responseHandle: 'NONE', timeout: null,
175 | url: 'https://api.github.com/orgs/${orgName}',
176 | validResponseCodes: '200,404', validResponseContent: 'token'
177 | ----
178 |
179 | You can send ``multipart file`` and ``multipart entity name``:
180 |
181 | [source,groovy]
182 | ----
183 | def response = httpRequest acceptType: 'APPLICATION_JSON', contentType: 'APPLICATION_OCTETSTREAM',
184 | httpMode: 'POST', multipartName: 'file', quiet: true,
185 | responseHandle: 'NONE', timeout: null, uploadFile: './files/readme.txt',
186 | url: 'https://api.github.com/orgs/${orgName}'
187 | ----
188 |
189 | You can send a request with SSL error ignored
190 |
191 | [source,groovy]
192 | ----
193 | def response = httpRequest ignoreSslErrors: true, responseHandle: 'NONE',
194 | url: 'https://api.github.com/orgs/${orgName}'
195 | ----
196 |
197 | You can send a request with http proxy
198 |
199 | [source,groovy]
200 | ----
201 | def response = httpRequest httpProxy: 'http://proxy.local', responseHandle: 'NONE',
202 | url: 'https://api.github.com/orgs/${orgName}'
203 | ----
204 |
205 | You can send a request with http proxy authenticate
206 |
207 | [source,groovy]
208 | ----
209 | def response = httpRequest proxyAuthentication: Basic, 'http://proxy.local',
210 | responseHandle: 'NONE', url: 'https://api.github.com/orgs/${orgName}'
211 | ----
212 |
213 | You can send a request with accepted response codes
214 |
215 | [source,groovy]
216 | ----
217 | def response = httpRequest responseHandle: 'NONE', validResponseCodes: '200,404',
218 | url: 'https://api.github.com/orgs/${orgName}'
219 | ----
220 |
221 | You can send a request with accepted response content
222 |
223 | [source,groovy]
224 | ----
225 | def response = httpRequest responseHandle: 'STRING',
226 | url: 'https://api.github.com/orgs/${orgName}',
227 | validResponseCodes: '200,404', validResponseContent: 'token'
228 | ----
229 |
230 | You can send a request with connection timeout
231 |
232 | [source,groovy]
233 | ----
234 | def response = httpRequest timeout: 30, url: 'https://api.github.com/orgs/${orgName}'
235 | ----
236 |
237 | You can send a request where output is written to file
238 |
239 | [source,groovy]
240 | ----
241 | def response = httpRequest outputFile: 'readme.txt', url:'https://api.github.com/orgs/${orgName}'
242 | ----
243 |
244 | You can send a request where response is printed on the console
245 |
246 | [source,groovy]
247 | ----
248 | def response = httpRequest consoleLogResponseBody: true,
249 | url:'https://api.github.com/orgs/${orgName}'
250 | ----
251 |
252 | You can send a request without logging output — with logs turned off
253 |
254 | [source,groovy]
255 | ----
256 | def response = httpRequest quiet: true, url:'https://api.github.com/orgs/${orgName}'
257 | ----
258 |
259 | You can handle response
260 |
261 | [source,groovy]
262 | ----
263 | def response = httpRequest responseHandle: 'LEAVE_OPEN',
264 | url: "https://api.github.com/orgs/${orgName}"
265 | response.close() // must call response.close() after a LEAVE_OPEN
266 | ----
267 |
268 | You can use a Jenkins credential to authenticate the request
269 |
270 | [source,groovy]
271 | ----
272 | def response = httpRequest authentication: 'my-jenkins-credential-id',
273 | url: 'https://api.github.com/user/jenkinsci'
274 | ----
275 |
276 | You can send an SSL request with authentication by user certificate;
277 | for a private CA, make sure to first add the CA certificate is as
278 | "Trusted", then add the user key along with certification chain up
279 | to same CA certificate, into your PKCS12 keystore file which you
280 | upload to Jenkins credentials, and you also must use a non-trivial
281 | password for that keystore. Keep in mind that for systems under test
282 | which create their own self-signed CA and HTTPS protection, you can
283 | programmatically create and upload the credentials, into a domain
284 | where the job has write access (its folder etc.)
285 |
286 | [source,groovy]
287 | ----
288 | def response = httpRequest authentication: 'user_with_cert_and_ca',
289 | url: 'https://sut123.local.domain:8443/api/v1/status/debug'
290 | ----
291 |
292 | A basic WebDAV upload can be built using ``MKCOL`` and ``PUT`` like so:
293 |
294 | [source,groovy]
295 | ----
296 | // create directory aka a collection
297 | httpRequest authentication: 'my-jenkins-credential-id',
298 | httpMode: 'MKCOL',
299 | // on Apache httpd 201 = collection created, 405 = collection already exists
300 | validResponseCodes: '201,405',
301 | url: "https://example.com/webdav-enabled-server/reports/${version}/"
302 | // upload a file
303 | httpRequest authentication: 'my-jenkins-credential-id',
304 | httpMode: 'PUT',
305 | validResponseCodes: '201',
306 | url: "https://example.com/reports/${version}/your-report-maybe.html",
307 | uploadFile: './local/path/to/report.html'
308 | ----
309 |
310 | For details on the Pipeline features, use the Pipeline snippet generator in the Pipeline job
311 | configuration.
312 |
313 | [WARNING]
314 | .Known Limitations
315 | ====
316 | If Jenkins is restarted before the HTTP response comes back, the build will fail.
317 | ====
318 |
319 | == Building
320 |
321 | The plugin can be built and tested locally using a Maven Docker container:
322 |
323 | [source, bash]
324 | ----
325 | docker run -it --rm -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven maven:3.3-jdk-8 mvn test
326 | ----
327 |
328 | == Configure Global Settings
329 |
330 | image::docs/images/configure-http-request-global.png[]
331 |
332 | == Configure Build Step in your Jenkins job
333 |
334 | image::docs/images/configure-http-request-build-step.png[]
335 |
336 | == HTTP Request Parameters
337 |
338 | Parameters are escaped, which means if you try to pass another value inside a value, it will not
339 | happen.
340 |
341 | In the example below, the key "`name`" will be passed with a value of "`jenkins&os=linux`". Note
342 | that "`os`" is not a parameter - it is part of the value). At the HTTP server-side no parameter
343 | named "`os`" will exist.
344 |
345 | [CAUTION]
346 | .Regarding Logging & Sensitive Information
347 | ====
348 | Every execution will log all parameters. Be careful to not pass private information such as
349 | passwords or personal information.
350 | ====
351 |
352 | image:docs/images/log.png[]
353 |
354 | == Issues
355 |
356 | Report issues and enhancements in the https://issues.jenkins.io/[Jenkins issue tracker].
357 | Use the `http-request-plugin` component in the `JENKINS` project.
358 |
359 | == Contributing
360 |
361 | Refer to our https://github.com/jenkinsci/.github/blob/master/CONTRIBUTING.md[contribution guidelines].
362 |
363 | == License
364 |
365 | Licensed under link:LICENSE[the MIT License].
366 |
--------------------------------------------------------------------------------
/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import static org.hamcrest.MatcherAssert.assertThat;
4 | import static org.hamcrest.Matchers.*;
5 |
6 | import com.cloudbees.plugins.credentials.CredentialsProvider;
7 | import com.cloudbees.plugins.credentials.CredentialsScope;
8 | import com.cloudbees.plugins.credentials.CredentialsStore;
9 | import com.cloudbees.plugins.credentials.SecretBytes;
10 | import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
11 | import com.cloudbees.plugins.credentials.common.StandardCredentials;
12 | import com.cloudbees.plugins.credentials.domains.Domain;
13 | import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;
14 | import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
15 | import hudson.model.Descriptor.FormException;
16 | import hudson.model.Fingerprint;
17 | import hudson.model.Result;
18 |
19 | import java.io.ByteArrayOutputStream;
20 | import java.io.File;
21 | import java.io.IOException;
22 | import java.nio.file.Files;
23 | import java.util.Collections;
24 | import jenkins.model.Jenkins;
25 |
26 | import org.apache.commons.io.FileUtils;
27 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
28 | import org.jenkinsci.plugins.workflow.job.WorkflowJob;
29 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
30 |
31 | import org.htmlunit.html.HtmlPage;
32 | import org.junit.jupiter.api.BeforeEach;
33 | import org.junit.jupiter.api.Test;
34 | import org.junit.jupiter.api.io.TempDir;
35 | import org.jvnet.hudson.test.Issue;
36 | import org.jvnet.hudson.test.JenkinsRule;
37 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
38 |
39 | /**
40 | * @author Mark Waite
41 | */
42 | @WithJenkins
43 | class HttpRequestStepCredentialsTest extends HttpRequestTestBase {
44 | // For developers: set to `true` so that pipeline console logs show
45 | // up in System.out (and/or System.err) of the plugin test run by
46 | // mvn test -Dtest="HttpRequestStepCredentialsTest"
47 | private final boolean verbosePipelines = false;
48 |
49 | private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException {
50 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
51 | f.getLogText().writeLogTo(0, baos);
52 | return baos.toString();
53 | }
54 |
55 | // From CertificateCredentialImplTest
56 | @TempDir
57 | private File tmp;
58 | private File p12simple;
59 | private File p12trusted;
60 |
61 | private CredentialsStore store = null;
62 |
63 | private static StandardCredentials getInvalidCredential() throws FormException {
64 | String username = "bad-user";
65 | String password = "bad-password";
66 | CredentialsScope scope = CredentialsScope.GLOBAL;
67 | String id = "username-" + username + "-password-" + password;
68 | return new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, username, password);
69 | }
70 |
71 | private StandardCredentials getCertificateCredentialSimple() throws IOException {
72 | if (p12simple == null) {
73 | // Contains a private key + openvpn certs,
74 | // as alias named "1" (according to keytool)
75 | p12simple = File.createTempFile("test.p12", null, tmp);
76 | FileUtils.copyURLToFile(HttpRequestStepCredentialsTest.class.getResource("test.p12"), p12simple);
77 | }
78 |
79 | SecretBytes uploadedKeystore = SecretBytes.fromRawBytes(Files.readAllBytes(p12simple.toPath()));
80 | CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore);
81 | return new CertificateCredentialsImpl(null, "cred_cert_simple", null, "password", storeSource);
82 | }
83 |
84 | private StandardCredentials getCertificateCredentialTrusted() throws IOException {
85 | if (p12trusted == null) {
86 | // Contains a private key + openvpn certs as alias named "1",
87 | // and another alias named "ca" with trustedKeyEntry for CA
88 | p12trusted = File.createTempFile("testTrusted.p12", null, tmp);
89 | FileUtils.copyURLToFile(HttpRequestStepCredentialsTest.class.getResource("testTrusted.p12"), p12trusted);
90 | }
91 |
92 | SecretBytes uploadedKeystore = SecretBytes.fromRawBytes(Files.readAllBytes(p12trusted.toPath()));
93 | CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore);
94 | return new CertificateCredentialsImpl(null, "cred_cert_with_ca", null, "password", storeSource);
95 | }
96 |
97 | @BeforeEach
98 | void enableSystemCredentialsProvider() {
99 | SystemCredentialsProvider.getInstance()
100 | .setDomainCredentialsMap(
101 | Collections.singletonMap(Domain.global(), Collections.emptyList()));
102 | for (CredentialsStore s : CredentialsProvider.lookupStores(Jenkins.get())) {
103 | if (s.getProvider() instanceof SystemCredentialsProvider.ProviderImpl) {
104 | store = s;
105 | break;
106 | }
107 | }
108 | assertThat("The system credentials provider is enabled", store, notNullValue());
109 | }
110 |
111 | @Test
112 | void trackCredentials() throws Exception {
113 | StandardCredentials credential = getInvalidCredential();
114 | store.addCredentials(Domain.global(), credential);
115 |
116 | Fingerprint fingerprint = CredentialsProvider.getFingerprintOf(credential);
117 | assertThat("Fingerprint should not be set before job definition", fingerprint, nullValue());
118 |
119 | JenkinsRule.WebClient wc = j.createWebClient();
120 | HtmlPage page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId());
121 | assertThat("Have usage tracking reported", page.getElementById("usage"), notNullValue());
122 | assertThat(
123 | "No fingerprint created until first use on missing page",
124 | page.getElementById("usage-missing"),
125 | notNullValue());
126 | assertThat(
127 | "No fingerprint created until first use on present page",
128 | page.getElementById("usage-present"),
129 | nullValue());
130 |
131 | // Configure the build to use the credential
132 | WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
133 | proj.setDefinition(
134 | new CpsFlowDefinition(
135 | "def response = httpRequest(url: 'https://api.github.com/users/jenkinsci',\n"
136 | + " authentication: '" + credential.getId() + "')\n"
137 | + "println('Status: '+response.getStatus())\n"
138 | + "println('Response: '+response.getContent())\n",
139 | true));
140 |
141 | fingerprint = CredentialsProvider.getFingerprintOf(credential);
142 | assertThat("Fingerprint should not be set before first build", fingerprint, nullValue());
143 |
144 | // Execute the build
145 | WorkflowRun run = proj.scheduleBuild2(0).get();
146 |
147 | // Check expectations
148 | j.assertBuildStatus(Result.SUCCESS, run);
149 | j.assertLogContains("https://api.github.com/users/jenkinsci/followers", run);
150 |
151 | // Check the credential use was correctly tracked
152 | fingerprint = CredentialsProvider.getFingerprintOf(credential);
153 | assertThat("Fingerprint should be set after first build", fingerprint, notNullValue());
154 | assertThat(fingerprint.getJobs(), hasItem(is(proj.getFullName())));
155 | Fingerprint.RangeSet rangeSet = fingerprint.getRangeSet(proj);
156 | assertThat(rangeSet, notNullValue());
157 | assertThat(rangeSet.includes(proj.getLastBuild().getNumber()), is(true));
158 |
159 | page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId());
160 | assertThat(page.getElementById("usage-missing"), nullValue());
161 | assertThat(page.getElementById("usage-present"), notNullValue());
162 | assertThat(page.getAnchorByText(proj.getFullDisplayName()), notNullValue());
163 | }
164 |
165 | // A set of tests with certificate credentials in different contexts
166 | // TODO: Test on remote agent as in https://github.com/jenkinsci/credentials-plugin/pull/391
167 | // but this requires that PR to be merged first, so credentials-plugin
168 | // processes snapshot() and readable keystore data gets to remote agent.
169 | // Note that the tests below focus on ability of the plugin to load and
170 | // process the key store specified by the credential, rather than that
171 | // it is usable further. It would be a separate effort to mock up a web
172 | // server protected by HTTPS and using certificates for login (possibly
173 | // user and server backed by two different CA's), and query that.
174 | private static String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) {
175 | // Note: we accept any outcome (for the plugin, unresolved host is HTTP-404)
176 | // but it may not crash making use of the credential
177 | // Note: cases withLocalCertLookup also need cpsScriptCredentialTestImports()
178 | return "def authentication='" + id + "';\n"
179 | + "\n"
180 | + "echo \"Querying HTTPS with credential...\"\n"
181 | + "def response = httpRequest(url: 'https://github.xcom/api/v3',\n"
182 | + " httpMode: 'GET',\n"
183 | + " authentication: authentication,\n"
184 | + " consoleLogResponseBody: true,\n"
185 | + " contentType : 'APPLICATION_FORM',\n"
186 | + " validResponseCodes: '100:599',\n"
187 | + " quiet: false)\n"
188 | + "println('First HTTP Request Plugin Status: '+ response.getStatus())\n"
189 | + "println('First HTTP Request Plugin Response: '+ response.getContent())\n"
190 | + "\n"
191 | + "echo \"Querying HTTPS with credential again (reentrability)...\"\n"
192 | + "response = httpRequest(url: 'https://github.xcom/api/v3',\n"
193 | + " httpMode: 'GET',\n"
194 | + " authentication: authentication,\n"
195 | + " consoleLogResponseBody: true,\n"
196 | + " contentType : 'APPLICATION_FORM',\n"
197 | + " validResponseCodes: '100:599',\n"
198 | + " quiet: false)\n"
199 | + "println('Second HTTP Request Plugin Status: '+ response.getStatus())\n"
200 | + "println('Second HTTP Request Plugin Response: '+ response.getContent())\n"
201 | + "\n";
202 | }
203 |
204 | @Test
205 | @Issue({"JENKINS-70000", "JENKINS-70101"})
206 | void testCertSimpleHttpRequestOnController() throws Exception {
207 | // Check that credentials are usable with pipeline script
208 | // running without a node{}
209 | StandardCredentials credential = getCertificateCredentialSimple();
210 | store.addCredentials(Domain.global(), credential);
211 |
212 | // Configure the build to use the credential
213 | WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
214 | String script =
215 | cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER BUILT-IN");
216 | proj.setDefinition(new CpsFlowDefinition(script, false));
217 |
218 | // Execute the build
219 | WorkflowRun run = proj.scheduleBuild2(0).get();
220 | if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
221 |
222 | // Check expectations
223 | j.assertBuildStatus(Result.SUCCESS, run);
224 | // Got to the end?
225 | j.assertLogContains("HTTP Request Plugin Response: ", run);
226 | j.assertLogContains("Using authentication: cred_cert_simple", run);
227 | // Currently we always try adding the material
228 | // and report if not failed trying (might have
229 | // had 0 entries to add though):
230 | //j.assertLogNotContains("Added Trust Material from provided KeyStore", run);
231 | j.assertLogContains("Added Key Material from provided KeyStore", run);
232 | j.assertLogContains("Treating UnknownHostException", run);
233 | }
234 |
235 | @Test
236 | @Issue({"JENKINS-70000", "JENKINS-70101"})
237 | void testCertSimpleHttpRequestOnNodeLocal() throws Exception {
238 | // Check that credentials are usable with pipeline script
239 | // running on a node{} (provided by the controller)
240 | StandardCredentials credential = getCertificateCredentialSimple();
241 | store.addCredentials(Domain.global(), credential);
242 |
243 | // Configure the build to use the credential
244 | WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
245 | String script =
246 | "node {\n" +
247 | cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER NODE") +
248 | "}\n";
249 | proj.setDefinition(new CpsFlowDefinition(script, false));
250 |
251 | // Execute the build
252 | WorkflowRun run = proj.scheduleBuild2(0).get();
253 | if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
254 |
255 | // Check expectations
256 | j.assertBuildStatus(Result.SUCCESS, run);
257 | // Got to the end?
258 | j.assertLogContains("HTTP Request Plugin Response: ", run);
259 | j.assertLogContains("Using authentication: cred_cert_simple", run);
260 | // Currently we always try adding the material
261 | // and report if not failed trying (might have
262 | // had 0 entries to add though):
263 | //j.assertLogNotContains("Added Trust Material from provided KeyStore", run);
264 | j.assertLogContains("Added Key Material from provided KeyStore", run);
265 | j.assertLogContains("Treating UnknownHostException", run);
266 | }
267 |
268 | @Test
269 | @Issue({"JENKINS-70000", "JENKINS-70101"})
270 | void testCertTrustedHttpRequestOnController() throws Exception {
271 | // Check that credentials are usable with pipeline script
272 | // running without a node{}
273 | StandardCredentials credential = getCertificateCredentialTrusted();
274 | store.addCredentials(Domain.global(), credential);
275 |
276 | // Configure the build to use the credential
277 | WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
278 | String script =
279 | cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER BUILT-IN");
280 | proj.setDefinition(new CpsFlowDefinition(script, false));
281 |
282 | // Execute the build
283 | WorkflowRun run = proj.scheduleBuild2(0).get();
284 | if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
285 |
286 | // Check expectations
287 | j.assertBuildStatus(Result.SUCCESS, run);
288 | // Got to the end?
289 | j.assertLogContains("HTTP Request Plugin Response: ", run);
290 | j.assertLogContains("Using authentication: cred_cert_with_ca", run);
291 | j.assertLogContains("Added Trust Material from provided KeyStore", run);
292 | j.assertLogContains("Added Key Material from provided KeyStore", run);
293 | j.assertLogContains("Treating UnknownHostException", run);
294 | }
295 |
296 | @Test
297 | @Issue({"JENKINS-70000", "JENKINS-70101"})
298 | void testCertTrustedHttpRequestOnNodeLocal() throws Exception {
299 | // Check that credentials are usable with pipeline script
300 | // running on a node{} (provided by the controller)
301 | StandardCredentials credential = getCertificateCredentialTrusted();
302 | store.addCredentials(Domain.global(), credential);
303 |
304 | // Configure the build to use the credential
305 | WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
306 | String script =
307 | "node {\n" +
308 | cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER NODE") +
309 | "}\n";
310 | proj.setDefinition(new CpsFlowDefinition(script, false));
311 |
312 | // Execute the build
313 | WorkflowRun run = proj.scheduleBuild2(0).get();
314 | if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run));
315 |
316 | // Check expectations
317 | j.assertBuildStatus(Result.SUCCESS, run);
318 | // Got to the end?
319 | j.assertLogContains("HTTP Request Plugin Response: ", run);
320 | j.assertLogContains("Using authentication: cred_cert_with_ca", run);
321 | j.assertLogContains("Added Trust Material from provided KeyStore", run);
322 | j.assertLogContains("Added Key Material from provided KeyStore", run);
323 | j.assertLogContains("Treating UnknownHostException", run);
324 | }
325 |
326 | }
327 |
--------------------------------------------------------------------------------
/src/test/java/jenkins/plugins/http_request/Registers.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import static org.junit.jupiter.api.Assertions.*;
4 |
5 | import java.io.File;
6 | import java.io.IOException;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.Base64;
9 | import java.util.Enumeration;
10 | import java.util.Map;
11 |
12 | import jakarta.servlet.ServletException;
13 |
14 | import org.apache.hc.core5.http.ContentType;
15 | import org.apache.hc.core5.http.HttpHeaders;
16 | import org.eclipse.jetty.http.HttpHeader;
17 | import org.eclipse.jetty.http.HttpStatus;
18 | import org.eclipse.jetty.http.MultiPart;
19 | import org.eclipse.jetty.http.MultiPartConfig;
20 | import org.eclipse.jetty.http.MultiPartFormData;
21 | import org.eclipse.jetty.server.Request;
22 | import org.eclipse.jetty.server.Response;
23 | import org.eclipse.jetty.util.Callback;
24 |
25 | import jenkins.plugins.http_request.HttpRequestTestBase.SimpleHandler;
26 |
27 | /**
28 | * @author Janario Oliveira
29 | */
30 | class Registers {
31 |
32 | static void registerRequestChecker(final HttpMode method) {
33 | registerHandler("/do" + method.name(), method, new SimpleHandler() {
34 | @Override
35 | boolean doHandle(Request request, Response response, Callback callback) {
36 | assertEquals(method.name(), request.getMethod());
37 |
38 | String query = request.getHttpURI().getQuery();
39 | assertNull(query);
40 | return okAllIsWell(response, callback);
41 | }
42 | });
43 | }
44 |
45 | static void registerContentTypeRequestChecker(final MimeType mimeType, final HttpMode httpMode, final String responseMessage) {
46 | registerHandler("/incoming_" + mimeType.toString(), httpMode, new SimpleHandler() {
47 | @Override
48 | boolean doHandle(Request request, Response response, Callback callback) throws IOException {
49 | assertEquals(httpMode.name(), request.getMethod());
50 |
51 | Enumeration headers = request.getHeaders().getValues(HttpHeaders.CONTENT_TYPE);
52 | if (mimeType == MimeType.NOT_SET) {
53 | assertFalse(headers.hasMoreElements());
54 | } else {
55 | assertTrue(headers.hasMoreElements());
56 | String actual = headers.nextElement();
57 | assertFalse(headers.hasMoreElements());
58 |
59 | // ContentType charset value changed from UTF-8 to utf-8 in Jetty 12.1
60 | // Use a case insensitive comparison of the Content-Type header
61 | // https://www.rfc-editor.org/rfc/rfc9110#name-charset - charset names are matched case-insensitively
62 | String expected = mimeType.getContentType().toString();
63 | assertTrue(expected.equalsIgnoreCase(actual), "Content-Type mismatch, expected '" + expected + "', but was '" + actual + "'");
64 | }
65 |
66 | String query = request.getHttpURI().getQuery();
67 | assertNull(query);
68 | String body = responseMessage != null ? responseMessage : requestBody(request);
69 | return body(response, HttpStatus.OK_200, mimeType.getContentType(), body, callback);
70 | }
71 | });
72 | }
73 |
74 | static void registerAcceptedTypeRequestChecker(final MimeType mimeType) {
75 | registerHandler("/accept_" + mimeType.toString(), HttpMode.GET, new SimpleHandler() {
76 | @Override
77 | boolean doHandle(Request request, Response response, Callback callback) {
78 | assertEquals("GET", request.getMethod());
79 |
80 | Enumeration headers = request.getHeaders().getValues(HttpHeaders.ACCEPT);
81 |
82 |
83 | if (mimeType == MimeType.NOT_SET) {
84 | assertFalse(headers.hasMoreElements());
85 | } else {
86 | assertTrue(headers.hasMoreElements());
87 | String value = headers.nextElement();
88 | assertFalse(headers.hasMoreElements());
89 |
90 | assertEquals(mimeType.getValue(), value);
91 | }
92 | String query = request.getHttpURI().getQuery();
93 | assertNull(query);
94 | return okAllIsWell(response, callback);
95 | }
96 | });
97 | }
98 |
99 | static void registerTimeout() {
100 | // Timeout, do not respond!
101 | registerHandler("/timeout", HttpMode.GET, new SimpleHandler() {
102 | @Override
103 | boolean doHandle(Request request, Response response, Callback callback) {
104 | try {
105 | Thread.sleep(10000);
106 | } catch (InterruptedException ex) {
107 | // do nothing the sleep will be interrupted when the test ends
108 | }
109 | return true;
110 | }
111 | });
112 | }
113 |
114 | static void registerReqAction() {
115 | // Accept the form authentication
116 | registerHandler("/reqAction", HttpMode.GET, new SimpleHandler() {
117 | @Override
118 | boolean doHandle(Request request, Response response, Callback callback) throws ServletException {
119 | assertEquals("GET", request.getMethod());
120 |
121 | Map parameters;
122 | try {
123 | parameters = Request.getParameters(request).toStringArrayMap();
124 | } catch (Exception e) {
125 | throw new ServletException(e);
126 | }
127 |
128 | assertEquals(2, parameters.size());
129 | assertTrue(parameters.containsKey("param1"));
130 | String[] value = parameters.get("param1");
131 | assertEquals(1, value.length);
132 | assertEquals("value1", value[0]);
133 |
134 | assertTrue(parameters.containsKey("param2"));
135 | value = parameters.get("param2");
136 | assertEquals(1, value.length);
137 | assertEquals("value2", value[0]);
138 | return okAllIsWell(response, callback);
139 | }
140 | });
141 | }
142 |
143 | static void registerFormAuth() {
144 | // Check the form authentication
145 | registerHandler("/formAuth", HttpMode.GET, new SimpleHandler() {
146 | @Override
147 | boolean doHandle(Request request, Response response, Callback callback) {
148 | return okAllIsWell(response, callback);
149 | }
150 | });
151 | }
152 |
153 | static void registerFormAuthBad() {
154 | // Check the form authentication header
155 | registerHandler("/formAuthBad", HttpMode.GET, new SimpleHandler() {
156 | @Override
157 | boolean doHandle(Request request, Response response, Callback callback) {
158 | Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "Not allowed");
159 | return true;
160 | }
161 | });
162 | }
163 |
164 | static void registerBasicAuth() {
165 | // Check the basic authentication header
166 | registerHandler("/basicAuth", HttpMode.GET, new SimpleHandler() {
167 | @Override
168 | boolean doHandle(Request request, Response response, Callback callback) {
169 | Enumeration headers = request.getHeaders().getValues(HttpHeaders.AUTHORIZATION);
170 |
171 | String value = headers.nextElement();
172 | assertFalse(headers.hasMoreElements());
173 |
174 | byte[] bytes = Base64.getDecoder().decode(value.substring(6));
175 | String usernamePasswordPair = new String(bytes);
176 | String[] usernamePassword = usernamePasswordPair.split(":");
177 | assertEquals("username1", usernamePassword[0]);
178 | assertEquals("password1", usernamePassword[1]);
179 |
180 | return okAllIsWell(response, callback);
181 | }
182 | });
183 | }
184 |
185 | static void registerCheckRequestBodyWithTag() {
186 | // Check that request body is present and that the containing parameter ${Tag} has been resolved to "trunk"
187 | registerHandler("/checkRequestBodyWithTag", HttpMode.POST, new SimpleHandler() {
188 | @Override
189 | boolean doHandle(Request request, Response response, Callback callback) throws IOException {
190 | assertEquals("POST", request.getMethod());
191 | String requestBody = requestBody(request);
192 |
193 | assertEquals("cleanupDir=D:/continuousIntegration/deployments/Daimler/trunk/standalone", requestBody);
194 | return okAllIsWell(response, callback);
195 | }
196 | });
197 | }
198 |
199 | static void registerCustomHeaders() {
200 | // Check the custom headers
201 | registerHandler("/customHeaders", HttpMode.GET, new SimpleHandler() {
202 | @Override
203 | boolean doHandle(Request request, Response response, Callback callback) {
204 | Enumeration headers = request.getHeaders().getValues("customHeader");
205 |
206 | String value1 = headers.nextElement();
207 | String value2 = headers.nextElement();
208 |
209 | assertFalse(headers.hasMoreElements());
210 | assertEquals("value1", value1);
211 | assertEquals("value2", value2);
212 |
213 | return okAllIsWell(response, callback);
214 | }
215 | });
216 | }
217 |
218 | static void registerInvalidStatusCode() {
219 | // Return an invalid status code
220 | registerHandler("/invalidStatusCode", HttpMode.GET, new SimpleHandler() {
221 | @Override
222 | boolean doHandle(Request request, Response response, Callback callback) {
223 | assertEquals("GET", request.getMethod());
224 | String query = request.getHttpURI().getQuery();
225 | assertNull(query);
226 |
227 | Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "Throwing status 400 for test");
228 | return true;
229 | }
230 | });
231 | }
232 |
233 | static void registerCustomHeadersResolved() {
234 | // Check if the parameters in custom headers have been resolved
235 | registerHandler("/customHeadersResolved", HttpMode.POST, new SimpleHandler() {
236 | @Override
237 | boolean doHandle(Request request, Response response, Callback callback) {
238 | Enumeration headers = request.getHeaders().getValues("resolveCustomParam");
239 |
240 | String value = headers.nextElement();
241 | assertFalse(headers.hasMoreElements());
242 | assertEquals("trunk", value);
243 |
244 | headers = request.getHeaders().getValues("resolveEnvParam");
245 | value = headers.nextElement();
246 | assertFalse(headers.hasMoreElements());
247 | assertEquals("C:/path/to/my/workspace", value);
248 |
249 | return okAllIsWell(response, callback);
250 | }
251 | });
252 | }
253 |
254 | static void registerCheckBuildParameters() {
255 | // Check that exactly one build parameter is passed
256 | registerHandler("/checkBuildParameters", HttpMode.GET, new SimpleHandler() {
257 | @Override
258 | boolean doHandle(Request request, Response response, Callback callback) throws ServletException {
259 | assertEquals("GET", request.getMethod());
260 |
261 | Map parameters;
262 | try {
263 | parameters = Request.getParameters(request).toStringArrayMap();
264 | } catch (Exception e) {
265 | throw new ServletException(e);
266 | }
267 |
268 | assertEquals(1, parameters.size());
269 | assertTrue(parameters.containsKey("foo"));
270 | String[] value = parameters.get("foo");
271 | assertEquals(1, value.length);
272 | assertEquals("value", value[0]);
273 |
274 | return okAllIsWell(response, callback);
275 | }
276 | });
277 | }
278 |
279 | static void registerCheckRequestBody() {
280 | // Check that request body is present and equals to TestRequestBody
281 | registerHandler("/checkRequestBody", HttpMode.POST, new SimpleHandler() {
282 | @Override
283 | boolean doHandle(Request request, Response response, Callback callback) throws IOException {
284 | assertEquals("POST", request.getMethod());
285 | String requestBody = requestBody(request);
286 | assertEquals("TestRequestBody", requestBody);
287 | return okAllIsWell(response, callback);
288 | }
289 | });
290 | }
291 |
292 | static void registerFileUpload(final File uploadFile, final String responseText) {
293 | registerHandler("/uploadFile", HttpMode.POST, new SimpleHandler() {
294 |
295 | private static final String MULTIPART_FORMDATA_TYPE = "multipart/form-data";
296 |
297 | private boolean isMultipartRequest(Request request) {
298 | return request.getHeaders().get(HttpHeader.CONTENT_TYPE) != null && request.getHeaders().get(HttpHeader.CONTENT_TYPE).startsWith(MULTIPART_FORMDATA_TYPE);
299 | }
300 |
301 | @Override
302 | boolean doHandle(Request request, Response response, Callback callback) {
303 | assertEquals("POST", request.getMethod());
304 | assertTrue(isMultipartRequest(request));
305 |
306 | String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
307 | MultiPartFormData.Parts parts = MultiPartFormData.getParts(request, request, contentType, new MultiPartConfig.Builder().build());
308 |
309 | MultiPart.Part part = parts.getFirst("file-name");
310 | assertNotNull(part);
311 | assertEquals(uploadFile.length(), part.getLength());
312 | assertEquals(uploadFile.getName(), part.getFileName());
313 | assertEquals(MimeType.APPLICATION_ZIP.getValue(), part.getHeaders().get(HttpHeader.CONTENT_TYPE));
314 |
315 | return body(response, HttpStatus.CREATED_201, ContentType.TEXT_PLAIN, responseText, callback);
316 | }
317 | });
318 | }
319 |
320 | static void registerFormData(String content, final File file1,
321 | File file2, final String responseText) {
322 | registerHandler("/formData", HttpMode.POST, new SimpleHandler() {
323 |
324 | private static final String MULTIPART_FORMDATA_TYPE = "multipart/form-data";
325 |
326 | private boolean isMultipartRequest(Request request) {
327 | return request.getHeaders().get(HttpHeader.CONTENT_TYPE) != null
328 | && request.getHeaders().get(HttpHeader.CONTENT_TYPE).startsWith(MULTIPART_FORMDATA_TYPE);
329 | }
330 |
331 | @Override
332 | boolean doHandle(Request request, Response response, Callback callback) {
333 | assertEquals("POST", request.getMethod());
334 | assertTrue(isMultipartRequest(request));
335 |
336 | String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
337 | MultiPartFormData.Parts parts = MultiPartFormData.getParts(request, request, contentType, new MultiPartConfig.Builder().build());
338 |
339 | MultiPart.Part file1Part = parts.getFirst("file1");
340 | assertNotNull(file1Part);
341 | assertEquals(file1.length(), file1Part.getLength());
342 | assertEquals(file1.getName(), file1Part.getFileName());
343 | assertEquals(MimeType.TEXT_PLAIN.getValue(), file1Part.getHeaders().get(HttpHeader.CONTENT_TYPE));
344 |
345 | MultiPart.Part file2Part = parts.getFirst("file2");
346 | assertNotNull(file2Part);
347 | assertEquals(file2.length(), file2Part.getLength());
348 | assertEquals(file2.getName(), file2Part.getFileName());
349 | assertEquals(MimeType.APPLICATION_ZIP.getValue(), file2Part.getHeaders().get(HttpHeader.CONTENT_TYPE));
350 |
351 | MultiPart.Part modelPart = parts.getFirst("model");
352 | assertNotNull(modelPart);
353 | assertEquals(content, modelPart.getContentAsString(StandardCharsets.UTF_8));
354 | assertEquals(MimeType.APPLICATION_JSON.getValue(), modelPart.getHeaders().get(HttpHeader.CONTENT_TYPE));
355 |
356 | // So far so good
357 | return body(response, HttpStatus.CREATED_201, ContentType.TEXT_PLAIN,
358 | responseText, callback);
359 | }
360 | });
361 | }
362 |
363 | static void registerUnwrappedPutFileUpload(final File uploadFile, final String responseText) {
364 | registerHandler("/uploadFile/" + uploadFile.getName(), HttpMode.PUT, new SimpleHandler() {
365 |
366 | private static final String MULTIPART_FORMDATA_TYPE = "multipart/form-data";
367 |
368 | private boolean isMultipartRequest(Request request) {
369 | return request.getHeaders().get(HttpHeader.CONTENT_TYPE) != null && request.getHeaders().get(HttpHeader.CONTENT_TYPE).startsWith(MULTIPART_FORMDATA_TYPE);
370 | }
371 |
372 | @Override
373 | boolean doHandle(Request request, Response response, Callback callback) {
374 | assertEquals("PUT", request.getMethod());
375 | assertFalse(isMultipartRequest(request));
376 | assertEquals(uploadFile.length(), request.getLength());
377 | assertEquals(MimeType.APPLICATION_ZIP.getValue(), request.getHeaders().get(HttpHeader.CONTENT_TYPE));
378 | return body(response, HttpStatus.CREATED_201, ContentType.TEXT_PLAIN, responseText, callback);
379 | }
380 | });
381 | }
382 |
383 | private static void registerHandler(String target, HttpMode method, SimpleHandler handler) {
384 | HttpRequestTestBase.registerHandler(target, method, handler);
385 | }
386 | }
387 |
--------------------------------------------------------------------------------
/src/main/java/jenkins/plugins/http_request/HttpRequestStep.java:
--------------------------------------------------------------------------------
1 | package jenkins.plugins.http_request;
2 |
3 | import java.io.IOException;
4 | import java.io.Serial;
5 | import java.util.ArrayList;
6 | import java.util.Collections;
7 | import java.util.HashSet;
8 | import java.util.List;
9 | import java.util.Objects;
10 | import java.util.Set;
11 |
12 | import edu.umd.cs.findbugs.annotations.NonNull;
13 |
14 | import org.apache.hc.core5.http.HttpHeaders;
15 | import org.jenkinsci.plugins.workflow.steps.Step;
16 | import org.jenkinsci.plugins.workflow.steps.StepContext;
17 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
18 | import org.jenkinsci.plugins.workflow.steps.StepExecution;
19 | import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution;
20 | import org.kohsuke.stapler.AncestorInPath;
21 | import org.kohsuke.stapler.DataBoundConstructor;
22 | import org.kohsuke.stapler.DataBoundSetter;
23 | import org.kohsuke.stapler.QueryParameter;
24 |
25 | import hudson.Extension;
26 | import hudson.FilePath;
27 | import hudson.Launcher;
28 | import hudson.model.Item;
29 | import hudson.model.Run;
30 | import hudson.model.TaskListener;
31 | import hudson.remoting.VirtualChannel;
32 | import hudson.util.FormValidation;
33 | import hudson.util.ListBoxModel;
34 |
35 | import jenkins.plugins.http_request.util.HttpRequestFormDataPart;
36 | import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
37 |
38 | /**
39 | * @author Martin d'Anjou
40 | */
41 | public final class HttpRequestStep extends Step {
42 |
43 | private final @NonNull String url;
44 | private boolean ignoreSslErrors = DescriptorImpl.ignoreSslErrors;
45 | private HttpMode httpMode = DescriptorImpl.httpMode;
46 | private String httpProxy = DescriptorImpl.httpProxy;
47 | private String proxyAuthentication = DescriptorImpl.proxyAuthentication;
48 | private String validResponseCodes = DescriptorImpl.validResponseCodes;
49 | private String validResponseContent = DescriptorImpl.validResponseContent;
50 | private MimeType acceptType = DescriptorImpl.acceptType;
51 | private MimeType contentType = DescriptorImpl.contentType;
52 | private Integer timeout = DescriptorImpl.timeout;
53 | private Boolean consoleLogResponseBody = DescriptorImpl.consoleLogResponseBody;
54 | private Boolean quiet = DescriptorImpl.quiet;
55 | private String authentication = DescriptorImpl.authentication;
56 | private String requestBody = DescriptorImpl.requestBody;
57 | private String uploadFile = DescriptorImpl.uploadFile;
58 | private String multipartName = DescriptorImpl.multipartName;
59 | private boolean wrapAsMultipart = DescriptorImpl.wrapAsMultipart;
60 | private Boolean useSystemProperties = DescriptorImpl.useSystemProperties;
61 | private boolean useNtlm = DescriptorImpl.useNtlm;
62 | private List customHeaders = DescriptorImpl.customHeaders;
63 | private List formData = DescriptorImpl.formData;
64 | private String outputFile = DescriptorImpl.outputFile;
65 | private ResponseHandle responseHandle = DescriptorImpl.responseHandle;
66 |
67 | @DataBoundConstructor
68 | public HttpRequestStep(@NonNull String url) {
69 | this.url = url;
70 | }
71 |
72 | @NonNull
73 | public String getUrl() {
74 | return url;
75 | }
76 |
77 | public boolean isIgnoreSslErrors() {
78 | return ignoreSslErrors;
79 | }
80 |
81 | @DataBoundSetter
82 | public void setIgnoreSslErrors(boolean ignoreSslErrors) {
83 | this.ignoreSslErrors = ignoreSslErrors;
84 | }
85 |
86 | @DataBoundSetter
87 | public void setHttpMode(HttpMode httpMode) {
88 | this.httpMode = httpMode;
89 | }
90 |
91 | public HttpMode getHttpMode() {
92 | return httpMode;
93 | }
94 |
95 | @DataBoundSetter
96 | public void setHttpProxy(String httpProxy) {
97 | this.httpProxy = httpProxy;
98 | }
99 |
100 | public String getHttpProxy() {
101 | return httpProxy;
102 | }
103 |
104 | @DataBoundSetter
105 | public void setValidResponseCodes(String validResponseCodes) {
106 | this.validResponseCodes = validResponseCodes;
107 | }
108 |
109 | public String getValidResponseCodes() {
110 | return validResponseCodes;
111 | }
112 |
113 | @DataBoundSetter
114 | public void setValidResponseContent(String validResponseContent) {
115 | this.validResponseContent = validResponseContent;
116 | }
117 |
118 | public String getValidResponseContent() {
119 | return validResponseContent;
120 | }
121 |
122 | @DataBoundSetter
123 | public void setAcceptType(MimeType acceptType) {
124 | this.acceptType = acceptType;
125 | }
126 |
127 | public MimeType getAcceptType() {
128 | return acceptType;
129 | }
130 |
131 | @DataBoundSetter
132 | public void setContentType(MimeType contentType) {
133 | this.contentType = contentType;
134 | }
135 |
136 | public MimeType getContentType() {
137 | return contentType;
138 | }
139 |
140 | @DataBoundSetter
141 | public void setTimeout(Integer timeout) {
142 | this.timeout = timeout;
143 | }
144 |
145 | public Integer getTimeout() {
146 | return timeout;
147 | }
148 |
149 | @DataBoundSetter
150 | public void setConsoleLogResponseBody(Boolean consoleLogResponseBody) {
151 | this.consoleLogResponseBody = consoleLogResponseBody;
152 | }
153 |
154 | public Boolean getConsoleLogResponseBody() {
155 | return consoleLogResponseBody;
156 | }
157 |
158 | @DataBoundSetter
159 | public void setQuiet(Boolean quiet) {
160 | this.quiet = quiet;
161 | }
162 |
163 | public Boolean getQuiet() {
164 | return quiet;
165 | }
166 |
167 | @DataBoundSetter
168 | public void setAuthentication(String authentication) {
169 | this.authentication = authentication;
170 | }
171 |
172 | public String getAuthentication() {
173 | return authentication;
174 | }
175 |
176 | @DataBoundSetter
177 | public void setProxyAuthentication(String proxyAuthentication) {
178 | this.proxyAuthentication = proxyAuthentication;
179 | }
180 |
181 | public String getProxyAuthentication() {
182 | return proxyAuthentication;
183 | }
184 |
185 | @DataBoundSetter
186 | public void setRequestBody(String requestBody) {
187 | this.requestBody = requestBody;
188 | }
189 |
190 | public String getRequestBody() {
191 | return requestBody;
192 | }
193 |
194 | @DataBoundSetter
195 | public void setUseSystemProperties(Boolean useSystemProperties) {
196 | this.useSystemProperties = useSystemProperties;
197 | }
198 |
199 | public Boolean getUseSystemProperties() {
200 | return useSystemProperties;
201 | }
202 |
203 | @DataBoundSetter
204 | public void setCustomHeaders(List customHeaders) {
205 | this.customHeaders = customHeaders;
206 | }
207 |
208 | public List getCustomHeaders() {
209 | return customHeaders;
210 | }
211 |
212 | public List getFormData() {
213 | return formData;
214 | }
215 |
216 | @DataBoundSetter
217 | public void setFormData(List formData) {
218 | this.formData = Collections.unmodifiableList(formData);
219 | }
220 |
221 | public String getOutputFile() {
222 | return outputFile;
223 | }
224 |
225 | @DataBoundSetter
226 | public void setOutputFile(String outputFile) {
227 | this.outputFile = outputFile;
228 | }
229 |
230 | public ResponseHandle getResponseHandle() {
231 | return responseHandle;
232 | }
233 |
234 |
235 | @DataBoundSetter
236 | public void setResponseHandle(ResponseHandle responseHandle) {
237 | this.responseHandle = responseHandle;
238 | }
239 |
240 | public String getUploadFile() {
241 | return uploadFile;
242 | }
243 |
244 | @DataBoundSetter
245 | public void setUploadFile(String uploadFile) {
246 | this.uploadFile = uploadFile;
247 | }
248 |
249 | public String getMultipartName() {
250 | return multipartName;
251 | }
252 |
253 | @DataBoundSetter
254 | public void setMultipartName(String multipartName) {
255 | this.multipartName = multipartName;
256 | }
257 |
258 | public boolean isWrapAsMultipart() {
259 | return wrapAsMultipart;
260 | }
261 |
262 | @DataBoundSetter
263 | public void setWrapAsMultipart(boolean wrapAsMultipart) {
264 | this.wrapAsMultipart = wrapAsMultipart;
265 | }
266 |
267 | @DataBoundSetter
268 | public void setUseNtlm(boolean useNtlm) {
269 | this.useNtlm = useNtlm;
270 | }
271 |
272 | public boolean isUseNtlm() {
273 | return useNtlm;
274 | }
275 |
276 | @Override
277 | public StepExecution start(StepContext context) {
278 | return new Execution(context, this);
279 | }
280 |
281 | @Override
282 | public DescriptorImpl getDescriptor() {
283 | return (DescriptorImpl) super.getDescriptor();
284 | }
285 |
286 | List resolveHeaders() {
287 | final List headers = new ArrayList<>();
288 | if (contentType != null && contentType != MimeType.NOT_SET) {
289 | headers.add(new HttpRequestNameValuePair(HttpHeaders.CONTENT_TYPE, contentType.getContentType().toString()));
290 | }
291 | if (acceptType != null && acceptType != MimeType.NOT_SET) {
292 | headers.add(new HttpRequestNameValuePair(HttpHeaders.ACCEPT, acceptType.getValue()));
293 | }
294 | for (HttpRequestNameValuePair header : customHeaders) {
295 | String headerName = header.getName();
296 | String headerValue = header.getValue();
297 | boolean maskValue = headerName.equalsIgnoreCase(HttpHeaders.AUTHORIZATION) ||
298 | header.getMaskValue();
299 |
300 | headers.add(new HttpRequestNameValuePair(headerName, headerValue, maskValue));
301 | }
302 | return headers;
303 | }
304 |
305 | @Extension
306 | public static final class DescriptorImpl extends StepDescriptor {
307 | public static final boolean ignoreSslErrors = HttpRequest.DescriptorImpl.ignoreSslErrors;
308 | public static final HttpMode httpMode = HttpRequest.DescriptorImpl.httpMode;
309 | public static final String httpProxy = HttpRequest.DescriptorImpl.httpProxy;
310 | public static final String proxyAuthentication = HttpRequest.DescriptorImpl.proxyAuthentication;
311 | public static final String validResponseCodes = HttpRequest.DescriptorImpl.validResponseCodes;
312 | public static final String validResponseContent = HttpRequest.DescriptorImpl.validResponseContent;
313 | public static final MimeType acceptType = HttpRequest.DescriptorImpl.acceptType;
314 | public static final MimeType contentType = HttpRequest.DescriptorImpl.contentType;
315 | public static final int timeout = HttpRequest.DescriptorImpl.timeout;
316 | public static final Boolean consoleLogResponseBody = HttpRequest.DescriptorImpl.consoleLogResponseBody;
317 | public static final Boolean quiet = HttpRequest.DescriptorImpl.quiet;
318 | public static final String authentication = HttpRequest.DescriptorImpl.authentication;
319 | public static final String requestBody = HttpRequest.DescriptorImpl.requestBody;
320 | public static final String uploadFile = HttpRequest.DescriptorImpl.uploadFile;
321 | public static final String multipartName = HttpRequest.DescriptorImpl.multipartName;
322 | public static final boolean wrapAsMultipart = HttpRequest.DescriptorImpl.wrapAsMultipart;
323 | public static final Boolean useSystemProperties = HttpRequest.DescriptorImpl.useSystemProperties;
324 | public static final boolean useNtlm = HttpRequest.DescriptorImpl.useNtlm;
325 | public static final List customHeaders = Collections.emptyList();
326 | public static final List formData = Collections.emptyList();
327 | public static final String outputFile = "";
328 | public static final ResponseHandle responseHandle = ResponseHandle.STRING;
329 |
330 | @Override
331 | public Set extends Class>> getRequiredContext() {
332 | Set> context = new HashSet<>();
333 | Collections.addAll(context, Run.class, TaskListener.class);
334 | return Collections.unmodifiableSet(context);
335 | }
336 |
337 | @Override
338 | public String getFunctionName() {
339 | return "httpRequest";
340 | }
341 |
342 | @NonNull
343 | @Override
344 | public String getDisplayName() {
345 | return "Perform an HTTP Request and return a response object";
346 | }
347 |
348 | public ListBoxModel doFillHttpModeItems() {
349 | return HttpMode.getFillItems();
350 | }
351 |
352 | public ListBoxModel doFillAcceptTypeItems() {
353 | return MimeType.getContentTypeFillItems();
354 | }
355 |
356 | public ListBoxModel doFillContentTypeItems() {
357 | return MimeType.getContentTypeFillItems();
358 | }
359 |
360 | public ListBoxModel doFillResponseHandleItems() {
361 | ListBoxModel items = new ListBoxModel();
362 | for (ResponseHandle responseHandle : ResponseHandle.values()) {
363 | items.add(responseHandle.name());
364 | }
365 | return items;
366 | }
367 |
368 | public ListBoxModel doFillAuthenticationItems(@AncestorInPath Item project,
369 | @QueryParameter String url) {
370 | return HttpRequest.DescriptorImpl.fillAuthenticationItems(project, url);
371 | }
372 |
373 | public ListBoxModel doFillProxyAuthenticationItems(@AncestorInPath Item project,
374 | @QueryParameter String url) {
375 | return HttpRequest.DescriptorImpl.fillAuthenticationItems(project, url);
376 | }
377 |
378 | public FormValidation doCheckValidResponseCodes(@QueryParameter String value) {
379 | return HttpRequest.DescriptorImpl.checkValidResponseCodes(value);
380 | }
381 |
382 | }
383 |
384 | public static final class Execution extends SynchronousNonBlockingStepExecution {
385 |
386 | private final transient HttpRequestStep step;
387 |
388 | Execution(@NonNull StepContext context, HttpRequestStep step) {
389 | super(context);
390 | this.step = step;
391 | }
392 |
393 | @Override
394 | protected ResponseContentSupplier run() throws Exception {
395 | HttpRequestExecution exec = HttpRequestExecution.from(step,
396 | step.getQuiet() ? TaskListener.NULL : Objects.requireNonNull(getContext().get(TaskListener.class)),
397 | this);
398 |
399 | Launcher launcher = getContext().get(Launcher.class);
400 | if (launcher != null) {
401 | VirtualChannel channel = launcher.getChannel();
402 | if (channel == null) {
403 | throw new IllegalStateException("Launcher doesn't support remoting but it is required");
404 | }
405 | return channel.call(exec);
406 | }
407 |
408 | return exec.call();
409 | }
410 |
411 | @Serial
412 | private static final long serialVersionUID = 1L;
413 |
414 | FilePath resolveOutputFile() {
415 | String outputFile = step.getOutputFile();
416 | if (outputFile == null || outputFile.trim().isEmpty()) {
417 | return null;
418 | }
419 |
420 | try {
421 | FilePath workspace = getContext().get(FilePath.class);
422 | if (workspace == null) {
423 | throw new IllegalStateException("Could not find workspace to save file outputFile: " + outputFile +
424 | ". You should use it inside a 'node' block");
425 | }
426 | return workspace.child(outputFile);
427 | } catch (IOException | InterruptedException e) {
428 | throw new IllegalStateException(e);
429 | }
430 | }
431 |
432 | FilePath resolveUploadFile() {
433 | return resolveUploadFileInternal(step.getUploadFile());
434 | }
435 |
436 | public Item getProject() throws IOException, InterruptedException {
437 | return Objects.requireNonNull(getContext().get(Run.class)).getParent();
438 | }
439 |
440 | private FilePath resolveUploadFileInternal(String path) {
441 | if (path == null || path.trim().isEmpty()) {
442 | return null;
443 | }
444 |
445 | try {
446 | FilePath workspace = getContext().get(FilePath.class);
447 | if (workspace == null) {
448 | throw new IllegalStateException("Could not find workspace to check existence of upload file: " + path +
449 | ". You should use it inside a 'node' block");
450 | }
451 | FilePath uploadFilePath = workspace.child(path);
452 | if (!uploadFilePath.exists()) {
453 | throw new IllegalStateException("Could not find upload file: " + path);
454 | }
455 | return uploadFilePath;
456 | } catch (IOException | InterruptedException e) {
457 | throw new IllegalStateException(e);
458 | }
459 | }
460 |
461 | List resolveFormDataParts() {
462 | List formData = step.getFormData();
463 | if (formData == null || formData.isEmpty()) {
464 | return Collections.emptyList();
465 | }
466 |
467 | List resolved = new ArrayList<>(formData.size());
468 |
469 | for (HttpRequestFormDataPart part : formData) {
470 | HttpRequestFormDataPart newPart = new HttpRequestFormDataPart(part.getUploadFile(),
471 | part.getName(), part.getFileName(), part.getContentType(), part.getBody());
472 | newPart.setResolvedUploadFile(resolveUploadFileInternal(part.getUploadFile()));
473 | resolved.add(newPart);
474 | }
475 |
476 | return resolved;
477 | }
478 | }
479 | }
480 |
--------------------------------------------------------------------------------