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

sirius.web.http.WebContext Maven / Gradle / Ivy

There is a newer version: 22.2.3
Show newest version
/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.web.http;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Charsets;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import io.netty.buffer.ByteBufInputStream;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.QueryStringEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpData;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;
import sirius.kernel.async.CallContext;
import sirius.kernel.async.SubContext;
import sirius.kernel.commons.Callback;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
import sirius.kernel.commons.Value;
import sirius.kernel.di.std.ConfigValue;
import sirius.kernel.di.std.Part;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.HandledException;
import sirius.kernel.info.Product;
import sirius.kernel.nls.NLS;
import sirius.kernel.xml.StructuredInput;
import sirius.kernel.xml.XMLStructuredInput;
import sirius.web.http.session.ServerSession;
import sirius.web.http.session.SessionManager;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Provides access to a request received by the WebServer.
 * 

* This can be used to obtain all infos received for a HTTP request and also to create an appropriate response. *

* This context can either be passed along as variable or be accessed using {@link CallContext#get(Class)} */ public class WebContext implements SubContext { /** * Used to specify the source of a server session */ public enum ServerSessionSource { UNKNOWN, PARAMETER, COOKIE } /* * Underlying channel to send and receive data */ private ChannelHandlerContext ctx; /* * Internal attributes which can be set and read back during processing. This will not contain any posted or * other parameters. */ private Map attribute; /* * The underlying request created by netty */ protected HttpRequest request; /* * The effective request uri (without the query string) */ private String requestedURI; /* * The base url (without the uri, like: http://myhost.com) */ private String baseURL; /* * Contains the parameters submitted in the query string (?param=value...) */ private Map> queryString; /* * Contains decoded cookies which where sent by the client */ private Map cookiesIn; /* * Contains cookies which will be sent to the client */ protected Map cookiesOut; /* * Stores the decoder which was used to process a POST or PUT request */ protected InterfaceHttpPostRequestDecoder postDecoder; /* * Sometimes it is usefult to "hide" the fact that this is a POST request. * One case are login-forms. There are submitted for any URL but must not * interact with other POST handlers. Therefore a user manager can * call hidePost() so that isPOST() will return false even if a post request * is present. */ protected boolean hidePost = false; /* * A list of files to deleted once this call is handled */ private List filesToCleanup; /* * If the submitted data (from the client) was stored to a file, this will be stored here */ private File contentAsFile; /* * Raw content submitted via POST or PUT */ protected Attribute content; /* * Contains decoded data of the client session - this is sent back and forth using a cookie. This data * will not be stored on the server. */ private Map session; /* * Used to create IDs which are locally unique (for this web context). */ private AtomicLong localIdGenerator; /* * Internal key used to keep track of the TTL of the client session cookie */ private static final String TTL_SESSION_KEY = "_TTL"; /* * Stores the effective session cookie TTL. If null "defaultSessionCookieTTL" is used. */ private Long sessionCookieTTL; /* * Determines if the client session was modified and should be re-set via a cookie */ private volatile boolean sessionModified; /* * Contains the decoded language as two-letter code */ private String lang; /* * Specifies the microtiming key used for this request. If null, no microtiming will be recorded. */ protected String microtimingKey; /* * Used by Response - but stored here, since a new Response might be created.... */ protected volatile boolean responseCommitted; /* * Used by Response - but stored here, since a new Response might be created.... */ protected volatile boolean responseCompleted; /* * Invoked once the call is completely handled */ protected Callback completionCallback; /* * Stores the source of the server session */ private ServerSessionSource serverSessionSource; /* * Stores the requested session id */ private String requestedSessionId; /* * Stores the server session once it was fetched */ private ServerSession serverSession; /* * Determines if the requested has a trusted ip address */ private Boolean trusted; /* * Determines if the request is performed via a secured channel (SSL) */ protected Boolean ssl; /* * Contains the remote IP. If a proxyIP is specified (WebServer#proxyIPs), a X-Forwarded-For header is checked */ private InetAddress remoteIp; /* * If longCall is set to true (by the user), the idle-state handler is disabled for this request. */ private boolean longCall; /* * If set, will be supplied with all incoming content (instead of buffering on disk or in memory) */ protected ContentHandler contentHandler; /* * Contains the timestamp this request was dispatched. (Will not be filled in predispatch, as we only * want to measure how long it takes to generate an "average" result, not how long an upload took.... */ protected long started = 0; /* * Name of the cookie used to store and load the client session */ @ConfigValue("http.sessionCookieName") private static String sessionCookieName; /* * The ttl of the client session cookie. If this is 0, it will be a "session cookie" and therefore * be deleted when the browser is closed */ @ConfigValue("http.sessionCookieTTL") private static Duration defaultSessionCookieTTL; /* * Shared secret used to protect the client session. If empty one will be created on startup. */ @ConfigValue("http.sessionSecret") private static String sessionSecret; /* * Parameter name in which the server session is expected */ @ConfigValue("http.serverSessionParameterName") private static String serverSessionParameterName; /* * Cookie name used to store the server session */ @ConfigValue("http.serverSessionCookieName") private static String serverSessionCookieName; /* * Context prefix (constant path prefix) used for this server */ @ConfigValue("http.contextPrefix") private static String contextPrefix; /* * Input size limit for structured data (as this is loaded into heap) */ @ConfigValue("http.maxStructuredInputSize") private static long maxStructuredInputSize; /* * Determines if a dummy P3P header should be created to disable P3P handling. */ @ConfigValue("http.addP3PHeader") protected static boolean addP3PHeader; /* * Should the automatic CORS handling be done or not? */ @ConfigValue("http.corsAllowAll") protected static boolean corsAllowAll; /* * Should a Strict-Transport-Security header be sent? */ @ConfigValue("http.ssl.forceHSTS") protected static boolean forceHSTS; /* * Should the automatic CORS handling be done or not? */ @ConfigValue("http.ssl.hstsMaxAge") protected static int hstsMaxAge; @Part private static SessionManager sessionManager; /** * Provides access to the underlying ChannelHandlerContext * * @return the underlying channel handler context */ public ChannelHandlerContext getCtx() { return ctx; } /** * Enables microtiming for this request. *

* If null is passed in as key, the request uri is used. *

* If the microtiming was already enabled, it will remain enabled, with the original key * * @param key the key used to pass to the microtiming framework. * @return this to fluently work with this context */ public WebContext enableTiming(String key) { if (microtimingKey == null) { if (key == null) { microtimingKey = getRequestedURI(); } else { microtimingKey = key; } } return this; } /** * Determines if this context is attached to a request or not. * * @return true if this context is attached to a request, false otherwise */ public boolean isValid() { return request != null; } /** * Used to provide a handle which is invoked once the call is completely handled. *

* Note that calling this method, removes the last completion handler. * * @param onComplete the handler to be invoked once the request is completely handled */ public void onComplete(Callback onComplete) { completionCallback = onComplete; } /** * Sets the ChannelHandlerContext for this context. * * @param ctx the channel handler context to use */ protected void setCtx(ChannelHandlerContext ctx) { this.ctx = ctx; } /** * Provides access to the underlying netty HttpRequest * * @return the underlying request */ public HttpRequest getRequest() { return request; } /** * Sets the underlying HttpRequest * * @param request the request on which this context is based */ protected void setRequest(HttpRequest request) { this.request = request; } /** * Determines if this request was marked as long call. *

* This will effectively disable the idle timeout for this request. * * @return true if the request was marked as long call, false otherwise */ public boolean isLongCall() { return longCall; } /** * Marks the request as long call. *

* This will disable all idle timeout checks for this request. */ public void markAsLongCall() { this.longCall = true; } /** * Can be set from within {@link WebDispatcher#preDispatch(WebContext)} to manually handle incoming content. * * @param handler the handler to be supplied with content. If null, the default (memory/disk buffering) * handler is applied. */ public void setContentHandler(ContentHandler handler) { this.contentHandler = handler; } /** * Returns a value or parameter supplied by the request. *

* This method first checks if an attribute with the given key exists. If not, the query string is scanned. After * that, the posted content is looked through to find an appropriate value. * * @param key the key used to look for the value * @return a Value representing the provided data. */ @Nonnull public Value get(String key) { if (attribute != null && attribute.containsKey(key)) { return Value.of(attribute.get(key)); } if (queryString == null) { decodeQueryString(); } if (queryString.containsKey(key)) { List val = getParameters(key); if (val.size() == 1) { return Value.of(val.get(0)); } else if (val.isEmpty()) { return Value.of(null); } else { return Value.of(val); } } if (postDecoder != null) { try { InterfaceHttpData data = postDecoder.getBodyHttpData(key); if (data instanceof Attribute) { return Value.of(((Attribute) data).getValue()); } } catch (Throwable e) { Exceptions.handle() .to(WebServer.LOG) .error(e) .withSystemErrorMessage("Failed to fetch parameter %s: %s (%s)", key) .handle(); } } return Value.of(null); } /** * Returns the first non empty value for the given keys. *

* This is a boilerplate method for {@link #get(String)} in case the same value could be sent via different * parameter names. * * @param keys the keys to check * @return the first non empty value or an empty value if no data was found for all given keys. */ public Value getFirstFilled(String... keys) { for (String key : keys) { Value result = get(key); if (result.isFilled()) { return result; } } return Value.EMPTY; } /** * Determines if the parameter with the given name is contained in the request. Either as POST value or in the * query string. * * @param key the parameter to check for * @return true if the parameter is present (even if its value is null), false otherwise */ public boolean hasParameter(String key) { if (attribute != null && attribute.containsKey(key)) { return true; } if (postDecoder != null) { try { InterfaceHttpData data = postDecoder.getBodyHttpData(key); if (data instanceof Attribute) { return true; } } catch (Throwable e) { Exceptions.handle(WebServer.LOG, e); } } if (queryString == null) { decodeQueryString(); } return queryString.containsKey(key); } /** * Returns the value provided for the given key(s) or reports an error if no non empty value was found. *

* The first non empty value is used. If all values are empty, an exception is thrown. * * @param keys the keys to check for a value * @return the first non empty value found for one of the given keys */ public Value require(String... keys) { for (String key : keys) { Value result = get(key); if (result.isFilled()) { return result; } } throw Exceptions.createHandled() .withSystemErrorMessage( "A required parameter was not filled. Provide at least one value for: %s", Arrays.asList(keys)) .handle(); } /** * Returns the posted part with the given key. * * @param key used to specify which part of the post request should be returned. * @return the data provided for the given key or null if no data was supplied. */ public HttpData getHttpData(String key) { if (postDecoder == null) { return null; } try { InterfaceHttpData data = postDecoder.getBodyHttpData(key); if (data instanceof HttpData) { return (HttpData) data; } } catch (Throwable e) { Exceptions.handle(WebServer.LOG, e); } return null; } /** * Returns the file upload supplied for the given key. * * @param key used to specify which part of the post request should be used. * @return a file upload sent for the given key or null if no upload data is available */ public FileUpload getFileData(String key) { if (postDecoder == null) { return null; } try { InterfaceHttpData data = postDecoder.getBodyHttpData(key); if (data instanceof FileUpload) { return (FileUpload) data; } } catch (Throwable e) { Exceptions.handle(WebServer.LOG, e); } return null; } /** * Generates an ID (numeric value) which is unique withing this HTTP request. *

* This can be used to create IDs for HTML elements and the like. * * @return a locally unique ID as long as less than {@link Long#MAX_VALUE} IDs are requested. */ public long generateLocalId() { if (localIdGenerator == null) { localIdGenerator = new AtomicLong(1); } return localIdGenerator.getAndIncrement(); } /** * Sets an attribute for the current request. *

* Attributes are neither stored nor transmitted to the client. Therefore they are only visible during the * processing of this request. * * @param key name of the attribute * @param value value of the attribute */ public void setAttribute(String key, Object value) { if (attribute == null) { attribute = Maps.newTreeMap(); } attribute.put(key, value); } /* * Loads and parses the client session (cookie) */ private void initSession() { session = Maps.newHashMap(); String encodedSession = getCookieValue(sessionCookieName); if (Strings.isFilled(encodedSession)) { Tuple sessionInfo = Strings.split(encodedSession, ":"); if (checkSessionDataIntegrity(sessionInfo)) { QueryStringDecoder qsd = new QueryStringDecoder(encodedSession); for (Map.Entry> entry : qsd.parameters().entrySet()) { if (TTL_SESSION_KEY.equals(entry.getKey())) { sessionCookieTTL = Value.of(Iterables.getFirst(entry.getValue(), null)).getLong(); } else { session.put(entry.getKey(), Iterables.getFirst(entry.getValue(), null)); } } } else { WebServer.LOG.FINE("Resetting client session due to security breach: %s", encodedSession); } } } private boolean checkSessionDataIntegrity(Tuple sessionInfo) { return Strings.areEqual(sessionInfo.getFirst(), Hashing.sha512() .hashString(sessionInfo.getSecond() + getSessionSecret(), Charsets.UTF_8) .toString()); } /** * Sets an explicit session cookie TTL (time to live). *

* If a non null value is given, this will overwrite {@link #defaultSessionCookieTTL} for this request/response. * * @param customSessionCookieTTL the new TTL for the client session cookie. */ public void setCustomSessionCookieTTL(@Nullable Duration customSessionCookieTTL) { this.sessionCookieTTL = customSessionCookieTTL == null ? null : customSessionCookieTTL.getSeconds(); } /** * Stores a value in the client session. *

* As this session is transmitted to the client, the given value should not be large and needs a parseable * string representation * * @param key the name of th value to set * @param value the value to set */ public void setSessionValue(String key, Object value) { if (session == null) { initSession(); } if (value == null) { session.remove(key); } else { session.put(key, NLS.toMachineString(value)); } sessionModified = true; } /** * Loads a value from the client session * * @param key the name of the value to load * @return the value previously set in the session or an empty Value if no data is present */ public Value getSessionValue(String key) { if (session == null) { initSession(); } return Value.of(session.get(key)); } /** * Returns a list of all known session keys for the current session * * @return a list of all known keys for the current session */ public List getSessionKeys() { if (session == null) { return Collections.emptyList(); } return Lists.newArrayList(session.keySet()); } /** * Clears (invalidated) the client session by removing all values. */ public void clearSession() { if (session != null) { session.clear(); sessionModified = true; } } /** * Returns the server sided session based on the session parameter or cookie. *

* If no session was found, a new one is created if create is true. Otherwise null is * returned. * * @param create determines if a new session should be created if no active session was found * @return the session associated with the client (based on session id parameter or cookie) or null if * neither an active session was found nor a new one was created. */ public Optional getServerSession(boolean create) { if (serverSession != null) { return Optional.of(serverSession); } if (serverSessionSource == null) { requestedSessionId = getParameter(serverSessionParameterName); serverSessionSource = ServerSessionSource.PARAMETER; if (Strings.isEmpty(requestedSessionId)) { serverSessionSource = ServerSessionSource.COOKIE; requestedSessionId = getCookieValue(serverSessionCookieName); if (Strings.isEmpty(requestedSessionId)) { serverSessionSource = null; } } } if (Strings.isFilled(requestedSessionId)) { Optional sessionOptional = sessionManager.getSession(requestedSessionId); if (sessionOptional.isPresent()) { serverSession = sessionOptional.get(); return sessionOptional; } } if (!create) { return Optional.empty(); } serverSession = sessionManager.create(); serverSession.putValue(ServerSession.INITIAL_URI, getRequestedURI()); serverSession.putValue(ServerSession.USER_AGENT, getHeader(HttpHeaderNames.USER_AGENT)); serverSession.putValue(ServerSession.REMOTE_IP, getRemoteIP().toString()); return Optional.of(serverSession); } /** * Returns the server sided session based on the session parameter or cookie. *

* This method will create a new session if no active session was found. *

* This is a shortcut for {@code getServerSession(true)} * * @return the currently active session for this client. Will create a new session if no active session was found */ public ServerSession getServerSession() { return getServerSession(true).orElseThrow(() -> Exceptions.handle() .to(WebServer.LOG) .withSystemErrorMessage( "SessionManager was unable to create a session!") .handle()); } /** * Returns the session id requested by the client. * * @return the session id (server session) sent by the client. */ public String getRequestedSessionId() { if (serverSession == null) { getServerSession(false); } return requestedSessionId; } /** * Returns the source from which the server session id was obtained. *

* If a session id is submitted via cookie and via parameter, the parameter always has precedence. * * @return the source from which the session id for the current server session was obtained. */ public ServerSessionSource getServerSessionSource() { if (serverSessionSource == null && serverSession == null) { getServerSession(false); } return serverSessionSource; } /** * Returns the requested URI of the underlying HTTP request, without the query string * * @return the uri of the underlying request */ public String getRequestedURI() { if (requestedURI == null && request != null) { decodeQueryString(); } return requestedURI; } /** * Returns the base url (the protocol + host) for which this request was made. * * @return the base url which created this request, without the actual URI */ public String getBaseURL() { if (baseURL == null) { StringBuilder sb = new StringBuilder(); sb.append(isSSL() ? "https" : "http"); sb.append("://"); if (getRequest().headers().contains("X-Forwarded-Host")) { sb.append(getHeader("X-Forwarded-Host")); } else { sb.append(getHeader(HttpHeaderNames.HOST)); } baseURL = sb.toString(); } return baseURL; } /** * Returns the complete URL as requested by the browser. * * @return the complete url (base url + uri) which created this request */ public String getRequestedURL() { return getBaseURL() + getRequestedURI(); } /** * Returns the remote address which sent the request * * @return the remote address of the underlying TCP connection. This will take a X-Forwarded-For header into * account if the connection was opened from a known proxy ip. */ public InetAddress getRemoteIP() { if (remoteIp == null) { if (ctx == null || request == null) { try { return InetAddress.getByName("127.0.0.1"); } catch (UnknownHostException e) { throw Exceptions.handle(e); } } remoteIp = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress(); if (!WebServer.getProxyIPs().isEmpty()) { if (WebServer.getProxyIPs().accepts(remoteIp)) { Value forwardedFor = Value.of(request.headers().get("X-Forwarded-For")); if (forwardedFor.isFilled()) { try { // A X-Forwarded-For might contain many IPs like 1.2.3.4, 5.6.7.8... We're only interested // in the last IP -> cut appropriately Tuple splitIPs = Strings.splitAtLast(forwardedFor.asString(), ","); String forwardedForIp = Strings.isFilled(splitIPs.getSecond()) ? splitIPs.getSecond().trim() : splitIPs.getFirst().trim(); remoteIp = InetAddress.getByName(forwardedForIp); } catch (Throwable e) { Exceptions.ignore(e); WebServer.LOG.WARN(Strings.apply( "Cannot parse X-Forwarded-For address: %s, Remote-IP: %s, Request: %s, SSL: %s - %s (%s)", forwardedFor, remoteIp, request.uri(), NLS.toMachineString(isSSL()), e.getMessage(), e.getClass().getName())); } } } } } return remoteIp; } /** * Determines if the request is from a trusted IP. * * @return true if the request is from a trusted ip (see {@link WebServer#trustedIPs}), false * otherwise */ public boolean isTrusted() { if (trusted == null) { if (ctx == null) { return true; } trusted = WebServer.getTrustedRanges().accepts(getRemoteIP()); } return trusted; } /** * Determines if this is an HTTPS (SSL protected) call. * * @return true if this is an HTTPS request, false otherwise */ public boolean isSSL() { // If the request is coming from a SSL channel locally, ssl is already set true. if (ssl == null) { // Otherwise, we might sit behind a SSL offloading proxy, therefore we check // for the header "X-Forwarded-Proto". ssl = "https".equalsIgnoreCase(getHeaderValue("X-Forwarded-Proto").asString()); } return ssl; } /** * Determines if the current request is secured by SSL. *

* This is boilerplate for: {@code CallContext.getCurrent().get(WebContext.class).isSSL()} * * @return true if this is an HTTPS request, false otherwise */ public static boolean isCurrentRequestSSL() { return CallContext.getCurrent().get(WebContext.class).isSSL(); } /** * Returns the query string or POST parameter with the given name. *

* If a POST request with query string is present, parameters in the query string have precedence. * * @param key the name of the parameter to fetch * @return the first value or null if the parameter was not set or empty */ public String getParameter(String key) { return Iterables.getFirst(getParameters(key), null); } /** * Returns all query string or POST parameters with the given name. *

* If a POST request with query string is present, parameters in the query string have precedence. If values * in the query string are found, the POST parameters are discarded and not added to the resulting list. * * @param key the name of the parameter to fetch * @return all values in the query string */ public List getParameters(String key) { if (queryString == null) { decodeQueryString(); } if (queryString.containsKey(key)) { List result = queryString.get(key); if (result == null) { return Collections.emptyList(); } return result; } if (postDecoder != null) { try { List data = postDecoder.getBodyHttpDatas(key); if (data == null || data.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList(); for (InterfaceHttpData dataItem : data) { if (dataItem instanceof Attribute) { result.add(((Attribute) dataItem).getValue()); } } return result; } catch (Throwable e) { Exceptions.handle(WebServer.LOG, e); } } return Collections.emptyList(); } /* * Decodes the query string on demand */ private void decodeQueryString() { QueryStringDecoder qsd = new QueryStringDecoder(request.uri(), Charsets.UTF_8); requestedURI = QueryStringDecoder.decodeComponent(qsd.path()); queryString = qsd.parameters(); } /** * Returns all cookies submitted by the client * * @return a list of cookies sent by the client */ public Collection getCookies() { fillCookies(); return Collections.unmodifiableCollection(cookiesIn.values()); } /** * Returns a cookie with the given name, sent by the client * * @param name the cookie to fetch * @return the client cookie with the given name, or nzlnull if no matching cookie was found */ public Cookie getCookie(String name) { fillCookies(); return cookiesIn.get(name); } /* * Loads the cookies sent by the client */ private void fillCookies() { if (cookiesIn == null) { cookiesIn = Maps.newHashMap(); if (request != null) { String cookieHeader = request.headers().get(HttpHeaderNames.COOKIE); if (Strings.isFilled(cookieHeader)) { for (Cookie cookie : ServerCookieDecoder.LAX.decode(cookieHeader)) { this.cookiesIn.put(cookie.name(), cookie); } } } } } /** * Returns the data of the given client cookie wrapped as Value * * @param name the cookie to fetch * @return the contents of the cookie wrapped as Value */ @Nonnull public String getCookieValue(String name) { Cookie c = getCookie(name); if (c == null) { return null; } return c.value(); } /** * Sets the given cookie to be sent back to the client * * @param cookie the cookie to send to the client */ public void setCookie(Cookie cookie) { if (cookiesOut == null) { cookiesOut = Maps.newTreeMap(); } cookiesOut.put(cookie.name(), cookie); } /** * Sets a cookie value to be sent back to the client *

* The generated cookie will be a session cookie and varnish once the user agent is closed * * @param name the cookie to create * @param value the contents of the cookie */ public void setSessionCookie(String name, String value) { setCookie(name, value, Long.MIN_VALUE); } /** * Sets a http only cookie value to be sent back to the client. *

* The generated cookie will be a session cookie and varnish once the user agent is closed. Also this cookie * will not be accessible by JavaScript and therefore slightly more secure. * * @param name the cookie to create * @param value the contents of the cookie */ public void setHTTPSessionCookie(String name, String value) { DefaultCookie cookie = new DefaultCookie(name, value); cookie.setMaxAge(Long.MIN_VALUE); cookie.setHttpOnly(true); cookie.setPath("/"); setCookie(cookie); } /** * Sets a http only cookie value to be sent back to the client. * * @param name the cookie to create * @param value the contents of the cookie * @param maxAgeSeconds contains the max age of this cookie in seconds */ public void setCookie(String name, String value, long maxAgeSeconds) { DefaultCookie cookie = new DefaultCookie(name, value); cookie.setMaxAge(maxAgeSeconds); cookie.setPath("/"); setCookie(cookie); } /** * Removes the given cookie from the cookies sent back to the client. * * @param name the cookie to delete */ public void deleteCookie(@Nonnull String name) { setCookie(name, "", -1); } /** * Returns all cookies to be sent to the client. Used by {@link Response} to construct an appropriate header. * * @return a list of all cookies to be sent to the client. */ protected Collection getOutCookies() { if (serverSession != null && serverSession.isNew()) { setHTTPSessionCookie(serverSessionCookieName, serverSession.getId()); } if (sessionModified) { if (session.isEmpty()) { deleteCookie(sessionCookieName); } else { QueryStringEncoder encoder = new QueryStringEncoder(""); for (Map.Entry e : session.entrySet()) { encoder.addParam(e.getKey(), e.getValue()); } if (sessionCookieTTL != null) { encoder.addParam(TTL_SESSION_KEY, String.valueOf(sessionCookieTTL)); } String value = encoder.toString(); String protection = Hashing.sha512().hashString(value + getSessionSecret(), Charsets.UTF_8).toString(); long ttl = determineSessionCookieTTL(); if (ttl == 0) { setHTTPSessionCookie(sessionCookieName, protection + ":" + value); } else { setCookie(sessionCookieName, protection + ":" + value, ttl); } } } return cookiesOut == null ? null : cookiesOut.values(); } private long determineSessionCookieTTL() { if (sessionCookieTTL != null) { return sessionCookieTTL; } return defaultSessionCookieTTL.getSeconds(); } /** * Returns the accepted language of the client as two-letter language code. * * @return the two-letter code of the accepted language of the user agent. Returns the current language, if no * supported language was submitted. */ public String getLang() { if (lang == null) { lang = parseAcceptLanguage(); } return lang; } /* * Parses the accept language header */ private String parseAcceptLanguage() { double bestQ = 0; String lang = CallContext.getCurrent().getLang(); String header = getHeader(HttpHeaderNames.ACCEPT_LANGUAGE); if (Strings.isEmpty(header)) { return lang; } header = header.toLowerCase(); for (String str : header.split(",")) { String[] arr = str.trim().replace("-", "_").split(";"); //Parse the q-value double q = 1.0D; for (String s : arr) { s = s.trim(); if (s.startsWith("q=")) { q = Double.parseDouble(s.substring(2).trim()); break; } } //Parse the locale Locale locale = null; String[] l = arr[0].split("_"); if (l.length > 0 && q > bestQ && NLS.isSupportedLanguage(l[0])) { lang = l[0]; bestQ = q; } } return lang; } /* * Secret used to compute the protection keys for client sessions */ private String getSessionSecret() { if (Strings.isEmpty(sessionSecret)) { sessionSecret = UUID.randomUUID().toString(); } return sessionSecret; } /** * Creates a response for this request. * * @return a new response used to send data to the client. */ public Response respondWith() { return new Response(this); } /** * Determines if a response was already committed. *

* If a response is committed a HTTP state and some headers have already been sent. Therefore a new / other * response * cannot be created to this request. * * @return true if the response has been committed, false otherwise. */ public boolean isResponseCommitted() { return responseCommitted; } /** * Date format used by HTTP date headers */ public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; /** * Returns the request header with the given name * * @param header name of the header to fetch. * @return the value of the given header or null if no such header is present */ @Nullable public String getHeader(CharSequence header) { if (request == null) { return null; } return request.headers().get(header); } /** * Returns the request header wrapped as Value * * @param header name of the header to fetch. * @return the contents of the named header wrapped as Value */ @Nonnull public Value getHeaderValue(CharSequence header) { if (request == null) { return Value.EMPTY; } return Value.of(request.headers().get(header)); } /** * Returns the value of a date header as UNIX timestamp in milliseconds. * * @param header the name of the header to fetch * @return the value in milliseconds of the submitted date or 0 if the header was not present. */ public long getDateHeader(CharSequence header) { String value = request.headers().get(header); if (Strings.isEmpty(value)) { return 0; } try { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); return dateFormatter.parse(value).getTime(); } catch (ParseException e) { Exceptions.ignore(e); return 0; } } /** * Tries to perform a HTTP Basic authentication by parsing the Authorization header. *

* If no such header is found or if the contents are malformed, an 401 UNAUTHORIZED response will be generated * ({@link Response#unauthorized(String)}) and null will be returned. *

* In case the username and password returned by this method are invalid, use * {@link Response#unauthorized(String)} to notify the client. * * @param realm the realm to report to the client in case of missing or malformed credentials * @return a tuple containing username and password or null to indicate that a 401 UNAUTHORIZED response * was sent in order to make the client send credentials. */ @Nullable public Tuple tryBasicAuthentication(String realm) { String header = getHeaderValue("Authorization").asString(); if (Strings.isFilled(header) && header.startsWith("Basic ")) { header = header.substring(6); String nameAndPassword = new String(Base64.getDecoder().decode(header), Charsets.UTF_8); Tuple result = Strings.split(nameAndPassword, ":"); if (Strings.isFilled(result.getFirst()) && Strings.isFilled(result.getSecond())) { return result; } } respondWith().unauthorized(realm); return null; } /** * Returns a collection of all parameters names. *

* This will combine both, the query string and POST parameters. * * @return a collection of all parameters sent by the client */ public Collection getParameterNames() { if (queryString == null) { decodeQueryString(); } Set names = Sets.newTreeSet(queryString.keySet()); if (postDecoder != null) { try { for (InterfaceHttpData data : postDecoder.getBodyHttpDatas()) { names.add(data.getName()); } } catch (Throwable e) { Exceptions.handle(WebServer.LOG, e); } } return names; } /** * Returns the original query string sent by the client. *

* This will not include the initial question mark. * * @return the query string (x=y&z=a...) or an empty string if there is no query string */ @Nonnull public String getQueryString() { String fullURI = request.uri(); String uri = getRequestedURI(); if (uri.length() >= fullURI.length()) { return ""; } return request.uri().substring(uri.length() + 1); } /** * Returns the context prefix (constant path prefix). *

* Can be used to let the app behave like it would be hosted in a sub directory. * * @return the content prefix or "" if no prefix is set */ public static String getContextPrefix() { return contextPrefix; } /** * Returns the post decoder used to decode the posted data. * * @return the post decoder or null, if no post request is available */ public InterfaceHttpPostRequestDecoder getPostDecoder() { return postDecoder; } /** * Determines if the current request is a POST request. *

* A POST request signal the server to alter its state, knowing that side effects will occur. * * @return true if the method of the current request is POST, false otherwise */ public boolean isPOST() { return request.method() == HttpMethod.POST && !hidePost; } /** * Hide the fact that this request is a POST request. *

* Sometimes it is useful to make {@link #isPOST()} return false even if the * current request is a POST requests. Login forms woule be one example. As * a login request is sent to any URL, we don't want a common POST handler to * trigger on that post data. */ public void hidePost() { this.hidePost = true; } /* * Sets the post decoder used to decode the posted data */ void setPostDecoder(HttpPostRequestDecoder postDecoder) { this.postDecoder = postDecoder; } /** * Provides the body of the request as stream. * * @return an input stream reading from the body of the request. * @throws java.io.IOException in case of an io error */ public InputStream getContent() throws IOException { if (content == null) { return null; } if (!content.isInMemory()) { return new FileInputStream(content.getFile()); } //Backup the original size... contentSize = (long) content.getByteBuf().readableBytes(); return new ByteBufInputStream(content.getByteBuf()); } /** * Sets the charset of the body of the request. * * @param charset the charset to be applied to the body of the request */ public void setContentCharset(Charset charset) { if (content == null) { return; } content.setCharset(charset); } /** * Returns the charset of the body of the request * * @return the charset used by the body of the request */ public Charset getContentCharset() { if (content == null) { return Charsets.UTF_8; } return content.getCharset(); } /* * Caches the content size as the "readableBytes" value changes once a stream is on it. */ private Long contentSize; /** * Returns the size in bytes of the body of the request. * * @return the size in bytes of the http body. */ public long getContentSize() { if (contentSize == null) { try { if (content == null) { contentSize = 0L; } else if (!content.isInMemory()) { contentSize = content.getFile().length(); } else { contentSize = (long) content.getByteBuf().readableBytes(); } } catch (IOException e) { Exceptions.handle(WebServer.LOG, e); return 0; } } return contentSize; } /** * Returns the content of the HTTP request as file on disk. *

* Note that the file will be deleted once the request is completely handled. * * @return the file pointing to the content sent by the client * @throws IOException in case of an IO error */ public File getContentAsFile() throws IOException { if (content == null) { return null; } if (!content.isInMemory()) { return content.getFile(); } if (contentAsFile == null) { contentAsFile = File.createTempFile("http", ""); addFileToCleanup(contentAsFile); try (FileOutputStream outputStream = new FileOutputStream(contentAsFile)) { outputStream.write(content.get()); } } return contentAsFile; } /** * Adds a file to the cleanup list. *

* All files in this list will be deleted once the request is completely handled. This can be used to wipe * any intermediate files created while handling this request. * * @param file the file to be deleted once the request is completed. */ public void addFileToCleanup(File file) { if (filesToCleanup == null) { filesToCleanup = Lists.newArrayList(); } filesToCleanup.add(file); } /** * Returns the body of the HTTP request as XML data. *

* Note that all data is loaded into the heap. Therefore certain limits apply. If the data is too large, an * exception will be thrown. * * @return the body of the HTTP request as XML input */ public StructuredInput getXMLContent() { try { if (content == null) { throw Exceptions.handle() .to(WebServer.LOG) .withSystemErrorMessage("Expected valid XML as body of this request.") .handle(); } if (content.isInMemory()) { return new XMLStructuredInput(new ByteArrayInputStream(content.get()), true); } else { if (content.getFile().length() > maxStructuredInputSize && maxStructuredInputSize > 0) { throw Exceptions.handle() .to(WebServer.LOG) .withSystemErrorMessage( "Request body is too large to parse as XML. The limit is %d bytes", maxStructuredInputSize) .handle(); } return new XMLStructuredInput(new FileInputStream(content.getFile()), true); } } catch (HandledException e) { throw e; } catch (Throwable e) { throw Exceptions.handle() .to(WebServer.LOG) .error(e) .withSystemErrorMessage("Expected valid XML as body of this request: %s (%s).") .handle(); } } /** * Returns the body of the HTTP request as JSON data. *

* Note that all data is loaded into the heap. Therefore certain limits apply. If the data is too large, an * exception will be thrown. * * @return the body of the HTTP request as JSON input */ public JSONObject getJSONContent() { try { if (content == null) { throw Exceptions.handle() .to(WebServer.LOG) .withSystemErrorMessage("Expected a valid JSON map as body of this request.") .handle(); } if (content.isInMemory()) { return JSON.parseObject(content.getString(getRequestEncoding())); } else { if (content.getFile().length() > maxStructuredInputSize && maxStructuredInputSize > 0) { throw Exceptions.handle() .to(WebServer.LOG) .withSystemErrorMessage( "Request body is too large to parse as JSON. The limit is %d bytes", maxStructuredInputSize) .handle(); } return JSON.parseObject(content.getString(getRequestEncoding())); } } catch (HandledException e) { throw e; } catch (Throwable e) { throw Exceptions.handle() .to(WebServer.LOG) .error(e) .withSystemErrorMessage("Expected a valid JSON map as body of this request: %s (%s).") .handle(); } } /** * Tries to determine the charset used for the INCOMING request. *

* This is not to be confused with the desired charset of the outgoing data (specified via * Accept-Charset). * * @return the charset specified via Content-Type or UTF-8 if no value is given. */ public Charset getRequestEncoding() { try { Value contentType = getHeaderValue(HttpHeaderNames.CONTENT_TYPE); if (contentType.isFilled()) { for (String property : contentType.asString().split(";")) { Tuple nameValue = Strings.split(property.trim(), "="); if ("charset".equals(nameValue.getFirst())) { return Charset.forName(nameValue.getSecond()); } } } } catch (UnsupportedCharsetException e) { Exceptions.ignore(e); } return Charsets.UTF_8; } /** * Determines if a content is available for this request. * * @return true if content is available, false otherwise */ public boolean hasContent() { return content != null; } /** * Determines if the content body might contain XML (rather than JSON). *

* The detection is kind of crude as we only check if the first non whitespace character is a < * * @return true if the content is believed to be XML, false otherwise */ public boolean isContentProbablyXML() { if (!hasContent()) { return false; } try { try (Reader r = new InputStreamReader(getContent())) { // Trim whitespace and detect if the first readable character is a < int c; while ((c = r.read()) != -1) { if (!Character.isWhitespace(c)) { return c == '<'; } } return false; } } catch (HandledException e) { throw e; } catch (Throwable e) { throw Exceptions.handle() .to(WebServer.LOG) .error(e) .withSystemErrorMessage("Error parsing request content: %s (%s).") .handle(); } } /** * Releases all data associated with this request. */ void release() { if (contentHandler != null) { try { contentHandler.cleanup(); } catch (Exception e) { Exceptions.handle(WebServer.LOG, e); } contentHandler = null; } if (postDecoder != null) { try { postDecoder.cleanFiles(); } catch (Exception e) { Exceptions.handle(WebServer.LOG, e); } postDecoder = null; } if (content != null) { // Delete manually if anything like a file or so was allocated try { content.delete(); } catch (Exception e) { Exceptions.handle(WebServer.LOG, e); } // Also tell the factory to release all allocated data, as it keeps an internal reference to the request // (...along with all its data!). try { WebServer.getHttpDataFactory().cleanRequestHttpData(request); } catch (Exception e) { Exceptions.handle(WebServer.LOG, e); } content = null; contentAsFile = null; } if (filesToCleanup != null) { for (File file : filesToCleanup) { try { if (file != null && file.exists()) { file.delete(); } } catch (Exception e) { Exceptions.handle(WebServer.LOG, e); } } filesToCleanup = null; } } /** * Returns a token which can be added to dynamic asset-URLS (/asset/dynamic/TOKEN/...) to force a reload of the * specified resource. * * @return a random token which is guaranteed to be free of special chars (like / and the like) */ public String getDynamicAssetToken() { return Product.getProduct().getUniqueVersionString(); } @Override public String toString() { return "WebContext (Committed: " + responseCommitted + "): " + request.toString(); } @Override public SubContext fork() { // There is no reasonable way to clone this context. So we simply return the original instance as there // is only one request to answer anyway. return this; } @Override public void detach() { // Detaching the context from the current thread has no consequences as // a request cann be passed on to another thread... } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy