/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.server.http; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.Principal; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.MultipartConfigElement; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestAttributeEvent; import javax.servlet.ServletRequestAttributeListener; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import com.caucho.config.scope.ScopeRemoveListener; import com.caucho.i18n.CharacterEncoding; import com.caucho.network.listen.SocketLink; import com.caucho.network.listen.SocketLinkDuplexController; import com.caucho.remote.websocket.MaskedFrameInputStream; import com.caucho.remote.websocket.UnmaskedFrameInputStream; import com.caucho.remote.websocket.WebSocketConstants; import com.caucho.security.AbstractLogin; import com.caucho.security.Login; import com.caucho.server.cluster.Server; import com.caucho.server.dispatch.Invocation; import com.caucho.server.session.SessionManager; import com.caucho.server.webapp.WebApp; import com.caucho.util.Base64; import com.caucho.util.CharBuffer; import com.caucho.util.CharSegment; import com.caucho.util.HashMapImpl; import com.caucho.util.L10N; import com.caucho.util.NullEnumeration; import com.caucho.vfs.Encoding; import com.caucho.vfs.FilePath; import com.caucho.vfs.Path; import com.caucho.vfs.ReadStream; import com.caucho.vfs.Vfs; import com.caucho.vfs.WriteStream; import com.caucho.websocket.WebSocketContext; import com.caucho.websocket.WebSocketListener; import com.caucho.websocket.WebSocketServletRequest; /** * User facade for http requests. */ public final class HttpServletRequestImpl extends AbstractCauchoRequest implements CauchoRequest, WebSocketServletRequest { private static final Logger log = Logger.getLogger(HttpServletRequestImpl.class.getName()); private static final L10N L = new L10N(HttpServletRequestImpl.class); private static final String CHAR_ENCODING = "resin.form.character.encoding"; private static final String FORM_LOCALE = "resin.form.local"; private static final String CAUCHO_CHAR_ENCODING = "caucho.form.character.encoding"; private static final Charset UTF8 = Charset.forName("UTF-8"); private AbstractHttpRequest _request; private final HttpServletResponseImpl _response; private Boolean _isSecure; private Invocation _invocation; // form private HashMapImpl<String,String[]> _filledForm; private List<Part> _parts; // session/cookies private Cookie []_cookiesIn; private boolean _varyCookies; // True if the page depends on cookies private boolean _hasCookie; private boolean _isSessionIdFromCookie; // security private String _runAs; private boolean _isLoginRequested; // input stream management private boolean _hasReader; private boolean _hasInputStream; // servlet attributes private HashMapImpl<String,Object> _attributes; // proxy caching private boolean _isSyntheticCacheHeader; // comet private long _asyncTimeout; private AsyncContextImpl _asyncContext; private ArrayList<Path> _closeOnExit; /** * Create a new Request. Because the actual initialization occurs with * the start() method, this just allocates statics. * * @param request */ public HttpServletRequestImpl(AbstractHttpRequest request) { _request = request; _response = new HttpServletResponseImpl(this, request.getAbstractHttpResponse()); } @Override public HttpServletResponseImpl getResponse() { return _response; } // // ServletRequest methods // /** * Returns the prococol, e.g. "HTTP/1.1" */ @Override public String getProtocol() { return _request.getProtocol(); } /** * Returns the request scheme, e.g. "http" */ @Override public String getScheme() { String scheme = _request.getScheme(); // server/12j2, server/1kkg if ("http".equals(scheme) || "https".equals(scheme)) return isSecure() ? "https" : "http"; else return scheme; } /** * Returns the server name handling the request. When using virtual hosts, * this returns the virtual host name, e.g. "vhost1.caucho.com". * * This call returns the host name as the client sees it, which means that * if ipchains, load balancing, or proxying is involved this call returns the * correct call for forming urls, but may not contain the host that Resin is * actually listening on. */ @Override public String getServerName() { AbstractHttpRequest request = _request; return request != null ? request.getServerName() : null; } /** * Returns the server port used by the client, e.g. 80. * * This call returns the port number as the client sees it, which means that * if ipchains, load balancing, or proxying is involved this call returns the * correct call for forming urls, but may not return the actual port that * Resin is listening on. * * This call should not be used to test for an ssl connection * (getServerPort() == 443), {@link #isSecure()} is provided for * that purpose. */ @Override public int getServerPort() { AbstractHttpRequest request = _request; return request != null ? request.getServerPort() : 0; } /** * Returns the IP address of the remote host, i.e. the client browser. */ @Override public String getRemoteAddr() { return _request.getRemoteAddr(); } /** * Returns the DNS hostname of the remote host, i.e. the client browser. */ @Override public String getRemoteHost() { return _request.getRemoteHost(); } /** * Returns the port of the remote host, i.e. the client browser. * * @since 2.4 */ @Override public int getRemotePort() { return _request.getRemotePort(); } /** * This call returns the ip of the host actually used to connect to the Resin * server, which means that if ipchains, load balancing, or proxying is * involved this call <i>does not</i> return the correct host for * forming urls. * * @since 2.4 */ @Override public String getLocalAddr() { return _request.getLocalHost(); } /** * Returns the IP address of the local host, i.e. the server. * * This call returns the name of the host actually used to connect to the * Resin server, which means that if ipchains, load balancing, or proxying * is involved this call <i>does not</i> return the correct host for * forming urls. * * @since 2.4 */ @Override public String getLocalName() { return _request.getLocalHost(); } /** * Returns the port of the local host. * * This call returns the port number actually used to connect to the Resin * server, which means that if ipchains, load balancing, or proxying is * involved this call <i>does not</i> return the correct port for * forming urls. * * This call should not be used to test for an ssl connection * (getServerPort() == 443), {@link #isSecure()} is provided for that purpose. * * @since 2.4 */ @Override public int getLocalPort() { return _request.getLocalPort(); } /** * Overrides the character encoding specified in the request. * <code>setCharacterEncoding</code> must be called before calling * <code>getReader</code> or reading any parameters. */ @Override public void setCharacterEncoding(String encoding) throws java.io.UnsupportedEncodingException { _request.setCharacterEncoding(encoding); } /** * Returns an InputStream to retrieve POST data from the request. * The stream will automatically end when the end of the POST data * is complete. */ @Override public ServletInputStream getInputStream() throws IOException { if (_hasReader) throw new IllegalStateException(L.l("getInputStream() can't be called after getReader()")); _hasInputStream = true; return _request.getInputStream(); } /** * Returns a reader to read POSTed data. Character encoding is * based on the request data and is the same as * <code>getCharacterEncoding()</code> */ @Override public BufferedReader getReader() throws IOException, IllegalStateException { if (_hasInputStream) throw new IllegalStateException(L.l("getReader() can't be called after getInputStream()")); _hasReader = true; return _request.getReader(); } /** * Returns the character encoding of the POSTed data. */ @Override public String getCharacterEncoding() { return _request.getCharacterEncoding(); } /** * Returns the content length of the data. This value may differ from * the actual length of the data. Newer browsers * supporting HTTP/1.1 may use "chunked" encoding which does * not make the content length available. * * <p>The upshot is, rely on the input stream to end when the data * completes. */ @Override public int getContentLength() { return _request.getContentLength(); } /** * Returns the request's mime-type. */ @Override public String getContentType() { return _request.getContentType(); } /** * Returns the request's preferred locale, based on the Accept-Language * header. If unspecified, returns the server's default locale. */ @Override public Locale getLocale() { return _request.getLocale(); } /** * Returns an enumeration of all locales acceptable by the client. */ @Override public Enumeration<Locale> getLocales() { return _request.getLocales(); } /** * Returns true if the connection is secure, e.g. it uses SSL. */ @Override public boolean isSecure() { if (_isSecure != null) return _isSecure; AbstractHttpRequest request = _request; if (request == null) return false; WebApp webApp = request.getWebApp(); if (webApp != null) { Boolean isSecure = webApp.isRequestSecure(); if (isSecure != null) return isSecure; } return request.isSecure(); } // // request attributes // /** * Returns the value of the named request attribute. * * @param name the attribute name. * * @return the attribute value. */ @Override public Object getAttribute(String name) { HashMapImpl<String,Object> attributes = _attributes; if (attributes != null) return attributes.get(name); else if (isSecure()) { _attributes = new HashMapImpl<String,Object>(); attributes = _attributes; _request.initAttributes(this); return attributes.get(name); } else return null; } /** * Returns an enumeration of the request attribute names. */ @Override public Enumeration<String> getAttributeNames() { HashMapImpl<String,Object> attributes = _attributes; if (attributes != null) { return Collections.enumeration(attributes.keySet()); } else if (isSecure()) { _attributes = new HashMapImpl<String,Object>(); attributes = _attributes; _request.initAttributes(this); return Collections.enumeration(attributes.keySet()); } else return NullEnumeration.create(); } /** * Sets the value of the named request attribute. * * @param name the attribute name. * @param value the new attribute value. */ @Override public void setAttribute(String name, Object value) { HashMapImpl<String,Object> attributes = _attributes; if (value != null) { if (attributes == null) { attributes = new HashMapImpl<String,Object>(); _attributes = attributes; _request.initAttributes(this); } Object oldValue = attributes.put(name, value); WebApp webApp = getWebApp(); if (webApp != null) { for (ServletRequestAttributeListener listener : webApp.getRequestAttributeListeners()) { ServletRequestAttributeEvent event; if (oldValue != null) { event = new ServletRequestAttributeEvent(webApp, this, name, oldValue); listener.attributeReplaced(event); } else { event = new ServletRequestAttributeEvent(webApp, this, name, value); listener.attributeAdded(event); } } } } else removeAttribute(name); } /** * Removes the value of the named request attribute. * * @param name the attribute name. */ @Override public void removeAttribute(String name) { HashMapImpl<String,Object> attributes = _attributes; if (attributes == null) return; Object oldValue = attributes.remove(name); WebApp webApp = getWebApp(); if (webApp == null) return; for (ServletRequestAttributeListener listener : webApp.getRequestAttributeListeners()) { ServletRequestAttributeEvent event; event = new ServletRequestAttributeEvent(webApp, this, name, oldValue); listener.attributeRemoved(event); } if (oldValue instanceof ScopeRemoveListener) { ((ScopeRemoveListener) oldValue).removeEvent(this, name); } } // // request dispatching // /** * Returns a request dispatcher for later inclusion or forwarding. This * is the servlet API equivalent to SSI includes. <code>uri</code> * is relative to the request URI. Absolute URIs are relative to * the application prefix (<code>getContextPath()</code>). * * <p>If <code>getRequestURI()</code> is /myapp/dir/test.jsp and the * <code>uri</code> is "inc.jsp", the resulting page is * /myapp/dir/inc.jsp. * <code><pre> * RequestDispatcher disp; * disp = getRequestDispatcher("inc.jsp?a=b"); * disp.include(request, response); * </pre></code> * * @param path path relative to <code>getRequestURI()</code> * (including query string) for the included file. * @return RequestDispatcher for later inclusion or forwarding. */ @Override public RequestDispatcher getRequestDispatcher(String path) { if (path == null || path.length() == 0) return null; else if (path.charAt(0) == '/') return getWebApp().getRequestDispatcher(path); else { CharBuffer cb = new CharBuffer(); WebApp webApp = getWebApp(); String servletPath = getPageServletPath(); if (servletPath != null) cb.append(servletPath); String pathInfo = getPagePathInfo(); if (pathInfo != null) cb.append(pathInfo); int p = cb.lastIndexOf('/'); if (p >= 0) cb.setLength(p); cb.append('/'); cb.append(path); if (webApp != null) return webApp.getRequestDispatcher(cb.toString()); return null; } } /** * Returns the servlet context for the request * * @since Servlet 3.0 */ @Override public ServletContext getServletContext() { Invocation invocation = _invocation; if (invocation != null) return invocation.getWebApp(); else return null; } /** * Returns the servlet response for the request * * @since Servlet 3.0 */ @Override public ServletResponse getServletResponse() { return _response; } // // HttpServletRequest APIs // /** * Returns the HTTP method, e.g. "GET" or "POST" * * <p/>Equivalent to CGI's <code>REQUEST_METHOD</code> */ @Override public String getMethod() { return _request.getMethod(); } /** * Returns the URI for the request */ @Override public String getRequestURI() { if (_invocation != null) return _invocation.getRawURI(); else return ""; } /** * Returns the URI for the page. getPageURI and getRequestURI differ * for included files. getPageURI gets the URI for the included page. * getRequestURI returns the original URI. */ public String getPageURI() { return _invocation.getRawURI(); } /** * Returns the context part of the uri. The context part is the part * that maps to an webApp. */ public String getContextPath() { if (_invocation != null) return _invocation.getContextPath(); else return ""; } /** * Returns the context part of the uri. For included files, this will * return the included context-path. */ public String getPageContextPath() { return getContextPath(); } /** * Returns the portion of the uri mapped to the servlet for the original * request. */ public String getServletPath() { if (_invocation != null) return _invocation.getServletPath(); else return ""; } /** * Returns the portion of the uri mapped to the servlet for the current * page. */ public String getPageServletPath() { if (_invocation != null) return _invocation.getServletPath(); else return ""; } /** * Returns the portion of the uri after the servlet path for the original * request. */ public String getPathInfo() { if (_invocation != null) return _invocation.getPathInfo(); else return null; } /** * Returns the portion of the uri after the servlet path for the current * page. */ public String getPagePathInfo() { if (_invocation != null) return _invocation.getPathInfo(); else return null; } /** * Returns the URL for the request */ @Override public StringBuffer getRequestURL() { StringBuffer sb = new StringBuffer(); sb.append(getScheme()); sb.append("://"); sb.append(getServerName()); int port = getServerPort(); if (port > 0 && port != 80 && port != 443) { sb.append(":"); sb.append(port); } sb.append(getRequestURI()); return sb; } /** * @deprecated As of JSDK 2.1 */ @Override public String getRealPath(String path) { if (path == null) return null; if (path.length() > 0 && path.charAt(0) == '/') return _invocation.getWebApp().getRealPath(path); String uri = getPageURI(); String context = getPageContextPath(); if (context != null) uri = uri.substring(context.length()); int p = uri.lastIndexOf('/'); if (p >= 0) path = uri.substring(0, p + 1) + path; return _invocation.getWebApp().getRealPath(path); } /** * Returns the real path of pathInfo. */ @Override public String getPathTranslated() { String pathInfo = getPathInfo(); if (pathInfo == null) return null; else return getRealPath(pathInfo); } /** * Returns the current page's query string. */ @Override public String getQueryString() { if (_invocation != null) return _invocation.getQueryString(); else return null; } /** * Returns the current page's query string. */ public String getPageQueryString() { return getQueryString(); } // // header management // /** * Returns the first value for a request header. * * <p/>Corresponds to CGI's <code>HTTP_*</code> * * <code><pre> * String userAgent = request.getHeader("User-Agent"); * </pre></code> * * @param name the header name * @return the header value */ @Override public String getHeader(String name) { return _request.getHeader(name); } /** * Returns all the values for a request header. In some rare cases, * like cookies, browsers may return multiple headers. * * @param name the header name * @return an enumeration of the header values. */ @Override public Enumeration<String> getHeaders(String name) { return _request.getHeaders(name); } /** * Returns an enumeration of all headers sent by the client. */ @Override public Enumeration<String> getHeaderNames() { return _request.getHeaderNames(); } /** * Converts a header value to an integer. * * @param name the header name * @return the header value converted to an integer */ @Override public int getIntHeader(String name) { return _request.getIntHeader(name); } /** * Converts a date header to milliseconds since the epoch. * * <pre><code> * long mod = request.getDateHeader("If-Modified-Since"); * </code></pre> * * @param name the header name * @return the header value converted to an date */ @Override public long getDateHeader(String name) { return _request.getDateHeader(name); } // // parameter/form // /** * Returns an enumeration of the form names. */ @Override public Enumeration<String> getParameterNames() { if (_filledForm == null) _filledForm = parseQuery(); return Collections.enumeration(_filledForm.keySet()); } /** * Returns a map of the form. */ @Override public Map<String,String[]> getParameterMap() { if (_filledForm == null) _filledForm = parseQuery(); return Collections.unmodifiableMap(_filledForm); } /** * Returns the form's values for the given name. * * @param name key in the form * @return value matching the key */ @Override public String []getParameterValues(String name) { if (_filledForm == null) _filledForm = parseQuery(); return (String []) _filledForm.get(name); } /** * Returns the form primary value for the given name. */ @Override public String getParameter(String name) { String []values = getParameterValues(name); if (values != null && values.length > 0) return values[0]; else return null; } /** * @since Servlet 3.0 */ @Override public Collection<Part> getParts() throws IOException, ServletException { MultipartConfigElement multipartConfig = _invocation.getMultipartConfig(); if (multipartConfig == null) throw new ServletException(L.l("multipart-form is disabled; check @MultipartConfig annotation on '{0}'.", _invocation.getServletName())); /* if (! getWebApp().doMultipartForm()) throw new ServletException("multipart-form is disabled; check <multipart-form> configuration tag."); */ if (! getContentType().startsWith("multipart/form-data")) throw new ServletException("Content-Type must be of 'multipart/form-data'."); if (_filledForm == null) _filledForm = parseQuery(); return _parts; } Part createPart(String name, Map<String, List<String>> headers) { return new PartImpl(name, headers); } /** * @since Servlet 3.0 */ @Override public Part getPart(String name) throws IOException, ServletException { for (Part part : getParts()) { if (name.equals(part.getName())) return part; } return null; } /** * Parses the query, either from the GET or the post. * * <p/>The character encoding is somewhat tricky. If it's a post, then * assume the encoded form uses the same encoding as * getCharacterEncoding(). * * <p/>If the request doesn't provide the encoding, use the * character-encoding parameter from the webApp. * * <p/>Otherwise use the default system encoding. */ private HashMapImpl<String,String[]> parseQuery() { HashMapImpl<String,String[]> form = _request.getForm(); try { String query = getQueryString(); CharSegment contentType = _request.getContentTypeBuffer(); if (query == null && contentType == null) return form; Form formParser = _request.getFormParser(); long contentLength = _request.getLongContentLength(); String charEncoding = getCharacterEncoding(); if (charEncoding == null) { charEncoding = (String) getAttribute(CAUCHO_CHAR_ENCODING); if (charEncoding == null) charEncoding = (String) getAttribute(CHAR_ENCODING); if (charEncoding == null) { Locale locale = (Locale) getAttribute(FORM_LOCALE); if (locale != null) charEncoding = Encoding.getMimeName(locale); } } if (query != null) { String queryEncoding = charEncoding; if (queryEncoding == null && getServer() != null) queryEncoding = getServer().getURLCharacterEncoding(); if (queryEncoding == null) queryEncoding = CharacterEncoding.getLocalEncoding(); String javaEncoding = Encoding.getJavaName(queryEncoding); formParser.parseQueryString(form, query, javaEncoding, true); } if (charEncoding == null) charEncoding = CharacterEncoding.getLocalEncoding(); String javaEncoding = Encoding.getJavaName(charEncoding); MultipartConfigElement multipartConfig = _invocation.getMultipartConfig(); if (contentType == null || ! "POST".equalsIgnoreCase(getMethod())) { } else if (contentType.startsWith("application/x-www-form-urlencoded")) { formParser.parsePostData(form, getInputStream(), javaEncoding); } else if ((getWebApp().doMultipartForm() || multipartConfig != null) && contentType.startsWith("multipart/form-data")) { int length = contentType.length(); int i = contentType.indexOf("boundary="); if (i < 0) return form; long formUploadMax = getWebApp().getFormUploadMax(); long parameterLengthMax = getWebApp().getFormParameterLengthMax(); if (parameterLengthMax < 0) parameterLengthMax = Long.MAX_VALUE / 2; Object uploadMax = getAttribute("caucho.multipart.form.upload-max"); if (uploadMax instanceof Number) formUploadMax = ((Number) uploadMax).longValue(); Object paramMax = getAttribute("caucho.multipart.form.parameter-length-max"); if (paramMax instanceof Number) parameterLengthMax = ((Number) paramMax).longValue(); // XXX: should this be an error? if (formUploadMax >= 0 && formUploadMax < contentLength) { setAttribute("caucho.multipart.form.error", L.l("Multipart form upload of '{0}' bytes was too large.", String.valueOf(contentLength))); setAttribute("caucho.multipart.form.error.size", new Long(contentLength)); return form; } long fileUploadMax = -1; if (multipartConfig != null) { formUploadMax = multipartConfig.getMaxRequestSize(); fileUploadMax = multipartConfig.getMaxFileSize(); } if (multipartConfig != null && formUploadMax > 0 && formUploadMax < contentLength) throw new IllegalStateException(L.l( "multipart form data request's Content-Length '{0}' is greater then configured in @MultipartConfig.maxRequestSize value: '{1}'", contentLength, formUploadMax)); i += "boundary=".length(); char ch = contentType.charAt(i); CharBuffer boundary = new CharBuffer(); if (ch == '\'') { for (i++; i < length && contentType.charAt(i) != '\''; i++) boundary.append(contentType.charAt(i)); } else if (ch == '\"') { for (i++; i < length && contentType.charAt(i) != '\"'; i++) boundary.append(contentType.charAt(i)); } else { for (; i < length && (ch = contentType.charAt(i)) != ' ' && ch != ';'; i++) { boundary.append(ch); } } _parts = new ArrayList<Part>(); try { MultipartFormParser.parsePostData(form, _parts, getStream(false), boundary.toString(), this, javaEncoding, formUploadMax, fileUploadMax, parameterLengthMax); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); setAttribute("caucho.multipart.form.error", e.getMessage()); } } } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } return form; } // // session/cookie management // /** * Returns an array of all cookies sent by the client. */ @Override public Cookie []getCookies() { if (_cookiesIn == null) { _cookiesIn = _request.getCookies(); SessionManager sessionManager = getSessionManager(); String sessionCookieName = getSessionCookie(sessionManager); for (int i = 0; i < _cookiesIn.length; i++) { Cookie cookie = _cookiesIn[i]; if (cookie.getName().equals(sessionCookieName) && sessionManager.isSecure()) { cookie.setSecure(true); break; } } /* // The page varies depending on the presense of any cookies setVaryCookie(null); // If any cookies actually exist, the page is not anonymous if (_cookiesIn != null && _cookiesIn.length > 0) setHasCookie(); */ } if (_cookiesIn == null || _cookiesIn.length == 0) return null; else return _cookiesIn; } /** * Returns the named cookie from the browser */ @Override public Cookie getCookie(String name) { /* // The page varies depending on the presense of any cookies setVaryCookie(name); */ return findCookie(name); } private Cookie findCookie(String name) { Cookie []cookies = getCookies(); if (cookies == null) return null; int length = cookies.length; for (int i = 0; i < length; i++) { Cookie cookie = cookies[i]; if (cookie.getName().equals(name)) { setHasCookie(); return cookie; } } return null; } /** * Returns the session id in the HTTP request. The cookie has * priority over the URL. Because the webApp might be using * the cookie to change the page contents, the caching sets * vary: JSESSIONID. */ @Override public String getRequestedSessionId() { SessionManager manager = getSessionManager(); if (manager != null && manager.enableSessionCookies()) { setVaryCookie(getSessionCookie(manager)); String id = findSessionIdFromCookie(); if (id != null) { _isSessionIdFromCookie = true; setHasCookie(); return id; } } String id = findSessionIdFromUrl(); if (id != null) { return id; } if (manager != null && manager.enableSessionCookies()) return null; else return _request.findSessionIdFromConnection(); } /** * Returns the session id in the HTTP request cookies. * Because the webApp might use the cookie to change * the page contents, the caching sets vary: JSESSIONID. */ protected String findSessionIdFromCookie() { SessionManager manager = getSessionManager(); if (manager == null || ! manager.enableSessionCookies()) return null; Cookie cookie = getCookie(getSessionCookie(manager)); if (cookie != null) { _isSessionIdFromCookie = true; return cookie.getValue(); } else return null; } @Override public boolean isSessionIdFromCookie() { return _isSessionIdFromCookie; } @Override public String getSessionId() { String sessionId = getResponse().getSessionId(); if (sessionId != null) return sessionId; else return getRequestedSessionId(); } @Override public void setSessionId(String sessionId) { getResponse().setSessionId(sessionId); } /** * Returns the session id in the HTTP request from the url. */ private String findSessionIdFromUrl() { // server/1319 // setVaryCookie(getSessionCookie(manager)); String id = _invocation != null ? _invocation.getSessionId() : null; if (id != null) setHasCookie(); return id; } /** * Returns true if the current sessionId came from a cookie. */ @Override public boolean isRequestedSessionIdFromCookie() { return findSessionIdFromCookie() != null; } /** * Returns true if the current sessionId came from the url. */ @Override public boolean isRequestedSessionIdFromURL() { return findSessionIdFromUrl() != null; } /** * @deprecated */ @Override public boolean isRequestedSessionIdFromUrl() { return isRequestedSessionIdFromURL(); } /** * Returns the session id in the HTTP request. The cookie has * priority over the URL. Because the webApp might be using * the cookie to change the page contents, the caching sets * vary: JSESSIONID. */ public String getRequestedSessionIdNoVary() { boolean varyCookies = _varyCookies; boolean hasCookie = _hasCookie; boolean privateCache = _response.getPrivateCache(); String id = getRequestedSessionId(); _varyCookies = varyCookies; _hasCookie = hasCookie; _response.setPrivateOrResinCache(privateCache); return id; } // // security // @Override protected String getRunAs() { return _runAs; } /** * Gets the authorization type */ public String getAuthType() { Object login = getAttribute(AbstractLogin.LOGIN_USER_NAME); if (login instanceof X509Certificate) return HttpServletRequest.CLIENT_CERT_AUTH; WebApp app = getWebApp(); if (app != null && app.getLogin() != null && getUserPrincipal() != null) return app.getLogin().getAuthType(); else return null; } /** * Returns the login for the request. */ protected Login getLogin() { WebApp webApp = getWebApp(); if (webApp != null) return webApp.getLogin(); else return null; } /** * Returns true if any authentication is requested */ @Override public boolean isLoginRequested() { return _isLoginRequested; } @Override public void requestLogin() { _isLoginRequested = true; } /** * Gets the remote user from the authorization type */ public String getRemoteUser() { Principal principal = getUserPrincipal(); if (principal != null) return principal.getName(); else return null; } /** * Internal logging return to get the remote user. If the request already * knows the user, get it, otherwise just return null. */ public String getRemoteUser(boolean create) { /* if (getSession(false) == null) return null; */ Principal user = (Principal) getAttribute(AbstractLogin.LOGIN_USER_NAME); if (user == null && create) user = getUserPrincipal(); if (user != null) return user.getName(); else return null; } /** * Logs out the principal. */ @Override public void logout() { Login login = getLogin(); if (login != null) { login.logout(getUserPrincipal(), this, getResponse()); } } /** * Clear the principal from the request object. */ public void logoutUserPrincipal() { // XXX: /* if (_session != null) _session.logout(); */ } /** * Sets the overriding role. */ public String runAs(String role) { String oldRunAs = _runAs; _runAs = role; return oldRunAs; } public void setSecure(Boolean isSecure) { // server/12ds _isSecure = isSecure; } // // deprecated // public ReadStream getStream() throws IOException { return _request.getStream(); } public ReadStream getStream(boolean isFlush) throws IOException { return _request.getStream(isFlush); } public int getRequestDepth(int depth) { return depth; } public void setHeader(String key, String value) { _request.setHeader(key, value); } @Override public void setSyntheticCacheHeader(boolean isSynthetic) { _isSyntheticCacheHeader = isSynthetic; } @Override public boolean isSyntheticCacheHeader() { return _isSyntheticCacheHeader; } /** * Called if the page depends on a cookie. If the cookie is null, then * the page depends on all cookies. * * @param cookie the cookie the page depends on. */ public void setVaryCookie(String cookie) { _varyCookies = true; // XXX: server/1315 vs 2671 // _response.setPrivateOrResinCache(true); } /** * Returns true if the page depends on cookies. */ public boolean getVaryCookies() { return _varyCookies; } /** * Set when the page actually has a cookie. */ public void setHasCookie() { _hasCookie = true; // XXX: 1171 vs 1240 // _response.setPrivateOrResinCache(true); } /** * True if this page uses cookies. */ public boolean getHasCookie() { if (_hasCookie) return true; else if (_invocation != null) return _invocation.getSessionId() != null; else return false; } public boolean isTop() { return true; } public boolean isComet() { return _request.isCometActive(); } /** * Adds a file to be removed at the end. */ public void addCloseOnExit(Path path) { if (_closeOnExit == null) _closeOnExit = new ArrayList<Path>(); _closeOnExit.add(path); } public boolean isDuplex() { return _request.isDuplex(); } @Override public void killKeepalive(String reason) { _request.killKeepalive(reason); } public boolean isConnectionClosed() { return _request.isConnectionClosed(); } public SocketLink getConnection() { return _request.getConnection(); } // // HttpServletRequestImpl methods // public AbstractHttpRequest getAbstractHttpRequest() { return _request; } public boolean isSuspend() { return _request.isSuspend(); } public boolean hasRequest() { return _request.hasRequest(); } public void setInvocation(Invocation invocation) { _invocation = invocation; } public Invocation getInvocation() { return _invocation; } public long getStartTime() { return _request.getStartTime(); } public void finishInvocation() { // server/11d4 /* AsyncContextImpl asyncContext = _asyncContext; if (asyncContext != null) asyncContext.onComplete(); */ _request.finishInvocation(); } // // servlet 3.0 async support // /** * Returns true if the request is in async. * * @since Servlet 3.0 */ @Override public boolean isAsyncStarted() { AsyncContextImpl asyncContext = _asyncContext; if (asyncContext == null) return false; return asyncContext.isAsyncStarted(); } /** * Returns true if the request supports async * * @since Servlet 3.0 */ @Override public boolean isAsyncSupported() { Invocation invocation = _invocation; if (invocation != null) return invocation.isAsyncSupported(); else return false; } /** * Starts an async mode * * @since Servlet 3.0 */ @Override public AsyncContext startAsync() { return startAsync(this, _response); } /** * Starts an async mode * * @since Servlet 3.0 */ @Override public AsyncContext startAsync(ServletRequest request, ServletResponse response) { if (! isAsyncSupported()) throw new IllegalStateException(L.l("The servlet '{0}' at '{1}' does not support async because the servlet or one of the filters does not support asynchronous mode. The servlet should be annotated with a @WebServlet(asyncSupported=true) annotation or have a <async-supported> tag in the web.xml.", getServletName(), getServletPath())); if (_request.isCometActive()) { throw new IllegalStateException(L.l("startAsync may not be called twice on the same dispatch.")); } boolean isOriginal = (request == this && response == _response); if (_asyncContext == null) { _asyncContext = new AsyncContextImpl(_request); if (_asyncTimeout > 0) _asyncContext.setTimeout(_asyncTimeout); } else _asyncContext.restart(); _asyncContext.init(request, response, isOriginal); return _asyncContext; } /** * Returns the async context for the request * * @since Servlet 3.0 */ @Override public AsyncContextImpl getAsyncContext() { if (_asyncContext != null) return _asyncContext; else throw new IllegalStateException(L.l("getAsyncContext() must be called after asyncStarted() has started a new AsyncContext.")); } // // WebSocket // @Override public WebSocketContext startWebSocket(WebSocketListener listener) throws IOException { if (log.isLoggable(Level.FINE)) log.fine(this + " upgrade HTTP to WebSocket " + listener); String method = getMethod(); if (! "GET".equals(method)) { getResponse().sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); throw new IllegalStateException(L.l("HTTP Method must be 'GET', because the WebSocket protocol requires 'GET'.\n remote-IP: {0}", getRemoteAddr())); } String connection = getHeader("Connection"); String upgrade = getHeader("Upgrade"); if (! "WebSocket".equalsIgnoreCase(upgrade)) { getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST); throw new IllegalStateException(L.l("HTTP Upgrade header '{0}' must be 'WebSocket', because the WebSocket protocol requires an Upgrade: WebSocket header.\n remote-IP: {1}", upgrade, getRemoteAddr())); } if (connection == null || connection.toLowerCase().indexOf("upgrade") < 0) { getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST); throw new IllegalStateException(L.l("HTTP Connection header '{0}' must be 'Upgrade', because the WebSocket protocol requires a Connection: Upgrade header.\n remote-IP: {1}", connection, getRemoteAddr())); } String key = getHeader("Sec-WebSocket-Key"); if (key == null) { getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST); throw new IllegalStateException(L.l("HTTP Sec-WebSocket-Key header is required, because the WebSocket protocol requires an Origin header.\n remote-IP: {0}", getRemoteAddr())); } else if (key.length() != 24) { getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST); throw new IllegalStateException(L.l("HTTP Sec-WebSocket-Key header is invalid '{0}' because it's not a 16-byte value.\n remote-IP: {1}", key, getRemoteAddr())); } String version = getHeader("Sec-WebSocket-Version"); String requiredVersion = WebSocketConstants.VERSION; if (! requiredVersion.equals(version)) { getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST); throw new IllegalStateException(L.l("HTTP Sec-WebSocket-Version header with value '{0}' is required, because the WebSocket protocol requires an Sec-WebSocket-Version header.\n remote-IP: {1}", requiredVersion, getRemoteAddr())); } String extensions = getHeader("Sec-WebSocket-Extensions"); boolean isMasked = true; if (extensions != null && extensions.indexOf("x-unmasked") >= 0) { isMasked = false; } String serverExtensions = null; if (! isMasked) serverExtensions = "x-unmasked"; _response.setStatus(101);//, "Switching Protocols"); _response.setHeader("Upgrade", "websocket"); String accept = calculateWebSocketAccept(key); _response.setHeader("Sec-WebSocket-Accept", accept); if (serverExtensions != null) _response.setHeader("Sec-WebSocket-Extensions", serverExtensions); _response.setContentLength(0); WebSocketContextImpl webSocket; if (isMasked) webSocket = new WebSocketContextImpl(this, _response, listener, new MaskedFrameInputStream()); else webSocket = new WebSocketContextImpl(this, _response, listener, new UnmaskedFrameInputStream()); SocketLinkDuplexController controller = _request.startDuplex(webSocket); webSocket.setController(controller); try { _response.getOutputStream().flush(); webSocket.flush(); webSocket.onStart(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } return webSocket; } private String calculateWebSocketAccept(String key) { try { MessageDigest md = MessageDigest.getInstance("SHA1"); int length = key.length(); for (int i = 0; i < length; i++) { md.update((byte) key.charAt(i)); } String guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; length = guid.length(); for (int i = 0; i < length; i++) { md.update((byte) guid.charAt(i)); } byte []digest = md.digest(); return Base64.encode(digest); } catch (Exception e) { throw new RuntimeException(e); } } int getAvailable() throws IOException { if (_request != null) return _request.getAvailable(); else return -1; } public DispatcherType getDispatcherType() { return DispatcherType.REQUEST; } @Override protected void finishRequest() throws IOException { AsyncContextImpl async = _asyncContext; _asyncContext = null; /* server/1ld5 if (comet != null) { comet.onComplete(); } */ if (async != null) { async.onComplete(); } super.finishRequest(); // ioc/0a10 cleanup(); if (_closeOnExit != null) { for (int i = _closeOnExit.size() - 1; i >= 0; i--) { Path path = _closeOnExit.get(i); try { path.remove(); } catch (Throwable e) { log.log(Level.FINE, e.toString(), e); } } } _request = null; } public void cleanup() { HashMapImpl<String,Object> attributes = _attributes; if (attributes != null) { for (Map.Entry<String,Object> entry : attributes.entrySet()) { Object value = entry.getValue(); if (value instanceof ScopeRemoveListener) { ((ScopeRemoveListener) value).removeEvent(this, entry.getKey()); } } } } // // XXX: unsorted // /** * Returns the servlet name. */ public String getServletName() { if (_invocation != null) { return _invocation.getServletName(); } else return null; } public final Server getServer() { return _request.getServer(); } /** * Returns the invocation's webApp. */ public final WebApp getWebApp() { if (_invocation != null) return _invocation.getWebApp(); else return null; } public boolean isClosed() { return _request == null; } @Override public String toString() { return getClass().getSimpleName() + "[" + _request + "]"; } public class PartImpl implements Part { private String _name; private Map<String, List<String>> _headers; private Object _value; private Path _newPath; private PartImpl(String name, Map<String, List<String>> headers) { _name = name; _headers = headers; } public void delete() throws IOException { if (_newPath != null) _newPath.remove(); Object value = getValue(); if (! (value instanceof FilePath)) throw new IOException(L.l("Part.delete() is not applicable to part '{0}':'{1}'", _name, value)); ((FilePath)value).remove(); } public String getContentType() { String[] value = _filledForm.get(_name + ".content-type"); if (value != null && value.length > 0) return value[0]; return null; } public String getHeader(String name) { List<String> values = _headers.get(name); if (values != null && values.size() > 0) return values.get(0); return null; } public Collection<String> getHeaderNames() { return _headers.keySet(); } public Collection<String> getHeaders(String name) { return _headers.get(name); } public InputStream getInputStream() throws IOException { Object value = getValue(); if (value instanceof FilePath) return ((FilePath) value).openRead(); ByteArrayInputStream is = new ByteArrayInputStream(value.toString().getBytes(UTF8)); return is; } public String getName() { return _name; } public long getSize() { Object value = getValue(); if (value instanceof FilePath) { return ((Path) value).getLength(); } else if (value instanceof String) { return -1; } else if (value == null) { return -1; } else { log.finest(L.l("Part.getSize() is not applicable to part'{0}':'{1}'", _name, value)); return -1; } } @Override public void write(String fileName) throws IOException { if (_newPath != null) throw new IOException(L.l( "Contents of part '{0}' has already been written to '{1}'", _name, _newPath)); Path path; Object value = getValue(); if (! (value instanceof FilePath)) throw new IOException(L.l( "Part.write() is not applicable to part '{0}':'{1}'", _name, value)); else path = (Path) value; MultipartConfigElement mc = _invocation.getMultipartConfig(); String location = mc.getLocation().replace('\\', '/'); fileName = fileName.replace('\\', '/'); String file; if (location.charAt(location.length() -1) != '/' && fileName.charAt(fileName.length() -1) != '/') file = location + '/' + fileName; else file = location + fileName; _newPath = Vfs.lookup(file); if (_newPath.exists()) throw new IOException(L.l("File '{0}' already exists.", _newPath)); Path parent = _newPath.getParent(); if (! parent.exists()) if (! parent.mkdirs()) throw new IOException(L.l("Unable to create path '{0}'. Check permissions.", parent)); if (! path.renameTo(_newPath)) { WriteStream out = null; try { out = _newPath.openWrite(); path.writeToStream(out); out.flush(); out.close(); } catch (IOException e) { log.log(Level.SEVERE, L.l("Cannot write contents of '{0}' to '{1}'", path, _newPath), e); throw e; } finally { if (out != null) out.close(); } } } public Object getValue() { if (_value != null) return _value; String []values = _filledForm.get(_name + ".file"); if (values != null && values.length > 0) { _value = Vfs.lookup(values[0]); } else { values = _filledForm.get(_name); if (values != null && values.length > 0) _value = values[0]; } return _value; } } }