/* * COMSAT * Copyright (c) 2013-2016, Parallel Universe Software Co. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by * the Eclipse Foundation * * or (per the licensee's choosing) * * under the terms of the GNU Lesser General Public License version 3.0 * as published by the Free Software Foundation. */ package co.paralleluniverse.comsat.webactors; import co.paralleluniverse.actors.ActorRef; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; /** * An HTTP request message. */ public abstract class HttpRequest extends HttpMessage { /** * The Internet Protocol (IP) host that sent the request or {@code null} if not available. */ public abstract String getSourceHost(); /** * The Internet Protocol (IP) port from which the request was sent or {@code -1} if not available. */ public abstract int getSourcePort(); /** * A multimap of the parameters contained in this message and (all) their values. * If the request has no parameters, returns an empty multimap. */ public abstract Multimap<String, String> getParameters(); /** * Returns the names values of all attributed associated with this request. * * <p> * The container may set * attributes to make available custom information about a request. * For example, for requests made using HTTPS, the attribute * <code>javax.servlet.request.X509Certificate</code> can be used to * retrieve information on the certificate of the client. * * @return an {@code Object} containing the value of the attribute, * or {@code null} if the attribute does not exist */ public abstract Map<String, Object> getAttributes(); /** * Returns all values associated with the given parameter * * @param name the parameter name * @return all values associated with the given parameter */ public Collection<String> getParametersValues(String name) { return getParameters().get(name); } /** * Returns the value of the given parameter. * If the parameter is not found in the message, this method returns {@code null}. * If the parameter has more than one value, this method returns the first value. * * @param name the parameter name * @return the (first) value of the given parameter name; {@code null} if the parameter is not found */ public String getParameter(String name) { return first(getParameters().get(name)); } /** * Returns the value of the given attribute. * If the attribute is not found in the message, this method returns {@code null}. * * @param name the attribute name * @return the value of the given attribute; {@code null} if the attribute is not found */ public Object getAttribute(String name) { return getAttributes().get(name); } /** * Returns the name of the scheme used to make this request, for example, {@code http}, {@code https}, or {@code ftp}. * Different schemes have different rules for constructing URLs, as noted in RFC 1738. */ public abstract String getScheme(); /** * The name of the HTTP method with which this request was made; * for example, GET, POST, or PUT. * * @return the name of the method with which this request was made */ public abstract String getMethod(); /** * Returns the value of the specified request header as a {@code long} value that represents a {@code Date} object. * Use this method with headers that contain dates, such as {@code If-Modified-Since}. * * <p> * The date is returned as the number of milliseconds since January 1, 1970 GMT. * * <p> * If the request does not have a header of the specified name, this method returns -1. * If the header can't be converted to a date, the method throws an {@code IllegalArgumentException}. * * @param name the name of the header * * @return a {@code long} value representing the date specified in the header expressed as * the number of milliseconds since January 1, 1970 GMT, * or {@code -1} if the named header was not included with the request * * @exception IllegalArgumentException If the header value can't be converted to a date */ public long getDateHeader(String name) { String value = getHeader(name); if (value == null) return (-1L); long result = parseDate(value); if (result != (-1L)) return result; throw new IllegalArgumentException(value); } /** * Returns any extra path information associated with the URL the client sent when it made this request. * The extra path information follows the servlet path but precedes the query string and will start with * a "/" character. * * <p> * This method returns {@code null} if there was no extra path information. * * @return a {@code String} decoded by the web container, specifying * extra path information that comes after the servlet path but before the query string in the request URL; * or {@code null} if the URL does not have any extra path information */ public abstract String getPathInfo(); /** * The portion of the request URI that indicates the context of the request. * The context path always comes first in a request URI. * The path starts with a "/" character but does not end with a "/" character. * For servlets in the default (root) context, this method returns "". * The container does not decode this string. * * <p> * It is possible that a container may match a context by * more than one context path. In such cases this method will return the * actual context path used by the request. * * @return the portion of the request URI that indicates the context of the request */ public abstract String getContextPath(); /** * The query string that is contained in the request URL after the path, * or {@code null} if the URL does not have a query string. */ public abstract String getQueryString(); /** * Returns the part of this request's URL from the protocol name up to the query string in the first line of the HTTP request. * The web container does not decode this string. * For example: * * <table summary="Examples of Returned Values"> * <tr align=left> * <th>First line of HTTP request </th><th> Returned Value</th> * <tr><td>POST /some/path.html HTTP/1.1<td><td>/some/path.html * <tr><td>GET http://foo.bar/a.html HTTP/1.0<td><td>/a.html * <tr><td>HEAD /xyz?a=b HTTP/1.1<td><td>/xyz * </table> * * @return the part of the URL from the protocol name up to the query string */ public abstract String getRequestURI(); /** * Reconstructs the URL the client used to make the request. * The returned URL contains a protocol, server name, port * number, and server path, but it does not include query * string parameters. * * <p> * This method is useful for creating redirect messages * and for reporting errors. * * @return the reconstructed URL */ public String getRequestURL() { StringBuilder url = new StringBuilder(); String scheme = getScheme(); int port = getServerPort(); if (port < 0) port = 80; // Work around java.net.URL bug url.append(scheme); url.append("://"); url.append(getServerName()); if ((scheme.equals("http") && (port != 80)) || (scheme.equals("https") && (port != 443))) { url.append(':'); url.append(port); } url.append(getRequestURI()); return url.toString(); } /** * The host name of the server to which the request was sent. * It is the value of the part before ":" in the {@code Host} header value, if any, * or the resolved server name, or the server IP address. */ public abstract String getServerName(); /** * The port number to which the request was sent. * It is the value of the part after ":" in the {@code Host} header value, if any, * or the server port where the client connection was accepted on. * * @return the port number */ public abstract int getServerPort(); /** * Returns an actor representing the client to which an {@link HttpResponse} should be sent as a response to this request. * All {@code HttpRequest}s from the same session will have the same sender. It will appear to have died (i.e. send an * {@link co.paralleluniverse.actors.ExitMessage ExitMessage} if {@link co.paralleluniverse.actors.Actor#watch(co.paralleluniverse.actors.ActorRef) watched}) * when the session is terminated. * * @return an actor representing the client */ @Override public abstract ActorRef<WebMessage> getFrom(); @Override protected String contentString() { StringBuilder sb = new StringBuilder(); sb.append(" ").append(getMethod()); sb.append(" sourceHost: ").append(getSourceHost()); sb.append(" uri: ").append(getRequestURI()); sb.append(" query: ").append(getQueryString()); sb.append(" params: ").append(getParameters()); sb.append(" headers: ").append(getHeaders()); sb.append(" cookies: ").append(getCookies()); sb.append(" contentLength: ").append(getContentLength()); sb.append(" charEncoding: ").append(getCharacterEncoding()); sb.append(" body: ").append(getStringBody()); return super.contentString() + sb; } public static class Builder { private final ActorRef<WebMessage> sender; private final String strBody; private final ByteBuffer binBody; private String sourceHost; private int sourcePort; private String contentType; private Charset charset; private List<Cookie> cookies; private ListMultimap<String, String> headers; private String method; private String scheme; private String server; private int port; private String path; private Multimap<String, String> params; public Builder(ActorRef<? super WebMessage> from, String body) { this.sender = (ActorRef<WebMessage>) from; this.strBody = body; this.binBody = null; } public Builder(ActorRef<? super WebMessage> from, ByteBuffer body) { this.sender = (ActorRef<WebMessage>) from; this.binBody = body; this.strBody = null; } public Builder(ActorRef<? super WebMessage> from) { this(from, (String) null); } public Builder setSourceHost(String sourceAddress) { this.sourceHost = sourceAddress; return this; } public Builder setSourcePort(int sourcePort) { this.sourcePort = sourcePort; return this; } /** * Sets the content type of the request being sent to the client. * <p/> * The given content type may include a character encoding * specification, for example, {@code text/html;charset=UTF-8}. * <p/> * The {@code Content-Type} header is used to communicate the content type and the character * encoding used in the response writer to the client * * @param contentType the MIME type of the content * */ public Builder setContentType(String contentType) { this.contentType = contentType; return this; } /** * Sets the character encoding (MIME charset) of the response being sent to the client, * for example, {@code UTF-8}. * If the character encoding has already been set by {@link #setContentType}, this method overrides it. * Calling {@link #setContentType} with {@code "text/html"} and calling this method with {@code Charset.forName("UTF-8")} * is equivalent with calling {@code setContentType} with {@code "text/html; charset=UTF-8"}. * <p/> * Note that the character encoding cannot be communicated via HTTP headers if * content type is not specified; however, it is still used to encode text * written in this response's body. * * @param charset only the character sets defined by IANA Character Sets * (http://www.iana.org/assignments/character-sets) * * @see #setContentType */ public Builder setCharacterEncoding(Charset charset) { this.charset = charset; return this; } /** * Adds a request header with the given name and value. * This method allows response headers to have multiple values. * * @param name the name of the header * @param value the additional header value. * If it contains octet string, it should be encoded according to RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt) */ public Builder addHeader(final String name, final String value) { if (headers == null) headers = LinkedListMultimap.create(); // Normalize header names by their conversion to lower case headers.put(name.toLowerCase(Locale.ENGLISH), value); return this; } /** * Adds the specified cookie to the request. * This method can be called multiple times to set multiple cookies. * * @param cookie the {@link Cookie} to return to the client * @return {@code this} */ public Builder addCookie(Cookie cookie) { if (cookies == null) cookies = new ArrayList<>(); cookies.add(cookie); return this; } public Builder setMethod(String method) { this.method = method; return this; } public Builder setScheme(String scheme) { this.scheme = scheme; return this; } public Builder setServer(String server) { this.server = server; return this; } public Builder setPort(int port) { this.port = port; return this; } public Builder setPath(String path) { this.path = path; return this; } /** * Adds a request parameter with the given name and value. * * @param name the name of the parameter * @param value the additional header value. * If it contains octet string, it should be encoded according to RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt) */ public Builder addParam(final String name, final String value) { if (params == null) params = LinkedListMultimap.create(); params.put(name, value); return this; } /** * Instantiates a new immutable {@link HttpRequest} based on the values set in this builder. * * @return a new {@link HttpRequest} */ public HttpRequest build() { return new SimpleHttpRequest(sender, this); } } private static class SimpleHttpRequest extends HttpRequest { private final String sourceHost; private final int sourcePort; private final ActorRef<WebMessage> sender; private final String contentType; private final Charset charset; private final String strBody; private final ByteBuffer binBody; private final Collection<Cookie> cookies; private final ListMultimap<String, String> headers; private final Multimap<String, String> params; private final String method; private final String scheme; private final String server; private final int port; private final String uri; /** * Use when forwarding * * @param from * @param httpRequest */ public SimpleHttpRequest(ActorRef<? super WebMessage> from, HttpRequest httpRequest) { this.sourceHost = httpRequest.getSourceHost(); this.sourcePort = httpRequest.getSourcePort(); this.sender = (ActorRef<WebMessage>) from; this.contentType = httpRequest.getContentType(); this.charset = httpRequest.getCharacterEncoding(); this.strBody = httpRequest.getStringBody(); this.binBody = httpRequest.getByteBufferBody() != null ? httpRequest.getByteBufferBody().asReadOnlyBuffer() : null; this.cookies = httpRequest.getCookies(); this.headers = httpRequest.getHeaders(); this.method = httpRequest.getMethod(); this.scheme = httpRequest.getScheme(); this.server = httpRequest.getServerName(); this.port = httpRequest.getServerPort(); this.params = httpRequest.getParameters(); this.uri = httpRequest.getRequestURI(); } public SimpleHttpRequest(ActorRef<? super WebMessage> from, HttpRequest.Builder builder) { this.sourceHost = builder.sourceHost; this.sourcePort = builder.sourcePort; this.sender = (ActorRef<WebMessage>) from; this.contentType = builder.contentType; this.charset = builder.charset; this.strBody = builder.strBody; this.binBody = builder.binBody != null ? builder.binBody.asReadOnlyBuffer() : null; this.cookies = builder.cookies != null ? ImmutableList.copyOf(builder.cookies) : null; this.headers = builder.headers != null ? ImmutableListMultimap.copyOf(builder.headers) : null; this.params = builder.params != null ? ImmutableListMultimap.copyOf(builder.params) : null; this.method = builder.method; this.scheme = builder.scheme; this.server = builder.server; this.port = builder.port; this.uri = builder.path; } @Override public String getSourceHost() { return sourceHost; } @Override public int getSourcePort() { return sourcePort; } @Override public ActorRef<WebMessage> getFrom() { return sender; } @Override public String getContentType() { return contentType; } @Override public Charset getCharacterEncoding() { return charset; } @Override public int getContentLength() { if (binBody != null) return binBody.remaining(); else return -1; } @Override public String getStringBody() { return strBody; } @Override public ByteBuffer getByteBufferBody() { return binBody != null ? binBody.duplicate() : null; } @Override public Collection<Cookie> getCookies() { return cookies; } @Override public ListMultimap<String, String> getHeaders() { return headers; } @Override public Map<String, Object> getAttributes() { return null; } @Override public String getScheme() { return scheme; } @Override public String getMethod() { return method; } @Override public String getServerName() { return server; } @Override public int getServerPort() { return port; } @Override public Multimap<String, String> getParameters() { return params; } @Override public String getRequestURI() { return uri; } @Override public String getQueryString() { if(params == null) return null; StringBuilder sb = new StringBuilder(); for(Map.Entry<String, String> entry : params.entries()) sb.append(entry.getKey()).append('=').append(entry.getValue()).append('&'); sb.delete(sb.length()-1, sb.length()); return sb.toString(); } @Override public String getPathInfo() { throw new UnsupportedOperationException(); } @Override public String getContextPath() { throw new UnsupportedOperationException(); } } /** * The only date format permitted when generating HTTP headers. */ public static final String RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"; private static final SimpleDateFormat format = new SimpleDateFormat(RFC1123_DATE, Locale.US); /** * The set of SimpleDateFormat formats to use in getDateHeader(). */ private static final SimpleDateFormat formats[] = { new SimpleDateFormat(RFC1123_DATE, Locale.US), new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) }; private static final TimeZone gmtZone = TimeZone.getTimeZone("GMT"); /** * All HTTP dates are on GMT */ static { format.setTimeZone(gmtZone); formats[0].setTimeZone(gmtZone); formats[1].setTimeZone(gmtZone); formats[2].setTimeZone(gmtZone); } private static long parseDate(String value) { Date date = null; for (int i = 0; (date == null) && (i < formats.length); i++) { try { date = formats[i].parse(value); } catch (ParseException e) { // Ignore } } if (date == null) return -1L; return date.getTime(); } }