/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 * * http://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.apache.jmeter.protocol.http.proxy; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import org.apache.commons.lang3.CharUtils; import org.apache.jmeter.protocol.http.config.MultipartUrlConfig; import org.apache.jmeter.protocol.http.control.Header; import org.apache.jmeter.protocol.http.control.HeaderManager; import org.apache.jmeter.protocol.http.gui.HeaderPanel; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory; import org.apache.jmeter.protocol.http.util.ConversionUtils; import org.apache.jmeter.protocol.http.util.HTTPConstants; import org.apache.jmeter.testelement.TestElement; import org.apache.jmeter.util.JMeterUtils; import org.slf4j.LoggerFactory; import org.slf4j.Logger; //For unit tests, @see TestHttpRequestHdr /** * The headers of the client HTTP request. * */ public class HttpRequestHdr { private static final Logger log = LoggerFactory.getLogger(HttpRequestHdr.class); private static final String HTTP = "http"; // $NON-NLS-1$ private static final String HTTPS = "https"; // $NON-NLS-1$ private static final String PROXY_CONNECTION = "proxy-connection"; // $NON-NLS-1$ public static final String CONTENT_TYPE = "content-type"; // $NON-NLS-1$ public static final String CONTENT_LENGTH = "content-length"; // $NON-NLS-1$ /** * Http Request method, uppercased, e.g. GET or POST. */ private String method = ""; // $NON-NLS-1$ /** CONNECT url. */ private String paramHttps = ""; // $NON-NLS-1$ /** * The requested url. The universal resource locator that hopefully uniquely * describes the object or service the client is requesting. */ private String url = ""; // $NON-NLS-1$ /** * Version of http being used. Such as HTTP/1.0. */ private String version = ""; // NOTREAD // $NON-NLS-1$ private byte[] rawPostData; private final Map<String, Header> headers = new HashMap<>(); private final String httpSamplerName; private HeaderManager headerManager; private String firstLine; // saved copy of first line for error reports public HttpRequestHdr() { this.httpSamplerName = ""; // $NON-NLS-1$ this.firstLine = "" ; // $NON-NLS-1$ } /** * @param httpSamplerName the http sampler name */ public HttpRequestHdr(String httpSamplerName) { this.httpSamplerName = httpSamplerName; } /** * Parses a http header from a stream. * * @param in * the stream to parse. * @return array of bytes from client. * @throws IOException when reading the input stream fails */ public byte[] parse(InputStream in) throws IOException { boolean inHeaders = true; int readLength = 0; int dataLength = 0; boolean firstLine = true; ByteArrayOutputStream clientRequest = new ByteArrayOutputStream(); ByteArrayOutputStream line = new ByteArrayOutputStream(); int x; while ((inHeaders || readLength < dataLength) && ((x = in.read()) != -1)) { line.write(x); clientRequest.write(x); if (firstLine && !CharUtils.isAscii((char) x)){// includes \n throw new IllegalArgumentException("Only ASCII supported in headers (perhaps SSL was used?)"); } if (inHeaders && (byte) x == (byte) '\n') { // $NON-NLS-1$ if (line.size() < 3) { inHeaders = false; firstLine = false; // cannot be first line either } final String reqLine = line.toString(); if (firstLine) { parseFirstLine(reqLine); firstLine = false; } else { // parse other header lines, looking for Content-Length final int contentLen = parseLine(reqLine); if (contentLen > 0) { dataLength = contentLen; // Save the last valid content length one } } if (log.isDebugEnabled()){ log.debug("Client Request Line: '" + reqLine.replaceFirst("\r\n$", "<CRLF>") + "'"); } line.reset(); } else if (!inHeaders) { readLength++; } } // Keep the raw post data rawPostData = line.toByteArray(); if (log.isDebugEnabled()){ log.debug("rawPostData in default JRE encoding: " + new String(rawPostData)); // TODO - charset? log.debug("Request: '" + clientRequest.toString().replaceAll("\r\n", "<CRLF>") + "'"); } return clientRequest.toByteArray(); } private void parseFirstLine(String firstLine) { this.firstLine = firstLine; if (log.isDebugEnabled()) { log.debug("browser request: " + firstLine.replaceFirst("\r\n$", "<CRLF>")); } StringTokenizer tz = new StringTokenizer(firstLine); method = getToken(tz).toUpperCase(java.util.Locale.ENGLISH); url = getToken(tz); version = getToken(tz); if (log.isDebugEnabled()) { log.debug("parsed method: " + method); log.debug("parsed url/host: " + url); // will be host:port for CONNECT log.debug("parsed version: " + version); } // SSL connection if (getMethod().startsWith(HTTPConstants.CONNECT)) { paramHttps = url; return; // Don't try to adjust the host name } /* The next line looks odd, but proxied HTTP requests look like: * GET http://www.apache.org/foundation/ HTTP/1.1 * i.e. url starts with "http:", not "/" * whereas HTTPS proxy requests look like: * CONNECT www.google.co.uk:443 HTTP/1.1 * followed by * GET /?gws_rd=cr HTTP/1.1 */ if (url.startsWith("/")) { // it must be a proxied HTTPS request url = HTTPS + "://" + paramHttps + url; // $NON-NLS-1$ } // JAVA Impl accepts URLs with unsafe characters so don't do anything if(HTTPSamplerFactory.IMPL_JAVA.equals(httpSamplerName)) { log.debug("First Line url: " + url); return; } try { // See Bug 54482 URI testCleanUri = new URI(url); if(log.isDebugEnabled()) { log.debug("Successfully built URI from url:"+url+" => " + testCleanUri.toString()); } } catch (URISyntaxException e) { log.warn("Url '" + url + "' contains unsafe characters, will escape it, message:"+e.getMessage()); try { String escapedUrl = ConversionUtils.escapeIllegalURLCharacters(url); if(log.isDebugEnabled()) { log.debug("Successfully escaped url:'"+url +"' to:'"+escapedUrl+"'"); } url = escapedUrl; } catch (Exception e1) { log.error("Error escaping URL:'"+url+"', message:"+e1.getMessage()); } } log.debug("First Line url: " + url); } /* * Split line into name/value pairs and store in headers if relevant * If name = "content-length", then return value as int, else return 0 */ private int parseLine(String nextLine) { int colon = nextLine.indexOf(':'); if (colon <= 0){ return 0; // Nothing to do } String name = nextLine.substring(0, colon).trim(); String value = nextLine.substring(colon+1).trim(); headers.put(name.toLowerCase(java.util.Locale.ENGLISH), new Header(name, value)); if (name.equalsIgnoreCase(CONTENT_LENGTH)) { return Integer.parseInt(value); } return 0; } private HeaderManager createHeaderManager() { HeaderManager manager = new HeaderManager(); for (Map.Entry<String, Header> entry : headers.entrySet()) { final String key = entry.getKey(); if (!key.equals(PROXY_CONNECTION) && !key.equals(CONTENT_LENGTH) && !key.equalsIgnoreCase(HTTPConstants.HEADER_CONNECTION)) { manager.add(entry.getValue()); } } manager.setName(JMeterUtils.getResString("header_manager_title")); // $NON-NLS-1$ manager.setProperty(TestElement.TEST_CLASS, HeaderManager.class.getName()); manager.setProperty(TestElement.GUI_CLASS, HeaderPanel.class.getName()); return manager; } public HeaderManager getHeaderManager() { if(headerManager == null) { headerManager = createHeaderManager(); } return headerManager; } public String getContentType() { Header contentTypeHeader = headers.get(CONTENT_TYPE); if (contentTypeHeader != null) { return contentTypeHeader.getValue(); } return null; } private boolean isMultipart(String contentType) { if (contentType != null && contentType.startsWith(HTTPConstants.MULTIPART_FORM_DATA)) { return true; } return false; } public MultipartUrlConfig getMultipartConfig(String contentType) { if(isMultipart(contentType)) { // Get the boundary string for the multiparts from the content type String boundaryString = contentType.substring(contentType.toLowerCase(java.util.Locale.ENGLISH).indexOf("boundary=") + "boundary=".length()); return new MultipartUrlConfig(boundaryString); } return null; } // // Parsing Methods // /** * Find the //server.name from an url. * * @return server's internet name */ public String serverName() { // chop to "server.name:x/thing" String str = url; int i = str.indexOf("//"); // $NON-NLS-1$ if (i > 0) { str = str.substring(i + 2); } // chop to server.name:xx i = str.indexOf('/'); // $NON-NLS-1$ if (0 < i) { str = str.substring(0, i); } // chop to server.name i = str.lastIndexOf(':'); // $NON-NLS-1$ if (0 < i) { str = str.substring(0, i); } // Handle IPv6 urls if(str.startsWith("[")&& str.endsWith("]")) { return str.substring(1, str.length()-1); } return str; } // TODO replace repeated substr() above and below with more efficient method. /** * Find the :PORT from http://server.ect:PORT/some/file.xxx * * @return server's port (or UNSPECIFIED if not found) */ public int serverPort() { String str = url; // chop to "server.name:x/thing" int i = str.indexOf("//"); if (i > 0) { str = str.substring(i + 2); } // chop to server.name:xx i = str.indexOf('/'); if (0 < i) { str = str.substring(0, i); } // chop to server.name i = str.lastIndexOf(':'); if (0 < i) { return Integer.parseInt(str.substring(i + 1).trim()); } return HTTPSamplerBase.UNSPECIFIED_PORT; } /** * Find the /some/file.xxxx from http://server.ect:PORT/some/file.xxx * * @return the path */ public String getPath() { String str = url; int i = str.indexOf("//"); if (i > 0) { str = str.substring(i + 2); } i = str.indexOf('/'); if (i < 0) { return ""; } return str.substring(i); } /** * Returns the url string extracted from the first line of the client request. * * @return the url */ public String getUrl(){ return url; } /** * Returns the method string extracted from the first line of the client request. * * @return the method (will always be upper case) */ public String getMethod(){ return method; } public String getFirstLine() { return firstLine; } /** * Returns the next token in a string. * * @param tk * String that is partially tokenized. * @return The remainder */ private String getToken(StringTokenizer tk) { if (tk.hasMoreTokens()) { return tk.nextToken(); } return "";// $NON-NLS-1$ } public String getUrlWithoutQuery(URL _url) { String fullUrl = _url.toString(); String urlWithoutQuery = fullUrl; String query = _url.getQuery(); if(query != null) { // Get rid of the query and the ? urlWithoutQuery = urlWithoutQuery.substring(0, urlWithoutQuery.length() - query.length() - 1); } return urlWithoutQuery; } /** * @return the httpSamplerName */ public String getHttpSamplerName() { return httpSamplerName; } /** * @return byte[] Raw post data */ public byte[] getRawPostData() { return rawPostData; } /** * @param sampler {@link HTTPSamplerBase} * @return String Protocol (http or https) */ public String getProtocol(HTTPSamplerBase sampler) { if (url.contains("//")) { String protocol = url.substring(0, url.indexOf(':')); if (log.isDebugEnabled()) { log.debug("Proxy: setting protocol to : " + protocol); } return protocol; } else if (sampler.getPort() == HTTPConstants.DEFAULT_HTTPS_PORT) { if (log.isDebugEnabled()) { log.debug("Proxy: setting protocol to https"); } return HTTPS; } else { if (log.isDebugEnabled()) { log.debug("Proxy setting default protocol to: http"); } return HTTP; } } }