diff --git a/vividus-plugin-rest-api/src/main/java/org/vividus/http/CurlUtils.java b/vividus-plugin-rest-api/src/main/java/org/vividus/http/CurlUtils.java new file mode 100644 index 0000000000..ea8d3705d3 --- /dev/null +++ b/vividus-plugin-rest-api/src/main/java/org/vividus/http/CurlUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.http; + +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpRequest; + +public final class CurlUtils +{ + private static final String CURL_REQUEST_COMMAND = "curl -X %s '%s' %n"; + private static final String HEADER = "-H '%s: %s' %n"; + private static final String BODY = "-d '%s'"; + private static final String FORM_TEXT = "-F \"%s=%s\" %n"; + private static final String FORM_FILE = "-F \"%s=@%s\" %n"; + + private static final Pattern NAME_FILE_NAME_PATTERN = Pattern.compile("name=\"(.+)\"; filename=\"(.+)\""); + private static final Pattern NAME_CONTENT_PATTERN = Pattern.compile("name=\"(.+)\"\nContent-Type:.+\n\n(.*)"); + + private CurlUtils() + { + } + + public static String buildCurl(HttpRequest request, String mimeType, byte[] body) + { + return getRequestAsString(request) + + getHeadersAsString(request.getHeaders()) + + getContentAsString(mimeType, body); + } + + private static String getRequestAsString(HttpRequest request) + { + String requestString; + try + { + requestString = String.format(CURL_REQUEST_COMMAND, request.getMethod(), request.getUri()); + } + catch (URISyntaxException e) + { + throw new IllegalArgumentException(e); + } + return requestString; + } + + private static String getHeadersAsString(Header[] headers) + { + StringBuilder headersString = new StringBuilder(); + Stream.of(headers).forEach(h -> headersString.append(String.format(HEADER, h.getName(), h.getValue()))); + return headersString.toString(); + } + + private static String getContentAsString(String mimeType, byte[] body) + { + if (body != null) + { + String bodyAsString = new String(body, StandardCharsets.UTF_8); + if (mimeType.contains("multipart")) + { + String formDataAsString = getFormDataAsString(bodyAsString); + return formDataAsString; + } + return String.format(BODY, bodyAsString); + } + return StringUtils.EMPTY; + } + + private static String getFormDataAsString(String bodyAsString) + { + String regex = bodyAsString.split("\\R", 2)[0]; + String[] formDataArray = bodyAsString.split(regex + ".*"); + + StringBuilder formDataString = new StringBuilder(); + Stream.of(formDataArray).forEach(e -> + { + Matcher m1 = NAME_FILE_NAME_PATTERN.matcher(e); + Matcher m2 = NAME_CONTENT_PATTERN.matcher(e); + if (m1.find()) + { + formDataString.append(String.format(FORM_FILE, m1.group(1), m1.group(2))); + } + else if (m2.find()) + { + formDataString.append(String.format(FORM_TEXT, m2.group(1), m2.group(2))); + } + }); + return formDataString.toString(); + } +} diff --git a/vividus-plugin-rest-api/src/main/java/org/vividus/http/PublishingAttachmentInterceptor.java b/vividus-plugin-rest-api/src/main/java/org/vividus/http/PublishingAttachmentInterceptor.java index 02373c8701..f9c831f845 100644 --- a/vividus-plugin-rest-api/src/main/java/org/vividus/http/PublishingAttachmentInterceptor.java +++ b/vividus-plugin-rest-api/src/main/java/org/vividus/http/PublishingAttachmentInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,25 +75,29 @@ public void process(HttpRequest request, EntityDetails entityDetails, HttpContex } } } - attachApiMessage("Request: " + request, request.getHeaders(), body, mimeType, -1); + attachApiMessage("Request: " + request, request.getHeaders(), body, mimeType, -1, + CurlUtils.buildCurl(request, mimeType, body)); } @Override - public void handle(HttpResponse response) throws IOException + public void handle(HttpResponse response) { Header[] headers = response.getResponseHeaders(); String attachmentTitle = String.format("Response: %s %s", response.getMethod(), response.getFrom()); String mimeType = MimeTypeUtils.getMimeTypeFromHeadersWithDefault(headers); - attachApiMessage(attachmentTitle, headers, response.getResponseBody(), mimeType, response.getStatusCode()); + attachApiMessage(attachmentTitle, headers, response.getResponseBody(), mimeType, + response.getStatusCode(), null); } - private void attachApiMessage(String title, Header[] headers, byte[] body, String mimeType, int statusCode) + private void attachApiMessage(String title, Header[] headers, byte[] body, String mimeType, + int statusCode, String curl) { Map dataMap = new HashMap<>(); dataMap.put("headers", headers); dataMap.put("body", body != null ? new String(body, StandardCharsets.UTF_8) : null); dataMap.put("bodyContentType", mimeType); dataMap.put("statusCode", statusCode); + dataMap.put("curl", curl); attachmentPublisher.publishAttachment("/org/vividus/http/attachment/api-message.ftl", dataMap, title); } diff --git a/vividus-plugin-rest-api/src/main/resources/org/vividus/http/attachment/api-message.ftl b/vividus-plugin-rest-api/src/main/resources/org/vividus/http/attachment/api-message.ftl index 755e39dea2..67a99aa622 100644 --- a/vividus-plugin-rest-api/src/main/resources/org/vividus/http/attachment/api-message.ftl +++ b/vividus-plugin-rest-api/src/main/resources/org/vividus/http/attachment/api-message.ftl @@ -94,6 +94,21 @@ + + <#if curl??> +
+
+

+ +

+
+
+
+
<#outputformat "HTML">${curl}
+
+
+
+ diff --git a/vividus-plugin-rest-api/src/test/java/org/vividus/http/CurlUtilsTests.java b/vividus-plugin-rest-api/src/test/java/org/vividus/http/CurlUtilsTests.java new file mode 100644 index 0000000000..e03902636d --- /dev/null +++ b/vividus-plugin-rest-api/src/test/java/org/vividus/http/CurlUtilsTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.vividus.http.HttpMethod.GET; +import static org.vividus.http.HttpMethod.POST; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.junit.jupiter.api.Test; + +class CurlUtilsTests +{ + public static final String ANY = "any"; + private static final Header[] EMPTY_HEADERS = {}; + + @Test + void testRequestWithHeadersAndWithoutBody() throws URISyntaxException + { + BasicHttpRequest request = mock(); + when(request.getMethod()).thenReturn(GET.name()); + when(request.getUri()).thenReturn(new URI("http://get.example.org/")); + Header[] requestHeaders = + { + new BasicHeader("Connection", "keep-alive"), + new BasicHeader("Host", "example.org") + }; + when(request.getHeaders()).thenReturn(requestHeaders); + + assertEquals(CurlUtils.buildCurl(request, ANY, null), + "curl -X GET 'http://get.example.org/' \n" + + "-H 'Connection: keep-alive' \n" + + "-H 'Host: example.org' \n"); + } + + @Test + void testRequestWithExceptionOnUri() throws URISyntaxException + { + BasicHttpRequest request = mock(); + when(request.getUri()).thenThrow(URISyntaxException.class); + + assertThrows(IllegalArgumentException.class, + () -> CurlUtils.buildCurl(request, ANY, null)); + } + + @Test + void testRequestWithHeadersAndWithBody() throws URISyntaxException + { + BasicHttpRequest request = mock(); + when(request.getMethod()).thenReturn(POST.name()); + when(request.getUri()).thenReturn(new URI("http://post.example.org/")); + when(request.getHeaders()).thenReturn(EMPTY_HEADERS); + + assertEquals(CurlUtils.buildCurl(request, ANY, "post body".getBytes(StandardCharsets.UTF_8)), + "curl -X POST 'http://post.example.org/' \n" + + "-d 'post body'"); + } + + @Test + void testRequestWithFormBody() throws URISyntaxException + { + BasicHttpRequest request = mock(); + when(request.getMethod()).thenReturn(POST.name()); + when(request.getUri()).thenReturn(new URI("http://post.form.data.example.org/")); + when(request.getHeaders()).thenReturn(EMPTY_HEADERS); + + // CHECKSTYLE:OFF + String formDataBody = """ + --Bbg5_2qfo5RoGjrGzlpR2MFzlAqzj2ie49bp7 + Content-Disposition: form-data; name="string-key" + Content-Type: text/plain + + string1 + --Bbg5_2qfo5RoGjrGzlpR2MFzlAqzj2ie49bp7 + Content-Disposition: form-data; name="binary-key"; filename="raw.txt" + Content-Type: text/plain + + raw + --Bbg5_2qfo5RoGjrGzlpR2MFzlAqzj2ie49bp7-- + """; + // CHECKSTYLE:ON + + assertEquals(CurlUtils.buildCurl(request, "multipart/form-data", + formDataBody.getBytes(StandardCharsets.UTF_8)), + "curl -X POST 'http://post.form.data.example.org/' \n" + + "-F \"string-key=string1\" \n" + + "-F \"binary-key=@raw.txt\" \n"); + } +}