org.parosproxy.paros.network.HttpRequestHeader Maven / Gradle / Ivy
Show all versions of zap Show documentation
/*
* Created on Jun 14, 2004
*
* Paros and its related class files.
*
* Paros is an HTTP/HTTPS proxy for assessing web application security.
* Copyright (C) 2003-2004 Chinotec Technologies Company
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Clarified Artistic License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Clarified Artistic License for more details.
*
* You should have received a copy of the Clarified Artistic License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
// ZAP: 2012:02/01 Changed getHostPort() to return proper port number even if it
// is not explicitly specified in URI
// ZAP: 2011/08/04 Changed to support Logging
// ZAP: 2011/10/29 Log errors
// ZAP: 2011/11/03 Changed isImage() to prevent a NullPointerException when the path doesn't exist
// ZAP: 2011/12/09 Changed HttpRequestHeader(String method, URI uri, String version) to add
// the Cache-Control header field when the HTTP version is 1.1 and changed a if condition to
// validate the variable version instead of the variable method.
// ZAP: 2012/03/15 Changed to use the class StringBuilder instead of StringBuffer. Reworked some methods.
// ZAP: 2012/04/23 Added @Override annotation to all appropriate methods.
// ZAP: 2012/06/24 Added method to add Cookies of type java.net.HttpCookie to request header
// ZAP: 2012/06/24 Added new method of getting cookies from the request header.
// ZAP: 2012/10/08 Issue 361: getHostPort on HttpRequestHeader for HTTPS CONNECT
// requests returns the wrong port
// ZAP: 2013/01/23 Clean up of exception handling/logging.
// ZAP: 2013/03/08 Improved parse error reporting
// ZAP: 2013/04/14 Issue 596: Rename the method HttpRequestHeader.getSecure to isSecure
// ZAP: 2013/05/02 Re-arranged all modifiers into Java coding standard order
// ZAP: 2013/12/09 Set Content-type only in case of POST or PUT HTTP methods
// ZAP: 2015/08/07 Issue 1768: Update to use a more recent default user agent
// ZAP: 2016/06/17 Remove redundant initialisations of instance variables
// ZAP: 2016/09/26 JavaDoc tweaks
// ZAP: 2017/02/23 Issue 3227: Limit API access to whitelisted IP addresses
// ZAP: 2017/04/24 Added more HTTP methods
// ZAP: 2017/10/19 Skip parsing of empty Cookie headers.
// ZAP: 2017/11/22 Address a NPE in isImage().
package org.parosproxy.paros.network;
import java.io.UnsupportedEncodingException;
import java.net.HttpCookie;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.log4j.Logger;
public class HttpRequestHeader extends HttpHeader {
private static final long serialVersionUID = 4156598327921777493L;
private static final Logger log = Logger.getLogger(HttpRequestHeader.class);
// method list
public static final String CONNECT = "CONNECT";
public static final String DELETE = "DELETE";
public static final String GET = "GET";
public static final String HEAD = "HEAD";
public static final String OPTIONS = "OPTIONS";
public static final String PATCH = "PATCH";
public static final String POST = "POST";
public static final String PUT = "PUT";
public static final String TRACE = "TRACE";
public static final String TRACK = "TRACK";
// ZAP: Added method array
public static final String[] METHODS = {
CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, TRACK
};
public static final String HOST = "Host";
private static final Pattern patternRequestLine = Pattern.compile(p_METHOD + p_SP + p_URI + p_SP + p_VERSION, Pattern.CASE_INSENSITIVE);
// private static final Pattern patternHostHeader
// = Pattern.compile("([^:]+)\\s*?:?\\s*?(\\d*?)");
private static final Pattern patternImage = Pattern.compile("\\.(bmp|ico|jpg|jpeg|gif|tiff|tif|png)\\z", Pattern.CASE_INSENSITIVE);
private static final Pattern patternPartialRequestLine = Pattern.compile("\\A *(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT)\\b", Pattern.CASE_INSENSITIVE);
private String mMethod;
private URI mUri;
private String mHostName;
private InetAddress senderAddress;
/**
* The host port number of this request message, a non-negative integer.
*
* Default is {@code 80}.
*
* Note: All the modifications to the instance variable
* {@code mHostPort} must be done through the method
* {@code setHostPort(int)}, so a valid and correct value is set when no
* port number is defined (which is represented with the negative integer
* -1).
*
* @see #getHostPort()
* @see #setHostPort(int)
* @see URI#getPort()
*/
private int mHostPort;
private boolean mIsSecure;
/**
* Constructor for an empty header.
*
*/
public HttpRequestHeader() {
super();
mMethod = "";
mHostName = "";
mHostPort = 80;
}
/**
* Constructor of a request header with the string.
*
* @param data the request header
* @param isSecure {@code true} if the request should be secure, {@code false} otherwise
* @throws HttpMalformedHeaderException if the request being set is malformed
* @see #setSecure(boolean)
*/
public HttpRequestHeader(String data, boolean isSecure) throws HttpMalformedHeaderException {
setMessage(data, isSecure);
}
/**
* Constructor of a request header with the string. Whether this is a secure
* header depends on the URL given.
*
* @param data the request header
* @throws HttpMalformedHeaderException if the request being set is malformed
*/
public HttpRequestHeader(String data) throws HttpMalformedHeaderException {
setMessage(data);
}
@Override
public void clear() {
super.clear();
mMethod = "";
mUri = null;
mHostName = "";
setHostPort(-1);
}
public HttpRequestHeader(String method, URI uri, String version) throws HttpMalformedHeaderException {
this(method, uri, version, null);
}
public HttpRequestHeader(String method, URI uri, String version,
ConnectionParam params) throws HttpMalformedHeaderException {
this(method + " " + uri.toString() + " " + version.toUpperCase() + CRLF + CRLF);
try {
setHeader(HOST, uri.getHost() + (uri.getPort() > 0 ? ":" + Integer.toString(uri.getPort()) : ""));
} catch (URIException e) {
log.error(e.getMessage(), e);
}
String userAgent = params != null
? params.getDefaultUserAgent()
: "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0;)";
setHeader(USER_AGENT, userAgent);
setHeader(PRAGMA, "no-cache");
// ZAP: added the Cache-Control header field to comply with HTTP/1.1
if (version.equalsIgnoreCase(HTTP11)) {
setHeader(CACHE_CONTROL, "no-cache");
}
// ZAP: set content type x-www-urlencoded only if it's a POST or a PUT
if (method.equalsIgnoreCase(POST) || method.equalsIgnoreCase(PUT)) {
setHeader(CONTENT_TYPE, "application/x-www-form-urlencoded");
}
setHeader(ACCEPT_ENCODING, null);
// ZAP: changed from method to version
if (version.equalsIgnoreCase(HTTP11)) {
setContentLength(0);
}
}
/**
* Set this request header with the given message.
*
* @param data the request header
* @param isSecure {@code true} if the request should be secure, {@code false} otherwise
* @throws HttpMalformedHeaderException if the request being set is malformed
* @see #setSecure(boolean)
*/
public void setMessage(String data, boolean isSecure) throws HttpMalformedHeaderException {
super.setMessage(data);
try {
parse(isSecure);
} catch (HttpMalformedHeaderException e) {
mMalformedHeader = true;
if (log.isDebugEnabled()) {
log.debug("Malformed header: " + data, e);
}
throw e;
} catch (Exception e) {
log.error(e.getMessage(), e);
mMalformedHeader = true;
throw new HttpMalformedHeaderException(e.getMessage());
}
}
/**
* Set this request header with the given message. Whether this is a secure
* header depends on the URL given.
*/
@Override
public void setMessage(String data) throws HttpMalformedHeaderException {
this.setMessage(data, false);
}
/**
* Get the HTTP method (GET, POST ... etc).
*
* @return the request method
*/
public String getMethod() {
return mMethod;
}
/**
* Set the HTTP method of this request header.
*
* @param method the new method, must not be {@code null}.
*/
public void setMethod(String method) {
mMethod = method.toUpperCase();
}
/**
* Get the URI of this request header.
*
* @return the request URI
*/
public URI getURI() {
return mUri;
}
/**
* Sets the URI of this request header.
*
* @param uri the new request URI
* @throws URIException if an error occurred while setting the request URI
*/
public void setURI(URI uri) throws URIException {
if (uri.getScheme() == null || uri.getScheme().equals("")) {
mUri = new URI(HTTP + "://" + getHeader(HOST) + "/" + mUri.toString(), true);
} else {
mUri = uri;
}
if (uri.getScheme().equalsIgnoreCase(HTTPS)) {
mIsSecure = true;
} else {
mIsSecure = false;
}
setHostPort(mUri.getPort());
}
/**
* Get if this request header is under secure connection.
*
* @return {@code true} if the request is secure, {@code false} otherwise
* @deprecated Replaced by {@link #isSecure()}. It will be removed in a
* future release.
*/
@Deprecated
public boolean getSecure() {
return mIsSecure;
}
/**
* Tells whether the request is secure, or not. A request is considered
* secure if it's using the HTTPS protocol.
*
* @return {@code true} if the request is secure, {@code false} otherwise.
*/
public boolean isSecure() {
return mIsSecure;
}
/**
* Sets whether or not the request is done using a secure scheme, HTTPS.
*
* @param isSecure {@code true} if the request should be secure, {@code false} otherwise
* @throws URIException if an error occurred while rebuilding the request URI
*/
public void setSecure(boolean isSecure) throws URIException {
mIsSecure = isSecure;
if (mUri == null) {
// mUri not yet set
return;
}
URI newUri = mUri;
// check if URI consistent
if (isSecure() && mUri.getScheme().equalsIgnoreCase(HTTP)) {
newUri = new URI(mUri.toString().replaceFirst(HTTP, HTTPS), true);
} else if (!isSecure() && mUri.getScheme().equalsIgnoreCase(HTTPS)) {
newUri = new URI(mUri.toString().replaceFirst(HTTPS, HTTP), true);
}
if (newUri != mUri) {
mUri = newUri;
setHostPort(mUri.getPort());
}
}
/**
* Set the HTTP version of this request header.
*/
@Override
public void setVersion(String version) {
mVersion = version.toUpperCase();
}
/**
* Get the content length in this request header. If the content length is
* undetermined, 0 will be returned.
*/
@Override
public int getContentLength() {
if (mContentLength == -1) {
return 0;
}
return mContentLength;
}
/**
* Parse this request header.
*
* @param isSecure {@code true} if the request is secure, {@code false} otherwise
* @throws URIException if failed to parse the URI
* @throws HttpMalformedHeaderException if the request being parsed is malformed
*/
private void parse(boolean isSecure) throws URIException, HttpMalformedHeaderException {
mIsSecure = isSecure;
Matcher matcher = patternRequestLine.matcher(mStartLine);
if (!matcher.find()) {
mMalformedHeader = true;
throw new HttpMalformedHeaderException("Failed to find pattern: " + patternRequestLine);
}
mMethod = matcher.group(1);
String sUri = matcher.group(2);
mVersion = matcher.group(3);
if (!mVersion.equalsIgnoreCase(HTTP09) && !mVersion.equalsIgnoreCase(HTTP10) && !mVersion.equalsIgnoreCase(HTTP11)) {
mMalformedHeader = true;
throw new HttpMalformedHeaderException("Unexpected version: " + mVersion);
}
mUri = parseURI(sUri);
if (mUri.getScheme() == null || mUri.getScheme().equals("")) {
mUri = new URI(HTTP + "://" + getHeader(HOST) + mUri.toString(), true);
}
if (isSecure() && mUri.getScheme().equalsIgnoreCase(HTTP)) {
mUri = new URI(mUri.toString().replaceFirst(HTTP, HTTPS), true);
}
if (mUri.getScheme().equalsIgnoreCase(HTTPS)) {
setSecure(true);
}
String hostHeader;
if (mMethod.equalsIgnoreCase(CONNECT)) {
hostHeader = sUri;
parseHostName(hostHeader);
} else {
mHostName = mUri.getHost();
setHostPort(mUri.getPort());
}
}
private void parseHostName(String hostHeader) {
// no host header given but a valid host name already exist.
if (hostHeader == null) {
return;
}
int port = -1;
int pos;
if ((pos = hostHeader.indexOf(':', 2)) > -1) {
mHostName = hostHeader.substring(0, pos).trim();
try {
port = Integer.parseInt(hostHeader.substring(pos + 1));
} catch (NumberFormatException e) {
}
} else {
mHostName = hostHeader.trim();
}
setHostPort(port);
}
/**
* Get the host name in this request header.
*
* @return Host name.
*/
public String getHostName() {
String hostName = mHostName;
try {
// ZAP: fixed cases, where host name is null
hostName = ((mUri.getHost() != null) ? mUri.getHost() : mHostName);
} catch (URIException e) {
if (log.isDebugEnabled()) {
log.warn(e);
}
}
return hostName;
}
/**
* Gets the host port number of this request message, a non-negative
* integer.
*
* If no port is defined the default port for the used scheme will be
* returned, either 80 for HTTP or 443 for HTTPS.
*
*
* @return the host port number, a non-negative integer
*/
public int getHostPort() {
return mHostPort;
}
/**
* Sets the host port number of this request message.
*
* If the given {@code port} number is negative (usually -1 to represent
* that no port number is defined), the port number set will be the default
* port number for the used scheme known using the method
* {@code isSecure()}, either 80 for HTTP or 443 for HTTPS.
*
*
* @param port the new port number
* @see #mHostPort
* @see #isSecure()
* @see URI#getPort()
*/
private void setHostPort(int port) {
if (port > -1) {
mHostPort = port;
} else if (this.isSecure()) {
mHostPort = 443;
} else {
mHostPort = 80;
}
}
/**
* Return if this request header is a image request basing on the path
* suffix.
*/
@Override
public boolean isImage() {
if (getURI() == null) {
return false;
}
try {
// ZAP: prevents a NullPointerException when no path exists
final String path = getURI().getPath();
if (path != null) {
return (patternImage.matcher(path).find());
}
} catch (URIException e) {
log.error(e.getMessage(), e);
}
return false;
}
/**
* Return if the data given is a request header basing on the first start
* line.
*
* @param data the data to be checked
* @return {@code true} if the data contains a request line, {@code false} otherwise.
*/
public static boolean isRequestLine(String data) {
return patternPartialRequestLine.matcher(data).find();
}
/**
* Return the prime header (first line).
*/
@Override
public String getPrimeHeader() {
return getMethod() + " " + getURI().toString() + " " + getVersion();
}
/*
* private static final char[] DELIM_UNWISE_CHAR = { '<', '>', '#', '"', '
* ', '{', '}', '|', '\\', '^', '[', ']', '`' };
*/
private static final String DELIM = "<>#\"";
private static final String UNWISE = "{}|\\^[]`";
private static final String DELIM_UNWISE = DELIM + UNWISE;
public static URI parseURI(String sUri) throws URIException {
URI uri;
int len = sUri.length();
StringBuilder sb = new StringBuilder(len);
char[] charray = new char[1];
String s;
for (int i = 0; i < len; i++) {
char ch = sUri.charAt(i);
//String ch = sUri.substring(i, i+1);
if (DELIM_UNWISE.indexOf(ch) >= 0) {
// check if unwise or delim in RFC. If so, encode it.
charray[0] = ch;
s = new String(charray);
try {
s = URLEncoder.encode(s, "UTF8");
} catch (UnsupportedEncodingException e1) {
}
sb.append(s);
} else if (ch == '%') {
// % is exception - no encoding to be done because some server may not handle
// correctly when % is invalid.
//
//sb.append(ch);
// if % followed by hex, no encode.
try {
String hex = sUri.substring(i + 1, i + 3);
Integer.parseInt(hex, 16);
sb.append(ch);
} catch (Exception e) {
charray[0] = ch;
s = new String(charray);
try {
s = URLEncoder.encode(s, "UTF8");
} catch (UnsupportedEncodingException e1) {
}
sb.append(s);
}
} else if (ch == ' ') {
// if URLencode, '+' will be appended.
sb.append("%20");
} else {
sb.append(ch);
}
}
uri = new URI(sb.toString(), true);
return uri;
}
// Construct new GET url of request
// Based on getParams
public void setGetParams(TreeSet getParams) {
if (mUri == null) {
return;
}
if (getParams.isEmpty()) {
try {
mUri.setQuery("");
} catch (URIException e) {
log.error(e.getMessage(), e);
}
return;
}
StringBuilder sbQuery = new StringBuilder();
for (HtmlParameter parameter : getParams) {
if (parameter.getType() != HtmlParameter.Type.url) {
continue;
}
sbQuery.append(parameter.getName());
sbQuery.append('=');
sbQuery.append(parameter.getValue());
sbQuery.append('&');
}
if (sbQuery.length() <= 2) {
try {
mUri.setQuery("");
} catch (URIException e) {
log.error(e.getMessage(), e);
}
return;
}
String query = sbQuery.substring(0, sbQuery.length() - 1);
try {
//The previous behaviour was escaping the query,
//so it is maintained with the use of setQuery.
mUri.setQuery(query);
} catch (URIException e) {
log.error(e.getMessage(), e);
}
}
/**
* Construct new "Cookie:" line in request header based on HttpCookies.
*
* @param cookies the new cookies
*/
public void setCookies(List cookies) {
if (cookies.isEmpty()) {
setHeader(HttpHeader.COOKIE, null);
}
StringBuilder sbData = new StringBuilder();
for (HttpCookie c : cookies) {
sbData.append(c.getName());
sbData.append('=');
sbData.append(c.getValue());
sbData.append("; ");
}
if (sbData.length() <= 3) {
setHeader(HttpHeader.COOKIE, null);
return;
}
final String data = sbData.substring(0, sbData.length() - 2);
setHeader(HttpHeader.COOKIE, data);
}
// Construct new "Cookie:" line in request header,
// based on cookieParams
public void setCookieParams(TreeSet cookieParams) {
if (cookieParams.isEmpty()) {
setHeader(HttpHeader.COOKIE, null);
}
StringBuilder sbData = new StringBuilder();
for (HtmlParameter parameter : cookieParams) {
if (parameter.getType() != HtmlParameter.Type.cookie) {
continue;
}
sbData.append(parameter.getName());
sbData.append('=');
sbData.append(parameter.getValue());
sbData.append("; ");
}
if (sbData.length() <= 3) {
setHeader(HttpHeader.COOKIE, null);
return;
}
final String data = sbData.substring(0, sbData.length() - 2);
setHeader(HttpHeader.COOKIE, data);
}
public TreeSet getCookieParams() {
TreeSet set = new TreeSet<>();
Vector cookieLines = getHeaders(HttpHeader.COOKIE);
if (cookieLines != null) {
for (String cookieLine : cookieLines) {
//watch out for the scenario where the first cookie name starts with "cookie" (uppercase or lowercase)
if (cookieLine.toUpperCase().startsWith(HttpHeader.COOKIE.toUpperCase() + ":")) {
// HttpCookie wont parse lines starting with "Cookie:"
cookieLine = cookieLine.substring(HttpHeader.COOKIE.length() + 1);
}
if (cookieLine.isEmpty()) {
// Nothing to parse.
continue;
}
// These can be comma separated type=value
String[] cookieArray = cookieLine.split(";");
for (String cookie : cookieArray) {
set.add(new HtmlParameter(cookie));
}
}
}
return set;
}
// ZAP: Added method for working directly with HttpCookie
/**
* Gets a list of the http cookies from this request Header.
*
* @return the http cookies
* @throws IllegalArgumentException if a problem is encountered while
* processing the "Cookie: " header line.
*/
public List getHttpCookies() {
List cookies = new LinkedList<>();
// Use getCookieParams to reduce the places we parse cookies
TreeSet ts = getCookieParams();
Iterator it = ts.iterator();
while (it.hasNext()) {
HtmlParameter htmlParameter = it.next();
if (!htmlParameter.getName().isEmpty()) {
try {
cookies.add(new HttpCookie(htmlParameter.getName(), htmlParameter.getValue()));
} catch (IllegalArgumentException e) {
// Occurs while scanning ;)
log.debug(e.getMessage() + " " + htmlParameter.getName());
}
}
}
return cookies;
}
/**
* Sets the senders IP address. Note that this is not persisted.
* @param inetAddress the senders IP address
* @since 2.6.0
*/
public void setSenderAddress(InetAddress inetAddress) {
this.senderAddress = inetAddress;
}
/**
* Gets the senders IP address
* @return the senders IP address
* @since 2.6.0
*/
public InetAddress getSenderAddress() {
return senderAddress;
}
}