All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.mvc.Http Maven / Gradle / Ivy

There is a newer version: 2.6.2
Show newest version
package play.mvc;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.Play;
import play.exceptions.UnexpectedException;
import play.libs.Codec;
import play.libs.Time;
import play.mvc.Scope.Params;
import play.utils.HTTP;
import play.utils.HTTP.ContentTypeWithEncoding;
import play.utils.Utils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;

public class Http {
    private static final Logger logger = LoggerFactory.getLogger(Http.class);

    public static final String invocationType = "HttpRequest";

    public static class StatusCode {

        public static final int OK = 200;
        public static final int CREATED = 201;
        public static final int ACCEPTED = 202;
        public static final int PARTIAL_INFO = 203;
        public static final int NO_RESPONSE = 204;
        public static final int MOVED = 301;
        public static final int FOUND = 302;
        public static final int METHOD = 303;
        public static final int NOT_MODIFIED = 304;
        public static final int BAD_REQUEST = 400;
        public static final int UNAUTHORIZED = 401;
        public static final int PAYMENT_REQUIRED = 402;
        public static final int FORBIDDEN = 403;
        public static final int NOT_FOUND = 404;
        public static final int INTERNAL_ERROR = 500;
        public static final int NOT_IMPLEMENTED = 501;
        public static final int OVERLOADED = 502;
        public static final int GATEWAY_TIMEOUT = 503;

        public static boolean success(int code) {
            return code / 100 == 2;
        }

        public static boolean redirect(int code) {
            return code / 100 == 3;
        }

        public static boolean error(int code) {
            return code / 100 == 4 || code / 100 == 5;
        }
    }
    
    public static class Methods {
        public static final String GET = "GET";
        public static final String PATCH = "PATCH";
        public static final String POST = "POST";
        public static final String PUT = "PUT";
        public static final String DELETE = "DELETE";
        public static final String OPTIONS = "OPTIONS";
        public static final String HEAD = "HEAD";
        public static final String TRACE = "TRACE";
    }

    public static class Headers {
        public static final class Values {
            public static final String CLOSE = "close";
        }
    }

    public static class Header implements Serializable {
        public final String name;
        public final List values;

        public Header(String name, String value) {
            this.name = name;
            this.values = singletonList(value);
        }

        public Header(String name, List values) {
            this.name = name;
            this.values = values;
        }

        /**
         * First value
         * 
         * @return The first value
         */
        public String value() {
            return values.get(0);
        }

        @Override
        public String toString() {
            return name + "=" + values;
        }
    }

    public static class Cookie implements Serializable {
        public final String name;
        public String domain;
        public String path = "/";
        public boolean secure;
        public String value;
        /**
         * Cookie max-age in seconds
         */
        public Integer maxAge;
        /**
         * Don't use
         */
        public boolean sendOnError;
        public boolean httpOnly;

        public Cookie(String name, String value) {
            this.value = value;
            this.name = name;
        }
    }

    private static final ThreadLocal currentRequest = new ThreadLocal<>();

    public static class Request {
        private static final Pattern IP_REGEX = Pattern.compile("[\\s,\\d.:/a-fA-F]*");
        private static final Pattern X_FWD_REGEX = Pattern.compile("[\\s,]+");

        public String host;
        public String path;
        public String querystring;
        /**
         * URL path (excluding scheme, host and port), starting with '/'
* * Example:
* With this full URL {@code http://localhost:9000/path0/path1?foo=bar}
* => url will be {@code /path0/path1?foo=bar} */ public String url; public String method; public String domain; public String remoteAddress; public String contentType; /** * This is the encoding used to decode this request. If encoding-info is not found in request, then * Play.defaultWebEncoding is used */ public Charset encoding = Play.defaultWebEncoding; public String controller; public String actionMethod; public Integer port; public Map headers; public Map cookies; public transient InputStream body; /** * Additional HTTP params extracted from route */ public Map routeArgs = emptyMap(); /** * Format (html,xml,json,text) */ public String format; /** * Full action (ex: Application.index) */ public String action; public Method invokedMethod; public Class controllerClass; public PlayController controllerInstance; /** * Free space to store your request specific data */ public final Map args = new HashMap<>(16); /** * When the request has been received */ public final Date date = new Date(); /** * HTTP Basic User */ public String user; /** * HTTP Basic Password */ public String password; /** * Request comes from loopback interface */ public boolean isLoopback; /** * ActionInvoker.resolvedRoutes was called? */ boolean resolved; @Nonnull public final Params params = new Params(this); public Boolean cachedIsSecure = null; /** * Deprecate the default constructor to encourage the use of createRequest() when creating new requests. * * Cannot hide it with protected because we have to be backward compatible with modules - ie * PlayGrizzlyAdapter.java */ @Deprecated public Request() { headers = new HashMap<>(16); cookies = new HashMap<>(16); } public static Request createRequest(String _remoteAddress, String _method, String _path, String _querystring, String _contentType, InputStream _body, String _url, String _host, boolean _isLoopback, int _port, String _domain, Map _headers, Map _cookies) { Request newRequest = new Request(); newRequest.remoteAddress = _remoteAddress; newRequest.method = _method; newRequest.path = _path; newRequest.querystring = _querystring; ContentTypeWithEncoding contentTypeEncoding = HTTP.parseContentType(_contentType); newRequest.contentType = contentTypeEncoding.contentType; newRequest.encoding = contentTypeEncoding.encoding; newRequest.body = _body; newRequest.url = _url; newRequest.host = _host; newRequest.isLoopback = _isLoopback; newRequest.port = _port; newRequest.domain = _domain; newRequest.headers = _headers != null ? _headers : new HashMap<>(16); newRequest.cookies = _cookies != null ? _cookies : new HashMap<>(16); newRequest.parseXForwarded(); newRequest.resolveFormat(); newRequest.authorizationInit(); validateXForwarded(newRequest.headers.get("x-forwarded-for")); return newRequest; } static void validateXForwarded(Header xForwardedFor) { if (xForwardedFor == null) return; if (!IP_REGEX.matcher(xForwardedFor.value()).matches()) { throw new IllegalArgumentException("Unacceptable X-Forwarded-For format: " + xForwardedFor.value()); } } private void parseXForwarded() { String _host = this.host; if (Play.configuration.containsKey("XForwardedSupport") && headers.get("x-forwarded-for") != null) { if (!"ALL".equalsIgnoreCase(Play.configuration.getProperty("XForwardedSupport")) && !asList(X_FWD_REGEX.split(Play.configuration.getProperty("XForwardedSupport", "127.0.0.1"))).contains(remoteAddress)) { throw new RuntimeException("This proxy request is not authorized: " + remoteAddress); } else { if (Play.configuration.containsKey("XForwardedHost")) { this.host = Play.configuration.getProperty("XForwardedHost"); } else if (this.headers.get("x-forwarded-host") != null) { this.host = this.headers.get("x-forwarded-host").value(); } if (this.headers.get("x-forwarded-for") != null) { this.remoteAddress = cleanupRemoteAddresses(this.headers.get("x-forwarded-for").value()); } } } if ("true".equalsIgnoreCase(Play.configuration.getProperty("XForwardedOverwriteDomainAndPort", "false")) && this.host != null && !this.host.equals(_host)) { if (this.host.contains(":")) { String[] hosts = this.host.split(":"); this.port = Integer.parseInt(hosts[1]); this.domain = hosts[0]; } else { this.port = 80; this.domain = this.host; } } } @Nonnull static String cleanupRemoteAddresses(String remoteAddress) { int index = remoteAddress.lastIndexOf(','); return index == -1 ? remoteAddress : remoteAddress.substring(index + 1).trim(); } public boolean isSecure() { if (cachedIsSecure != null) return cachedIsSecure; Header xForwardedProtoHeader = headers.get("x-forwarded-proto"); Header xForwardedSslHeader = headers.get("x-forwarded-ssl"); // Check the less common "front-end-https" header, used apparently only by // "Microsoft Internet Security and Acceleration Server" and Squid (when using Squid as // an SSL frontend). Header frontEndHttpsHeader = headers.get("front-end-https"); boolean result = ("https".equals(Play.configuration.getProperty("XForwardedProto")) || (xForwardedProtoHeader != null && "https".equals(xForwardedProtoHeader.value())) || (xForwardedSslHeader != null && "on".equals(xForwardedSslHeader.value())) || (frontEndHttpsHeader != null && "on".equalsIgnoreCase(frontEndHttpsHeader.value()))); cachedIsSecure = result; return result; } protected void authorizationInit() { Header header = headers.get("authorization"); if (header != null && header.value().startsWith("Basic ")) { String data = header.value().substring(6); // In basic auth, the password can contain a colon as well so split(":") may split // the string into 3 parts: username, part1 of password and part2 of password. // So, don't use split here. String decoded = new String(Codec.decodeBASE64(data), UTF_8); // splitting on ONLY first ':' allows user's password to contain a ':' int indexOf = decoded.indexOf(':'); if (indexOf < 0) return; String username = decoded.substring(0, indexOf); String thePasswd = decoded.substring(indexOf + 1); user = !username.isEmpty() ? username : null; password = !thePasswd.isEmpty() ? thePasswd : null; } } /** * Automatically resolve request format from the Accept header (in this order: html > xml * > json > text) */ public void resolveFormat() { if (format != null) { return; } if (headers.get("accept") == null) { format = "html"; return; } String accept = headers.get("accept").value(); if (accept.contains("application/xhtml") || accept.contains("text/html") || accept.startsWith("*/*")) { format = "html"; return; } if (accept.contains("application/xml") || accept.contains("text/xml")) { format = "xml"; return; } if (accept.contains("text/plain")) { format = "txt"; return; } if (accept.contains("application/json") || accept.contains("text/javascript")) { format = "json"; return; } if (accept.endsWith("*/*")) { format = "html"; } } /** * Retrieve the current request * * @return the current request */ @Deprecated public static Request current() { return currentRequest.get(); } @Deprecated public static void setCurrent(Request request) { currentRequest.set(request); } public static void removeCurrent() { currentRequest.remove(); } /** * This request was sent by an Ajax framework. (rely on the X-Requested-With header). * * @return True is the request is an Ajax, false otherwise */ public boolean isAjax() { Header header = headers.get("x-requested-with"); return header != null && "XMLHttpRequest".equals(header.value()); } @Nullable public String getUserAgent() { Header agent = headers.get("user-agent"); return agent != null ? agent.value() : "n/a"; } /** * Get the request base (e.g.: http://localhost:9000) * * @return the request base of the url (protocol, host and port) */ @Nonnull public String getBase() { if (port == 80 || port == 443) { return String.format("%s://%s", isSecure() ? "https" : "http", domain).intern(); } return String.format("%s://%s:%s", isSecure() ? "https" : "http", domain, port).intern(); } @Override public String toString() { return method + " " + path + (querystring != null && !querystring.isEmpty() ? "?" + querystring : ""); } /** * Return the languages requested by the browser, ordered by preference (preferred first). If no Accept-Language * header is present, an empty list is returned. * * @return Language codes in order of preference, e.g. "en-us,en-gb,en,de". */ public List acceptLanguage() { final Pattern qpattern = Pattern.compile("q=([0-9.]+)"); if (!headers.containsKey("accept-language")) { return Collections.emptyList(); } String acceptLanguage = headers.get("accept-language").value(); List languages = asList(acceptLanguage.split(",")); languages.sort((lang1, lang2) -> { double q1 = 1.0; double q2 = 1.0; Matcher m1 = qpattern.matcher(lang1); Matcher m2 = qpattern.matcher(lang2); if (m1.find()) { q1 = Double.parseDouble(m1.group(1)); } if (m2.find()) { q2 = Double.parseDouble(m2.group(1)); } return (int) (q2 - q1); }); List result = new ArrayList<>(10); for (String lang : languages) { result.add(lang.trim().split(";")[0]); } return result; } public boolean isModified(String etag, long last) { if (headers.containsKey("if-none-match") && headers.containsKey("if-modified-since")) { String browserEtag = headers.get("if-none-match").value(); if (browserEtag.equals(etag)) { try { Date browserDate = Utils.getHttpDateFormatter() .parse(headers.get("if-modified-since").value()); if (browserDate.getTime() >= last) return false; } catch (ParseException ex) { logger.error("Can't parse date", ex); } } } return true; } public void setCookie(String key, String value) { cookies.put(key, new Cookie(key, value)); } public void setHeader(String key, String value) { key = key.toLowerCase(); headers.put(key, new Header(key, value)); } public T getActionAnnotation(Class annotationClass) { T annotation = invokedMethod.getAnnotation(annotationClass); if (annotation == null) { annotation = controllerClass.getAnnotation(annotationClass); } return annotation; } } private static final ThreadLocal currentResponse = new ThreadLocal<>(); public static class Response { public int status = StatusCode.OK; public String contentType; public final Map headers = new HashMap<>(16); public Map cookies = new HashMap<>(16); public ByteArrayOutputStream out; public Object direct; public Charset encoding = Play.defaultWebEncoding; @Deprecated public static Response current() { return currentResponse.get(); } @Deprecated public static void setCurrent(Response response) { currentResponse.set(response); } public static void removeCurrent() { currentResponse.remove(); } /** * Get a response header * * @param name * Header name case-insensitive * @return the header value as a String */ @Nullable public String getHeader(@Nonnull String name) { for (Map.Entry entry : headers.entrySet()) { if (entry.getKey().equalsIgnoreCase(name)) { if (entry.getValue() != null) { return entry.getValue().value(); } } } return null; } public void setHeader(String name, String value) { headers.put(name, new Header(name, value)); } public void setContentTypeIfNotSet(String contentType) { if (this.contentType == null) { this.contentType = contentType; } } public void setCookie(String name, String value) { setCookie(name, value, null, "/", null, false); } public void removeCookie(String name) { removeCookie(name, "/"); } public void removeCookie(String name, String path) { setCookie(name, "", null, path, 0, false); } /** * Set a new cookie that will expire in (current) + duration * @param duration the cookie duration (Ex: "3d") */ public void setCookie(String name, String value, String duration) { setCookie(name, value, null, "/", Time.parseDuration(duration), false); } public void setCookie(String name, String value, String domain, String path, Integer maxAge, boolean secure) { setCookie(name, value, domain, path, maxAge, secure, false); } public void setCookie(String name, String value, String domain, String path, Integer maxAge, boolean secure, boolean httpOnly) { if (cookies.containsKey(name) && cookies.get(name).path.equals(path) && ((cookies.get(name).domain == null && domain == null) || (cookies.get(name).domain.equals(domain)))) { cookies.get(name).value = value; cookies.get(name).maxAge = maxAge; cookies.get(name).secure = secure; } else { Cookie cookie = new Cookie(name, value); cookie.path = path; cookie.secure = secure; cookie.httpOnly = httpOnly; if (domain != null) { cookie.domain = domain; } if (maxAge != null) { cookie.maxAge = maxAge; } cookies.put(name, cookie); } } /** * Add a cache-control header * * @param duration * Ex: 3h */ public void cacheFor(String duration) { int maxAge = Time.parseDuration(duration); setHeader("Cache-Control", "max-age=" + maxAge); } /** * Add cache-control headers * * @param etag * the Etag value * * @param duration * the cache duration (Ex: 3h) * @param lastModified * The last modified date */ public void cacheFor(String etag, String duration, long lastModified) { int maxAge = Time.parseDuration(duration); setHeader("Cache-Control", "max-age=" + maxAge); setHeader("Last-Modified", Utils.getHttpDateFormatter().format(new Date(lastModified))); setHeader("Etag", etag); } /** * Add headers to allow cross-domain requests. Be careful, a lot of browsers don't support these features and * will ignore the headers. Refer to the browsers' documentation to know what versions support them. * * @param allowOrigin * a comma separated list of domains allowed to perform the x-domain call, or "*" for all. */ public void accessControl(String allowOrigin) { accessControl(allowOrigin, null, false); } /** * Add headers to allow cross-domain requests. Be careful, a lot of browsers don't support these features and * will ignore the headers. Refer to the browsers' documentation to know what versions support them. * * @param allowOrigin * a comma separated list of domains allowed to perform the x-domain call, or "*" for all. * @param allowCredentials * Let the browser send the cookies when doing an x-domain request. Only respected by the browser if * allowOrigin != "*" */ public void accessControl(String allowOrigin, boolean allowCredentials) { accessControl(allowOrigin, null, allowCredentials); } /** * Add headers to allow cross-domain requests. Be careful, a lot of browsers don't support these features and * will ignore the headers. Refer to the browsers' documentation to know what versions support them. * * @param allowOrigin * a comma separated list of domains allowed to perform the x-domain call, or "*" for all. * @param allowMethods * a comma separated list of HTTP methods allowed, or null for all. * @param allowCredentials * Let the browser send the cookies when doing an x-domain request. Only respected by the browser if * allowOrigin != "*" */ public void accessControl(String allowOrigin, String allowMethods, boolean allowCredentials) { setHeader("Access-Control-Allow-Origin", allowOrigin); if (allowMethods != null) { setHeader("Access-Control-Allow-Methods", allowMethods); } if (allowCredentials) { if ("*".equals(allowOrigin)) { logger.warn( "Response.accessControl: When the allowed domain is \"*\", Allow-Credentials is likely to be ignored by the browser."); } setHeader("Access-Control-Allow-Credentials", "true"); } } public void print(String text) { try { out.write(text.getBytes(encoding)); } catch (IOException ex) { throw new UnexpectedException("Failed to print response '" + text + "'", ex); } } public boolean chunked; private final List> writeChunkHandlers = new ArrayList<>(); public void writeChunk(Object o) { this.chunked = true; if (writeChunkHandlers.isEmpty()) { throw new UnsupportedOperationException("Your HTTP server doesn't yet support chunked response stream"); } for (Consumer handler : writeChunkHandlers) { handler.accept(o); } } public void onWriteChunk(Consumer handler) { writeChunkHandlers.add(handler); } } }