com.github.dzieciou.testing.curl.Http2Curl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of curl-logger Show documentation
Show all versions of curl-logger Show documentation
Logs REST-assured requests as curl commands
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2009 Anthony Ricaud
* Copyright (C) 2011 Google Inc. All rights reserved.
* Copyright (C) 2016 Maciej Gawinecki
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.github.dzieciou.testing.curl;
import io.restassured.internal.multipart.RestAssuredMultiPartEntity;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Generates CURL command for a given HTTP request.
*/
@SuppressWarnings("deprecation")
public class Http2Curl {
private static final Logger log = LoggerFactory.getLogger(Http2Curl.class);
private final Options options;
public Http2Curl(Options options) {
this.options = options;
}
private static String getContent(FormBodyPart bodyPart) throws IOException {
ContentBody content = bodyPart.getBody();
ByteArrayOutputStream out = new ByteArrayOutputStream((int) content.getContentLength());
content.writeTo(out);
return out.toString();
}
private static String removeQuotes(String s) {
return s.replaceAll("^\"|\"$", "");
}
private static boolean isBasicAuthentication(Header h) {
return h.getName().equals("Authorization") && h.getValue().startsWith("Basic");
}
@SuppressWarnings("deprecation")
private static String getOriginalRequestUri(HttpRequest request) {
if (request instanceof HttpRequestWrapper) {
return ((HttpRequestWrapper) request).getOriginal().getRequestLine().getUri();
} else if (request instanceof RequestWrapper) {
return ((RequestWrapper) request).getOriginal().getRequestLine().getUri();
} else {
throw new IllegalArgumentException("Unsupported request class type: " + request.getClass());
}
}
private static String getHost(HttpRequest request) {
return tryGetHeaderValue(Arrays.asList(request.getAllHeaders()), "Host")
.orElseGet(() -> URI.create(getOriginalRequestUri(request)).getHost());
}
private static boolean isValidUrl(String url) {
try {
new URL(url);
return true;
} catch (MalformedURLException e) {
return false;
}
}
private static Optional tryGetHeaderValue(List headers, String headerName) {
return headers
.stream()
.filter(h -> h.getName().equals(headerName))
.map(Header::getValue)
.findFirst();
}
private static Object getFieldValue(T obj, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
Field f = getField(obj.getClass(), fieldName);
f.setAccessible(true);
return f.get(obj);
}
private static Field getField(Class clazz, String fieldName)
throws NoSuchFieldException {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
Class superClass = clazz.getSuperclass();
if (superClass == null) {
throw e;
} else {
return getField(superClass, fieldName);
}
}
}
/**
* Generates single-line CURL command for a given HTTP request.
*
* @param request HTTP request
* @return CURL command
* @throws Exception if failed to generate CURL command
*/
public String generateCurl(HttpRequest request) throws Exception {
CurlCommand curl = http2curl(request);
options.getCurlUpdater().ifPresent(updater -> updater.accept(curl));
return curl
.asString(options.getTargetPlatform(),
options.useShortForm(),
options.printMultiliner(),
options.escapeNonAscii());
}
private static class Headers {
List toProcess;
Set ignored;
public Headers(List toProcess) {
this.toProcess = toProcess;
this.ignored = new HashSet<>();
}
}
@SuppressWarnings("deprecation")
private CurlCommand http2curl(HttpRequest request)
throws NoSuchFieldException, IllegalAccessException, IOException {
Headers headers = new Headers(Arrays.asList(request.getAllHeaders()));
CurlCommand curl = new CurlCommand();
String inferredUri = inferUri(request);
curl.setUrl(inferredUri);
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest requestWithEntity = (HttpEntityEnclosingRequest) request;
try {
HttpEntity entity = requestWithEntity.getEntity();
if (entity != null) {
Optional maybeRequestContentType = tryGetHeaderValue(headers.toProcess,
"Content-Type");
String contentType = maybeRequestContentType
.orElseThrow(() -> new IllegalStateException("Missing Content-Type header"));
handleEntity(entity, contentType, headers, curl);
}
} catch (IOException e) {
log.error("Failed to consume form data (entity) from HTTP request", e);
throw e;
}
}
String requestMethod = request.getRequestLine().getMethod();
if ("GET".equals(requestMethod)) {
// skip
} else if ("POST".equals(requestMethod) && curl.hasData()) {
// skip
} else {
curl.setMethod(requestMethod);
}
headers.toProcess = handleAuthenticationHeader(headers.toProcess, curl);
List cookiesHeaders = headers.toProcess.stream()
.filter(h -> h.getName().equals("Cookie"))
.collect(Collectors.toList());
if (cookiesHeaders.size() == 1) {
curl.setCookieHeader(cookiesHeaders.get(0).getValue());
headers.toProcess = headers.toProcess.stream().filter(h -> !h.getName().equals("Cookie"))
.collect(Collectors.toList());
} else if (cookiesHeaders.size() > 1) {
// RFC 6265: When the user agent generates an HTTP request, the user agent MUST NOT attach
// more than one Cookie header field.
log.warn("More than one Cookie header in HTTP Request not allowed by RFC 6265");
}
handleNotIgnoredHeaders(headers, curl);
curl.setCompressed(true);
curl.setInsecure(true);
curl.setVerbose(true);
return curl;
}
// The method updates headers and curl arguments
private void handleEntity(HttpEntity entity,
String contentType,
Headers headers,
CurlCommand curl) throws IOException {
List parameters = Arrays.asList(contentType.split(";"));
parameters = parameters.stream().map(s -> s.trim()).collect(Collectors.toList());
contentType = parameters.remove(0);
headers.ignored.add("Content-Length");
switch (contentType) {
case "multipart/form-data":
headers.ignored.add("Content-Type"); // let curl command decide
handleMultipartEntity(entity, curl);
break;
case "multipart/mixed":
// Removing header
headers.toProcess = filterOutHeader(headers.toProcess, "Content-Type");
headers.toProcess.add(new BasicHeader("Content-Type", "multipart/mixed"));
handleMultipartEntity(entity, curl);
break;
default:
String data = EntityUtils.toString(entity);
curl.addDataBinary(data);
}
}
private List filterOutHeader(List headers, String s) {
return headers.stream().filter(h -> !h.getName().equals(s))
.collect(Collectors.toList());
}
private String inferUri(HttpRequest request) {
String inferredUri = request.getRequestLine().getUri();
if (!isValidUrl(inferredUri)) { // Missing schema and domain name
String host = getHost(request);
String inferredScheme = "http";
if (host.endsWith(":443")) {
inferredScheme = "https";
} else if ((request instanceof RequestWrapper) || (request instanceof HttpRequestWrapper)) {
if (getOriginalRequestUri(request).startsWith("https")) {
// This is for original URL, so if during redirects we go out of HTTPs, this might be a wrong guess
inferredScheme = "https";
}
}
if ("CONNECT".equals(request.getRequestLine().getMethod())) {
inferredUri = String.format("%s://%s", inferredScheme, host);
} else {
inferredUri =
String.format("%s://%s/%s", inferredScheme, host, inferredUri)
.replaceAll("(? bodyParts = (List) getFieldValue(multipartEntityBuilder,
"bodyParts");
bodyParts.forEach(p -> handlePart(p, curl));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private void handlePart(FormBodyPart bodyPart, CurlCommand curl) {
String contentDisposition = bodyPart.getHeader().getFields().stream()
.filter(f -> f.getName().equals("Content-Disposition"))
.findFirst()
.orElseThrow(() -> new RuntimeException("Multipart missing Content-Disposition header"))
.getBody();
List elements = Arrays.asList(contentDisposition.split(";"));
Map map = elements.stream().map(s -> s.trim().split("="))
.collect(Collectors.toMap(a -> a[0], a -> a.length == 2 ? a[1] : ""));
if (map.containsKey("form-data")) {
String partName = removeQuotes(map.get("name"));
StringBuilder partContent = new StringBuilder();
if (map.get("filename") != null) {
partContent.append("@").append(removeQuotes(map.get("filename")));
} else {
try {
partContent.append(getContent(bodyPart));
} catch (IOException e) {
throw new RuntimeException("Could not read content of the part", e);
}
}
partContent.append(";type=").append(bodyPart.getHeader().getField("Content-Type").getBody());
curl.addFormPart(partName, partContent.toString());
} else {
throw new RuntimeException("Unsupported type " + map.entrySet().stream().findFirst().get());
}
}
private void handleNotIgnoredHeaders(Headers headers, CurlCommand curl) {
headers.toProcess
.stream()
.filter(h -> !headers.ignored.contains(h.getName()))
.forEach(h -> curl.addHeader(h.getName(), h.getValue()));
}
private List handleAuthenticationHeader(List headers, CurlCommand curl) {
List remainingHeaders = new ArrayList<>(headers);
Iterator it = remainingHeaders.iterator();
while (it.hasNext()) {
Header h = it.next();
if (isBasicAuthentication(h)) {
try {
String credentials = h.getValue().replaceAll("Basic ", "");
String decodedCredentials = new String(Base64.getDecoder().decode(credentials));
String[] userAndPassword = decodedCredentials.split(":", -1);
curl.setServerAuthentication(userAndPassword[0], userAndPassword[1]);
it.remove();
break; // There can be only one authentication headers
} catch (IllegalArgumentException | IndexOutOfBoundsException e) {
log.warn("This is not valid Basic authentication header: {}", h.getValue());
}
}
}
return remainingHeaders;
}
}