/* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008, 2009 Anthony Ricaud <[email protected]> * Copyright (C) 2011 Google Inc. All rights reserved. * Copyright (C) 2016 Maciej Gawinecki <[email protected]> * * 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<String> tryGetHeaderValue(List<Header> headers, String headerName) { return headers .stream() .filter(h -> h.getName().equals(headerName)) .map(Header::getValue) .findFirst(); } private static <T> 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<Header> toProcess; Set<String> ignored; public Headers(List<Header> 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<String> 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<Header> 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<String> 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<Header> filterOutHeader(List<Header> 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("(?<!http(s)?:)//", "/"); } } return inferredUri; } private void handleMultipartEntity(HttpEntity entity, CurlCommand curl) { try { HttpEntity wrappedEntity = (HttpEntity) getFieldValue(entity, "wrappedEntity"); RestAssuredMultiPartEntity multiPartEntity = (RestAssuredMultiPartEntity) wrappedEntity; MultipartEntityBuilder multipartEntityBuilder = (MultipartEntityBuilder) getFieldValue( multiPartEntity, "builder"); @SuppressWarnings("unchecked") List<FormBodyPart> bodyParts = (List<FormBodyPart>) 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<String> elements = Arrays.asList(contentDisposition.split(";")); Map<String, String> 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<Header> handleAuthenticationHeader(List<Header> headers, CurlCommand curl) { List<Header> remainingHeaders = new ArrayList<>(headers); Iterator<Header> 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; } }