/* GNU GENERAL LICENSE Copyright (C) 2006 The Lobo Project. Copyright (C) 2014 - 2017 Lobo Evolution This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either verion 3 of the License, or (at your option) any later version. 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 GNU General License for more details. You should have received a copy of the GNU General Public along with this program. If not, see <http://www.gnu.org/licenses/>. Contact info: lobochief@users.sourceforge.net; ivan.difrancesco@yahoo.it */ package org.lobobrowser.http; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lobobrowser.http.Header.Element; import org.lobobrowser.xpath.XPathUtils; import org.w3c.dom.Document; /** * <p> * Represents an http request. A <code>Request</code> is constructed and then * passed to a Session for execution. The <code>Session</code> then returns a * Response after execution finishes. * </p> * * <p> * It is not possible to reuse <code>Request</code>s with content bodies because * those bodies are specified as <code>InputStream</code>s. This is done for * efficient handling of large files that may be used as content bodies. * </p> * * <p> * To help simplify reuse of <code>Request</code>, a copy constructor is * provided which will copy everything _except_ the content body from the source * <code>Request</code> * </p> * . * * <p> * A Request is composed of a URL and HTTP method and optionally Headers, * Parameters, and a body. * </p> * * <p> * The URL for convenience is specified as a String, not a java.net.URL. The * evaluation of the URL is done when the Request is executed, as opposed to * when it is first set. The HTTP {@link Method} must be non-null. * </p> * * <p> * HTTP headers are represented by the {@link Header} API. All HTTP headers that * will be sent as part of this request are represented with a Header in this * class. By default, all Request objects are created with an Accept-Encoding * header set to "gzip", and have a Content-Type header set to 'text/plain; * charset="UTF-8"'. If you send other data be sure to replace the value of the * content type header. * </p> * * <p> * According to the HTTP specification, HTTP headers are not case sensivite. * Therefore, this class will allow headers to be lookedup in a case insensitive * manner, unlike parameters which are case sensitive. * </p> * * <p> * For convenience, this class supports automatic header generation for basic * authentication when the <code>username</code> property is set. Whenever * <code>username</code> or <code>password</code> is set it will reset the * "Authentication" header. Be aware that manual modifications of this header * will be lost whenever the username/password is changed. * </p> * * <p> * Request also supports setting query parameters. A URL is composed of the * protocol part, path part, and optionally the query parameter part. * </p> * * <pre> * <code> * http://www.example.com/foo.html?a=b;c=d * |-----|------------------------|-------| * proto path portion of URI params * </code> * </pre> * * <p> * Request supports the setting of query parameters either in the URL or * separately from it. In the next code snippet, the query parameters are set as * part of the URL. As you can see from the code snippet, the query parameters, * even though specified as part of the URL, are extracted from the URL and can * be read and/or modified via the parameter API: * </p> * * <pre> * <code> * Request req = new Request("http://www.example.com/foo.html?a=b;c=d"); * System.out.println(req.getUrl()); // prints out http://www.example.com/foo.html * System.out.println(req.getParameter("a")); // prints out a=b * System.out.println(req.getParameter("c")); // prints out c=d * </code> * </pre> * * <p> * You may also specify the query parameters completely separately from the URL: * </p> * * <pre> * <code> * Request req = new Request("http://www.example.com/foo.html"); * req.setParameter("a", "b"); * req.setParameter("c", "d"); * </code> * </pre> * * <p> * HTTP parameters must be URL encoded prior to transmission. This task is not * handled by the Request, but by the Session. All parameter names and values * are not URL encoded. * </p> * * <p> * Some HTTP oriented APIs distinguish between "GET" parameters and "POST" * parameters. This one does not. All parameters in this Request class are "GET" * parameters, meaning that regardless of the HTTP method being used the * parameters are set on the query string in the URL, not the body of the * request. A subclass, FormRequest, handles "POST" parameters in a more * complete way by also supporting different encoding schemes for POST requests. * </p> * * @author rbair */ public class Request extends AbstractBean { /** * Header keys are stored in a case insensitive manner. */ /** The Constant logger. */ private static final Logger logger = LogManager .getLogger(Request.class); /** The headers. */ private Map<String, Header> headers = new HashMap<String, Header>(); /** The params. */ private Map<String, Parameter> params = new HashMap<String, Parameter>(); /** The follow redirects. */ private boolean followRedirects = true; /** The method. */ private String method = Method.GET; /** The url. */ private String url; /** The request body. */ private InputStream requestBody; /** The username. */ private String username; /** The password. */ private char[] password; /** * Used in the toString() method call only if the body was set as a String. * If set as an InputStream or as bytes then this will be null. */ private String stringBody; /** * Creates a new instance of Request. The following default values are used: * <ul> * <li>headers: Accept-Encoding = gzip</li> * <li>parameters: empty set</li> * <li>followRedirects: true</li> * <li>method: GET</li> * <li>url: null</li> * <li>requestBody: null</li> * </ul> */ public Request() { this(Method.GET, null); } /** * Creaets a new instance of Request with the specified URL. Other default * values are the same as for the default constructor. * * @param url * the url */ public Request(String url) { this(Method.GET, url); } /** * Creates a new instance of Request with the specified HTTP method and url. * All other default values are the same as for the default consturctor. * * @param method * The HTTP method. If null, Method.GET is used. * @param url * The url. If non null, any query parameters are extracted and * set as params for this request. */ public Request(String method, String url) { this.method = method == null ? Method.GET : method; setHeader("Accept-Encoding", "gzip"); setHeader("Content-Type", "text/plain; charset=UTF-8"); if (url != null) { setUrlImpl(url); } } /** * <p> * Creates a new instance of Request, using <code>source</code> as the basis * for all of the initial property values (except for requestBody, which is * always null). This is a copy constructor. * </p> * * @param source * The source Request to copy */ public Request(Request source) { if (source != null) { username = source.username; password = source.password; headers.putAll(source.headers); params.putAll(source.params); followRedirects = source.followRedirects; method = source.method; url = source.url; } } /** * Returns the Header with the given name, or null if there is no such * header. Header names are checked in a case insensitive manner. * * @param name * the name to look for. If null then a null value will be * returned * @return the Header with the given name. */ public final Header getHeader(String name) { if (name == null) { return null; } return headers.get(name.toLowerCase()); } /** * Creates a new Header with the given name and value, and no elements and * adds it to the set of headers. * * @param name * The name. Must not be null. * @param value * The value. May be null. */ public final void setHeader(String name, String value) { if (name == null) { throw new IllegalArgumentException("Name cannot be null"); } setHeader(new Header(name, value)); } /** * Creates a new Header with the given name, value, and elements and adds it * to the set of headers. * * @param name * The name. Must not be null. * @param value * The value. May be null. * @param elements * The elements. May be null. */ public final void setHeader(String name, String value, Element... elements) { if (name == null) { throw new IllegalArgumentException("Name cannot be null"); } setHeader(new Header(name, value, elements)); } /** Sets the header. * * @param header * the new header */ public void setHeader(Header header) { if (header == null) { throw new IllegalArgumentException("header cannot be null"); } else if (header.getName() == null) { throw new IllegalArgumentException("header name cannot be null"); } headers.put(header.getName().toLowerCase(), header); // update the username/password if an auth header was just set if ("Authentication".equals(header.getName())) { try { String encoded = header.getValue().substring(6); String tmp = base64Decode(encoded); String u = tmp.substring(0, tmp.indexOf(":")); String p = tmp.substring(tmp.indexOf(":") + 1); String oldUsername = this.username; firePropertyChange("username", oldUsername, this.username = u); this.password = p.toCharArray(); } catch (Exception e) { /* oh well */ } } } /** * Removes the given header from this Request. * * @param header * the Header to remove. If null, nothing happens. If the header * is not specified in this Request, nothing happens. */ public final void removeHeader(Header header) { if (header != null) { headers.remove(header.getName().toLowerCase()); } } /** * Removes the given named header from this Request. The header is * case-insensitive. * * @param header * the name of the Header to remove. If null, nothing happens. If * the header is not specified in this Request, nothing happens. * Matches in a case-insensitive manner. */ public final void removeHeader(String header) { headers.remove(header.toLowerCase()); } /** Gets the headers. * * @return the headers */ public final Header[] getHeaders() { return headers.values().toArray(new Header[0]); } /** Sets the headers. * * @param headers * the new headers */ public final void setHeaders(Header... headers) { this.headers.clear(); if (headers != null) { for (Header h : headers) { setHeader(h); } } } /** * Returns the Parameter with the given name, or null if there is no such * Parameter. * * @param name * the name to look for. If null, null is returned. * @return the Parameter with the given name. */ public final Parameter getParameter(String name) { if (name == null) { return null; } return params.get(name); } /** * Creates a Parameter using the given name and value and then adds it to * the set of parameters. * * @param name * must not be null * @param value * the value */ public final void setParameter(String name, String value) { if (name == null) { throw new IllegalArgumentException("Parameter name cannot be null"); } setParameter(new Parameter(name, value)); } /** Sets the parameter. * * @param param * the new parameter */ public void setParameter(Parameter param) { if (param == null) { throw new IllegalArgumentException("param cannot be null"); } else if (param.getName() == null) { throw new IllegalArgumentException("parameter name cannot be null"); } params.put(param.getName(), param); } /** Gets the parameters. * * @return the parameters */ public final Parameter[] getParameters() { return params.values().toArray(new Parameter[0]); } /** Sets the parameters. * * @param params * the new parameters */ public final void setParameters(Parameter... params) { this.params.clear(); if (params != null) { for (Parameter p : params) { setParameter(p); } } } /** Sets the follow redirects. * * @param b * the new follow redirects */ // TODO need to support a count of maximium redirects public void setFollowRedirects(boolean b) { boolean old = getFollowRedirects(); this.followRedirects = b; firePropertyChange("followRedirects", old, this.followRedirects); } /** Gets the follow redirects. * * @return the follow redirects */ public final boolean getFollowRedirects() { return followRedirects; } /** Sets the method. * * @param method * the new method */ public void setMethod(String method) { String old = getMethod(); this.method = method == null ? Method.GET : method; firePropertyChange("method", old, this.method); } /** Gets the method. * * @return the method */ public final String getMethod() { return method; } /** Sets the url. * * @param url * the new url */ public void setUrl(String url) throws IllegalArgumentException { String old = getUrl(); setUrlImpl(url); firePropertyChange("url", old, this.url); } /** Sets the url impl. * * @param url * the new url impl */ private void setUrlImpl(String url) { this.url = url; if (url != null) { // if there is a ? in the url, then there are query params // If there are query params, then substring the url, decode the // params, etc. int index = url.indexOf("?"); if (index >= 0) { this.url = url.substring(0, index); String[] parts = url.substring(index + 1).split("&"); try { for (String part : parts) { String key = null; String value = null; index = part.indexOf("="); if (index < 0) { // no value, just a key key = part; key = URLDecoder.decode(key, "UTF-8"); } else { key = part.substring(0, index); value = part.substring(index + 1); key = URLDecoder.decode(key, "UTF-8"); value = URLDecoder.decode(value, "UTF-8"); } setParameter(key, value); } } catch (Exception e) { logger.error(e.getMessage()); } } } } /** Gets the url. * * @return the url */ public final String getUrl() { return url; } /** Sets the username. * * @param username * the new username */ public void setUsername(String username) { String old = this.username; this.username = username; resetAuthenticationHeader(); firePropertyChange("username", old, this.username); } /** * Reset authentication header. */ private void resetAuthenticationHeader() { try { if (username == null) { removeHeader("authentication"); } else { headers.put("authentication", new Header("Authentication", "Basic " + base64Encode( username + ":" + getPassword()))); } } catch (Exception e) { logger.error(e.getMessage()); } } /** Gets the username. * * @return the username */ public final String getUsername() { return username; } /** Sets the password. * * @param password * the new password */ public void setPassword(String password) { this.password = password == null ? new char[0] : password.toCharArray(); resetAuthenticationHeader(); } /** Gets the password. * * @return the password */ final String getPassword() { return password == null ? "" : new String(password); } /** Sets the body. * * @param body * the new body */ public void setBody(String body) { stringBody = body; setBody(body == null ? null : body.getBytes()); } /** Sets the body. * * @param body * the new body */ public void setBody(byte[] body) { if ((body == null) || (body.length == 0)) { requestBody = null; } else { setBody(new ByteArrayInputStream(body)); } } /** Sets the body. * * @param body * the new body */ public void setBody(Document body) { setBody(body == null ? null : XPathUtils.toXML(body)); } /** Sets the body. * * @param body * the new body */ public void setBody(InputStream body) { this.requestBody = body; } /** Gets the body. * * @return the body * @throws Exception * the exception */ protected InputStream getBody() throws Exception { return requestBody; } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(getMethod()); buffer.append(" " + getUrl() + "\n"); for (Header h : getHeaders()) { buffer.append(" ").append(h.getName()).append(": ") .append(h.getValue()); buffer.append("\n"); } if (stringBody != null) { buffer.append(stringBody); } else { buffer.append("<<body content>>"); } return buffer.toString(); } /** * Base64 encode. * * @param s * the s * @return the string * @throws Exception * the exception */ private static String base64Encode(String s) throws Exception { return new String( Base64.getEncoder().encode(s.getBytes(StandardCharsets.UTF_8))); } /** * Base64 decode. * * @param s * the s * @return the string * @throws Exception * the exception */ private static String base64Decode(String s) throws Exception { byte[] asBytes = Base64.getDecoder().decode(s); return new String(new String(asBytes, "utf-8")); } }