diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/server/Request.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/server/Request.java new file mode 100644 index 0000000..70bfc75 --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/server/Request.java @@ -0,0 +1,2484 @@ +/* + * Copyright (c) 2023 Fujitsu Limited. + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2008, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.grizzly.http.server; + +import java.io.CharConversionException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.security.auth.Subject; + +import org.glassfish.grizzly.Buffer; +import org.glassfish.grizzly.Connection; +import org.glassfish.grizzly.Grizzly; +import org.glassfish.grizzly.ReadHandler; +import org.glassfish.grizzly.ThreadCache; +import org.glassfish.grizzly.WriteHandler; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.http.Cookie; +import org.glassfish.grizzly.http.Cookies; +import org.glassfish.grizzly.http.HttpRequestPacket; +import org.glassfish.grizzly.http.Method; +import org.glassfish.grizzly.http.Note; +import org.glassfish.grizzly.http.Protocol; +import org.glassfish.grizzly.http.io.InputBuffer; +import org.glassfish.grizzly.http.io.NIOInputStream; +import org.glassfish.grizzly.http.io.NIOReader; +import org.glassfish.grizzly.http.server.http2.PushBuilder; +import org.glassfish.grizzly.http.server.io.ServerInputBuffer; +import org.glassfish.grizzly.http.server.util.Globals; +import org.glassfish.grizzly.http.server.util.MappingData; +import org.glassfish.grizzly.http.server.util.ParameterMap; +import org.glassfish.grizzly.http.server.util.RequestUtils; +import org.glassfish.grizzly.http.server.util.SimpleDateFormats; +import org.glassfish.grizzly.http.server.util.StringParser; +import org.glassfish.grizzly.http.util.Chunk; +import org.glassfish.grizzly.http.util.DataChunk; +import org.glassfish.grizzly.http.util.FastHttpDateFormat; +import org.glassfish.grizzly.http.util.Header; +import org.glassfish.grizzly.http.util.Parameters; +import org.glassfish.grizzly.localization.LogMessages; +import org.glassfish.grizzly.utils.Charsets; + +import static org.glassfish.grizzly.http.util.Constants.FORM_POST_CONTENT_TYPE; + +/** + * Wrapper object for the Coyote request. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + * @version $Revision: 1.2 $ $Date: 2007/03/14 02:15:42 $ + */ + +public class Request { + // @TODO remove this property support once we're sure nobody + // relies on this functionality. + private static final Boolean FORCE_CLIENT_AUTH_ON_GET_USER_PRINCIPAL = Boolean + .getBoolean(Request.class.getName() + ".force-client-auth-on-get-user-principal"); + + private static final Logger LOGGER = Grizzly.logger(Request.class); + + private static final ThreadCache.CachedTypeIndex CACHE_IDX = ThreadCache.obtainIndex(Request.class, 16); + + // Duplicated in http2 Constants. Keep values in sync. + private static final String HTTP2_PUSH_ENABLED = "http2-push-enabled"; + + private static final LocaleParser localeParser = new TagLocaleParser(); + private static final AtomicLong REQUEST_ID_GENERATOR = new AtomicLong(0); + + public static Request create() { + final Request request = ThreadCache.takeFromCache(CACHE_IDX); + if (request != null) { + return request; + } + + return new Request(new Response()); + } + + /** + * Request attribute will be associated with a boolean value indicating whether or not it's possible to transfer a + * {@link java.io.File} using sendfile. + * + * @since 2.2 + */ + public static final String SEND_FILE_ENABLED_ATTR = "org.glassfish.grizzly.http.SEND_FILE_ENABLED"; + + /** + *

+ * The value of this request attribute, as set by the developer must be a {@link java.io.File} that exists, is not a + * directory, and is readable. This {@link java.io.File} will be transferred using sendfile if + * {@link #SEND_FILE_ENABLED_ATTR} is true. If sendfile support isn't enabled, an IllegalStateException will be raised + * at runtime. The {@link HttpHandler} using this functionality should refrain from writing content via the response. + *

+ * + *

+ * Note that once this attribute is set, the sendfile process will begin. + *

+ * + * @since 2.2 + */ + public static final String SEND_FILE_ATTR = "org.glassfish.grizzly.http.SEND_FILE"; + + /** + *

+ * The value of this request attribute signifies the starting offset of the file transfer. If not specified, an offset + * of zero will be assumed. The type of the value must be {@link Long}. + *

+ * + *

+ * NOTE: In order for this attribute to take effect, it must be set before the {@link #SEND_FILE_ATTR} + * is set. + *

+ * + * @since 2.2 + */ + public static final String SEND_FILE_START_OFFSET_ATTR = "org.glassfish.grizzly.http.FILE_START_OFFSET"; + + /** + *

+ * The value of this request attribute signifies the total number of bytes to transfer. If not specified, the entire + * file will be transferred. The type of the value must be {@link Long} + *

+ * + *

+ * NOTE: In order for this attribute to take effect, it must be set before the {@link #SEND_FILE_ATTR} + * is set. + *

+ * + * @since 2.2 + */ + public static final String SEND_FILE_WRITE_LEN_ATTR = "org.glassfish.grizzly.http.FILE_WRITE_LEN"; + + // ------------------------------------------------------------- Properties + /** + * The match string for identifying a session ID parameter. + */ + private static final String match = ';' + Globals.SESSION_PARAMETER_NAME + '='; + + // -------------------------------------------------------------------- // + + public final MappingData obtainMappingData() { + if (cachedMappingData == null) { + cachedMappingData = new MappingData(); + } + + return cachedMappingData; + } + + // --------------------------------------------------------------------- // + + // ----------------------------------------------------- Instance Variables + + /** + * HTTP Request Packet + */ + protected HttpRequestPacket request; + private final String requestId = Long.toString(REQUEST_ID_GENERATOR.incrementAndGet()); + + protected FilterChainContext ctx; + + protected HttpServerFilter httpServerFilter; + + protected final List afterServicesList = new ArrayList<>(4); + + private Session session; + + /** + * The HTTP request scheme. + */ + private String scheme; + + private final PathData contextPath = new PathData(this, "", null); + private final PathData httpHandlerPath = new PathData(this); + private final PathData pathInfo = new PathData(this); + + private MappingData cachedMappingData; + + /** + * The set of cookies associated with this Request. + */ + protected Cookie[] cookies = null; + + protected Cookies rawCookies; + + // Session cookie name + protected String sessionCookieName; + + // Session manager + protected SessionManager sessionManager; + + /** + * The default Locale if none are specified. + */ + protected static final Locale defaultLocale = Locale.getDefault(); + + /** + * The preferred Locales associated with this Request. + */ + protected final ArrayList locales = new ArrayList<>(); + + /** + * The current dispatcher type. + */ + protected Object dispatcherType = null; + + /** + * The associated input buffer. + */ + protected final ServerInputBuffer inputBuffer = new ServerInputBuffer(); + + /** + * NIOInputStream. + */ + private final NIOInputStreamImpl inputStream = new NIOInputStreamImpl(); + + /** + * Reader. + */ + private final NIOReaderImpl reader = new NIOReaderImpl(); + + /** + * Using stream flag. + */ + protected boolean usingInputStream = false; + + /** + * Using writer flag. + */ + protected boolean usingReader = false; + + /** + * User principal. + */ + protected Principal userPrincipal = null; + + /** + * Session parsed flag. + */ + protected boolean sessionParsed = false; + + /** + * Request parameters parsed flag. + */ + protected boolean requestParametersParsed = false; + + /** + * Cookies parsed flag. + */ + protected boolean cookiesParsed = false; + + /** + * Secure flag. + */ + protected boolean secure = false; + + /** + * The Subject associated with the current AccessControllerContext + */ + protected Subject subject = null; + + /** + * Hash map used in the getParametersMap method. + */ + protected final ParameterMap parameterMap = new ParameterMap(); + + protected final Parameters parameters = new Parameters(); + + /** + * The current request dispatcher path. + */ + protected Object requestDispatcherPath = null; + + /** + * Was the requested session ID received in a cookie? + */ + protected boolean requestedSessionCookie = false; + + /** + * The requested session ID (if any) for this request. + */ + protected String requestedSessionId = null; + + /** + * Was the requested session ID received in a URL? + */ + protected boolean requestedSessionURL = false; + + /** + * Parse locales. + */ + protected boolean localesParsed = false; + + /** + * The string parser we will use for parsing request lines. + */ + private StringParser parser; + + // START S1AS 4703023 + /** + * The current application dispatch depth. + */ + private int dispatchDepth = 0; + + /** + * The maximum allowed application dispatch depth. + */ + private static int maxDispatchDepth = Constants.DEFAULT_MAX_DISPATCH_DEPTH; + // END S1AS 4703023 + + // START SJSAS 6346226 + private String jrouteId; + // END SJSAS 6346226 + + /** + * The {@link RequestExecutorProvider} responsible for executing user's code in + * {@link HttpHandler#service(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response)} + * and notifying {@link ReadHandler}, {@link WriteHandler} registered by the user + */ + private RequestExecutorProvider requestExecutorProvider; + + /** + * The response with which this request is associated. + */ + protected final Response response; + + /** + * Trailer headers, if any. + */ + protected Map trailers; + + private static boolean disableSchemeMappingValidation = Boolean.getBoolean("com.fujitsu.launcher.http.disableSchemeMappingValidation"); + + // ----------------------------------------------------------- Constructors + /** + * Temporarily introduce public constructor to fix GRIZZLY-1782. Just to make request instances proxiable. This + * constructor is not intended for client code consumption and should not be used explicitly as it creates an invalid + * instance. + * + * @deprecated + */ + @Deprecated + public Request() { + this.response = null; + } + + protected Request(final Response response) { + this.response = response; + } + + // --------------------------------------------------------- Public Methods + + public void initialize(final HttpRequestPacket request, final FilterChainContext ctx, final HttpServerFilter httpServerFilter) { + this.request = request; + this.ctx = ctx; + this.httpServerFilter = httpServerFilter; + inputBuffer.initialize(this, ctx); + + parameters.setHeaders(request.getHeaders()); + parameters.setQuery(request.getQueryStringDC()); + + final DataChunk remoteUser = request.remoteUser(); + + if (httpServerFilter != null) { + final ServerFilterConfiguration configuration = httpServerFilter.getConfiguration(); + parameters.setQueryStringEncoding(configuration.getDefaultQueryEncoding()); + + final BackendConfiguration backendConfiguration = configuration.getBackendConfiguration(); + + if (backendConfiguration != null) { + // Set the protocol scheme based on backend config + if (backendConfiguration.getScheme() != null) { + scheme = backendConfiguration.getScheme(); + } else if (backendConfiguration.getSchemeMapping() != null) { + String suggestedScheme = request.getHeader(backendConfiguration.getSchemeMapping()); + if (disableSchemeMappingValidation) { + scheme = suggestedScheme; + } else { + if (suggestedScheme == null || "http".equalsIgnoreCase(suggestedScheme) || "https".equalsIgnoreCase(suggestedScheme)) { + scheme = suggestedScheme; + } else { + scheme = "http"; + } + } + } + + if ("https".equalsIgnoreCase(scheme)) { + // this ensures that JSESSIONID cookie has the "Secure" attribute + // when using scheme-mapping + request.setSecure(true); + } + + if (remoteUser.isNull() && backendConfiguration.getRemoteUserMapping() != null) { + remoteUser.setString(request.getHeader(backendConfiguration.getRemoteUserMapping())); + } + } + } + + if (scheme == null) { + scheme = request.isSecure() ? "https" : "http"; + } + + if (!remoteUser.isNull()) { + setUserPrincipal(new GrizzlyPrincipal(remoteUser.toString())); + } + } + + final HttpServerFilter getServerFilter() { + return httpServerFilter; + } + + /** + * @return the Coyote request. + */ + public HttpRequestPacket getRequest() { + return this.request; + } + + /** + * @return the Response with which this Request is associated. + */ + public Response getResponse() { + return response; + } + + /** + * @return session cookie name, if not set default JSESSIONID name will be used + */ + public String getSessionCookieName() { + return obtainSessionCookieName(); + } + + /** + * Set the session cookie name, if not set default JSESSIONID name will be used + */ + public void setSessionCookieName(String sessionCookieName) { + this.sessionCookieName = sessionCookieName; + } + + /** + * @return true if HTTP/2 push is enabled, otherwise, false. + */ + public boolean isPushEnabled() { + final Boolean result = (Boolean) getContext().getConnection().getAttributes().getAttribute(HTTP2_PUSH_ENABLED); + return result != null ? result : false; + } + + /** + * @return {@link #sessionCookieName} if set, or the value returned by {@link SessionManager#getSessionCookieName()} if + * {@link #sessionCookieName} is not set. + */ + protected String obtainSessionCookieName() { + return sessionCookieName != null ? sessionCookieName : getSessionManager().getSessionCookieName(); + } + + /** + * @return {@link SessionManager} + */ + protected SessionManager getSessionManager() { + return sessionManager != null ? sessionManager : DefaultSessionManager.instance(); + } + + /** + * Set {@link SessionManager}, null value implies {@link DefaultSessionManager} + */ + protected void setSessionManager(final SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + /** + * @return the {@link Executor} responsible for notifying {@link ReadHandler}, {@link WriteHandler} associated with this + * Request processing. + */ + public Executor getRequestExecutor() { + return requestExecutorProvider.getExecutor(this); + } + + /** + * Sets @return the {@link RequestExecutorProvider} responsible for executing user's code in + * {@link HttpHandler#service(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response)} + * and notifying {@link ReadHandler}, {@link WriteHandler} registered by the user. + * + * @param requestExecutorProvider {@link RequestExecutorProvider} + */ + protected void setRequestExecutorProvider(final RequestExecutorProvider requestExecutorProvider) { + this.requestExecutorProvider = requestExecutorProvider; + } + + /** + * Add the listener, which will be notified, once Request processing will be finished. + * + * @param listener the listener, which will be notified, once Request processing will be finished. + */ + public void addAfterServiceListener(final AfterServiceListener listener) { + afterServicesList.add(listener); + } + + /** + * Remove the "after-service" listener, which was previously added by + * {@link #addAfterServiceListener(org.glassfish.grizzly.http.server.AfterServiceListener)}. + * + * @param listener the "after-service" listener, which was previously added by + * {@link #addAfterServiceListener(org.glassfish.grizzly.http.server.AfterServiceListener)}. + */ + public void removeAfterServiceListener(final AfterServiceListener listener) { + afterServicesList.remove(listener); + } + + protected void onAfterService() { + if (!inputBuffer.isFinished()) { + inputBuffer.terminate(); + } + + if (!afterServicesList.isEmpty()) { + for (final AfterServiceListener anAfterServicesList : afterServicesList) { + try { + anAfterServicesList.onAfterService(this); + } catch (Exception e) { + LOGGER.log(Level.WARNING, LogMessages.WARNING_GRIZZLY_HTTP_SERVER_REQUEST_AFTERSERVICE_NOTIFICATION_ERROR(), e); + } + } + } + } + + /** + * Release all object references, and initialize instance variables, in preparation for reuse of this object. + */ + protected void recycle() { + scheme = null; + contextPath.setPath(""); + httpHandlerPath.reset(); + pathInfo.reset(); + dispatcherType = null; + requestDispatcherPath = null; + + inputBuffer.recycle(); + inputStream.recycle(); + reader.recycle(); + usingInputStream = false; + usingReader = false; + userPrincipal = null; + subject = null; + sessionParsed = false; + requestParametersParsed = false; + cookiesParsed = false; + + if (rawCookies != null) { + rawCookies.recycle(); + } + + locales.clear(); + localesParsed = false; + secure = false; + + request.recycle(); + request = null; + ctx = null; + httpServerFilter = null; + + cookies = null; + requestedSessionId = null; + sessionCookieName = null; + sessionManager = null; + session = null; + dispatchDepth = 0; // S1AS 4703023 + + parameterMap.setLocked(false); + parameterMap.clear(); + parameters.recycle(); + + requestExecutorProvider = null; + + trailers = null; + + afterServicesList.clear(); + + // Notes holder shouldn't be recycled. + // notesHolder.recycle(); + + if (cachedMappingData != null) { + cachedMappingData.recycle(); + } + + ThreadCache.putToCache(CACHE_IDX, this); + } + + // -------------------------------------------------------- Request Methods + + /** + * @return the authorization credentials sent with this request. + */ + public String getAuthorization() { + return request.getHeader(Constants.AUTHORIZATION_HEADER); + } + + // ------------------------------------------------- Request Public Methods + + /** + * @return a new {@link PushBuilder} for issuing server push responses from the current request. If the current + * connection does not support server push, or server push has been disabled by the client, it will return + * null. + */ + public PushBuilder newPushBuilder() { + return isPushEnabled() ? new PushBuilder(this) : null; + } + + /** + * Replays request's payload by setting new payload {@link Buffer}. If request parameters have been parsed based on + * prev. request's POST payload - the parameters will be recycled and ready to be parsed again. + * + * @param buffer payload + * + * @throws IllegalStateException, if previous request payload has not been read off. + */ + public void replayPayload(final Buffer buffer) { + inputBuffer.replayPayload(buffer); + usingReader = false; + usingInputStream = false; + + if (Method.POST.equals(getMethod()) && requestParametersParsed) { + requestParametersParsed = false; + parameterMap.setLocked(false); + parameterMap.clear(); + parameters.recycle(); + } + } + + /** + * Create and return a NIOInputStream to read the content associated with this Request. + * + * @return {@link NIOInputStream} + */ + public NIOInputStream createInputStream() { + + inputStream.setInputBuffer(inputBuffer); + return inputStream; + } + + /** + * Create a named {@link Note} associated with this Request. + * + * @param the {@link Note} type. + * @param name the {@link Note} name. + * @return the {@link Note}. + */ + @SuppressWarnings({ "unchecked" }) + public static Note createNote(final String name) { + return HttpRequestPacket.createNote(name); + } + + /** + * Return the {@link Note} value associated with this Request, or null if no such binding exists. + * Use {@link #createNote(java.lang.String)} to create a new {@link Note}. + * + * @param note {@link Note} value to be returned + */ + public E getNote(final Note note) { + return request.getNote(note); + } + + /** + * Return a {@link Set} containing the String names of all note bindings that exist for this request. Use + * {@link #createNote(java.lang.String)} to create a new {@link Note}. + * + * @return a {@link Set} containing the String names of all note bindings that exist for this request. + */ + public Set getNoteNames() { + return request.getNoteNames(); + } + + /** + * Remove the {@link Note} value associated with this request. Use {@link #createNote(java.lang.String)} to create a new + * {@link Note}. + * + * @param note {@link Note} value to be removed + */ + public E removeNote(final Note note) { + return request.removeNote(note); + } + + /** + * Bind the {@link Note} value to this Request, replacing any existing binding for this name. Use + * {@link #createNote(java.lang.String)} to create a new {@link Note}. + * + * @param note {@link Note} to which the object should be bound + * @param value the {@link Note} value be bound to the specified {@link Note}. + */ + public void setNote(final Note note, final E value) { + request.setNote(note, value); + } + + /** + * Set the name of the server (virtual host) to process this request. + * + * @param name The server name + */ + public void setServerName(String name) { + request.serverName().setString(name); + } + + /** + * Set the port number of the server to process this request. + * + * @param port The server port + */ + public void setServerPort(int port) { + request.setServerPort(port); + } + + /** + * @return {@link HttpServerFilter}, which dispatched this request + */ + public HttpServerFilter getHttpFilter() { + return httpServerFilter; + } + + /** + * Returns 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 + * {@link HttpHandler}s in the default (root) context, this method returns "". The container does not decode this + * string. + * + * @return a String specifying the portion of the request URI that indicates the context of the request + */ + public String getContextPath() { + return contextPath.get(); + } + + protected void setContextPath(final String contextPath) { + this.contextPath.setPath(contextPath); + } + + protected void setContextPath(final PathResolver contextPath) { + this.contextPath.setResolver(contextPath); + } + + /** + * Returns the part of this request's URL that calls the HttpHandler. This includes either the HttpHandler name or a + * path to the HttpHandler, but does not include any extra path information or a query string. + * + * @return a String containing the name or path of the HttpHandler being called, as specified in the request URL + * @throws IllegalStateException if HttpHandler path was not set explicitly and attempt to URI-decode + * {@link org.glassfish.grizzly.http.util.RequestURIRef#getDecodedURI()} failed. + */ + public String getHttpHandlerPath() { + return httpHandlerPath.get(); + } + + protected void setHttpHandlerPath(final String httpHandlerPath) { + this.httpHandlerPath.setPath(httpHandlerPath); + } + + protected void setHttpHandlerPath(final PathResolver httpHandlerPath) { + this.httpHandlerPath.setResolver(httpHandlerPath); + } + + /** + * Returns any extra path information associated with the URL the client sent when it made this request. The extra path + * information follows the HttpHandler path but precedes the query string. This method returns null if there was no + * extra path information. + * + * @return a String specifying extra path information that comes after the HttpHandler path but before the query string + * in the request URL; or null if the URL does not have any extra path information + */ + public String getPathInfo() { + return pathInfo.get(); + } + + protected void setPathInfo(final String pathInfo) { + this.pathInfo.setPath(pathInfo); + } + + protected void setPathInfo(final PathResolver pathInfo) { + this.pathInfo.setResolver(pathInfo); + } + + // ------------------------------------------------- ServletRequest Methods + + /** + * Return the specified request attribute if it exists; otherwise, return null. + * + * @param name Name of the request attribute to return + * @return the specified request attribute if it exists; otherwise, return null. + */ + public Object getAttribute(final String name) { + if (SEND_FILE_ENABLED_ATTR.equals(name)) { + assert response != null; + return response.isSendFileEnabled(); + } + Object attribute = request.getAttribute(name); + + if (attribute != null) { + return attribute; + } + + if (Globals.SSL_CERTIFICATE_ATTR.equals(name)) { + attribute = RequestUtils.populateCertificateAttribute(this); + + if (attribute != null) { + request.setAttribute(name, attribute); + } + } else if (isSSLAttribute(name)) { + RequestUtils.populateSSLAttributes(this); + + attribute = request.getAttribute(name); + } else if (Globals.DISPATCHER_REQUEST_PATH_ATTR.equals(name)) { + return requestDispatcherPath; + } + + return attribute; + } + + /** + * Test if a given name is one of the special Servlet-spec SSL attributes. + */ + static boolean isSSLAttribute(final String name) { + return Globals.CERTIFICATES_ATTR.equals(name) || Globals.CIPHER_SUITE_ATTR.equals(name) || Globals.KEY_SIZE_ATTR.equals(name); + } + + /** + * Return the names of all request attributes for this Request, or an empty {@link Set} if there are none. + */ + public Set getAttributeNames() { + return request.getAttributeNames(); + } + + /** + * Return the character encoding for this Request. + */ + public String getCharacterEncoding() { + return request.getCharacterEncoding(); + } + + /** + * Return the content length for this Request. + */ + public int getContentLength() { + return (int) request.getContentLength(); + } + + /** + * Return the content length for this Request represented by Java long type. + */ + public long getContentLengthLong() { + return request.getContentLength(); + } + + /** + * Return the content type for this Request. + */ + public String getContentType() { + return request.getContentType(); + } + + /** + *

+ * Return the {@link InputStream} for this {@link Request}. + *

+ * + * By default the returned {@link NIOInputStream} will work as blocking {@link InputStream}, but it will be possible to + * call {@link NIOInputStream#isReady()}, {@link NIOInputStream#available()}, or + * {@link NIOInputStream#notifyAvailable(org.glassfish.grizzly.ReadHandler)} to avoid blocking. + * + * @return the {@link NIOInputStream} for this {@link Request}. + * + * @exception IllegalStateException if {@link #getReader()} or {@link #getNIOReader()} has already been called for this + * request. + * + * @since 2.2 + */ + public InputStream getInputStream() { + return getNIOInputStream(); + } + + /** + *

+ * Return the {@link NIOInputStream} for this {@link Request}. This stream will not block when reading content. + *

+ * + *

+ * NOTE: For now, in order to use non-blocking functionality, this method must be invoked before the + * {@link HttpHandler#service(Request, Response)} method returns. We hope to have this addressed in the next release. + *

+ * + * @return the {@link NIOInputStream} for this {@link Request}. + * + * @exception IllegalStateException if {@link #getReader()} or {@link #getNIOReader()} has already been called for this + * request. + */ + public NIOInputStream getNIOInputStream() { + if (usingReader) { + throw new IllegalStateException("Illegal attempt to call getInputStream() after getReader() has already been called."); + } + + usingInputStream = true; + inputStream.setInputBuffer(inputBuffer); + return inputStream; + } + + + /** + * @return true if this request requires acknowledgment. + */ + public boolean requiresAcknowledgement() { + return request.requiresAcknowledgement(); + } + + /** + * Return the preferred Locale that the client will accept content in, based on the value for the first + * Accept-Language header that was encountered. If the request did not specify a preferred language, the + * server's default Locale is returned. + */ + public Locale getLocale() { + + if (!localesParsed) { + parseLocales(); + } + + if (!locales.isEmpty()) { + return locales.get(0); + } else { + return defaultLocale; + } + + } + + /** + * Return the set of preferred Locales that the client will accept content in, based on the values for any + * Accept-Language headers that were encountered. If the request did not specify a preferred language, the + * server's default Locale is returned. + */ + public List getLocales() { + + if (!localesParsed) { + parseLocales(); + } + + if (!locales.isEmpty()) { + return locales; + } + + final ArrayList results = new ArrayList<>(); + results.add(defaultLocale); + return results; + + } + + /** + * Returns the low-level parameters holder for finer control over parameters. + * + * @return {@link Parameters}. + */ + public Parameters getParameters() { + return parameters; + } + + /** + * Return the value of the specified request parameter, if any; otherwise, return null. If there is more + * than one value defined, return only the first one. + * + * @param name Name of the desired request parameter + */ + public String getParameter(final String name) { + + if (!requestParametersParsed) { + parseRequestParameters(); + } + + return parameters.getParameter(name); + + } + + /** + * Returns a {@link java.util.Map} of the parameters of this request. Request parameters are extra information sent with + * the request. For HTTP servlets, parameters are contained in the query string or posted form data. + * + * @return A {@link java.util.Map} containing parameter names as keys and parameter values as map values. + */ + public Map getParameterMap() { + + if (parameterMap.isLocked()) { + return parameterMap; + } + + for (final String name : getParameterNames()) { + final String[] values = getParameterValues(name); + parameterMap.put(name, values); + } + + parameterMap.setLocked(true); + + return parameterMap; + + } + + /** + * Return the names of all defined request parameters for this request. + */ + public Set getParameterNames() { + + if (!requestParametersParsed) { + parseRequestParameters(); + } + + return parameters.getParameterNames(); + + } + + /** + * Return the defined values for the specified request parameter, if any; otherwise, return null. + * + * @param name Name of the desired request parameter + */ + public String[] getParameterValues(String name) { + + if (!requestParametersParsed) { + parseRequestParameters(); + } + + return parameters.getParameterValues(name); + + } + + /** + * Return the protocol and version used to make this Request. + */ + public Protocol getProtocol() { + return request.getProtocol(); + } + + /** + *

+ * Returns the {@link Reader} associated with this {@link Request}. + *

+ * + * By default the returned {@link NIOReader} will work as blocking {@link java.io.Reader}, but it will be possible to + * call {@link NIOReader#isReady()} or {@link NIOReader#notifyAvailable(org.glassfish.grizzly.ReadHandler)} to avoid + * blocking. + * + * @return the {@link NIOReader} associated with this {@link Request}. + * + * @throws IllegalStateException if {@link #getInputStream()} or {@link #getNIOInputStream()} has already been called + * for this request. + * + * @since 2.2 + */ + public Reader getReader() { + return getNIOReader(); + } + + /** + *

+ * Returns the {@link NIOReader} associated with this {@link Request}. This {@link NIOReader} will not block while + * reading content. + *

+ * + * @return {@link NIOReader} + * @throws IllegalStateException if {@link #getInputStream()} or {@link #getNIOInputStream()} has already been called + * for this request. + */ + public NIOReader getNIOReader() { + if (usingInputStream) { + throw new IllegalStateException("Illegal attempt to call getReader() after getInputStream() has alread been called."); + } + + usingReader = true; + inputBuffer.processingChars(); + reader.setInputBuffer(inputBuffer); + return reader; + } + + /** + * Return the remote IP address making this Request. + */ + public String getRemoteAddr() { + return request.getRemoteAddress(); + } + + /** + * Return the remote host name making this Request. + */ + public String getRemoteHost() { + return request.getRemoteHost(); + } + + /** + * Returns the Internet Protocol (IP) source port of the client or last proxy that sent the request. + */ + public int getRemotePort() { + return request.getRemotePort(); + } + + /** + * Returns the host name of the Internet Protocol (IP) interface on which the request was received. + */ + public String getLocalName() { + return request.getLocalName(); + } + + /** + * Returns the Internet Protocol (IP) address of the interface on which the request was received. + */ + public String getLocalAddr() { + return request.getLocalAddress(); + } + + /** + * Returns the Internet Protocol (IP) port number of the interface on which the request was received. + */ + public int getLocalPort() { + return request.getLocalPort(); + } + + /** + * @return the scheme used to make this Request. + */ + public String getScheme() { + return scheme; + } + + /** + * @return the server name responding to this Request. + */ + public String getServerName() { + return request.serverName().toString(); + } + + /** + * @return the server port responding to this Request. + */ + public int getServerPort() { + return request.getServerPort(); + } + + /** + * @return true if this request received on a secure connection + */ + public boolean isSecure() { + return request.isSecure(); + } + + /** + * Remove the specified request attribute if it exists. + * + * @param name Name of the request attribute to remove + */ + public void removeAttribute(String name) { + request.removeAttribute(name); + } + + /** + * Set the specified request attribute to the specified value. + * + * @param name Name of the request attribute to set + * @param value The associated value + */ + public void setAttribute(final String name, final Object value) { + + // Name cannot be null + if (name == null) { + throw new IllegalArgumentException("Argument 'name' cannot be null"); + } + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + if (name.equals(Globals.DISPATCHER_TYPE_ATTR)) { + dispatcherType = value; + return; + } else if (name.equals(Globals.DISPATCHER_REQUEST_PATH_ATTR)) { + requestDispatcherPath = value; + return; + } + + request.setAttribute(name, value); + + assert response != null; + if (response.isSendFileEnabled() && SEND_FILE_ATTR.equals(name)) { + RequestUtils.handleSendFile(this); + } + + } + + /** + * Overrides the name of the character encoding used in the body of this request. + * + * This method must be called prior to reading request parameters or reading input using getReader(). + * Otherwise, it has no effect. + * + * @param encoding String containing the name of the character encoding. + * @throws java.io.UnsupportedEncodingException if this ServletRequest is still in a state where a character encoding + * may be set, but the specified encoding is invalid + * + * @since Servlet 2.3 + */ + public void setCharacterEncoding(final String encoding) throws UnsupportedEncodingException { + + // START SJSAS 4936855 + if (requestParametersParsed || usingReader) { + return; + } + // END SJSAS 4936855 + + Charsets.lookupCharset(encoding); + + // Save the validated encoding + request.setCharacterEncoding(encoding); + + } + + /** + * Static setter method for the maximum dispatch depth + */ + public static void setMaxDispatchDepth(int depth) { + maxDispatchDepth = depth; + } + + public static int getMaxDispatchDepth() { + return maxDispatchDepth; + } + + /** + * Increment the depth of application dispatch + */ + public int incrementDispatchDepth() { + return ++dispatchDepth; + } + + /** + * Decrement the depth of application dispatch + */ + public int decrementDispatchDepth() { + return --dispatchDepth; + } + + /** + * Check if the application dispatching has reached the maximum + */ + public boolean isMaxDispatchDepthReached() { + return dispatchDepth > maxDispatchDepth; + } + + /** + * @return identifier of the request generated by this class. + */ + public String getRequestId() { + return this.requestId; + } + + /** + * @return an empty string or identifier managed by the underlying protocol (HTTP2) + */ + public String getProtocolRequestId() { + return this.request.getProtocolRequestId(); + } + + /** + * @return underlying connection used by the request + */ + public Connection getConnection() { + return this.request.getConnection(); + } + + // ---------------------------------------------------- HttpRequest Methods + + /** + * Add a Cookie to the set of Cookies associated with this Request. + * + * @param cookie The new cookie + */ + public void addCookie(Cookie cookie) { + + // For compatibility only + if (!cookiesParsed) { + parseCookies(); + } + + int size = 0; + if (cookie != null) { + size = cookies.length; + } + + Cookie[] newCookies = new Cookie[size + 1]; + System.arraycopy(cookies, 0, newCookies, 0, size); + newCookies[size] = cookie; + + cookies = newCookies; + + } + + /** + * Add a Locale to the set of preferred Locales for this Request. The first added Locale will be the first one returned + * by getLocales(). + * + * @param locale The new preferred Locale + */ + public void addLocale(Locale locale) { + locales.add(locale); + } + + /** + * Add a parameter name and corresponding set of values to this Request. (This is used when restoring the original + * request on a form based login). + * + * @param name Name of this request parameter + * @param values Corresponding values for this request parameter + */ + public void addParameter(String name, String values[]) { + parameters.addParameterValues(name, values); + } + + /** + * Clear the collection of Cookies associated with this Request. + */ + public void clearCookies() { + cookiesParsed = true; + cookies = null; + } + + /** + * Clear the collection of Headers associated with this Request. + */ + public void clearHeaders() { + // Not used + } + + /** + * Clear the collection of Locales associated with this Request. + */ + public void clearLocales() { + locales.clear(); + } + + /** + * Clear the collection of parameters associated with this Request. + */ + public void clearParameters() { + // Not used + } + + /** + * Get the decoded request URI. + * + * @return the URL decoded request URI + */ + public String getDecodedRequestURI() throws CharConversionException { + return request.getRequestURIRef().getDecodedURI(); + } + + /** + * Set the Principal who has been authenticated for this Request. This value is also used to calculate the value to be + * returned by the getRemoteUser() method. + * + * @param principal The user Principal + */ + public void setUserPrincipal(Principal principal) { + this.userPrincipal = principal; + } + + // --------------------------------------------- HttpServletRequest Methods + + /** + * Return the authentication type used for this Request. + */ + public String getAuthType() { + return request.authType().toString(); + } + + /** + * Return the set of Cookies received with this Request. + */ + public Cookie[] getCookies() { + + if (!cookiesParsed) { + parseCookies(); + } + + return cookies; + + } + + /** + * Set the set of cookies received with this Request. + */ + public void setCookies(Cookie[] cookies) { + + this.cookies = cookies; + + } + + /** + * Return the value of the specified date header, if any; otherwise return -1. + * + * @param name Name of the requested date header + * + * @exception IllegalArgumentException if the specified header value cannot be converted to a date + */ + public long getDateHeader(String name) { + + String value = getHeader(name); + if (value == null) { + return -1L; + } + + final SimpleDateFormats formats = SimpleDateFormats.create(); + + try { + // Attempt to convert the date header in a variety of formats + long result = FastHttpDateFormat.parseDate(value, formats.getFormats()); + if (result != -1L) { + return result; + } + throw new IllegalArgumentException(value); + } finally { + formats.recycle(); + } + } + + /** + * Return the value of the specified date header, if any; otherwise return -1. + * + * @param header the requested date {@link Header} + * + * @exception IllegalArgumentException if the specified header value cannot be converted to a date + * + * @since 2.1.2 + */ + public long getDateHeader(Header header) { + + String value = getHeader(header); + if (value == null) { + return -1L; + } + + final SimpleDateFormats formats = SimpleDateFormats.create(); + + try { + // Attempt to convert the date header in a variety of formats + long result = FastHttpDateFormat.parseDate(value, formats.getFormats()); + if (result != -1L) { + return result; + } + throw new IllegalArgumentException(value); + } finally { + formats.recycle(); + } + } + + /** + * Return the first value of the specified header, if any; otherwise, return null + * + * @param name Name of the requested header + */ + public String getHeader(String name) { + return request.getHeader(name); + } + + /** + * Return the first value of the specified header, if any; otherwise, return null + * + * @param header the requested {@link Header} + * + * @since 2.1.2 + */ + public String getHeader(final Header header) { + return request.getHeader(header); + } + + /** + * Return all of the values of the specified header, if any; otherwise, return an empty enumeration. + * + * @param name Name of the requested header + */ + public Iterable getHeaders(String name) { + return request.getHeaders().values(name); + } + + /** + * Return all of the values of the specified header, if any; otherwise, return an empty enumeration. + * + * @param header the requested {@link Header} + * + * @since 2.1.2 + */ + public Iterable getHeaders(final Header header) { + return request.getHeaders().values(header); + } + + /** + * Get the request trailer headers. Values may only be returned in the case of HTTP/1.1 when the request is using the + * transfer-encoding chunked or in HTTP/2 sends a second HEADERS frame + * terminating the stream. An empty Map will be returned in all other cases. + * + * While headers are typical case insensitive, the headers stored in the returned Map will be done so in + * lower-case. + * + * This method should typically be called after the application has read the request body. It is safe to invoke this + * method if there is no body content. + * + * @return A {@link Map} of trailers headers, if any were present. + * + * @throws IllegalStateException if neither {@link ReadHandler#onAllDataRead} has been called or an EOF indication has + * been returned from the {@link #getReader}, {@link #getNIOReader()}, {@link #getInputStream}, + * {@link #getNIOInputStream()}. + * + * @see #areTrailersAvailable() + * + * @since 2.4.0 + */ + public Map getTrailers() { + if (inputBuffer.isFinished()) { + return inputBuffer.getTrailers(); + } + throw new IllegalStateException(); + } + + /** + * @return true if trailers are available to be accessed otherwise returns false. + * + * @since 2.4.0 + */ + public boolean areTrailersAvailable() { + return inputBuffer.areTrailersAvailable(); + } + + /** + * Return the names of all headers received with this request. + */ + public Iterable getHeaderNames() { + return request.getHeaders().names(); + } + + /** + * Return the value of the specified header as an integer, or -1 if there is no such header for this request. + * + * @param name Name of the requested header + * + * @exception IllegalArgumentException if the specified header value cannot be converted to an integer + */ + public int getIntHeader(String name) { + + String value = getHeader(name); + if (value == null) { + return -1; + } else { + return Integer.parseInt(value); + } + + } + + /** + * Return the value of the specified header as an integer, or -1 if there is no such header for this request. + * + * @param header the requested {@link Header} + * + * @exception IllegalArgumentException if the specified header value cannot be converted to an integer + * + * @since 2.1.2 + */ + public int getIntHeader(final Header header) { + + String value = getHeader(header); + if (value == null) { + return -1; + } else { + return Integer.parseInt(value); + } + + } + + /** + * Return the HTTP request method used in this Request. + */ + public Method getMethod() { + return request.getMethod(); + } + + /** + * Sets the HTTP request method used in this Request. + * + * @param method the HTTP request method used in this Request. + */ + public void setMethod(String method) { + request.setMethod(method); + } + + /** + * @return the query string associated with this request. + */ + public String getQueryString() { + final String queryString = request.getQueryStringDC().toString(parameters.getQueryStringEncoding()); + + return queryString == null || queryString.isEmpty() ? null : queryString; + } + + /** + * Sets the query string associated with this request. + * + * @param queryString the query string associated with this request. + */ + public void setQueryString(String queryString) { + request.setQueryString(queryString); + } + + /** + * Return the name of the remote user that has been authenticated for this Request. + */ + public String getRemoteUser() { + + if (userPrincipal != null) { + return userPrincipal.getName(); + } else { + return null; + } + + } + + /** + * Return the session identifier included in this request, if any. + */ + public String getRequestedSessionId() { + return requestedSessionId; + } + + /** + * Return the request URI for this request. + */ + public String getRequestURI() { + return request.getRequestURI(); + } + + /** + * Sets the request URI for this request. + * + * @param uri the request URI for this request. + */ + public void setRequestURI(String uri) { + request.setRequestURI(uri); + } + + /** + * 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. + *

+ * Because this method returns a StringBuilder, not a String, you can modify the URL easily, + * for example, to append query parameters. + *

+ * This method is useful for creating redirect messages and for reporting errors. + * + * @return A StringBuffer object containing the reconstructed URL + */ + public StringBuilder getRequestURL() { + + final StringBuilder url = new StringBuilder(); + return appendRequestURL(this, url); + + } + + /** + * Appends the reconstructed URL the client used to make the request. The appended URL contains a protocol, server name, + * port number, and server path, but it does not include query string parameters. + *

+ * Because this method returns a StringBuilder, not a String, you can modify the URL easily, + * for example, to append query parameters. + *

+ * This method is useful for creating redirect messages and for reporting errors. + * + * @return A StringBuilder object containing the appended reconstructed URL + */ + public static StringBuilder appendRequestURL(final Request request, final StringBuilder buffer) { + + final String scheme = request.getScheme(); + int port = request.getServerPort(); + if (port < 0) { + port = 80; // Work around java.net.URL bug + } + + buffer.append(scheme); + buffer.append("://"); + buffer.append(request.getServerName()); + if (scheme.equals("http") && port != 80 || scheme.equals("https") && port != 443) { + buffer.append(':'); + buffer.append(port); + } + buffer.append(request.getRequestURI()); + return buffer; + + } + + /** + * Appends the reconstructed URL the client used to make the request. The appended URL contains a protocol, server name, + * port number, and server path, but it does not include query string parameters. + *

+ * Because this method returns a StringBuffer, not a String, you can modify the URL easily, + * for example, to append query parameters. + *

+ * This method is useful for creating redirect messages and for reporting errors. + * + * @return A StringBuffer object containing the appended reconstructed URL + */ + public static StringBuffer appendRequestURL(final Request request, final StringBuffer buffer) { + + final String scheme = request.getScheme(); + int port = request.getServerPort(); + if (port < 0) { + port = 80; // Work around java.net.URL bug + } + + buffer.append(scheme); + buffer.append("://"); + buffer.append(request.getServerName()); + if (scheme.equals("http") && port != 80 || scheme.equals("https") && port != 443) { + buffer.append(':'); + buffer.append(port); + } + buffer.append(request.getRequestURI()); + return buffer; + + } + + /** + * Return the principal that has been authenticated for this Request. + */ + public Principal getUserPrincipal() { + if (userPrincipal == null) { + if (getRequest().isSecure()) { + X509Certificate[] certs = (X509Certificate[]) getAttribute(Globals.CERTIFICATES_ATTR); + if (FORCE_CLIENT_AUTH_ON_GET_USER_PRINCIPAL && (certs == null || certs.length < 1)) { + // Force SSL re-handshake and request client auth + certs = (X509Certificate[]) getAttribute(Globals.SSL_CERTIFICATE_ATTR); + } + + if (certs != null && certs.length > 0) { + userPrincipal = certs[0].getSubjectX500Principal(); + } + } + } + + return userPrincipal; + } + + public FilterChainContext getContext() { + return ctx; + } + + protected String unescape(String s) { + if (s == null) { + return null; + } + if (s.indexOf('\\') == -1) { + return s; + } + + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c != '\\') { + buf.append(c); + } else { + if (++i >= s.length()) { + // invalid escape, hence invalid cookie + throw new IllegalArgumentException(); + } + c = s.charAt(i); + buf.append(c); + } + } + return buf.toString(); + } + + /** + * Parse cookies. + */ + protected void parseCookies() { + cookiesParsed = true; + + final Cookies serverCookies = getRawCookies(); + cookies = serverCookies.get(); + + } + + /** + * @return the {@link InputBuffer} associated with this request, which is the source for {@link #getInputStream()}, + * {@link #getReader()}, {@link #getNIOInputStream()}, and {@link #getNIOReader()} + */ + public InputBuffer getInputBuffer() { + + return inputBuffer; + + } + + /** + * This method may be used if some other entity processed request parameters and wishes to expose them via the request. + * When this method is called, it will mark the internal request parameter state as having been processed. + * + * @param parameters the parameters to expose via this request. + * + * @since 2.2 + */ + public void setRequestParameters(final Parameters parameters) { + + this.requestParametersParsed = true; + for (final String name : parameters.getParameterNames()) { + this.parameters.addParameterValues(name, parameters.getParameterValues(name)); + } + + } + + /** + * TODO DOCS + */ + protected Cookies getRawCookies() { + if (rawCookies == null) { + rawCookies = new Cookies(); + } + if (!rawCookies.initialized()) { + rawCookies.setHeaders(request.getHeaders()); + } + + return rawCookies; + } + + /** + * Parse request parameters. + */ + protected void parseRequestParameters() { + + // Delay updating requestParametersParsed to TRUE until + // after getCharacterEncoding() has been called, because + // getCharacterEncoding() may cause setCharacterEncoding() to be + // called, and the latter will ignore the specified encoding if + // requestParametersParsed is TRUE + requestParametersParsed = true; + + Charset charset = null; + + if (parameters.getEncoding() == null) { + // getCharacterEncoding() may have been overridden to search for + // hidden form field containing request encoding + charset = lookupCharset(getCharacterEncoding()); + + parameters.setEncoding(charset); + } + + if (parameters.getQueryStringEncoding() == null) { + if (charset == null) { + // getCharacterEncoding() may have been overridden to search for + // hidden form field containing request encoding + charset = lookupCharset(getCharacterEncoding()); + } + + parameters.setQueryStringEncoding(charset); + } + + parameters.handleQueryParameters(); + + if (usingInputStream || usingReader) { + return; + } + + if (!Method.POST.equals(getMethod())) { + return; + } + + if (!checkPostContentType(getContentType())) { + return; + } + + final int maxFormPostSize = httpServerFilter.getConfiguration().getMaxFormPostSize(); + + int len = getContentLength(); + if (len < 0) { + if (!request.isChunked()) { + return; + } + + len = maxFormPostSize; + } + + if (maxFormPostSize > 0 && len > maxFormPostSize) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning(LogMessages.WARNING_GRIZZLY_HTTP_SERVER_REQUEST_POST_TOO_LARGE()); + } + + throw new IllegalStateException(LogMessages.WARNING_GRIZZLY_HTTP_SERVER_REQUEST_POST_TOO_LARGE()); + } + + int read = 0; + try { + final Buffer formData = getPostBody(len); + read = formData.remaining(); + parameters.processParameters(formData, formData.position(), read); + } catch (Exception ignored) { + } finally { + try { + skipPostBody(read); + } catch (Exception e) { + LOGGER.log(Level.WARNING, LogMessages.WARNING_GRIZZLY_HTTP_SERVER_REQUEST_BODY_SKIP(), e); + } + } + + } + + private Charset lookupCharset(final String enc) { + Charset charset; + if (enc != null) { + try { + charset = Charsets.lookupCharset(enc); + } catch (Exception e) { + charset = org.glassfish.grizzly.http.util.Constants.DEFAULT_HTTP_CHARSET; + } + } else { + charset = org.glassfish.grizzly.http.util.Constants.DEFAULT_HTTP_CHARSET; + } + + return charset; + } + + private boolean checkPostContentType(final String contentType) { + return contentType != null && contentType.trim().startsWith(FORM_POST_CONTENT_TYPE); + + } + + /** + * Gets the POST body of this request. + * + * @return The POST body of this request + */ + public Buffer getPostBody(final int len) throws IOException { + inputBuffer.fillFully(len); + return inputBuffer.getBuffer(); + } + + /** + * Skips the POST body of this request. + * + * @param len how much of the POST body to skip. + */ + protected void skipPostBody(final int len) throws IOException { + inputBuffer.skip(len); + } + + /** + * Parse request locales. + */ + protected void parseLocales() { + + localesParsed = true; + + final Iterable values = getHeaders("accept-language"); + + for (String value : values) { + parseLocalesHeader(value); + } + + } + + /** + * Parse accept-language header value. + */ + protected void parseLocalesHeader(String value) { + + // Store the accumulated languages that have been requested in + // a local collection, sorted by the quality value (so we can + // add Locales in descending order). The values will be ArrayLists + // containing the corresponding Locales to be added + TreeMap> localLocalesMap = new TreeMap<>(); + + // Preprocess the value to remove all whitespace + int white = value.indexOf(' '); + if (white < 0) { + white = value.indexOf('\t'); + } + if (white >= 0) { + int len = value.length(); + StringBuilder sb = new StringBuilder(len - 1); + for (int i = 0; i < len; i++) { + char ch = value.charAt(i); + if (ch != ' ' && ch != '\t') { + sb.append(ch); + } + } + value = sb.toString(); + } + + if (parser == null) { + parser = new StringParser(); + } + + // Process each comma-delimited language specification + parser.setString(value); // ASSERT: parser is available to us + int length = parser.getLength(); + while (true) { + + // Extract the next comma-delimited entry + int start = parser.getIndex(); + if (start >= length) { + break; + } + int end = parser.findChar(','); + String entry = parser.extract(start, end).trim(); + parser.advance(); // For the following entry + + // Extract the quality factor for this entry + double quality = 1.0; + int semi = entry.indexOf(";q="); + if (semi >= 0) { + final String qvalue = entry.substring(semi + 3); + // qvalues, according to the RFC, may not contain more + // than three values after the decimal. + if (qvalue.length() <= 5) { + try { + quality = Double.parseDouble(qvalue); + } catch (NumberFormatException e) { + quality = 0.0; + } + } else { + quality = 0.0; + } + entry = entry.substring(0, semi); + } + + // Skip entries we are not going to keep track of + if (quality < 0.00005) { + continue; // Zero (or effectively zero) quality factors + } + if ("*".equals(entry)) { + continue; // FIXME - "*" entries are not handled + } + + Locale locale = localeParser.parseLocale(entry); + if (locale == null) { + continue; + } + + // Add a new Locale to the list of Locales for this quality level + Double key = -quality; // Reverse the order + List values = localLocalesMap.get(key); + if (values == null) { + values = new ArrayList<>(); + localLocalesMap.put(key, values); + } + values.add(locale); + + } + + // Process the quality values in highest->lowest order (due to + // negating the Double value when creating the key) + for (List localLocales : localLocalesMap.values()) { + for (Locale locale : localLocales) { + addLocale(locale); + } + } + + } + + /** + * Parses the value of the JROUTE cookie, if present. + */ + void parseJrouteCookie() { + if (!cookiesParsed) { + parseCookies(); + } + + final Cookie cookie = getRawCookies().findByName(Constants.JROUTE_COOKIE); + if (cookie != null) { + setJrouteId(cookie.getValue()); + } + } + + /** + * @return true if the given string is composed of upper- or lowercase letters only, false + * otherwise. + */ + static boolean isAlpha(String value) { + + if (value == null) { + return false; + } + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { + return false; + } + } + + return true; + } + + /** + * Sets the jroute id of this request. + * + * @param jrouteId The jroute id + */ + void setJrouteId(String jrouteId) { + this.jrouteId = jrouteId; + } + + /** + * Gets the jroute id of this request, which may have been sent as a separate JROUTE cookie or appended to + * the session identifier encoded in the URI (if cookies have been disabled). + * + * @return The jroute id of this request, or null if this request does not carry any jroute id + */ + public String getJrouteId() { + return jrouteId; + } + + // ------------------------------------------------------ Session support --/ + + /** + * Return the session associated with this Request, creating one if necessary. + */ + public Session getSession() { + return doGetSession(true); + } + + /** + * Return the session associated with this Request, creating one if necessary and requested. + * + * @param create Create a new session if one does not exist + */ + public Session getSession(final boolean create) { + return doGetSession(create); + } + + /** + * Change the session id of the current session associated with this request and return the new session id. + * + * @return the original session id + * + * @throws IllegalStateException if there is no session associated with the request + * + * @since 2.3 + */ + public String changeSessionId() { + Session sessionLocal = doGetSession(false); + if (sessionLocal == null) { + throw new IllegalStateException("changeSessionId has been called without a session"); + } + + String oldSessionId = getSessionManager().changeSessionId(this, sessionLocal); + final String newSessionId = sessionLocal.getIdInternal(); + requestedSessionId = newSessionId; + + if (isRequestedSessionIdFromURL()) { + return oldSessionId; + } + + if (response != null) { + final Cookie cookie = new Cookie(obtainSessionCookieName(), newSessionId); + configureSessionCookie(cookie); + response.addSessionCookieInternal(cookie); + } + + return oldSessionId; + } + + protected Session doGetSession(final boolean create) { + // Return the current session if it exists and is valid + if (session != null && session.isValid()) { + return session; + } + + session = null; + + if (requestedSessionId == null) { + final Cookie[] cookiesLocale = getCookies(); + assert cookiesLocale != null; + + final String sessionCookieNameLocal = obtainSessionCookieName(); + for (final Cookie c : cookiesLocale) { + if (sessionCookieNameLocal.equals(c.getName())) { + setRequestedSessionId(c.getValue()); + setRequestedSessionCookie(true); + break; + } + } + } + + session = getSessionManager().getSession(this, requestedSessionId); + if (session != null && !session.isValid()) { + session = null; + } + + if (session != null) { + session.access(); + return session; + } + + if (!create) { + return null; + } + + session = getSessionManager().createSession(this); + session.setSessionTimeout(httpServerFilter.getConfiguration().getSessionTimeoutSeconds() * 1000); + requestedSessionId = session.getIdInternal(); + + // Creating a new session cookie based on the newly created session + final Cookie cookie = new Cookie(obtainSessionCookieName(), session.getIdInternal()); + configureSessionCookie(cookie); + + assert response != null; + response.addCookie(cookie); + + return session; + } + + /** + * @return true if the session identifier included in this request came from a cookie. + */ + public boolean isRequestedSessionIdFromCookie() { + + return requestedSessionId != null && requestedSessionCookie; + + } + + /** + * Return true if the session identifier included in this request came from the request URI. + */ + public boolean isRequestedSessionIdFromURL() { + + return requestedSessionId != null && requestedSessionURL; + + } + + /** + * @return true if the session identifier included in this request identifies a valid session. + */ + public boolean isRequestedSessionIdValid() { + if (requestedSessionId == null) { + return false; + } + + if (session != null && requestedSessionId.equals(session.getIdInternal())) { + return session.isValid(); + } + + final Session localSession = getSessionManager().getSession(this, requestedSessionId); + return localSession != null && localSession.isValid(); + + } + + /** + * Configures the given session cookie. + * + * @param cookie The session cookie to be configured + */ + protected void configureSessionCookie(final Cookie cookie) { + cookie.setMaxAge(-1); + cookie.setPath("/"); + + if (isSecure()) { + cookie.setSecure(true); + } + + getSessionManager().configureSessionCookie(this, cookie); + } + + /** + * Parse session id in URL. + */ + protected void parseSessionId() { + if (sessionParsed) { + return; + } + + sessionParsed = true; + final DataChunk uriDC = request.getRequestURIRef().getRequestURIBC(); + + final boolean isUpdated; + + switch (uriDC.getType()) { + case Bytes: + isUpdated = parseSessionId(uriDC.getByteChunk()); + break; + case Buffer: + isUpdated = parseSessionId(uriDC.getBufferChunk()); + break; + case Chars: + isUpdated = parseSessionId(uriDC.getCharChunk()); + break; + case String: + isUpdated = parseSessionId(uriDC); + break; + default: + throw new IllegalStateException("Unexpected DataChunk type: " + uriDC.getType()); + } + + if (isUpdated) { + uriDC.notifyDirectUpdate(); + } + } + + private boolean parseSessionId(final Chunk uriChunk) { + final String sessionParamNameMatch = sessionCookieName != null ? ';' + sessionCookieName + '=' : match; + + boolean isUpdated = false; + final int semicolon = uriChunk.indexOf(sessionParamNameMatch, 0); + + if (semicolon > 0) { + // Parse session ID, and extract it from the decoded request URI +// final int start = uriChunk.getStart(); + + final int sessionIdStart = semicolon + sessionParamNameMatch.length(); + final int semicolon2 = uriChunk.indexOf(';', sessionIdStart); + + isUpdated = semicolon2 >= 0; + final int end = isUpdated ? semicolon2 : uriChunk.getLength(); + + final String sessionId = uriChunk.toString(sessionIdStart, end); + + final int jrouteIndex = sessionId.lastIndexOf(':'); + if (jrouteIndex > 0) { + setRequestedSessionId(sessionId.substring(0, jrouteIndex)); + if (jrouteIndex < sessionId.length() - 1) { + setJrouteId(sessionId.substring(jrouteIndex + 1)); + } + } else { + setRequestedSessionId(sessionId); + } + + setRequestedSessionURL(true); + + uriChunk.delete(semicolon, end); + } else { + setRequestedSessionId(null); + setRequestedSessionURL(false); + } + + return isUpdated; + } + + private boolean parseSessionId(DataChunk dataChunkStr) { + assert dataChunkStr.getType() == DataChunk.Type.String; + + final String uri = dataChunkStr.toString(); + final String sessionParamNameMatch = sessionCookieName != null ? ';' + sessionCookieName + '=' : match; + + boolean isUpdated = false; + final int semicolon = uri.indexOf(sessionParamNameMatch); + + if (semicolon > 0) { + // Parse session ID, and extract it from the decoded request URI +// final int start = uriChunk.getStart(); + + final int sessionIdStart = semicolon + sessionParamNameMatch.length(); + final int semicolon2 = uri.indexOf(';', sessionIdStart); + + isUpdated = semicolon2 >= 0; + final int end = isUpdated ? semicolon2 : uri.length(); + + final String sessionId = uri.substring(sessionIdStart, end); + + final int jrouteIndex = sessionId.lastIndexOf(':'); + if (jrouteIndex > 0) { + setRequestedSessionId(sessionId.substring(0, jrouteIndex)); + if (jrouteIndex < sessionId.length() - 1) { + setJrouteId(sessionId.substring(jrouteIndex + 1)); + } + } else { + setRequestedSessionId(sessionId); + } + + setRequestedSessionURL(true); + + dataChunkStr.setString(uri.substring(0, semicolon)); + } else { + setRequestedSessionId(null); + setRequestedSessionURL(false); + } + + return isUpdated; + } + + /** + * Set a flag indicating whether or not the requested session ID for this request came in through a cookie. This is + * normally called by the HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionCookie(boolean flag) { + + this.requestedSessionCookie = flag; + + } + + /** + * Set the requested session ID for this request. This is normally called by the HTTP Connector, when it parses the + * request headers. + * + * @param id The new session id + */ + public void setRequestedSessionId(String id) { + + this.requestedSessionId = id; + + } + + /** + * Set a flag indicating whether or not the requested session ID for this request came in through a URL. This is + * normally called by the HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionURL(boolean flag) { + + this.requestedSessionURL = flag; + + } + + private static class PathData { + private final Request request; + private String path; + private PathResolver resolver; + + public PathData(Request request) { + this.request = request; + } + + public PathData(final Request request, final String path, final PathResolver resolver) { + this.request = request; + this.path = path; + this.resolver = resolver; + } + + public void setPath(final String path) { + this.path = path; + resolver = null; + } + + public void setResolver(final PathResolver resolver) { + this.resolver = resolver; + path = null; + } + + public String get() { + return path != null ? path : resolver != null ? (path = resolver.resolve(request)) : null; + } + + public void reset() { + path = null; + resolver = null; + } + } + + protected interface PathResolver { + String resolve(Request request); + } +}