From fde91f4aa9d2a4e9b7395e8d81cd7a976661ecd8 Mon Sep 17 00:00:00 2001 From: costin Date: Thu, 26 Nov 2009 06:41:00 +0000 Subject: [PATCH] The http implementation - it may be hard to recognize the original connector code from tomcat after many iterations. Changes compared with coyote: - both server and client mode - HttpRequest/HttpResponse implement most of methods in the HttpServletRequest - with the addition of setters, for use in client mode. They don't implement the interfaces - or 'servlet framework' specific methods - but should look familiar to people using this as a library - mapping is moved in this package, also support running HttpServices in the selector thread (proxy will run this way) - MimeHeaders are gone, so are the parameters - replaced with the MultiMap, which is based on MimeHeaders but adds a HashMap instead of linear scanning See tests for examples. git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@884412 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/tomcat/lite/http/BaseMapper.java | 1092 +++++++++++++++ .../org/apache/tomcat/lite/http/ContentType.java | 96 ++ .../tomcat/lite/http/DefaultHttpConnector.java | 23 + .../org/apache/tomcat/lite/http/Dispatcher.java | 180 +++ .../apache/tomcat/lite/http/FutureCallbacks.java | 170 +++ .../java/org/apache/tomcat/lite/http/HttpBody.java | 589 ++++++++ .../org/apache/tomcat/lite/http/HttpChannel.java | 1443 ++++++++++++++++++++ .../org/apache/tomcat/lite/http/HttpConnector.java | 625 +++++++++ .../org/apache/tomcat/lite/http/HttpMessage.java | 439 ++++++ .../org/apache/tomcat/lite/http/HttpRequest.java | 980 +++++++++++++ .../org/apache/tomcat/lite/http/HttpResponse.java | 283 ++++ .../org/apache/tomcat/lite/http/HttpWriter.java | 313 +++++ .../org/apache/tomcat/lite/http/MappingData.java | 69 + .../java/org/apache/tomcat/lite/http/MultiMap.java | 358 +++++ .../org/apache/tomcat/lite/http/ServerCookie.java | 819 +++++++++++ .../java/org/apache/tomcat/lite/http/package.html | 0 16 files changed, 7479 insertions(+) create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java create mode 100644 modules/tomcat-lite/java/org/apache/tomcat/lite/http/package.html diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java new file mode 100644 index 000000000..7e0e392f2 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/BaseMapper.java @@ -0,0 +1,1092 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + + +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.FileConnector; +import org.apache.tomcat.lite.io.BBucket; + +/** + * Mapper, which implements the servlet API mapping rules (which are derived + * from the HTTP rules). + * + * This class doesn't use JNDI. + */ +public class BaseMapper { + + private static Logger logger = + Logger.getLogger(BaseMapper.class.getName()); + + // TODO: + /** + * Mapping should be done on bytes - as received from net, before + * translation to chars. This would allow setting the default charset + * for the context - or even executing the servlet and letting it specify + * the charset to use for further decoding. + * + */ + public static interface Mapper { + public void map(BBucket host, BBucket url, MappingData md); + } + + + /** + * Like BaseMapper, for a Context. + */ + public static class ServiceMapper extends BaseMapper { + /** + * Context associated with this wrapper, used for wrapper mapping. + */ + public BaseMapper.ContextMapping contextMapElement = new BaseMapper.ContextMapping(); + + /** + * Set context, used for wrapper mapping (request dispatcher). + * + * @param welcomeResources Welcome files defined for this context + */ + public void setContext(String path, String[] welcomeResources) { + contextMapElement.name = path; + contextMapElement.welcomeResources = welcomeResources; + } + + + /** + * Add a wrapper to the context associated with this wrapper. + * + * @param path Wrapper mapping + * @param wrapper The Wrapper object + */ + public void addWrapper(String path, Object wrapper) { + addWrapper(contextMapElement, path, wrapper); + } + + + public void addWrapper(String path, Object wrapper, boolean jspWildCard) { + addWrapper(contextMapElement, path, wrapper, jspWildCard); + } + + + + /** + * Remove a wrapper from the context associated with this wrapper. + * + * @param path Wrapper mapping + */ + public void removeWrapper(String path) { + removeWrapper(contextMapElement, path); + } + + +// /** +// * Map the specified URI relative to the context, +// * mutating the given mapping data. +// * +// * @param uri URI +// * @param mappingData This structure will contain the result of the mapping +// * operation +// */ +// public void map(CBuffer uri, MappingData mappingData) +// throws Exception { +// +// CBuffer uricc = uri.getCharBuffer(); +// internalMapWrapper(contextMapElement, uricc, mappingData); +// +// } + } + + /** + * Array containing the virtual hosts definitions. + */ + Host[] hosts = new Host[0]; + + /** + * If no other host is found. + * For single-host servers ( most common ) this is the only one + * used. + */ + Host defaultHost = new Host(); + + public BaseMapper() { + defaultHost.contextList = new ContextList(); + } + + // --------------------------------------------------------- Public Methods + + public synchronized Host addHost(String name) { + if (name == null) { + name = "localhost"; + } + Host[] newHosts = new Host[hosts.length + 1]; + Host newHost = new Host(); + newHost.name = name; + newHost.contextList = new ContextList(); + + if (insertMap(hosts, newHosts, newHost)) { + hosts = newHosts; + } + return newHost; + } + + + /** + * Remove a host from the mapper. + * + * @param name Virtual host name + */ + public synchronized void removeHost(String name) { + // Find and remove the old host + int pos = find(hosts, name); + if (pos < 0) { + return; + } + Object host = hosts[pos].object; + Host[] newHosts = new Host[hosts.length - 1]; + if (removeMap(hosts, newHosts, name)) { + hosts = newHosts; + } + // Remove all aliases (they will map to the same host object) + for (int i = 0; i < newHosts.length; i++) { + if (newHosts[i].object == host) { + Host[] newHosts2 = new Host[hosts.length - 1]; + if (removeMap(hosts, newHosts2, newHosts[i].name)) { + hosts = newHosts2; + } + } + } + } + + /** + * Add an alias to an existing host. + * @param name The name of the host + * @param alias The alias to add + */ + public synchronized void addHostAlias(String name, String alias) { + int pos = find(hosts, name); + if (pos < 0) { + // Should not be adding an alias for a host that doesn't exist but + // just in case... + return; + } + Host realHost = hosts[pos]; + + Host[] newHosts = new Host[hosts.length + 1]; + Host newHost = new Host(); + newHost.name = alias; + newHost.contextList = realHost.contextList; + newHost.object = realHost; + if (insertMap(hosts, newHosts, newHost)) { + hosts = newHosts; + } + } + + private Host getHost(String host) { + return getHost(CBuffer.newInstance().append(host)); + } + + private Host getHost(CBuffer host) { + if (hosts == null || hosts.length <= 1 || host == null + || host.length() == 0 || host.equals("")) { + return defaultHost; + } else { + Host[] hosts = this.hosts; + // TODO: if hosts.length == 1 or defaultHost ? + int pos = findIgnoreCase(hosts, host); + if ((pos != -1) && (host.equalsIgnoreCase(hosts[pos].name))) { + return hosts[pos]; + } else { + return defaultHost; + } + } + } + + private Host getOrCreateHost(String hostName) { + Host host = getHost(CBuffer.newInstance().append(hostName)); + if (host == null) { + host = addHost(hostName); + } + return host; + } + + // Contexts + + /** + * Add a new Context to an existing Host. + * + * @param hostName Virtual host name this context belongs to + * @param path Context path + * @param context Context object + * @param welcomeResources Welcome files defined for this context + * @param resources Static resources of the context + * @param ctxService + */ + public BaseMapper.ContextMapping addContext(String hostName, String path, Object context, + String[] welcomeResources, FileConnector resources, + HttpChannel.HttpService ctxService) { + + if (path == null) { + path = "/"; + } + + Host host = getOrCreateHost(hostName); + + int slashCount = slashCount(path); + synchronized (host) { + BaseMapper.ContextMapping[] contexts = host.contextList.contexts; + // Update nesting + if (slashCount > host.contextList.nesting) { + host.contextList.nesting = slashCount; + } + BaseMapper.ContextMapping[] newContexts = new BaseMapper.ContextMapping[contexts.length + 1]; + BaseMapper.ContextMapping newContext = new BaseMapper.ContextMapping(); + newContext.name = path; + newContext.object = context; + if (welcomeResources != null) { + newContext.welcomeResources = welcomeResources; + } + newContext.resources = resources; + if (ctxService != null) { + newContext.defaultWrapper = new BaseMapper.ServiceMapping(); + newContext.defaultWrapper.object = ctxService; + } + + if (insertMap(contexts, newContexts, newContext)) { + host.contextList.contexts = newContexts; + } + return newContext; + } + + } + + + /** + * Remove a context from an existing host. + * + * @param hostName Virtual host name this context belongs to + * @param path Context path + */ + public void removeContext(String hostName, String path) { + Host host = getHost(hostName); + synchronized (host) { + BaseMapper.ContextMapping[] contexts = host.contextList.contexts; + if( contexts.length == 0 ){ + return; + } + BaseMapper.ContextMapping[] newContexts = new BaseMapper.ContextMapping[contexts.length - 1]; + if (removeMap(contexts, newContexts, path)) { + host.contextList.contexts = newContexts; + // Recalculate nesting + host.contextList.nesting = 0; + for (int i = 0; i < newContexts.length; i++) { + int slashCount = slashCount(newContexts[i].name); + if (slashCount > host.contextList.nesting) { + host.contextList.nesting = slashCount; + } + } + } + } + } + + + /** + * Add a new Wrapper to an existing Context. + * + * @param hostName Virtual host name this wrapper belongs to + * @param contextPath Context path this wrapper belongs to + * @param path Wrapper mapping + * @param wrapper Wrapper object + */ + public void addWrapper(String hostName, String contextPath, String path, + Object wrapper) { + addWrapper(hostName, contextPath, path, wrapper, false); + } + + + public void addWrapper(String hostName, String contextPath, String path, + Object wrapper, boolean jspWildCard) { + Host host = getHost(hostName); + BaseMapper.ContextMapping[] contexts = host.contextList.contexts; + int pos2 = find(contexts, contextPath); + if( pos2<0 ) { + logger.severe("No context found: " + contextPath ); + return; + } + BaseMapper.ContextMapping context = contexts[pos2]; + if (context.name.equals(contextPath)) { + addWrapper(context, path, wrapper, jspWildCard); + } + } + + + public void addWrapper(BaseMapper.ContextMapping context, String path, Object wrapper) { + addWrapper(context, path, wrapper, false); + } + + + /** + * Adds a wrapper to the given context. + * + * @param context The context to which to add the wrapper + * @param path Wrapper mapping + * @param wrapper The Wrapper object + * @param jspWildCard true if the wrapper corresponds to the JspServlet + * and the mapping path contains a wildcard; false otherwise + */ + protected void addWrapper(BaseMapper.ContextMapping context, String path, Object wrapper, + boolean jspWildCard) { + + synchronized (context) { + BaseMapper.ServiceMapping newWrapper = new BaseMapper.ServiceMapping(); + newWrapper.object = wrapper; + newWrapper.jspWildCard = jspWildCard; + if (path.endsWith("/*")) { + // Wildcard wrapper + newWrapper.name = path.substring(0, path.length() - 2); + BaseMapper.ServiceMapping[] oldWrappers = context.wildcardWrappers; + BaseMapper.ServiceMapping[] newWrappers = + new BaseMapper.ServiceMapping[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.wildcardWrappers = newWrappers; + int slashCount = slashCount(newWrapper.name); + if (slashCount > context.nesting) { + context.nesting = slashCount; + } + } + } else if (path.startsWith("*.")) { + // Extension wrapper + newWrapper.name = path.substring(2); + BaseMapper.ServiceMapping[] oldWrappers = context.extensionWrappers; + BaseMapper.ServiceMapping[] newWrappers = + new BaseMapper.ServiceMapping[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.extensionWrappers = newWrappers; + } + } else if (path.equals("/")) { + // Default wrapper + newWrapper.name = ""; + context.defaultWrapper = newWrapper; + } else { + // Exact wrapper + newWrapper.name = path; + BaseMapper.ServiceMapping[] oldWrappers = context.exactWrappers; + BaseMapper.ServiceMapping[] newWrappers = + new BaseMapper.ServiceMapping[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.exactWrappers = newWrappers; + } + } + } + } + + /** + * Remove a wrapper from an existing context. + * + * @param hostName Virtual host name this wrapper belongs to + * @param contextPath Context path this wrapper belongs to + * @param path Wrapper mapping + */ + public void removeWrapper(String hostName, String contextPath, + String path) { + Host host = getHost(hostName); + BaseMapper.ContextMapping[] contexts = host.contextList.contexts; + int pos2 = find(contexts, contextPath); + if (pos2 < 0) { + return; + } + BaseMapper.ContextMapping context = contexts[pos2]; + if (context.name.equals(contextPath)) { + removeWrapper(context, path); + } + } + + protected void removeWrapper(BaseMapper.ContextMapping context, String path) { + synchronized (context) { + if (path.endsWith("/*")) { + // Wildcard wrapper + String name = path.substring(0, path.length() - 2); + BaseMapper.ServiceMapping[] oldWrappers = context.wildcardWrappers; + BaseMapper.ServiceMapping[] newWrappers = + new BaseMapper.ServiceMapping[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + // Recalculate nesting + context.nesting = 0; + for (int i = 0; i < newWrappers.length; i++) { + int slashCount = slashCount(newWrappers[i].name); + if (slashCount > context.nesting) { + context.nesting = slashCount; + } + } + context.wildcardWrappers = newWrappers; + } + } else if (path.startsWith("*.")) { + // Extension wrapper + String name = path.substring(2); + BaseMapper.ServiceMapping[] oldWrappers = context.extensionWrappers; + BaseMapper.ServiceMapping[] newWrappers = + new BaseMapper.ServiceMapping[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + context.extensionWrappers = newWrappers; + } + } else if (path.equals("/")) { + // Default wrapper + context.defaultWrapper = null; + } else { + // Exact wrapper + String name = path; + BaseMapper.ServiceMapping[] oldWrappers = context.exactWrappers; + BaseMapper.ServiceMapping[] newWrappers = + new BaseMapper.ServiceMapping[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + context.exactWrappers = newWrappers; + } + } + } + } + + /** + * Map the specified host name and URI, mutating the given mapping data. + * + * @param host Virtual host name + * @param uri URI + * @param mappingData This structure will contain the result of the mapping + * operation + */ + public void map(CBuffer host, CBuffer uri, + MappingData mappingData) + throws Exception { + + internalMap(host.length() == 0 ? null : + host, uri, mappingData); + } + + + // -------------------------------------------------------- Private Methods + + // public Context mapContext(CBuffer host, CBuffer url); + + /** + * Map the specified URI. + */ + private final void internalMap(CBuffer host, CBuffer uri, + MappingData mappingData) + throws Exception { + BaseMapper.ContextMapping[] contexts = null; + BaseMapper.ContextMapping context = null; + int nesting = 0; + + // Virtual host mapping + Host mappedHost = getHost(host); + contexts = mappedHost.contextList.contexts; + nesting = mappedHost.contextList.nesting; + + // Context mapping + if (contexts.length == 0) { + return; + } + + if (mappingData.context == null) { + if (nesting < 1 || contexts.length == 1 && "".equals(contexts[0].name)) { + // if 1 context (default) -> fast return + context = contexts[0]; + } else if (nesting == 1) { + // if all contexts are 1-component-only + int nextSlash = uri.indexOf('/', 1); + if (nextSlash == -1) { + nextSlash = uri.length(); + } + mappingData.contextPath.set(uri, 0, nextSlash); + int pos = find(contexts, uri); + if (pos == -1) { + pos = find(contexts, "/"); + } + if (pos >= 0) { + context = contexts[pos]; + } + } else { + int pos = find(contexts, uri); + if (pos >= 0) { + int lastSlash = -1; + int length = -1; + boolean found = false; + CBuffer tmp = mappingData.tmpPrefix; + tmp.wrap(uri, 0, uri.length()); + + while (pos >= 0) { + if (tmp.startsWith(contexts[pos].name)) { + length = contexts[pos].name.length(); + if (tmp.length() == length) { + found = true; + break; + } else if (tmp.startsWithIgnoreCase("/", length)) { + found = true; + break; + } + } + if (lastSlash == -1) { + lastSlash = tmp.nthSlash(nesting + 1); + } else { + lastSlash = tmp.lastIndexOf('/'); + } + tmp.delete(lastSlash); + pos = find(contexts, tmp); + } + + if (!found) { + if (contexts[0].name.equals("")) { + context = contexts[0]; + } + } else { + context = contexts[pos]; + } + } + } + + if (context != null) { + mappingData.context = context.object; + mappingData.contextPath.set(context.name); + } + } + + // Wrapper mapping + if ((context != null) && (mappingData.getServiceObject() == null)) { + internalMapWrapper(context, uri, mappingData); + } + + } + + + /** + * Wrapper mapping, using servlet rules. + */ + protected final void internalMapWrapper( + BaseMapper.ContextMapping context, + CBuffer url, + MappingData mappingData) + throws Exception { + + boolean noServletPath = false; + if (url.length() < context.name.length()) { + throw new IOException("Invalid mapping " + context.name + " " + + url); + } + + try { + + mappingData.tmpServletPath.set(url, + context.name.length(), + url.length() - context.name.length()); + if (mappingData.tmpServletPath.length() == 0) { + mappingData.tmpServletPath.append('/'); + noServletPath = true; + } + + mapAfterContext(context, url, mappingData.tmpServletPath, mappingData, + noServletPath); + } catch (ArrayIndexOutOfBoundsException ex) { + System.err.println(1); + } + } + + void mapAfterContext(BaseMapper.ContextMapping context, + CBuffer url, CBuffer urlNoContext, + MappingData mappingData, boolean noServletPath) + throws Exception { + + + // Rule 1 -- Exact Match + BaseMapper.ServiceMapping[] exactWrappers = context.exactWrappers; + internalMapExactWrapper(exactWrappers, urlNoContext, mappingData); + + // Rule 2 -- Prefix Match + boolean checkJspWelcomeFiles = false; + BaseMapper.ServiceMapping[] wildcardWrappers = context.wildcardWrappers; + if (mappingData.getServiceObject() == null) { + + internalMapWildcardWrapper(wildcardWrappers, context.nesting, + urlNoContext, mappingData); + + if (mappingData.getServiceObject() != null + && mappingData.service.jspWildCard) { + if (urlNoContext.lastChar() == '/') { + /* + * Path ending in '/' was mapped to JSP servlet based on + * wildcard match (e.g., as specified in url-pattern of a + * jsp-property-group. + * Force the context's welcome files, which are interpreted + * as JSP files (since they match the url-pattern), to be + * considered. See Bugzilla 27664. + */ + mappingData.service = null; + checkJspWelcomeFiles = true; + } else { + // See Bugzilla 27704 + mappingData.wrapperPath.set(urlNoContext); + mappingData.pathInfo.recycle(); + } + } + } + + if(mappingData.getServiceObject() == null && noServletPath) { + // The path is empty, redirect to "/" + mappingData.redirectPath.set(context.name); + mappingData.redirectPath.append("/"); + return; + } + + // Rule 3 -- Extension Match + BaseMapper.ServiceMapping[] extensionWrappers = context.extensionWrappers; + if (mappingData.getServiceObject() == null && !checkJspWelcomeFiles) { + internalMapExtensionWrapper(extensionWrappers, urlNoContext, mappingData); + } + + // Rule 4 -- Welcome resources processing for servlets + if (mappingData.getServiceObject() == null) { + boolean checkWelcomeFiles = checkJspWelcomeFiles; + if (!checkWelcomeFiles) { + checkWelcomeFiles = (urlNoContext.lastChar() == '/'); + } + if (checkWelcomeFiles) { + for (int i = 0; (i < context.welcomeResources.length) + && (mappingData.getServiceObject() == null); i++) { + + CBuffer wpath = mappingData.tmpWelcome; + wpath.set(urlNoContext); + wpath.append(context.welcomeResources[i]); + + // Rule 4a -- Welcome resources processing for exact macth + internalMapExactWrapper(exactWrappers, urlNoContext, mappingData); + + // Rule 4b -- Welcome resources processing for prefix match + if (mappingData.getServiceObject() == null) { + internalMapWildcardWrapper + (wildcardWrappers, context.nesting, + urlNoContext, mappingData); + } + + // Rule 4c -- Welcome resources processing + // for physical folder + if (mappingData.getServiceObject() == null + && context.resources != null) { + String pathStr = urlNoContext.toString(); + + mapWelcomResource(context, urlNoContext, mappingData, + extensionWrappers, pathStr); + + } + } + } + + } + + + // Rule 7 -- Default servlet + if (mappingData.getServiceObject() == null && !checkJspWelcomeFiles) { + if (context.defaultWrapper != null) { + mappingData.service = context.defaultWrapper; + mappingData.requestPath.set(urlNoContext); + mappingData.wrapperPath.set(urlNoContext); + } + // Redirection to a folder + if (context.resources != null && urlNoContext.lastChar() != '/') { + String pathStr = urlNoContext.toString(); + mapDefaultServlet(context, urlNoContext, mappingData, + url, + pathStr); + } + } + } + + /** + * Filesystem-dependent method: + * if pathStr corresponds to a directory, we'll need to redirect with / + * at end. + */ + protected void mapDefaultServlet(BaseMapper.ContextMapping context, + CBuffer path, + MappingData mappingData, + CBuffer url, + String pathStr) throws IOException { + + if (context.resources != null + && context.resources.isDirectory(pathStr)) { + mappingData.redirectPath.set(url); + mappingData.redirectPath.append("/"); + } else { + mappingData.requestPath.set(pathStr); + mappingData.wrapperPath.set(pathStr); + } + } + + + /** + * Filesystem dependent method: + * check if a resource exists in filesystem. + */ + protected void mapWelcomResource(BaseMapper.ContextMapping context, CBuffer path, + MappingData mappingData, + BaseMapper.ServiceMapping[] extensionWrappers, String pathStr) { + + if (context.resources != null && + context.resources.isFile(pathStr)) { + internalMapExtensionWrapper(extensionWrappers, + path, mappingData); + if (mappingData.getServiceObject() == null + && context.defaultWrapper != null) { + mappingData.service = context.defaultWrapper; + mappingData.requestPath.set(path); + mappingData.wrapperPath.set(path); + mappingData.requestPath.set(pathStr); + mappingData.wrapperPath.set(pathStr); + } + } + } + + /** + * Exact mapping. + */ + private final void internalMapExactWrapper + (BaseMapper.ServiceMapping[] wrappers, CBuffer path, MappingData mappingData) { + int pos = find(wrappers, path); + if ((pos != -1) && (path.equals(wrappers[pos].name))) { + mappingData.requestPath.set(wrappers[pos].name); + mappingData.wrapperPath.set(wrappers[pos].name); + mappingData.service = wrappers[pos]; + } + } + + + /** + * Prefix mapping. ( /foo/* ) + */ + private final void internalMapWildcardWrapper + (BaseMapper.ServiceMapping[] wrappers, int nesting, CBuffer path, + MappingData mappingData) { + + int lastSlash = -1; + int length = -1; + + CBuffer tmp = mappingData.tmpPrefix; + tmp.wrap(path, 0, path.length()); + + int pos = find(wrappers, tmp); + if (pos != -1) { + boolean found = false; + while (pos >= 0) { + if (tmp.startsWith(wrappers[pos].name)) { + length = wrappers[pos].name.length(); + if (tmp.length() == length) { + found = true; + break; + } else if (tmp.startsWithIgnoreCase("/", length)) { + found = true; + break; + } + } + if (lastSlash == -1) { + lastSlash = tmp.nthSlash(nesting + 1); + } else { + lastSlash = tmp.lastIndexOf('/'); + } + tmp.delete(lastSlash); + pos = find(wrappers, tmp); + } + if (found) { + mappingData.wrapperPath.set(wrappers[pos].name); + + if (path.length() > length) { + mappingData.pathInfo.set + (path, length, path.length() - length); + } + mappingData.requestPath.set(path); + + mappingData.service = wrappers[pos]; + } + } + } + + + /** + * Extension mappings. + */ + protected final void internalMapExtensionWrapper + (BaseMapper.ServiceMapping[] wrappers, CBuffer path, MappingData mappingData) { + + int dot = path.getExtension(mappingData.ext, '/', '.'); + if (dot >= 0) { + int pos = find(wrappers, mappingData.ext); + + if ((pos != -1) + && (mappingData.ext.equals(wrappers[pos].name))) { + + mappingData.wrapperPath.set(path); + mappingData.requestPath.set(path); + + mappingData.service = wrappers[pos]; + } + } + } + + + /** + * Find a map elemnt given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ + private static final int find(BaseMapper.Mapping[] map, CBuffer name) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (name.compare(map[0].name) < 0 ) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) / 2; + int result = name.compare(map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = name.compare(map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + /** + * Find a map elemnt given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ + private static final int findIgnoreCase(BaseMapper.Mapping[] map, + CBuffer name) { + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + if (name.compareIgnoreCase(map[0].name) < 0 ) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) / 2; + int result = name.compareIgnoreCase(map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = name.compareIgnoreCase(map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + /** + * Find a map element given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ + private static final int find(BaseMapper.Mapping[] map, String name) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (name.compareTo(map[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) / 2; + int result = name.compareTo(map[i].name); + if (result > 0) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = name.compareTo(map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + /** + * Return the slash count in a given string. + */ + private static final int slashCount(String name) { + int pos = -1; + int count = 0; + while ((pos = name.indexOf('/', pos + 1)) != -1) { + count++; + } + return count; + } + + + /** + * Insert into the right place in a sorted MapElement array, and prevent + * duplicates. + */ + private static final boolean insertMap + (BaseMapper.Mapping[] oldMap, BaseMapper.Mapping[] newMap, BaseMapper.Mapping newElement) { + int pos = find(oldMap, newElement.name); + if ((pos != -1) && (newElement.name.equals(oldMap[pos].name))) { + return false; + } + System.arraycopy(oldMap, 0, newMap, 0, pos + 1); + newMap[pos + 1] = newElement; + System.arraycopy + (oldMap, pos + 1, newMap, pos + 2, oldMap.length - pos - 1); + return true; + } + + + /** + * Insert into the right place in a sorted MapElement array. + */ + private static final boolean removeMap + (BaseMapper.Mapping[] oldMap, BaseMapper.Mapping[] newMap, String name) { + int pos = find(oldMap, name); + if ((pos != -1) && (name.equals(oldMap[pos].name))) { + System.arraycopy(oldMap, 0, newMap, 0, pos); + System.arraycopy(oldMap, pos + 1, newMap, pos, + oldMap.length - pos - 1); + return true; + } + return false; + } + + + // ------------------------------------------------- MapElement Inner Class + + + protected static final class Host + extends BaseMapper.Mapping { + //Map contexts = new HashMap(); + //Context rootContext; + + public ContextList contextList = null; + + } + + + // ------------------------------------------------ ContextList Inner Class + + // Shared among host aliases. + protected static final class ContextList { + + public BaseMapper.ContextMapping[] contexts = new BaseMapper.ContextMapping[0]; + public int nesting = 0; + + } + + + public static final class ContextMapping extends BaseMapper.Mapping { + + public String[] welcomeResources = new String[0]; + public FileConnector resources = null; + + public BaseMapper.ServiceMapping defaultWrapper = null; + + public BaseMapper.ServiceMapping[] exactWrappers = new BaseMapper.ServiceMapping[0]; + public BaseMapper.ServiceMapping[] wildcardWrappers = new BaseMapper.ServiceMapping[0]; + public BaseMapper.ServiceMapping[] extensionWrappers = new BaseMapper.ServiceMapping[0]; + public int nesting = 0; + + } + + + public static class ServiceMapping extends BaseMapper.Mapping { + public boolean jspWildCard = false; + // If set, the service will run in the selector thread ( should + // be non-blocking ) + public boolean selectorThread = false; + + } + + + protected static abstract class Mapping { + public String name = null; + public Object object = null; + + public String toString() { + return name; + } + } + + + // ---------------------------------------------------- Context Inner Class + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java new file mode 100644 index 000000000..993566c13 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ContentType.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + + +/** + * Usefull methods for Content-Type processing + * + * @author James Duncan Davidson [duncan@eng.sun.com] + * @author James Todd [gonzo@eng.sun.com] + * @author Jason Hunter [jch@eng.sun.com] + * @author Harish Prabandham + * @author costin@eng.sun.com + */ +public class ContentType { + + /** + * Parse the character encoding from the specified content type header. + * If the content type is null, or there is no explicit character encoding, + * null is returned. + * + * @param contentType a content type header + */ + public static String getCharsetFromContentType(String contentType) { + + if (contentType == null) + return (null); + int start = contentType.indexOf("charset="); + if (start < 0) + return (null); + String encoding = contentType.substring(start + 8); + int end = encoding.indexOf(';'); + if (end >= 0) + encoding = encoding.substring(0, end); + encoding = encoding.trim(); + if ((encoding.length() > 2) && (encoding.startsWith("\"")) + && (encoding.endsWith("\""))) + encoding = encoding.substring(1, encoding.length() - 1); + return (encoding.trim()); + + } + + + /** + * Returns true if the given content type contains a charset component, + * false otherwise. + * + * @param type Content type + * @return true if the given content type contains a charset component, + * false otherwise + */ + public static boolean hasCharset(String type) { + + boolean hasCharset = false; + + int len = type.length(); + int index = type.indexOf(';'); + while (index != -1) { + index++; + while (index < len && Character.isSpace(type.charAt(index))) { + index++; + } + if (index+8 < len + && type.charAt(index) == 'c' + && type.charAt(index+1) == 'h' + && type.charAt(index+2) == 'a' + && type.charAt(index+3) == 'r' + && type.charAt(index+4) == 's' + && type.charAt(index+5) == 'e' + && type.charAt(index+6) == 't' + && type.charAt(index+7) == '=') { + hasCharset = true; + break; + } + index = type.indexOf(';', index); + } + + return hasCharset; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java new file mode 100644 index 000000000..a20aa41a9 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/DefaultHttpConnector.java @@ -0,0 +1,23 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import org.apache.tomcat.lite.io.SocketConnector; + +public class DefaultHttpConnector { + + public synchronized static HttpConnector getNew() { + return new HttpConnector(new SocketConnector()); + } + + public synchronized static HttpConnector get() { + if (DefaultHttpConnector.defaultHttpConnector == null) { + DefaultHttpConnector.defaultHttpConnector = + new HttpConnector(new SocketConnector()); + } + return DefaultHttpConnector.defaultHttpConnector; + } + + private static HttpConnector defaultHttpConnector; + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java new file mode 100644 index 000000000..59a3c7c20 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/Dispatcher.java @@ -0,0 +1,180 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.http.BaseMapper.ContextMapping; +import org.apache.tomcat.lite.http.HttpChannel.HttpService; +import org.apache.tomcat.lite.http.HttpChannel.RequestCompleted; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.FileConnector; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.UrlEncoding; + +/** + * This class has several functions: + * - maps the request to another HttpService + * - decide if the request should be run in the selector thread + * or in a thread pool + * - finalizes the request ( close / flush ) + * - detectsif the request is complete or set callbacks + * for receive/flush/done. + * + */ +public class Dispatcher implements HttpService { + + private BaseMapper mapper; + static boolean debug = false; + static Logger log = Logger.getLogger("Mapper"); + Executor tp = Executors.newCachedThreadPool(); + + public Dispatcher() { + init(); + } + + protected void init() { + mapper = new BaseMapper(); + } + + public void runService(HttpChannel ch) { + MappingData mapRes = ch.getRequest().getMappingData(); + HttpService h = (HttpService) mapRes.getServiceObject(); + try { + //log.info("Service "); + h.service(ch.getRequest(), ch.getResponse()); + if (!ch.getRequest().isAsyncStarted()) { + ch.complete(); + ch.release(); // recycle objects. + } else { + // Nothing - complete must be called when done. + } + + } catch (IOException e) { + e.printStackTrace(); + } catch( Throwable t ) { + t.printStackTrace(); + } + } + + @Override + public void service(HttpRequest httpReq, HttpResponse httpRes) throws IOException { + service(httpReq, httpRes, false); + } + + + public void service(HttpRequest httpReq, HttpResponse httpRes, boolean noThread) + throws IOException { + long t0 = System.currentTimeMillis(); + HttpChannel http = httpReq.getHttpChannel(); + + http.setCompletedCallback(doneCallback); + + try { + // compute decodedURI - not done by connector + MappingData mapRes = httpReq.getMappingData(); + mapRes.recycle(); + + mapper.map(httpReq.serverName(), + httpReq.decodedURI(), mapRes); + + HttpService h = (HttpService) mapRes.getServiceObject(); + + if (h != null) { + if (debug) { + log.info(">>>>>>>> START: " + http.getRequest().method() + " " + + http.getRequest().decodedURI() + " " + + h.getClass().getSimpleName()); + } + + if (mapRes.service.selectorThread || noThread) { + runService(http); + } else { + tp.execute(httpReq.getHttpChannel().dispatcherRunnable); + } + + } else { + httpRes.setStatus(404); + http.complete(); + } + + } catch (IOException ex) { + if ("Broken pipe".equals(ex.getMessage())) { + log.warning("Connection interrupted while writting"); + } + throw ex; + } catch( Throwable t ) { + t.printStackTrace(); + httpRes.setStatus(500); + http.abort(t); + } + } + + private RequestCompleted doneCallback = new RequestCompleted() { + @Override + public void handle(HttpChannel client, Object extraData) throws IOException { + if (debug) { + log.info("<<<<<<<< DONE: " + client.getRequest().method() + " " + + client.getRequest().decodedURI() + " " + + client.getResponse().getStatus() + " " + ); + } + } + }; + + public BaseMapper.ContextMapping addContext(String hostname, String ctxPath, + Object ctx, String[] welcomeResources, FileConnector resources, + HttpService ctxService) { + return mapper.addContext(hostname, ctxPath, ctx, welcomeResources, resources, + ctxService); + } + + public void map(CBuffer hostMB, CBuffer urlMB, MappingData md) { + try { + mapper.map(hostMB, urlMB, md); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public void map(BaseMapper.ContextMapping ctx, + CBuffer uri, MappingData md) { + try { + mapper.internalMapWrapper(ctx, uri, md); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public void addWrapper(BaseMapper.ContextMapping ctx, String path, + HttpService service) { + mapper.addWrapper(ctx, path, service); + } + + + public void setDefaultService(HttpService service) { + BaseMapper.ContextMapping mCtx = + mapper.addContext(null, "/", null, null, null, null); + mapper.addWrapper(mCtx, "/", service); + } + + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java new file mode 100644 index 000000000..0b8324dcb --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/FutureCallbacks.java @@ -0,0 +1,170 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + + +/** + * Support for blocking calls and callbacks. + * + * Unlike FutureTask, it is possible to reuse this and hopefully + * easier to extends. Also has callbacks. + * + * @author Costin Manolache + */ +public class FutureCallbacks implements Future { + + // Other options: ReentrantLock uses AbstractQueueSynchronizer, + // more complex. Same for CountDownLatch + // FutureTask - uses Sync as well, ugly interface with + // Callable, can't be recycled. + // Mina: simple object lock, doesn't extend java.util.concurent.Future + + private Sync sync = new Sync(); + + private V value; + + public static interface Callback { + public void run(V param); + } + + private List> callbacks = new ArrayList(); + + public FutureCallbacks() { + } + + /** + * Unlocks the object if it was locked. Should be called + * when the object is reused. + * + * Callbacks will not be invoked. + */ + public void reset() { + sync.releaseShared(0); + sync.reset(); + } + + public void recycle() { + callbacks.clear(); + sync.releaseShared(0); + sync.reset(); + } + + /** + * Unlocks object and calls the callbacks. + * @param v + * + * @throws IOException + */ + public void signal(V v) throws IOException { + sync.releaseShared(0); + onSignal(v); + } + + protected boolean isSignaled() { + return true; + } + + /** + * Override to call specific callbacks + */ + protected void onSignal(V v) { + for (Callback cb: callbacks) { + if (cb != null) { + cb.run(v); + } + } + } + + /** + * Set the response. Will cause the callback to be called and lock to be + * released. + * + * @param value + * @throws IOException + */ + public void setValue(V value) throws IOException { + synchronized (this) { + this.value = value; + signal(value); + } + } + + public void waitSignal(long to) throws IOException { + try { + get(to, TimeUnit.MILLISECONDS); + } catch (InterruptedException e1) { + throw new IOException(e1.getMessage()); + } catch (TimeoutException e1) { + throw new IOException(e1.getMessage()); + } catch (ExecutionException e) { + throw new IOException(e.getMessage()); + } + } + + @Override + public V get() throws InterruptedException, ExecutionException { + sync.acquireSharedInterruptibly(0); + return value; + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + if (!sync.tryAcquireSharedNanos(0, unit.toNanos(timeout))) { + throw new TimeoutException(); + } + return value; + } + + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return sync.isSignaled(); + } + + private class Sync extends AbstractQueuedSynchronizer { + + static final int DONE = 1; + static final int BLOCKED = 0; + Object result; + Throwable t; + + @Override + protected int tryAcquireShared(int ignore) { + return getState() == DONE ? 1 : -1; + } + + @Override + protected boolean tryReleaseShared(int ignore) { + setState(DONE); + return true; + } + + public void reset() { + setState(BLOCKED); + } + + boolean isSignaled() { + return getState() == DONE; + } + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java new file mode 100644 index 000000000..11e84df33 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpBody.java @@ -0,0 +1,589 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.io.BBucket; +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.Hex; +import org.apache.tomcat.lite.io.IOBuffer; +import org.apache.tomcat.lite.io.IOChannel; + +/** + * Transport decoded buffer, representing the body + * of HTTP messages. + * + * Supports: + * - Chunked and Content-Length delimitation + * - "Close" delimitation ( no content delimitation - TCP close ) + * + * TODO: continue support + * TODO: gzip encoding + * + * For sending, data is kept in this buffer until flush() is called. + */ +class HttpBody extends IOBuffer { + protected static Logger log = Logger.getLogger("HttpBody"); + + static int DEFAULT_CHUNK_SIZE = 4096; + + private HttpChannel http; + + protected boolean chunked = false; + protected long contentLength = -1; // C-L header + + /** True: Http/1.x + chunked || C-L - + * False: for http/0.9, connection:close, errors. -> + * close delimited + */ + boolean frameError = false; + + + /** Bytes remaining in the current chunk or body ( if CL ) */ + protected long remaining = 0; // both chunked and C-L + + // used for chunk parsing + ChunkState chunk = new ChunkState(); + + boolean noBody; + + boolean endSent = false; + boolean sendBody = false; + + private HttpMessage httpMsg; + + HttpBody(HttpChannel asyncHttp, boolean sendBody) { + this.http = asyncHttp; + if (sendBody) { + this.sendBody = true; + // For flush and close to work - need to fix + //this.ch = http; + } else { + this.ch = null; + } + } + + public String toString() { + return "{" + super.toString() + " " + + (chunked ? "CNK/" + remaining : "") + + (contentLength >= 0 ? "C-L/" + contentLength + "/" + remaining : "") + + (isAppendClosed() ? ", C" : "") + + "}"; + } + + public void recycle() { + chunked = false; + remaining = 0; + contentLength = -1; + chunk.recycle(); + chunk.recycle(); + super.recycle(); + frameError = false; + noBody = false; + endSent = false; + } + + public boolean isContentDelimited() { + return chunked || contentLength >= 0; + } + + + + /** + * Updates chunked, contentLength, remaining + */ + protected void processContentDelimitation() { + + contentLength = httpMsg.getContentLength(); + if (contentLength >= 0) { + remaining = contentLength; + } + + // TODO: multiple transfer encoding headers, only process the last + String transferEncodingValue = httpMsg.getHeader(HttpChannel.TRANSFERENCODING); + if (transferEncodingValue != null) { + int startPos = 0; + int commaPos = transferEncodingValue.indexOf(','); + String encodingName = null; + while (commaPos != -1) { + encodingName = transferEncodingValue.substring + (startPos, commaPos).toLowerCase().trim(); + if ("chunked".equalsIgnoreCase(encodingName)) { + chunked = true; + } + startPos = commaPos + 1; + commaPos = transferEncodingValue.indexOf(',', startPos); + } + encodingName = transferEncodingValue.substring(startPos) + .toLowerCase().trim(); + if ("chunked".equals(encodingName)) { + chunked = true; + httpMsg.chunked = true; + } else { + System.err.println("TODO: ABORT 501"); + //return 501; // Currently only chunked is supported for + // transfer encoding. + } + } + + if (chunked) { + remaining = 0; + } + } + + void updateCloseOnEnd() { + if (!isContentDelimited() && !noBody) { + http.closeStreamOnEnd("not content delimited"); + } + } + + void processContentEncoding() { + // Content encoding - set it on the buffer, will be processed in blocking + // mode, after transfer encoding. +// MessageBytes contentEncodingValueMB = +// headers.getValue("content-encoding"); + +// if (contentEncodingValueMB != null) { +// if (contentEncodingValueMB.equals("gzip")) { +// buffer.addActiveFilter(gzipIF); +// } +// // TODO: other encoding filters +// // TODO: this should be separate layer +// } + } + + /** + * Determine if we must drop the connection because of the HTTP status + * code. Use the same list of codes as Apache/httpd. + */ + protected boolean statusDropsConnection(int status) { + return status == 400 /* SC_BAD_REQUEST */ || + status == 408 /* SC_REQUEST_TIMEOUT */ || + status == 411 /* SC_LENGTH_REQUIRED */ || + status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ || + status == 414 /* SC_REQUEST_URI_TOO_LARGE */ || + status == 500 /* SC_INTERNAL_SERVER_ERROR */ || + status == 503 /* SC_SERVICE_UNAVAILABLE */ || + status == 501 /* SC_NOT_IMPLEMENTED */; + } + + static final int NEED_MORE = -1; + static final int ERROR = -4; + static final int DONE = -5; + + class ChunkState { + int partialChunkLen; + boolean readDigit = false; + boolean trailer = false; + protected boolean needChunkCrlf = false; + + // Buffer used for chunk length conversion. + protected byte[] sendChunkLength = new byte[10]; + + /** End chunk marker - will include chunked end or empty */ + protected BBuffer endSendBuffer = BBuffer.wrapper(); + + public ChunkState() { + sendChunkLength[8] = (byte) '\r'; + sendChunkLength[9] = (byte) '\n'; + } + + void recycle() { + partialChunkLen = 0; + readDigit = false; + trailer = false; + needChunkCrlf = false; + endSendBuffer.recycle(); + } + + /** + * Parse the header of a chunk. + * A chunk header can look like + * A10CRLF + * F23;chunk-extension to be ignoredCRLF + * The letters before CRLF but after the trailer mark, must be valid hex digits, + * we should not parse F23IAMGONNAMESSTHISUP34CRLF as a valid header + * according to spec + */ + int parseChunkHeader(IOBuffer buffer) throws IOException { + if (buffer.peekFirst() == null) { + return NEED_MORE; + } + if (needChunkCrlf) { + // TODO: Trailing headers + int c = buffer.read(); + if (c == BBuffer.CR) { + if (buffer.peekFirst() == null) { + return NEED_MORE; + } + c = buffer.read(); + } + if (c == BBuffer.LF) { + needChunkCrlf = false; + } else { + System.err.println("Bad CRLF " + c); + return ERROR; + } + } + + while (true) { + if (buffer.peekFirst() == null) { + return NEED_MORE; + } + int c = buffer.read(); + + if (c == BBuffer.CR) { + continue; + } else if (c == BBuffer.LF) { + break; + } else if (c == HttpChannel.SEMI_COLON) { + trailer = true; + } else if (c == BBuffer.SP) { + // ignore + } else if (trailer) { + // ignore + } else { + //don't read data after the trailer + if (Hex.DEC[c] != -1) { + readDigit = true; + partialChunkLen *= 16; + partialChunkLen += Hex.DEC[c]; + } else { + //we shouldn't allow invalid, non hex characters + //in the chunked header + log.info("Chunk parsing error1 " + c + " " + buffer); + http.abort("Chunk error"); + return ERROR; + } + } + } + + if (!readDigit) { + log.info("Chunk parsing error2 " + buffer); + return ERROR; + } + + needChunkCrlf = true; // next time I need to parse CRLF + int result = partialChunkLen; + partialChunkLen = 0; + trailer = false; + readDigit = false; + return result; + } + + + ByteBuffer prepareChunkHeader(int current) { + int pos = 7; // 8, 9 are CRLF + while (current > 0) { + int digit = current % 16; + current = current / 16; + sendChunkLength[pos--] = Hex.HEX[digit]; + } + if (needChunkCrlf) { + sendChunkLength[pos--] = (byte) '\n'; + sendChunkLength[pos--] = (byte) '\r'; + } else { + needChunkCrlf = true; + } + // TODO: pool - this may stay in the queue while we flush more + ByteBuffer chunkBB = ByteBuffer.allocate(16); + chunkBB.put(sendChunkLength, pos + 1, 9 - pos); + chunkBB.flip(); + return chunkBB; + } + + public BBuffer endChunk() { + if (! needChunkCrlf) { + endSendBuffer.setBytes(HttpChannel.END_CHUNK_BYTES, 2, + HttpChannel.END_CHUNK_BYTES.length - 2); // CRLF + } else { // 0 + endSendBuffer.setBytes(HttpChannel.END_CHUNK_BYTES, 0, + HttpChannel.END_CHUNK_BYTES.length); + } + return endSendBuffer; + } + } + + private int receiveDone(boolean frameError) throws IOException { + // Content-length case, we're done reading + close(); + + this.frameError = frameError; + if (frameError) { + http.closeStreamOnEnd("frame error"); + } + + return DONE; + } + + /** + * Called when raw body data is received. + * Callback should not consume past the end of the body. + * @param rawReceiveBuffers + * + */ + void rawDataReceived(IOBuffer rawReceiveBuffers) throws IOException { + // TODO: Make sure we don't process more than we need ( eat next req ). + // If we read too much: leave it in readBuf, the finalzation code + // should skip KeepAlive and start processing it. + // we need to read at least something - to detect -1 ( we could + // suspend right away, but seems safer. + while (http.inMessage.state == HttpMessage.State.BODY_DATA) { + //log.info("RAW DATA: " + this + " RAW: " + rawReceiveBuffers); + if (noBody) { + receiveDone(false); + return; + } + if (rawReceiveBuffers.isClosedAndEmpty()) { + if (isContentDelimited()) { + if (contentLength >= 0 && remaining == 0) { + receiveDone(false); + } else { + // End of input - other side closed, no more data + //log.info("CLOSE while reading " + this); + // they're not supposed to close ! + receiveDone(true); + } + } else { + receiveDone(false); // ok + } + // input connection closed ? + http.closeStreamOnEnd("Closed input"); + return; + } + BBucket rawBuf = rawReceiveBuffers.peekFirst(); + if (rawBuf == null) { + return; // need more data + } + + if (!isContentDelimited()) { + while (true) { + BBucket first = rawReceiveBuffers.popFirst(); + if (first == null) { + break; // will go back to check if done. + } else { + super.queue(first); + } + } + } else { + + if (contentLength >= 0 && remaining == 0) { + receiveDone(false); + return; + } + + if (chunked && remaining == 0) { + int rc = NEED_MORE; + while (rc == NEED_MORE) { + rc = chunk.parseChunkHeader(rawReceiveBuffers); + if (rc == ERROR) { + receiveDone(true); + return; + } else if (rc == NEED_MORE) { + return; + } + } + if (rc == 0) { // last chunk + receiveDone(false); + return; + } else { + remaining = rc; + } + } + + rawBuf = (BBucket) rawReceiveBuffers.peekFirst(); + if (rawBuf == null) { + return; // need more data + } + + + if (remaining < rawBuf.remaining()) { + // To buffer has more data than we need. + int lenToConsume = (int) remaining; + BBucket sb = rawReceiveBuffers.popLen(lenToConsume); + super.queue(sb); + //log.info("Queue received buffer " + this + " " + lenToConsume); + remaining = 0; + } else { + BBucket first = rawReceiveBuffers.popFirst(); + remaining -= first.remaining(); + super.queue(first); + //log.info("Queue full received buffer " + this + " RAW: " + rawReceiveBuffers); + } + if (contentLength >= 0 && remaining == 0) { + // Content-Length, all done + super.close(); + receiveDone(false); + } + } + } + } + + void flushToNext() throws IOException { + http.sendHeaders(); // if needed + + if (getNet() == null) { + return; // not connected yet. + } + + synchronized (this) { + if (noBody) { + for (int i = 0; i < super.getBufferCount(); i++) { + Object bc = super.peekBucket(i); + if (bc instanceof BBucket) { + ((BBucket) bc).release(); + } + } + super.clear(); + return; + } + // TODO: only send < remainingWrite, if buffer + // keeps changing after startWrite() is called (shouldn't) + boolean done = false; + + if (chunked) { + done = sendChunked(); + } else if (contentLength >= 0) { + // content-length based + done = sendContentLen(); + } else { + // Close delimitation + while (true) { + Object bc = popFirst(); + if (bc == null) { + break; + } + getNet().getOut().queue(bc); + } + if (super.isClosedAndEmpty()) { + done = true; + if (getNet() != null) { + getNet().getOut().close(); // no content-delimitation + } + } + } + } + } + + private boolean sendContentLen() throws IOException { + while (true) { + BBucket bucket = super.peekFirst(); + if (bucket == null) { + break; + } + int len = bucket.remaining(); + if (len <= remaining) { + remaining -= len; + bucket = super.popFirst(); + getNet().getOut().queue(bucket); + } else { + // Write over the end of the buffer ! + log.severe(http.dbgName + + ": write more than Content-Length"); + len = (int) remaining; + // data between position and limit + bucket = popLen((int) remaining); + getNet().getOut().queue(bucket); + while (bucket != null) { + bucket = super.popFirst(); + if (bucket != null) { + bucket.release(); + } + } + + // forced close + //close(); + remaining = 0; + return true; + } + } + if (super.isClosedAndEmpty()) { + //http.rawSendBuffers.queue(IOBrigade.MARK); + if (remaining > 0) { + http.closeStreamOnEnd("sent more than content-length"); + log.severe("Content-Length > body"); + } + return true; + } + return false; + } + + public void close() throws IOException { + if (sendBody && !http.error) { + flushToNext(); // will send any remaining data. + } + + if (isContentDelimited() && !http.error) { + if (!chunked && remaining > 0) { + log.severe("CLOSE CALLED WITHOUT FULL LEN"); + // TODO: abort ? + } else { + super.close(); + if (sendBody) { + flushToNext(); // will send '0' + } + } + } else { + super.close(); + if (sendBody) { + flushToNext(); + } + } + } + + private boolean sendChunked() throws IOException { + int len = 0; + int cnt = super.getBufferCount(); + for (int i = 0; i < cnt; i++) { + BBucket iob = super.peekBucket(i); + len += iob.remaining(); + } + + if (len > 0) { + ByteBuffer sendChunkBuffer = chunk.prepareChunkHeader(len); + remaining = len; + getNet().getOut().queue(sendChunkBuffer); + while (cnt > 0) { + Object bc = super.popFirst(); + getNet().getOut().queue(bc); + cnt --; + } + } + + if (super.isClosedAndEmpty()) { + if (!endSent) { + getNet().getOut().append(chunk.endChunk()); + endSent = true; + } + //http.rawSendBuffers.queue(IOBrigade.MARK); + return true; + } else { + return false; + } + } + + boolean isDone() { + if (noBody) { + return true; + } + if (isContentDelimited()) { + if (!chunked && remaining == 0) { + return true; + } else if (chunked && super.isAppendClosed()) { + return true; + } + } + return false; + } + + IOChannel getNet() { + return http.getNet(); + } + + public void setMessage(HttpMessage httpMsg) { + this.httpMsg = httpMsg; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java new file mode 100644 index 000000000..df8878c6e --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpChannel.java @@ -0,0 +1,1443 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.http.HttpMessage.HttpMessageBytes; +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.DumpChannel; +import org.apache.tomcat.lite.io.FastHttpDateFormat; +import org.apache.tomcat.lite.io.BBucket; +import org.apache.tomcat.lite.io.IOBuffer; +import org.apache.tomcat.lite.io.IOChannel; +import org.apache.tomcat.lite.io.IOConnector; + +/** + * HTTP async client and server, based on tomcat NIO/APR connectors + * + * 'Input', 'read', 'Recv' refers to information we get from the remote side - + * the request body for server-mode or response body for client. + * + * 'Output', 'write', 'Send' is for info we send - the post in client mode + * and the response body for server mode. + * + * @author Costin Manolache + */ +public class HttpChannel extends IOChannel { + + static final int HEADER_SIZE = 8192; + + static AtomicInteger serCnt = new AtomicInteger(); + + protected static Logger log = Logger.getLogger("HttpCh"); + + boolean debug = false; + // Used to receive an entity - headers + maybe some body + // read() must first consume extra data from this buffer. + // Next reads will be direct from socket. + protected BBuffer headRecvBuf = BBuffer.allocate(HEADER_SIZE); + BBuffer line = BBuffer.wrapper(); + + // ---- Buffers owned by the AsyncHttp object ---- + + BBuffer headB = BBuffer.wrapper(); + FutureCallbacks doneLock = new FutureCallbacks(); + ArrayList filters = new ArrayList(); + + // ---------- Body read side ------------ + + // Set if Exect: 100-continue was set on reqest. + // If this is the case - body won't be sent until + // server responds ( client ) and server will only + // read body after ack() - or skip to next request + // without swallowing the body. + protected boolean expectation = false; + + /** Ready for recycle, if send/receive are done */ + protected boolean release = false; + + protected boolean sendReceiveDone = false; + + + // ----------- Body write side ------------ + + + // TODO: setters + /** + * Called when the incoming headers have been received. + * ( response for client mode, request for server mode ) + * @throws IOException + */ + HttpService httpService; + /** + * Called when: + * - body sent + * - body received + * - release() called - either service() done or client done with the + * buffers. + * + * After this callback: + * - socket closed if closeOnEndSend, or put in keep-alive + * - AsyncHttp.recycle() + * - returned to the pool. + */ + private RequestCompleted doneAllCallback; + + + HttpMessage inMessage; + + + HttpMessage outMessage; + + // receive can be for request ( server mode ) or response ( client ) + HttpBody receiveBody = new HttpBody(this, false); + HttpBody sendBody = new HttpBody(this, true); + + private HttpRequest httpReq; + private HttpResponse httpRes; + + boolean headersDone = false; + protected boolean serverMode = false; + // ---------- Client only ------------ + + //protected HttpParser parser = new HttpParser(); + + protected String dbgName = this.getClass().getSimpleName(); + // ----- Pools - sockets, heavy objects ------------- + // If client mode - what host we are connected to. + protected String host; + + protected int port; + + private HttpConnector httpConnector; + // ------ JMX + protected int ser; // id - for jmx registration and logs + // Server side only + protected String serverHeader = "TomcatLite"; + + protected boolean http11 = false; + + protected boolean http09 = false; + protected boolean error = false; + protected boolean abortDone = false; + FutureCallbacks doneFuture; + boolean doneCallbackCalled = false; + + /** + * Close connection when done writting, no content-length/chunked, + * or no keep-alive ( http/1.0 ) or error. + * + * ServerMode: set if HTTP/0.9 &1.0 || !keep-alive + * ClientMode: not currently used + */ + boolean keepAlive = true; + + // Will be signalled (open) when the buffer is empty. + private FutureCallbacks flushLock = new FutureCallbacks(); + + // -- Lifecycle -- + + Runnable dispatcherRunnable = new Runnable() { + @Override + public void run() { + getConnector().getDispatcher().runService(HttpChannel.this); + } + }; + + long ioTimeout = 30 * 60000; // 30 min seems high enough + + public static final String CONTENT_LENGTH= "Content-Length"; + + /** + * HTTP/1.0. + */ + public static final String HTTP_10 = "HTTP/1.0"; + + public static final String HTTP_11 = "HTTP/1.1"; + + public static final String CHUNKED = "chunked"; + + public static final String CLOSE = "close"; + + public static final String KEEPALIVE_S = "keep-alive"; + + + public static final String CONNECTION = "Connection"; + + public static final String TRANSFERENCODING = "Transfer-Encoding"; + + /** + * SEMI_COLON. + */ + public static final byte SEMI_COLON = (byte) ';'; + + static byte[] END_CHUNK_BYTES = { + (byte) '\r', (byte) '\n', + (byte) '0', + (byte) '\r', (byte) '\n', + (byte) '\r', (byte) '\n'}; + + public static final byte QUESTION = (byte) '?'; + + static final byte COLON = (byte) ':'; + public HttpChannel() { + ser = serCnt.incrementAndGet(); + httpReq = new HttpRequest(this); + httpRes = new HttpResponse(this); + init(); + serverMode(false); + } + + /** + * Close the connection, return to pool. Called if a + * framing error happens, or if we want to force the connection + * to close, without waiting for all data to be sent/received. + * @param t + * + * @throws IOException + */ + public void abort(Throwable t) throws IOException { + abort(t.toString()); + } + + public void abort(String t) throws IOException { + synchronized (this) { + if (abortDone) { + return; + } + abortDone = true; + } + + checkRelease(); + trace("abort " + t); + log.info("Abort connection " + t); + if (net != null ) { + if (net.isOpen()) { + net.close(); + net.startSending(); + } + } + inMessage.state = HttpMessage.State.DONE; + outMessage.state = HttpMessage.State.DONE; + sendReceiveDone = true; + error = true; + close(); + handleEndSendReceive(); + } + + public HttpChannel addFilterAfter(IOChannel filter) { + filters.add(filter); + return this; + } + + + private void checkRelease() throws IOException { + if (release && sendReceiveDone) { + throw new IOException("Object released"); + } + } + + void closeStreamOnEnd(String cause) { + if (debug) + log.info("Not reusing connection because: " + cause); + keepAlive = false; + } + + public void complete() throws IOException { + checkRelease(); + if (!getOut().isAppendClosed()) { + getOut().close(); + } + if (!getIn().isAppendClosed()) { + getIn().close(); + } + + startSending(); + } + + public int doRead(BBuffer chunk) + throws IOException { + checkRelease(); + BBucket next = null; + while (true) { + getIn().waitData(0); + next = (BBucket) getIn().popFirst(); + if (next != null) { + break; + } else if (getIn().isAppendClosed()) { + return -1; + } else { + System.err.println("Spurious waitData signal, no data"); + } + } + chunk.append(next.array(), next.position(), next.remaining()); + int read = next.remaining(); + next.release(); + return read; + } + + public void flushNet() throws IOException { + checkRelease(); + if (net != null) { + net.startSending(); + } + } + + public HttpConnector getConnector() { + return httpConnector; + } + + public FutureCallbacks getDoneFuture() { + return doneLock; + } + + public boolean getError() { + return error; + } + + // ---------------- Writting ------------------------------- + + public String getId() { + return Integer.toString(ser); + } + + public IOBuffer getIn() { + return receiveBody; + } + + + public long getIOTimeout() { + return ioTimeout; + } + + public IOChannel getNet() { + return net; + } + + + public IOBuffer getOut() { + return sendBody; + } + + public HttpRequest getRequest() { + return httpReq; + } + + + public HttpResponse getResponse() { + return httpRes; + } + + + public String getState() { + return + (serverMode ? "SRV:" : "") + + (keepAlive() ? " KA " : "") + + "RCV=[" + inMessage.state.toString() + " " + + receiveBody.toString() + + "] SND=[" + outMessage.state.toString() + + " " + sendBody.toString() + "]"; + } + + + public String getStatus() { + return getResponse().getStatus() + " " + getResponse().getMessage(); + } + + + + public String getTarget() { + if (host == null) { + return ":" + port; + } + return host + ":" + port; + } + + + /** + * Called from IO thread, after the request body + * is completed ( or if there is no req body ) + * @throws IOException + */ + protected void handleEndReceive(boolean frameError) throws IOException { + if (inMessage.state == HttpMessage.State.DONE) { + return; + } + if (debug) { + trace("END_RECV " + ((frameError) ? " FRAME_ERROR" : "")); + } + if (frameError) { + closeStreamOnEnd("frame error"); + // TODO: next read() should throw exception !! + error = true; + } + + getIn().close(); + + inMessage.state = HttpMessage.State.DONE; + handleEndSendReceive(); + } + + /* + * Called when sending, receiving and processing is done. + * Can be called: + * - from IO thread, if this is a result of a read/write event that + * finished the send/recev pair. + * - from an arbitrary thread, if read was complete and the last write + * was a success and done in that thread ( write is not bound to IO thr) + * + */ + protected void handleEndSendReceive() throws IOException { + this.doneLock.signal(this); + synchronized (this) { + if (doneCallbackCalled) { + return; + } + if (outMessage.state != HttpMessage.State.DONE || + inMessage.state != HttpMessage.State.DONE) { + return; + } + doneCallbackCalled = true; + } + + if (!keepAlive() && net != null) { + net.getOut().close(); // shutdown output if not done + net.getIn().close(); // this should close the socket + net.startSending(); + } + + if (doneAllCallback != null) { + doneAllCallback.handle(this, error ? new Throwable() : null); + } + + // Remove the net object - will be pooled separtely + IOChannel ch = this.net; + if (ch != null && keepAlive()) { + + boolean keepOpen = ch.isOpen(); + + resetBuffers(); // net is now NULL - can't send anything more + if (getConnector() != null) { + getConnector().returnSocket(ch, serverMode, keepOpen); + } + } + + if (debug) { + trace("END_SEND_RECEIVE" + + (!keepAlive() ? " CLOSE_ON_END " : "") + + (release ? " REL" : "")); + } + + synchronized(this) { + sendReceiveDone = true; + maybeRelease(); + } + } + + /** + * called from IO thread OR servlet thread when last block has been sent. + * If not using the socket ( net.getOut().flushCallback ) - this must + * be called explicitely after flushing the body. + */ + void handleEndSent() throws IOException { + if (outMessage.state == HttpMessage.State.DONE) { + // Only once. + if (debug) { + trace("Duplicate END SEND"); + } + return; + } + outMessage.state = HttpMessage.State.DONE; + + getOut().close(); + + // Make sure the send/receive callback is called once + if (debug) { + trace("END_SEND"); + } + handleEndSendReceive(); + } + + // ----- End Selector thread callbacks ---- + public void handleError(String type) { + System.err.println("Error " + type + " " + outMessage.state); + } + + @Override + public void handleFlushed(IOChannel net) throws IOException { + flushLock.signal(this); + super.handleFlushed(this); + if (sendBody.isClosedAndEmpty()) { + handleEndSent(); + } + } + + /** + * Called when the net has readable data. + */ + @Override + public void handleReceived(IOChannel net) throws IOException { + try { + if (getConnector() == null) { + throw new IOException("Data received after release"); + } + if (net == null) { + return; // connection released + } + if (net.getIn().isClosedAndEmpty()) { + // Close received + closeStreamOnEnd("close on input 2"); + if (inMessage.state == HttpMessage.State.HEAD) { + trace("NET CLOSE WHILE READING HEAD"); + abort(new IOException("Connection closed")); + return; + } else if (inMessage.state == HttpMessage.State.DONE) { + // Close received - make sure we close out + if (sendBody.isClosedAndEmpty()) { + net.getOut().close(); + } + return; + } + } + if (debug) { + trace("Http data received " + inMessage.state + " " + + net.getIn() + " headerDone=" + headersDone); + } + + if (inMessage.state == HttpMessage.State.HEAD) { + headDataReceived(); + if (inMessage.state == HttpMessage.State.HEAD) { + return; // still parsing head + } + if (serverMode && httpReq.decodedUri.remaining() == 0) { + abort("Invalid url"); + } + } + + if (inMessage.state == HttpMessage.State.BODY_DATA) { + if (net != null) { + receiveBody.rawDataReceived(net.getIn()); + } + } + + // Send header callbacks - we process any incoming data + // first, so callbacks have more info + if (httpService != null && !headersDone) { + headersDone = true; + try { + httpService.service(getRequest(), getResponse()); + } catch (Throwable t) { + t.printStackTrace(); + abort(t); + } + } + + // If header callback or previous dataReceived() hasn't consumed all + if (receiveBody.getBufferCount() > 0) { + // Has data + super.sendHandleReceivedCallback(); // callback + } + + // Receive has marked the body as closed + if (receiveBody.isAppendClosed() + && inMessage.state != HttpMessage.State.DONE) { + if (net != null && net.getIn().getBufferCount() > 0) { + if (debug) { + trace("Pipelined requests"); // may be a crlf + } + } + handleEndReceive(receiveBody.frameError); + } + + if (inMessage.state == HttpMessage.State.DONE) { + // TCP end ? + if (net == null || net.getIn() == null) { + trace("NO NET"); + return; + } + if (net.getIn().isClosedAndEmpty()) { + // If not already closed. + closeStreamOnEnd("closed on input 3"); + if (outMessage.state == HttpMessage.State.DONE) { + // need to flush out. + net.getOut().close(); + flushNet(); + } + } else { + // Next request, ignore it. + } + + } + } catch (IOException ex) { + ex.printStackTrace(); + abort(ex); + } + } + + + /** + * Read and process a chunk of head, called from dataReceived() if + * in HEAD mode. + * + * @return <= 0 - still in head mode. > 0 moved to body mode, some + * body chunk may have been received. + */ + protected void headDataReceived() throws IOException { + while (true) { + // we know we have one + int read = net.getIn().readLine(headRecvBuf); + if (read < 0) { + if (debug) { + trace("CLOSE while reading HEAD"); + } + // too early - we don't have the head + abort("Close in head"); + return; + } + // Remove starting empty lines. + headRecvBuf.skipEmptyLines(); + + // Do we have another full line in the input ? + if (BBuffer.hasLFLF(headRecvBuf)) { + break; + } + if (read == 0) { // no more data + return; + } + } + headRecvBuf.wrapTo(headB); + + + parseMessage(headB); + + + if (debug) { + trace("HEAD_RECV " + getRequest().requestURI() + " " + + getResponse().getMsgBytes().status() + " " + net.getIn()); + } + + } + + public void parseMessage(BBuffer headB) throws IOException { + //Parse the response + headB.readLine(line); + + HttpMessageBytes msgBytes; + + if (serverMode) { + msgBytes = httpReq.getMsgBytes(); + parseRequestLine(line, msgBytes.method(), + msgBytes.url(), + msgBytes.query(), + msgBytes.protocol()); + } else { + msgBytes = httpRes.getMsgBytes(); + parseResponseLine(line, msgBytes.protocol(), + msgBytes.status(), msgBytes.message()); + } + + parseHeaders(msgBytes, headB); + + inMessage.state = HttpMessage.State.BODY_DATA; + + // TODO: hook to allow specific charsets ( can be done later ) + + inMessage.processReceivedHeaders(); + } + + private void init() { + headRecvBuf.recycle(); + headersDone = false; + sendReceiveDone = false; + + receiveBody.recycle(); + sendBody.recycle(); + expectation = false; + + http11 = false; + http09 = false; + error = false; + abortDone = false; + + + getRequest().recycle(); + getResponse().recycle(); + host = null; + filters.clear(); + + line.recycle(); + headB.recycle(); + + doneLock.recycle(); + flushLock.recycle(); + + doneCallbackCalled = false; + keepAlive = true; + // Will be set again after pool + setHttpService(null); + doneAllCallback = null; + release = false; + } + + public boolean isDone() { + return outMessage.state == HttpMessage.State.DONE && inMessage.state == HttpMessage.State.DONE; + } + + public boolean keepAlive() { + if (http09) { + return false; + } + return keepAlive; + } + + /** + * Called when all done: + * - service finished ( endService was called ) + * - output written + * - input read + * + * or by abort(). + * + * @throws IOException + */ + private void maybeRelease() throws IOException { + synchronized (this) { + if (release && sendReceiveDone) { + if (debug) { + trace("RELEASE"); + } + if (getConnector() != null) { + getConnector().returnToPool(this); + } else { + log.severe("Attempt to release with no pool"); + } + } + } + } + + + + /* + The field-content does not include any leading or trailing LWS: + linear white space occurring before the first non-whitespace + character of the field-value or after the last non-whitespace + character of the field-value. Such leading or trailing LWS MAY + be removed without changing the semantics of the field value. + Any LWS that occurs between field-content MAY be replaced with + a single Http11Parser.SP before interpreting the field value or forwarding + the message downstream. + */ + int normalizeHeader(BBuffer value) { + byte[] buf = value.array(); + int cstart = value.position(); + int end = value.limit(); + + int realPos = cstart; + int lastChar = cstart; + byte chr = 0; + boolean gotSpace = true; + + for (int i = cstart; i < end; i++) { + chr = buf[i]; + if (chr == BBuffer.CR) { + // skip + } else if(chr == BBuffer.LF) { + // skip + } else if (chr == BBuffer.SP || chr == BBuffer.HT) { + if (gotSpace) { + // skip + } else { + buf[realPos++] = BBuffer.SP; + gotSpace = true; + } + } else { + buf[realPos++] = chr; + lastChar = realPos; // to skip trailing spaces + gotSpace = false; + } + } + realPos = lastChar; + + // so buffer is clean + for (int i = realPos; i < end; i++) { + buf[i] = BBuffer.SP; + } + value.setEnd(realPos); + return realPos; + } + + + /** + * Parse one header. + * Line must be populated. On return line will be populated + * with the next header: + * + * @param line current header line, not empty. + */ + public int parseHeader(BBuffer head, + BBuffer line, BBuffer name, BBuffer value) + throws IOException { + + int newPos = line.readToDelimOrSpace(COLON, name); + line.skipSpace(); + if (line.readByte() != COLON) { + throw new IOException("Missing ':' in header name " + line); + } + line.skipSpace(); + line.read(value); // remaining of the line + + while (true) { + head.readLine(line); + if (line.remaining() == 0) { + break; + } + byte first = line.get(0); + if (first != BBuffer.SP && first != BBuffer.HT) { + break; + } + // continuation line - append it to value + value.setEnd(line.getEnd()); + line.position(line.limit()); + } + + // We may want to keep the original and use separate buffer ? + normalizeHeader(value); + return 1; + } + + public void parseHeaders(HttpMessageBytes msgBytes, + BBuffer head) + throws IOException { + + head.readLine(line); + + int idx = 0; + while(line.remaining() > 0) { + // not empty.. + idx = msgBytes.addHeader(); + BBuffer nameBuf = msgBytes.getHeaderName(idx); + BBuffer valBuf = msgBytes.getHeaderValue(idx); + parseHeader(head, line, nameBuf, valBuf); + + // TODO: process 'interesting' headers here. + } + } + + /** + * Read the request line. This function is meant to be used during the + * HTTP request header parsing. Do NOT attempt to read the request body + * using it. + * + * @throws IOException If an exception occurs during the underlying socket + * read operations, or if the given buffer is not big enough to accomodate + * the whole line. + */ + public boolean parseRequestLine(BBuffer line, + BBuffer methodMB, BBuffer requestURIMB, + BBuffer queryMB, + BBuffer protoMB) + throws IOException { + + line.readToSpace(methodMB); + line.skipSpace(); + + line.readToDelimOrSpace(QUESTION, requestURIMB); + if (line.remaining() > 0 && line.get(0) == QUESTION) { + // Has query + line.readToSpace(queryMB); + // don't include '?' + queryMB.position(queryMB.position() + 1); + } else { + queryMB.setBytes(line.array(), line.position(), 0); + } + line.skipSpace(); + + line.readToSpace(protoMB); + + // proto is optional ( for 0.9 ) + return requestURIMB.remaining() > 0; + } + + public boolean parseResponseLine(BBuffer line, + BBuffer protoMB, BBuffer statusCode, BBuffer status) + throws IOException { + line.skipEmptyLines(); + + line.readToSpace(protoMB); + line.skipSpace(); + line.readToSpace(statusCode); + line.skipSpace(); + line.wrapTo(status); + + // message may be empty + return statusCode.remaining() > 0; + } + + /** + * Update keepAlive based on Connection header and protocol. + */ + void processConnectionHeader(MultiMap headers) { + if (http09) { + return; + } + + CBuffer value = headers.getHeader(HttpChannel.CONNECTION); + String conHeader = (value == null) ? null : value.toString(); + if (conHeader != null) { + if (HttpChannel.CLOSE.equalsIgnoreCase(conHeader)) { + closeStreamOnEnd("connection close"); + } + if (!HttpChannel.KEEPALIVE_S.equalsIgnoreCase(conHeader)) { + closeStreamOnEnd("connection != keep alive"); + } + } else { + // no connection header + if (!http11) { + closeStreamOnEnd("http1.0 no connection header"); + } + } + } + + void processExpectation() throws IOException { + expectation = false; + MultiMap headers = getRequest().getMimeHeaders(); + + CBuffer expect = headers.getHeader("expect"); + if ((expect != null) + && (expect.indexOf("100-continue") != -1)) { + expectation = true; + + // TODO: configure, use the callback or the servlet 'read'. + net.getOut().append("HTTP/1.1 100 Continue\r\n\r\n"); + net.startSending(); + } + } + + void processProtocol() throws IOException { + http11 = true; + http09 = false; + + CBuffer protocolMB = getRequest().protocol(); + if (protocolMB.equals(HttpChannel.HTTP_11)) { + http11 = true; + } else if (protocolMB.equals(HttpChannel.HTTP_10)) { + http11 = false; + } else if (protocolMB.equals("")) { + http09 = true; + http11 = false; + } else { + // Unsupported protocol + http11 = false; + error = true; + // Send 505; Unsupported HTTP version + getResponse().setStatus(505); + abort("Invalid protocol"); + } + } + + protected void recycle() { + if (debug) { + trace("RECYCLE"); + } + init(); + } + + /** + * Finalize sending and receiving. + * Indicates client is no longer interested, some IO may still be in flight. + * If in a POST and you're not interested in the body - it may be + * better to call abort(). + * + * MUST be called to allow connection reuse and pooling. + * + * @throws IOException + */ + public void release() throws IOException { + synchronized(this) { + if (release) { + return; + } + trace("RELEASE"); + release = true; + // If send/receive is done - we can reuse this object + maybeRelease(); + } + } + + public void resetBuffers() { + if (net != null) { + net.setDataFlushedCallback(null); + net.setDataReceivedCallback(null); + setSink(null); + } + } + + public void sendHeaders() throws IOException { + checkRelease(); + if (serverMode) { + sendResponseHeaders(); + } else { + sendRequest(); + } + } + + /** + * Can be called from any thread. + * + * @param host + * @param port + * @throws IOException + */ + public void sendRequest() throws IOException { + if (getRequest().isCommitted()) { + return; + } + getRequest().setCommitted(true); + + String target = host + ":" + port; + + if (getRequest().getMimeHeaders().getHeader("Host") == null + && host != null) { + CBuffer hostH = getRequest().getMimeHeaders().addValue("Host"); + hostH.set(host); // TODO: port + } + + outMessage.state = HttpMessage.State.HEAD; + + IOChannel ch = getConnector().cpool.getChannel(target); + + if (ch == null) { + if (debug) { + trace("HTTP_CONNECT: New connection " + target); + } + IOConnector.ConnectedCallback connected = new IOConnector.ConnectedCallback() { + @Override + public void handleConnected(IOChannel ch) throws IOException { + if (httpConnector.debugHttp) { + IOChannel ch1 = new DumpChannel(""); + ch.addFilterAfter(ch1); + ch = ch1; + } + + sendRequestHeaders(ch); + } + }; + getConnector().getIOConnector().connect(host, port, connected); + } else { + if (debug) { + trace("HTTP_CONNECT: Reuse connection " + target + " " + this); + } + // TODO retry if closed + sendRequestHeaders(ch); + } + } + + /** + * Used in request mode. + * + * @throws IOException + */ + void sendRequestHeaders(IOChannel ch) throws IOException { + if (getConnector() == null) { + throw new IOException("after release"); + } + if (!ch.isOpen()) { + abort("Closed channel"); + return; + } + setChannel(ch); // register read/write callbacks + + // Update transfer fields based on headers. + processProtocol(); + + processConnectionHeader(getRequest().getMimeHeaders()); + + + // 1.0: The presence of an entity body in a request is signaled by + // the inclusion of a Content-Length header field in the request + // message headers. HTTP/1.0 requests containing an entity body + // must include a valid Content-Length header field. + + if (!sendBody.isContentDelimited()) { + // Will not close connection - just flush and mark the body + // as sent + sendBody.noBody = true; + getOut().close(); + } + + if (sendBody.noBody) { + getRequest().getMimeHeaders().remove(HttpChannel.CONTENT_LENGTH); + getRequest().getMimeHeaders().remove(HttpChannel.TRANSFERENCODING); + } else { + long contentLength = + getRequest().getContentLength(); + if (contentLength < 0) { + getRequest().getMimeHeaders().addValue(HttpChannel.TRANSFERENCODING). + set(HttpChannel.CHUNKED); + } + sendBody.processContentDelimitation(); + } + + sendBody.updateCloseOnEnd(); + + try { + getRequest().serialize(net.getOut()); + if (debug) { + trace("S: \n" + net.getOut()); + } + + } catch (Throwable t) { + log.log(Level.SEVERE, "Error sending request", t); + } + + if (outMessage.state == HttpMessage.State.HEAD) { + outMessage.state = HttpMessage.State.BODY_DATA; + } + + // TODO: add any body and flush. More body can be added later - + // including 'end'. + + startSending(); + + } + + /** + * When committing the response, we have to validate the set of headers, as + * well as setup the response filters. + * Only in server mode. + */ + void sendResponseHeaders() throws IOException { + checkRelease(); + if (!serverMode) { + throw new IOException("Only in server mode"); + } + + if (getResponse().isCommitted()) { + return; + } + getResponse().setCommitted(true); + + sendBody.noBody = !getResponse().hasBody(); + + if (sendBody.statusDropsConnection(getResponse().getStatus())) { + closeStreamOnEnd("status drops connection"); + } + if (error) { + closeStreamOnEnd("error"); + } + + // A header explicitely set. + CBuffer transferEncHeader = + getResponse().getMimeHeaders().getHeader(HttpChannel.TRANSFERENCODING); + if (!sendBody.noBody + && keepAlive()) { + if (getResponse().getContentLength() < 0) { + // Use chunked by default, if no c-l + if (transferEncHeader == null) { + getResponse().getMimeHeaders().addValue(HttpChannel.TRANSFERENCODING).set(HttpChannel.CHUNKED); + } else { + transferEncHeader.set(HttpChannel.CHUNKED); + } + } + } + + sendBody.processContentDelimitation(); + + sendBody.updateCloseOnEnd(); + + MultiMap headers = getResponse().getMimeHeaders(); + + // Add date header + if (headers.getHeader("Date") == null) { + headers.setValue("Date").set(FastHttpDateFormat.getCurrentDate()); + } + + // Add server header + if (serverHeader.length() > 0) { + headers.setValue("Server").set(serverHeader); + } + + // did the user set a connection header that may override what we have ? + processConnectionHeader(headers); + + if (!keepAlive()) { + headers.setValue(HttpChannel.CONNECTION).set(HttpChannel.CLOSE); + } else { + if (!http11 && !http09) { + headers.setValue(HttpChannel.CONNECTION).set(HttpChannel.KEEPALIVE_S); + } + } + + if (debug) { + trace("Send response headers " + net); + } + if (net != null) { + getResponse().serialize(net.getOut()); + } + + if (outMessage.state == HttpMessage.State.HEAD) { + outMessage.state = HttpMessage.State.BODY_DATA; + } + + if (sendBody.isDone()) { + getOut().close(); + } + + if (net != null) { + net.startSending(); + } + } + + public HttpChannel serverMode(boolean enabled) { + if (enabled) { + serverMode = true; + dbgName = "AsyncHttpServer"; + httpReq.setBody(receiveBody); + httpRes.setBody(sendBody); + sendBody.setMessage(httpRes); + receiveBody.setMessage(httpReq); + inMessage = httpReq; + outMessage = httpRes; + } else { + serverMode = false; + dbgName = "AsyncHttp"; + httpReq.setBody(sendBody); + httpRes.setBody(receiveBody); + sendBody.setMessage(httpReq); + receiveBody.setMessage(httpRes); + inMessage = httpRes; + outMessage = httpReq; + } + if (debug) { + log = Logger.getLogger(dbgName); + } + return this; + } + + public void setChannel(IOChannel ch) throws IOException { + for (IOChannel filter: filters) { + ch.addFilterAfter(filter); + ch = filter; + } + + withBuffers(ch); + } + + public void setCompletedCallback(RequestCompleted doneAllCallback) + throws IOException { + this.doneAllCallback = doneAllCallback; + synchronized (this) { + if (doneCallbackCalled) { + return; + } + if (outMessage.state != HttpMessage.State.DONE || inMessage.state != HttpMessage.State.DONE) { + return; + } + } + doneCallbackCalled = true; + if (doneAllCallback != null) { + doneAllCallback.handle(this, error ? new Throwable() : null); + } + } + + public void setConnector(HttpConnector pool) { + this.httpConnector = pool; + } + + public void setHttpService(HttpService headersReceivedCallback) { + this.httpService = headersReceivedCallback; + } + + public void setIOTimeout(long timeout) { + ioTimeout = timeout; + } + + public void setTarget(String host, int port) { + this.host = host; + this.port = port; + } + + public void startSending() throws IOException { + checkRelease(); + + sendBody.flushToNext(); + flushNet(); + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("id=").append(ser) + .append(",rs=").append(getState()) + .append(")"); + return sb.toString(); + } + + + void trace(String msg) { + if(debug) { + log.info(this.toString() + " " + msg + " done=" + doneCallbackCalled); + } + } + + public void waitFlush(long timeMs) throws IOException { + if (getOut().getBufferCount() == 0) { + return; + } + flushLock.waitSignal(timeMs); + } + + public HttpChannel withBuffers(IOChannel net) { + setSink(net); + net.setDataFlushedCallback(this); + net.setDataReceivedCallback(this); + return this; + } + + /** + * Normalize URI. + *

+ * This method normalizes "\", "//", "/./" and "/../". This method will + * return false when trying to go above the root, or if the URI contains + * a null byte. + * + * @param uriMB URI to be normalized, will be modified + */ + public static boolean normalize(BBuffer uriBC) { + + byte[] b = uriBC.array(); + int start = uriBC.getStart(); + int end = uriBC.getEnd(); + + // URL * is acceptable + if ((end - start == 1) && b[start] == (byte) '*') + return true; + + if (b[start] != '/') { + // TODO: http://.... URLs + return true; + } + + int pos = 0; + int index = 0; + + // Replace '\' with '/' + // Check for null byte + for (pos = start; pos < end; pos++) { + if (b[pos] == (byte) '\\') + b[pos] = (byte) '/'; + if (b[pos] == (byte) 0) + return false; + } + + // The URL must start with '/' + if (b[start] != (byte) '/') { + return false; + } + + // Replace "//" with "/" + for (pos = start; pos < (end - 1); pos++) { + if (b[pos] == (byte) '/') { + while ((pos + 1 < end) && (b[pos + 1] == (byte) '/')) { + copyBytes(b, pos, pos + 1, end - pos - 1); + end--; + } + } + } + + // If the URI ends with "/." or "/..", then we append an extra "/" + // Note: It is possible to extend the URI by 1 without any side effect + // as the next character is a non-significant WS. + if (((end - start) >= 2) && (b[end - 1] == (byte) '.')) { + if ((b[end - 2] == (byte) '/') + || ((b[end - 2] == (byte) '.') + && (b[end - 3] == (byte) '/'))) { + b[end] = (byte) '/'; + end++; + } + } + + uriBC.setEnd(end); + + index = 0; + + // Resolve occurrences of "/./" in the normalized path + while (true) { + index = uriBC.indexOf("/./", 0, 3, index); + if (index < 0) + break; + copyBytes(b, start + index, start + index + 2, + end - start - index - 2); + end = end - 2; + uriBC.setEnd(end); + } + + index = 0; + + // Resolve occurrences of "/../" in the normalized path + while (true) { + index = uriBC.indexOf("/../", 0, 4, index); + if (index < 0) + break; + // Prevent from going outside our context + if (index == 0) + return false; + int index2 = -1; + for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos --) { + if (b[pos] == (byte) '/') { + index2 = pos; + } + } + copyBytes(b, start + index2, start + index + 3, + end - start - index - 3); + end = end + index2 - index - 3; + uriBC.setEnd(end); + index = index2; + } + + //uriBC.setBytes(b, start, end); + uriBC.setEnd(end); + return true; + + } + + /** + * Copy an array of bytes to a different position. Used during + * normalization. + */ + private static void copyBytes(byte[] b, int dest, int src, int len) { + for (int pos = 0; pos < len; pos++) { + b[pos + dest] = b[pos + src]; + } + } + + + + public static interface HttpService { + void service(HttpRequest httpReq, HttpResponse httpRes) throws IOException; + } + + public static interface RequestCompleted { + void handle(HttpChannel data, Object extraData) throws IOException; + } + + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java new file mode 100644 index 000000000..3bad3b0af --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpConnector.java @@ -0,0 +1,625 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.Timer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.http.HttpChannel.HttpService; +import org.apache.tomcat.lite.io.DumpChannel; +import org.apache.tomcat.lite.io.BBucket; +import org.apache.tomcat.lite.io.IOChannel; +import org.apache.tomcat.lite.io.IOConnector; + +/** + * Manages HttpChannels and associated socket pool. + * + * + * @author Costin Manolache + */ +public class HttpConnector { + + public static interface HttpChannelEvents { + /** HttpChannel object created. It'll be used many times. + * @throws IOException + */ + public void onCreate(HttpChannel ch, HttpConnector con) throws IOException; + + /** + * HttpChannel object no longer needed, out of pool. + * @throws IOException + */ + public void onDestroy(HttpChannel ch, HttpConnector con) throws IOException; + } + + private static Logger log = Logger.getLogger("HttpConnector"); + private int maxHttpPoolSize = 20; + + private int maxSocketPoolSize = 100; + private int keepAliveTimeMs = 30000; + + private Queue httpChannelPool = new ConcurrentLinkedQueue(); + + private IOConnector ioConnector; + + boolean debugHttp = false; + boolean debug = false; + + boolean clientKeepAlive = true; + boolean serverKeepAlive = true; + + HttpChannelEvents httpEvents; + + public AtomicInteger inUse = new AtomicInteger(); + public AtomicInteger newHttpChannel = new AtomicInteger(); + public AtomicInteger totalHttpChannel = new AtomicInteger(); + public AtomicInteger totalClientHttpChannel = new AtomicInteger(); + public AtomicInteger recycledChannels = new AtomicInteger(); + public AtomicInteger reusedChannels = new AtomicInteger(); + + public ConnectionPool cpool = new ConnectionPool(); + + + public HttpConnector(IOConnector ioConnector) { + this.ioConnector = ioConnector; + dispatcher = new Dispatcher(); + defaultService = dispatcher; + } + + protected HttpConnector() { + this(null); + } + + public Dispatcher getDispatcher() { + return dispatcher; + } + + public HttpConnector withIOConnector(IOConnector selectors) { + ioConnector = selectors; + return this; + } + + public void setDebug(boolean b) { + this.debug = b; + } + + public void setDebugHttp(boolean b) { + this.debugHttp = b; + } + + public void setClientKeepAlive(boolean b) { + this.clientKeepAlive = b; + } + + public void setServerKeepAlive(boolean b) { + this.serverKeepAlive = b; + } + + public boolean isDebug() { + return debug; + } + + public boolean isClientKeepAlive() { + return clientKeepAlive; + } + + public boolean isServerKeepAlive() { + return serverKeepAlive; + } + + public int getInUse() { + return inUse.get(); + } + + public int getMaxHttpPoolSize() { + return maxHttpPoolSize; + } + + public void setMaxHttpPoolSize(int maxHttpPoolSize) { + this.maxHttpPoolSize = maxHttpPoolSize; + } + + public void setOnCreate(HttpChannelEvents callback) { + httpEvents = callback; + } + + /** + * Override to create customized client/server connections. + * + * @return + * @throws IOException + */ + protected HttpChannel create() throws IOException { + HttpChannel res = new HttpChannel(); + newHttpChannel.incrementAndGet(); + res.setConnector(this); + if (httpEvents != null) { + httpEvents.onCreate(res, this); + } + if (debugHttp) { + res.debug = debugHttp; + } + return res; + } + + public HttpChannel get(String host, int port) throws IOException { + HttpChannel http = get(false, host, port); + http.setTarget(host, port); + return http; + } + + public HttpChannel getServer() { + try { + return get(true, null, 0); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } + } + + /** + * Get an existing AsyncHttp object. Since it uses many buffers and + * objects - it's more efficient to pool it. + * + * release will return the object to the pool. + * @throws IOException + */ + public HttpChannel get(CharSequence urlString) throws IOException { + URL url = new URL(urlString.toString()); + String host = url.getHost(); + int port = url.getPort(); + boolean secure = "http".equals(url.getAuthority()); + if (port == -1) { + port = secure ? 443: 80; + } + // TODO: insert SSL filter + HttpChannel http = get(false, host, port); + http.setTarget(host, port); + String path = url.getFile(); // path + qry + // TODO: query string + http.getRequest().requestURI().set(path); + return http; + } + + protected HttpChannel get(boolean server, CharSequence host, int port) throws IOException { + HttpChannel processor = null; + synchronized (httpChannelPool) { + processor = httpChannelPool.poll(); + } + boolean reuse = false; + totalHttpChannel.incrementAndGet(); + if (processor == null) { + processor = create(); + } else { + reuse = true; + reusedChannels.incrementAndGet(); + processor.release = false; + } + if (!server) { + totalClientHttpChannel.incrementAndGet(); + } + processor.serverMode(server); + if (debug) { + log.info((reuse ? "REUSE ": "Create ") + + host + ":" + port + + (server? " S" : "") + + " id=" + processor.ser + + " " + processor + + " size=" + httpChannelPool.size()); + } + + processor.setConnector(this); + inUse.incrementAndGet(); + return processor; + } + + + /** + * Called by HttpChannel when the HTTP request is done, i.e. all + * sending/receiving is complete. The service may still use the + * HttpChannel object. + * + * If keepOpen: clients will wait in the pool, detecting server close. + * For server: will wait for new requests. + * + * TODO: timeouts, better pool management + */ + protected void returnSocket(IOChannel ch, boolean serverMode, + boolean keepOpen) + throws IOException { + // Now handle net - note that we could have reused the async object + if (serverMode) { + BBucket first = ch.getIn().peekFirst(); + if (first != null) { + HttpChannel http = getServer(); + if (debug) { + http.trace("PIPELINED request " + first + " " + http.httpService); + } + http.setChannel(ch); + http.setHttpService(defaultService); + + // In case it was disabled + if (ch != null) { + if (ch.isOpen()) { + ch.readInterest(true); + } + // Notify that data was received. The callback must deal with + // pre-existing data. + ch.sendHandleReceivedCallback(); + } + http.handleReceived(http.getSink()); + return; + } + } + if (serverMode && !serverKeepAlive) { + keepOpen = false; + } + if (!serverMode && !clientKeepAlive) { + keepOpen = false; + } + + + if (keepOpen) { + // reuse the socket + if (serverMode) { + if (debug) { + log.info(">>> server socket KEEP_ALIVE " + ch.getTarget() + + " " + ch); + } + ch.readInterest(true); + ch.setDataReceivedCallback(receiveCallback); + ch.setDataFlushedCallback(null); + + cpool.returnChannel(ch); + // TODO: timeout event to close it + // ch.setTimer(10000, new Runnable() { + // @Override + // public void run() { + // System.err.println("Keep alive timeout"); + // } + // }); + } else { + if (debug) { + log.info(">>> client socket KEEP_ALIVE " + ch.getTarget() + + " " + ch); + } + ch.readInterest(true); + ch.setDataReceivedCallback(clientReceiveCallback); + ch.setDataFlushedCallback(null); + + cpool.returnChannel(ch); + } + } else { + if (debug) { + log.info("--- Close socket, no keepalive " + ch); + } + if (ch != null) { + ch.close(); + } + } + } + + protected void returnToPool(HttpChannel http) throws IOException { + inUse.decrementAndGet(); + recycledChannels.incrementAndGet(); + if (debug) { + log.info("Return " + http.getTarget() + " obj=" + + http + " size=" + httpChannelPool.size()); + } + + http.recycle(); + + // No more data - release the object + synchronized (httpChannelPool) { + http.resetBuffers(); + http.setConnector(null); + if (httpChannelPool.contains(http)) { + System.err.println("dup ? "); + } + if (httpChannelPool.size() >= maxHttpPoolSize) { + if (httpEvents != null) { + httpEvents.onDestroy(http, this); + } + } else { + httpChannelPool.add(http); + } + } + } + + + public IOConnector getIOConnector() { + return ioConnector; + } + + // Host + context mapper. + Dispatcher dispatcher; + HttpService defaultService; + int port = 8080; + + + public void setHttpService(HttpService s) { + defaultService = s; + } + + public void start() throws IOException { + if (ioConnector != null) { + ioConnector.acceptor(new AcceptorCallback(this, defaultService), + Integer.toString(port), null); + } + } + + /** + * + * TODO: only clean our state and sockets we listen on. + * + */ + public void stop() { + if (ioConnector != null) { + ioConnector.stop(); + } + } + + private static class AcceptorCallback implements IOConnector.ConnectedCallback { + HttpConnector httpCon; + HttpService callback; + + public AcceptorCallback(HttpConnector asyncHttpConnector, + HttpService headersReceived) { + this.httpCon = asyncHttpConnector; + this.callback = headersReceived; + } + + @Override + public void handleConnected(IOChannel accepted) throws IOException { + HttpChannel shttp = httpCon.getServer(); + if (callback != null) { + shttp.setHttpService(callback); + } + if (httpCon.debugHttp) { + IOChannel ch = new DumpChannel(""); + accepted.addFilterAfter(ch); + shttp.setChannel(ch); + } else { + shttp.setChannel(accepted); + } + // TODO: JSSE filter + + + // Will read any data in the channel. + + accepted.handleReceived(accepted); + } + + } + + + private IOConnector.DataReceivedCallback receiveCallback = + new IOConnector.DataReceivedCallback() { + /** For keepalive - for server + * + * @param peer + * @throws IOException + */ + @Override + public void handleReceived(IOChannel net) throws IOException { + cpool.stopKeepAlive(net); + if (!net.isOpen()) { + return; + } + HttpChannel shttp = getServer(); + shttp.setChannel(net); + shttp.setHttpService(defaultService); + net.sendHandleReceivedCallback(); + } + }; + + + // Sate-less, just closes the net. + private IOConnector.DataReceivedCallback clientReceiveCallback = + new IOConnector.DataReceivedCallback() { + + @Override + public void handleReceived(IOChannel net) throws IOException { + if (!net.isOpen()) { + cpool.stopKeepAlive(net); + return; + } + log.warning("Unexpected message from server in client keep alive " + + net.getIn()); + if (net.isOpen()) { + net.close(); + } + } + + }; + + public HttpConnector setPort(int port2) { + this.port = port2; + return this; + } + + /** + * Connections for one remote host. + * This should't be restricted by IP:port or even hostname, + * for example if a server has multiple IPs or LB replicas - any would work. + */ + public static class RemoteServer { + public ConnectionPool pool; + public ArrayList connections = new ArrayList(); + } + + + // TODO: add timeouts, limits per host/total, expire old entries + // TODO: discover apr and use it + + public class ConnectionPool { + // visible for debugging - will be made private, with accessor + /** + * Map from client names to socket pools. + */ + public Map hosts = new HashMap(); + boolean keepOpen = true; + + // Statistics + public AtomicInteger waitingSockets = new AtomicInteger(); + public AtomicInteger closedSockets = new AtomicInteger(); + + public AtomicInteger hits = new AtomicInteger(); + public AtomicInteger misses = new AtomicInteger(); + + Timer timer; + + public int getTargetCount() { + return hosts.size(); + } + + public int getSocketCount() { + return waitingSockets.get(); + } + + public int getClosedSockets() { + return closedSockets.get(); + } + + public String dumpSockets() { + StringBuffer sb = new StringBuffer(); + for (CharSequence k: hosts.keySet()) { + RemoteServer t = hosts.get(k); + sb.append(k).append("=").append(t.connections.size()).append("\n"); + } + return sb.toString(); + } + + public Set getKeepAliveTargets() { + return hosts.keySet(); + } + + /** + * @param key host:port, or some other key if multiple hosts:ips + * are connected to equivalent servers ( LB ) + * @throws IOException + */ + public IOChannel getChannel(CharSequence key) throws IOException { + RemoteServer t = null; + synchronized (hosts) { + t = hosts.get(key); + if (t == null) { + misses.incrementAndGet(); + return null; + } + } + IOChannel res = null; + synchronized (t) { + if (t.connections.size() == 0) { + misses.incrementAndGet(); + hosts.remove(key); + return null; + } // one may be added - no harm. + res = t.connections.remove(t.connections.size() - 1); + + if (t.connections.size() == 0) { + hosts.remove(key); + } + if (res == null) { + log.fine("Null connection ?"); + misses.incrementAndGet(); + return null; + } + + if (!res.isOpen()) { + res.setDataReceivedCallback(null); + res.close(); + log.fine("Already closed " + res); + //res.keepAliveServer = null; + res = null; + } + + waitingSockets.decrementAndGet(); + } + hits.incrementAndGet(); + if (debug) { + log.info("REUSE channel ..." + key + " " + res); + } + return res; + } + + /** + * Must be called in IOThread for the channel + */ + public void returnChannel(IOChannel ch) + throws IOException { + CharSequence key = ch.getTarget(); + if (key == null) { + ch.close(); + if (debug) { + log.info("Return channel, no target ..." + key + " " + ch); + } + return; + } + + if (!keepOpen) { + ch.close(); + return; + } + +// SocketIOChannel sdata = (SocketIOChannel) ch; + if (!ch.isOpen()) { + ch.close(); // make sure all closed + if (debug) { + log.info("Return closed channel ..." + key + " " + ch); + } + return; + } + + RemoteServer t = null; + synchronized (hosts) { + t = hosts.get(key); + if (t == null) { + t = new RemoteServer(); + t.pool = this; + hosts.put(key, t); + } + } + waitingSockets.incrementAndGet(); + + ch.ts = System.currentTimeMillis(); + synchronized (t) { + // sdata.keepAliveServer = t; + t.connections.add(ch); + //sdata.ch.setDataCallbacks(readable, null, cc); + ch.readInterest(true); + } + } + + // Called by handleClosed + void stopKeepAlive(IOChannel schannel) { + CharSequence target = schannel.getTarget(); + RemoteServer t = null; + synchronized (hosts) { + t = hosts.get(target); + if (t == null) { + return; + } + } + synchronized (t) { + if (t.connections.remove(schannel)) { + waitingSockets.decrementAndGet(); + if (t.connections.size() == 0) { + hosts.remove(target); + } + } + } + } + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java new file mode 100644 index 000000000..eb81f0701 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpMessage.java @@ -0,0 +1,439 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.BufferedIOReader; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.IOBuffer; +import org.apache.tomcat.lite.io.IOInputStream; +import org.apache.tomcat.lite.io.IOOutputStream; +import org.apache.tomcat.lite.io.IOReader; +import org.apache.tomcat.lite.io.IOWriter; +import org.apache.tomcat.lite.io.UrlEncoding; + + +/** + * Basic Http request or response message. + * + * Because the HttpChannel can be used for both client and + * server, and to make proxy and other code simpler - the request + * and response are represented by the same class. + * + * @author Costin Manolache + */ +public abstract class HttpMessage { + + public static enum State { + HEAD, + BODY_DATA, + DONE + } + + /** + * Raw, off-the-wire message. + */ + public static class HttpMessageBytes { + BBuffer head1 = BBuffer.wrapper(); + BBuffer head2 = BBuffer.wrapper(); + BBuffer proto = BBuffer.wrapper(); + + BBuffer query = BBuffer.wrapper(); + + List headerNames = new ArrayList(); + List headerValues = new ArrayList(); + + int headerCount; + + public BBuffer status() { + return head1; + } + + public BBuffer method() { + return head1; + } + + public BBuffer url() { + return head2; + } + + public BBuffer query() { + return query; + } + + public BBuffer protocol() { + return proto; + } + + public BBuffer message() { + return head2; + } + + public int addHeader() { + headerNames.add(BBuffer.wrapper()); + headerValues.add(BBuffer.wrapper()); + return headerCount++; + } + + public BBuffer getHeaderName(int i) { + if (i >= headerNames.size()) { + return null; + } + return headerNames.get(i); + } + + public BBuffer getHeaderValue(int i) { + if (i >= headerValues.size()) { + return null; + } + return headerValues.get(i); + } + + public void recycle() { + head1.recycle(); + head2.recycle(); + proto.recycle(); + query.recycle(); + headerCount = 0; + for (int i = 0; i < headerCount; i++) { + headerNames.get(i).recycle(); + headerValues.get(i).recycle(); + } + } + } + + private HttpMessageBytes msgBytes = new HttpMessageBytes(); + + protected HttpMessage.State state = HttpMessage.State.HEAD; + + protected HttpChannel httpCh; + + protected MultiMap headers = new MultiMap().insensitive(); + + protected CBuffer protoMB; + + // Cookies + protected boolean cookiesParsed = false; + + // TODO: cookies parsed when headers are added ! + protected ArrayList cookies; + protected ArrayList cookiesCache; + + protected UrlEncoding urlDecoder = new UrlEncoding(); + protected String charEncoding; + + IOReader reader; + BufferedIOReader bufferedReader; + HttpWriter writer; + IOWriter conv; + + IOOutputStream out; + private IOInputStream in; + + boolean commited; + + protected IOBuffer body; + + long contentLength = -2; + boolean chunked; + + BBuffer clBuffer = BBuffer.allocate(64); + + public HttpMessage(HttpChannel httpCh) { + this.httpCh = httpCh; + + out = new IOOutputStream(httpCh.getOut(), this); + conv = new IOWriter(httpCh); + writer = new HttpWriter(this, out, conv); + + in = new IOInputStream(httpCh, httpCh.getIOTimeout()); + + reader = new IOReader(httpCh.getIn()); + bufferedReader = new BufferedIOReader(reader); + + cookies = new ArrayList(); + cookiesCache = new ArrayList(); + protoMB = CBuffer.newInstance(); + } + + public void addHeader(String name, String value) { + getMimeHeaders().addValue(name).set(value); + } + + public void setHeader(String name, String value) { + getMimeHeaders().setValue(name).set(value); + } + + public void setMimeHeaders(MultiMap resHeaders) { + this.headers = resHeaders; + } + + public String getHeader(String name) { + CBuffer cb = headers.getHeader(name); + return (cb == null) ? null : cb.toString(); + } + + public MultiMap getMimeHeaders() { + return headers; + } + + public Collection getHeaderNames() { + + MultiMap headers = getMimeHeaders(); + int n = headers.size(); + ArrayList result = new ArrayList(); + for (int i = 0; i < n; i++) { + result.add(headers.getName(i).toString()); + } + return result; + } + + public boolean containsHeader(String name) { + return headers.getHeader(name) != null; + } + + public void setContentLength(long len) { + contentLength = len; + clBuffer.setLong(len); + setCLHeader(); + } + + public void setContentLength(int len) { + contentLength = len; + clBuffer.setLong(len); + setCLHeader(); + } + + private void setCLHeader() { + MultiMap.Entry clB = headers.setEntry("content-length"); + clB.valueB = clBuffer; + } + + public long getContentLengthLong() { + if (contentLength == -2) { + CBuffer clB = headers.getHeader("content-length"); + contentLength = (clB == null) ? + -1 : clB.getLong(); + } + return contentLength; + } + + public int getContentLength() { + long length = getContentLengthLong(); + + if (length < Integer.MAX_VALUE) { + return (int) length; + } + return -1; + } + + public String getContentType() { + CBuffer contentTypeMB = headers.getHeader("content-type"); + if (contentTypeMB == null) { + return null; + } + return contentTypeMB.toString(); + } + + public void setContentType(String contentType) { + CBuffer clB = getMimeHeaders().getHeader("content-type"); + if (clB == null) { + setHeader("Content-Type", contentType); + } else { + clB.set(contentType); + } + } + + /** + * Get the character encoding used for this request. + * Need a field because it can be overriden. Used to construct the + * Reader. + */ + public String getCharacterEncoding() { + if (charEncoding != null) + return charEncoding; + + charEncoding = ContentType.getCharsetFromContentType(getContentType()); + return charEncoding; + } + + private static final String DEFAULT_ENCODING = "ISO-8859-1"; + + public String getEncoding() { + String charEncoding = getCharacterEncoding(); + if (charEncoding == null) { + return DEFAULT_ENCODING; + } else { + return charEncoding; + } + } + + public void setCharacterEncoding(String enc) + throws UnsupportedEncodingException { + this.charEncoding = enc; + } + + + + public void recycle() { + commited = false; + headers.recycle(); + protoMB.set("HTTP/1.1"); + for (int i = 0; i < cookies.size(); i++) { + cookies.get(i).recycle(); + } + cookies.clear(); + charEncoding = null; + bufferedReader.recycle(); + + writer.recycle(); + conv.recycle(); + + contentLength = -2; + chunked = false; + clBuffer.recycle(); + state = State.HEAD; + cookiesParsed = false; + getMsgBytes().recycle(); + + } + + + public String getProtocol() { + return protoMB.toString(); + } + + public void setProtocol(String proto) { + protoMB.set(proto); + } + + public CBuffer protocol() { + return protoMB; + } + + public ServerCookie getCookie(String name) { + for (ServerCookie sc: getServerCookies()) { + if (sc.getName().equalsIgnoreCase(name)) { + return sc; + } + } + return null; + } + + public List getServerCookies() { + if (!cookiesParsed) { + cookiesParsed = true; + ServerCookie.processCookies(cookies, cookiesCache, getMsgBytes()); + } + return cookies; + } + + public UrlEncoding getURLDecoder() { + return urlDecoder; + } + + public boolean isCommitted() { + return commited; + } + + public void setCommitted(boolean b) { + commited = b; + } + + // Not used in coyote connector ( hack ) + + public void sendHead() throws IOException { + } + + public HttpChannel getHttpChannel() { + return httpCh; + } + + public IOBuffer getBody() { + return body; + } + + void setBody(IOBuffer body) { + this.body = body; + } + + public void flush() throws IOException { + httpCh.startSending(); + } + + // not servlet input stream + public IOInputStream getBodyInputStream() { + return in; + } + + public IOOutputStream getBodyOutputStream() { + return out; + } + + public IOReader getBodyReader() throws IOException { + reader.setEncoding(getCharacterEncoding()); + return reader; + } + + /** + * Returns a buffered reader. + */ + public BufferedReader getReader() throws IOException { + reader.setEncoding(getCharacterEncoding()); + return bufferedReader; + } + + public HttpWriter getBodyWriter() { + conv.setEncoding(getCharacterEncoding()); + return writer; + } + + // + public abstract void serialize(IOBuffer out) throws IOException; + + + public void serializeHeaders(IOBuffer rawSendBuffers2) throws IOException { + MultiMap mimeHeaders = getMimeHeaders(); + + for (int i = 0; i < mimeHeaders.size(); i++) { + CBuffer name = mimeHeaders.getName(i); + CBuffer value = mimeHeaders.getValue(i); + if (name.length() == 0 || value.length() == 0) { + continue; + } + rawSendBuffers2.append(name); + rawSendBuffers2.append(HttpChannel.COLON); + rawSendBuffers2.append(value); + rawSendBuffers2.append(BBuffer.CRLF_BYTES); + } + rawSendBuffers2.append(BBuffer.CRLF_BYTES); + } + + protected void processMimeHeaders() { + for (int idx = 0; idx < getMsgBytes().headerCount; idx++) { + BBuffer nameBuf = getMsgBytes().getHeaderName(idx); + BBuffer valBuf = getMsgBytes().getHeaderValue(idx); + + MultiMap.Entry header = headers.addEntry(nameBuf); + header.valueB = valBuf; + } + } + + + protected abstract void processReceivedHeaders() throws IOException; + + public abstract boolean hasBody(); + + public HttpMessageBytes getMsgBytes() { + // TODO: serialize if not set + return msgBytes; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java new file mode 100644 index 000000000..398d60d90 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpRequest.java @@ -0,0 +1,980 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.tomcat.lite.http.MultiMap.Entry; +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.Hex; +import org.apache.tomcat.lite.io.IOBuffer; +import org.apache.tomcat.lite.io.IOChannel; +import org.apache.tomcat.lite.io.IOReader; +import org.apache.tomcat.lite.io.IOWriter; +import org.apache.tomcat.lite.io.SocketIOChannel; +import org.apache.tomcat.lite.io.UrlEncoding; + +public class HttpRequest extends HttpMessage { + public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1"; + + protected CBuffer schemeMB; + protected CBuffer methodMB; + protected CBuffer remoteAddrMB; + protected CBuffer remoteHostMB; + protected int remotePort; + + protected CBuffer localNameMB; + protected CBuffer localAddrMB; + protected int localPort = -1; + + // Host: header, or default:80 + protected CBuffer serverNameMB; + protected int serverPort = -1; + + + // ==== Derived fields, computed after request is received === + + protected CBuffer requestURI; + protected CBuffer queryMB; + + protected BBuffer decodedUri = BBuffer.allocate(); + protected CBuffer decodedUriMB; + + // Decoded query + protected MultiMap parameters; + + boolean parametersParsed = false; + + protected IOWriter charEncoder = new IOWriter(null); + protected IOReader charDecoder = new IOReader(null); + protected UrlEncoding urlEncoding = new UrlEncoding(); + + // Reference to 'real' request object + // will not be recycled + public Object nativeRequest; + public Object wrapperRequest; + + boolean ssl = false; + + boolean async = false; + + private Map attributes = new HashMap(); + + /** + * Mapping data. + */ + protected MappingData mappingData = new MappingData(); + + + HttpRequest(HttpChannel httpCh) { + super(httpCh); + decodedUriMB = CBuffer.newInstance(); + requestURI = CBuffer.newInstance(); + queryMB = CBuffer.newInstance(); + serverNameMB = CBuffer.newInstance(); + + parameters = new MultiMap(); + + schemeMB = + CBuffer.newInstance(); + methodMB = CBuffer.newInstance(); + initRemote(); + } + + protected void initRemote() { + remoteAddrMB = CBuffer.newInstance(); + localNameMB = CBuffer.newInstance(); + remoteHostMB = CBuffer.newInstance(); + localAddrMB = CBuffer.newInstance(); + } + + public void recycle() { + super.recycle(); + schemeMB.recycle(); + methodMB.set("GET"); + requestURI.recycle(); + queryMB.recycle(); + decodedUriMB.recycle(); + + parameters.recycle(); + remoteAddrMB.recycle(); + remoteHostMB.recycle(); + parametersParsed = false; + ssl = false; + async = false; + asyncTimeout = -1; + charEncoder.recycle(); + + localPort = -1; + remotePort = -1; + localAddrMB.recycle(); + localNameMB.recycle(); + + serverPort = -1; + serverNameMB.recycle(); + decodedUri.recycle(); + decodedQuery.recycle(); + } + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public void setAttribute(String name, Object o) { + if (o == null) { + attributes.remove(name); + } else { + attributes.put(name, o); + } + } + // getAttributeNames not supported + + public Map attributes() { + return attributes; + } + + + + public CBuffer method() { + return methodMB; + } + + public String getMethod() { + return methodMB.toString(); + } + + public void setMethod(String method) { + methodMB.set(method); + } + + public CBuffer scheme() { + return schemeMB; + } + + public String getScheme() { + String scheme = schemeMB.toString(); + if (scheme == null) { + return "http"; + } + return scheme; + } + + public void setScheme(String s) { + schemeMB.set(s); + } + + public MappingData getMappingData() { + return (mappingData); + } + + /** + * Parse query parameters - but not POST body. + * + * If you don't call this method, getParameters() will + * also read the body for POST with x-www-url-encoded + * mime type. + */ + public void parseQueryParameters() { + parseQuery(); + } + + /** + * Explicitely parse the body, adding the parameters to + * those from the query ( if already parsed ). + * + * By default servlet mode ( both query and body ) is used. + */ + public void parsePostParameters() { + parseBody(); + } + + MultiMap getParameters() { + if (!parametersParsed) { + parseQuery(); + parseBody(); + } + return parameters; + } + + public Enumeration getParameterNames() { + return getParameters().names(); + } + + /** + * Expensive, creates a copy on each call. + * @param name + * @return + */ + public String[] getParameterValues(String name) { + Entry entry = getParameters().getEntry(name); + if (entry == null) { + return null; + } + String[] values = new String[entry.values.size()]; + for (int j = 0; j < values.length; j++) { + values[j] = entry.values.get(j).toString(); + } + return values; + } + + // Inefficient - we convert from a different representation. + public Map getParameterMap() { + // we could allow 'locking' - I don't think this is + // a very useful optimization + Map map = new HashMap(); + for (int i = 0; i < getParameters().size(); i++) { + Entry entry = getParameters().getEntry(i); + if (entry == null) { + continue; + } + if (entry.key == null) { + continue; + } + String name = entry.key.toString(); + String[] values = new String[entry.values.size()]; + for (int j = 0; j < values.length; j++) { + values[j] = entry.values.get(j).toString(); + } + map.put(name, values); + } + return map; + } + + public String getParameter(String name) { + CharSequence value = getParameters().get(name); + if (value == null) { + return null; + } + return value.toString(); + } + + public void setParameter(String name, String value) { + getParameters().set(name, value); + } + + public void addParameter(String name, String values) { + getParameters().add(name, values); + } + + public CBuffer queryString() { + return queryMB; + } + + // TODO + void serializeParameters(Appendable cc) throws IOException { + int keys = parameters.size(); + boolean notFirst = false; + for (int i = 0; i < parameters.size(); i++) { + Entry entry = parameters.getEntry(i); + for (int j = 0; j < entry.values.size(); j++) { + // TODO: Uencode + if (notFirst) { + cc.append('&'); + } else { + notFirst = true; + } + cc.append(entry.key); + cc.append("="); + cc.append(entry.values.get(j).getValue()); + } + } + } + + public void setURI(CharSequence encoded) { + decodedUriMB.recycle(); + decodedUriMB.append(encoded); + // TODO: generate % encoding ( reverse of decodeRequest ) + } + + public CBuffer decodedURI() { + return decodedUriMB; + } + + public CBuffer requestURI() { + return requestURI; + } + + /** + * Not decoded - %xx as in original. + * @return + */ + public String getRequestURI() { + return requestURI.toString(); + } + + public void setRequestURI(String encodedUri) { + requestURI.set(encodedUri); + } + + CBuffer getOrAdd(String name) { + CBuffer header = getMimeHeaders().getHeader(name); + if (header == null) { + header = getMimeHeaders().addValue(name); + } + return header; + } + + /** + * Set the Host header of the request. + * @param target + */ + public void setHost(String target) { + serverNameMB.recycle(); + getOrAdd("Host").set(target); + } + + // XXX + public CBuffer serverName() { + if (serverNameMB.length() == 0) { + parseHost(); + } + return serverNameMB; + } + + public String getServerName() { + return serverName().toString(); + } + + public void setServerName(String name) { + serverName().set(name); + } + + public int getServerPort() { + serverName(); + return serverPort; + } + + public void setServerPort(int serverPort ) { + this.serverPort=serverPort; + } + + public CBuffer remoteAddr() { + if (remoteAddrMB.length() == 0) { + HttpChannel asyncHttp = getHttpChannel(); + IOChannel iochannel = asyncHttp.getNet().getFirst(); + if (iochannel instanceof SocketIOChannel) { + SocketIOChannel channel = (SocketIOChannel) iochannel; + + String addr = (channel == null) ? + "127.0.0.1" : + channel.getAddress(true).getHostAddress(); + + remoteAddrMB.set(addr); + } + } + return remoteAddrMB; + } + + public CBuffer remoteHost() { + if (remoteHostMB.length() == 0) { + HttpChannel asyncHttp = getHttpChannel(); + IOChannel iochannel = asyncHttp.getNet().getFirst(); + if (iochannel instanceof SocketIOChannel) { + SocketIOChannel channel = (SocketIOChannel) iochannel; + String addr = (channel == null) ? + "127.0.0.1" : + channel.getAddress(true).getCanonicalHostName(); + + remoteHostMB.set(addr); + } + } + return remoteHostMB; + } + + public CBuffer localName() { + return localNameMB; + } + + public CBuffer localAddr() { + return localAddrMB; + } + + public int getRemotePort(){ + if (remotePort == -1) { + HttpChannel asyncHttp = getHttpChannel(); + IOChannel iochannel = asyncHttp.getNet().getFirst(); + if (iochannel instanceof SocketIOChannel) { + SocketIOChannel channel = (SocketIOChannel) iochannel; + remotePort = (channel == null) ? + 0 : channel.getPort(true); + } + } + return remotePort; + } + + public void setRemotePort(int port){ + this.remotePort = port; + } + + public int getLocalPort(){ + if (localPort == -1) { + HttpChannel asyncHttp = getHttpChannel(); + IOChannel iochannel = asyncHttp.getNet().getFirst(); + if (iochannel instanceof SocketIOChannel) { + SocketIOChannel channel = (SocketIOChannel) iochannel; + localPort = (channel == null) ? + 0 : channel.getPort(false); + } + } + return localPort; + } + + public void setLocalPort(int port){ + this.localPort = port; + } + + public void sendHead() throws IOException { + httpCh.sendRequestHeaders(httpCh); + } + + /** + * Convert the request to bytes, ready to send. + */ + public void serialize(IOBuffer rawSendBuffers2) throws IOException { + rawSendBuffers2.append(method()); + rawSendBuffers2.append(BBuffer.SP); + + // TODO: encode or use decoded + rawSendBuffers2.append(requestURI()); + if (queryString().length() > 0) { + rawSendBuffers2.append("?"); + rawSendBuffers2.append(queryString()); + } + + rawSendBuffers2.append(BBuffer.SP); + rawSendBuffers2.append(protocol()); + rawSendBuffers2.append(BBuffer.CRLF_BYTES); + + super.serializeHeaders(rawSendBuffers2); + } + + /** + * Parse host. + * @param serverNameMB2 + * @throws IOException + */ + boolean parseHost() { + MultiMap.Entry hostHF = getMimeHeaders().getEntry("Host"); + if (hostHF == null) { + // HTTP/1.0 + // Default is what the socket tells us. Overriden if a host is + // found/parsed + return true; + } + + BBuffer valueBC = hostHF.valueB; + byte[] valueB = valueBC.array(); + int valueL = valueBC.getLength(); + int valueS = valueBC.getStart(); + int colonPos = -1; + + serverNameMB.recycle(); + + boolean ipv6 = (valueB[valueS] == '['); + boolean bracketClosed = false; + for (int i = 0; i < valueL; i++) { + char b = (char) valueB[i + valueS]; + if (b == ':') { + if (!ipv6 || bracketClosed) { + colonPos = i; + break; + } + } + serverNameMB.append(b); + if (b == ']') { + bracketClosed = true; + } + } + + if (colonPos < 0) { + if (!ssl) { + // 80 - Default HTTP port + setServerPort(80); + } else { + // 443 - Default HTTPS port + setServerPort(443); + } + } else { + int port = 0; + int mult = 1; + for (int i = valueL - 1; i > colonPos; i--) { + int charValue = Hex.DEC[(int) valueB[i + valueS]]; + if (charValue == -1) { + // we don't return 400 - could do it + return false; + } + port = port + (charValue * mult); + mult = 10 * mult; + } + setServerPort(port); + + } + return true; + } + + // TODO: this is from coyote - MUST be rewritten !!! + // - cleaner + // - chunked encoding for body + // - buffer should be in a pool, etc. + /** + * Post data buffer. + */ + public final static int CACHED_POST_LEN = 8192; + + public byte[] postData = null; + + private long asyncTimeout = -1; + + /** + * Parse request parameters. + */ + protected void parseQuery() { + + parametersParsed = true; + + // getCharacterEncoding() may have been overridden to search for + // hidden form field containing request encoding + String enc = getEncoding(); + +// boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); +// if (enc != null) { +// parameters.setEncoding(enc); +//// if (useBodyEncodingForURI) { +//// parameters.setQueryStringEncoding(enc); +//// } +// } else { +// parameters.setEncoding(DEFAULT_CHARACTER_ENCODING); +//// if (useBodyEncodingForURI) { +//// parameters.setQueryStringEncoding +//// (DEFAULT_CHARACTER_ENCODING); +//// } +// } + + handleQueryParameters(); + } + + // Copy - will be modified by decoding + BBuffer decodedQuery = BBuffer.allocate(1024); + + CBuffer tmpNameC = CBuffer.newInstance(); + BBuffer tmpName = BBuffer.wrapper(); + BBuffer tmpValue = BBuffer.wrapper(); + + CBuffer tmpNameCB = CBuffer.newInstance(); + CBuffer tmpValueCB = CBuffer.newInstance(); + + /** + * Process the query string into parameters + */ + public void handleQueryParameters() { + if( queryMB.length() == 0) { + return; + } + + decodedQuery.recycle(); + decodedQuery.append(getMsgBytes().query()); + // TODO: option 'useBodyEncodingForUri' - versus UTF or ASCII + String queryStringEncoding = getEncoding(); + processParameters( decodedQuery, queryStringEncoding ); + } + + public void processParameters( BBuffer bc, String encoding ) { + if( bc.isNull()) + return; + if (bc.remaining() ==0) { + return; + } + processParameters( bc.array(), bc.getOffset(), + bc.getLength(), encoding); + } + + public void processParameters( byte bytes[], int start, int len, + String enc ) { + int end=start+len; + int pos=start; + + do { + boolean noEq=false; + int valStart=-1; + int valEnd=-1; + + int nameStart=pos; + int nameEnd=BBuffer.indexOf(bytes, nameStart, end, '=' ); + // Workaround for a&b&c encoding + int nameEnd2=BBuffer.indexOf(bytes, nameStart, end, '&' ); + if( (nameEnd2!=-1 ) && + ( nameEnd==-1 || nameEnd > nameEnd2) ) { + nameEnd=nameEnd2; + noEq=true; + valStart=nameEnd; + valEnd=nameEnd; + } + if( nameEnd== -1 ) + nameEnd=end; + + if( ! noEq ) { + valStart= (nameEnd < end) ? nameEnd+1 : end; + valEnd=BBuffer.indexOf(bytes, valStart, end, '&'); + if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; + } + + pos=valEnd+1; + + if( nameEnd<=nameStart ) { + // No name eg ...&=xx&... will trigger this + continue; + } + + // TODO: use CBuffer, recycle + tmpName.setBytes( bytes, nameStart, nameEnd-nameStart ); + tmpValue.setBytes( bytes, valStart, valEnd-valStart ); + + try { + parameters.add(urlDecode(tmpName, enc), + urlDecode(tmpValue, enc)); + } catch (IOException e) { + // ignored + } + } while( pos nameEnd2) ) { +// nameEnd=nameEnd2; +// noEq=true; +// valStart=nameEnd; +// valEnd=nameEnd; +// } +// if( nameEnd== -1 ) +// nameEnd=end; +// +// if( ! noEq ) { +// valStart= (nameEnd < end) ? nameEnd+1 : end; +// valEnd=CBuffer.indexOf(bytes, valStart, end, '&'); +// if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; +// } +// +// pos=valEnd+1; +// +// if( nameEnd<=nameStart ) { +// // No name eg ...&=xx&... will trigger this +// continue; +// } +// +// // TODO: use CBuffer, recycle +// tmpNameCB.recycle(); +// tmpValueCB.recycle(); +// +// tmpNameCB.wrap( bytes, nameStart, nameEnd ); +// tmpValueCB.wrap( bytes, valStart, valEnd ); +// +// //CharChunk name = new CharChunk(); +// //CharChunk value = new CharChunk(); +// // TODO: +// try { +// parameters.add(urlDecode(tmpName, enc), +// urlDecode(tmpValue, enc)); +// } catch (IOException e) { +// // ignored +// } +// } while( pos= 0) { + contentType = contentType.substring(0, semicolon).trim(); + } else { + contentType = contentType.trim(); + } + if (!("application/x-www-form-urlencoded".equals(contentType))) + return; + + int len = getContentLength(); + + if (len > 0) { + try { + byte[] formData = null; + if (len < CACHED_POST_LEN) { + if (postData == null) + postData = new byte[CACHED_POST_LEN]; + formData = postData; + } else { + formData = new byte[len]; + } + int actualLen = readPostBody(formData, len); + if (actualLen == len) { + processParameters(formData, 0, len); + } + } catch (Throwable t) { + ; // Ignore + } + } + + } + + /** + * Read post body in an array. + */ + protected int readPostBody(byte body[], int len) + throws IOException { + + int offset = 0; + do { + int inputLen = getBodyInputStream().read(body, offset, len - offset); + if (inputLen <= 0) { + return offset; + } + offset += inputLen; + } while ((len - offset) > 0); + return len; + + } + + // Async support - a subset of servlet spec, the fancy stuff is in the + // facade. + + public boolean isAsyncStarted() { + return async; + } + + public void async() { + this.async = true; + } + + public void setAsyncTimeout(long timeout) { + this.asyncTimeout = timeout; + } + + /** + * Server mode, request just received. + */ + protected void processReceivedHeaders() throws IOException { + if (!httpCh.normalize(getMsgBytes().url())) { + httpCh.getResponse().setStatus(400); + httpCh.abort("Error normalizing url " + + getMsgBytes().url()); + return; + } + + method().set(getMsgBytes().method()); + requestURI().set(getMsgBytes().url()); + queryString().set(getMsgBytes().query()); + protocol().set(getMsgBytes().protocol()); + + processMimeHeaders(); + + // URL decode and normalize + decodedUri.append(getMsgBytes().url()); + getURLDecoder().urlDecode(decodedUri, false); + + // Need to normalize again - %decoding may decode / + if (!httpCh.normalize(decodedUri)) { + httpCh.getResponse().setStatus(400); + httpCh.abort("Invalid decoded uri " + decodedUri); + return; + } + decodedURI().set(decodedUri); + + httpCh.processProtocol(); + + // default response protocol + httpCh.getResponse().protocol().set(getMsgBytes().protocol()); + + // requested connection:close/keepAlive and proto + httpCh.processConnectionHeader(getMimeHeaders()); + + httpCh.processExpectation(); + + httpCh.receiveBody.processContentDelimitation(); + // Spec: + // The presence of a message-body in a request is signaled by the + // inclusion of a Content-Length or Transfer-Encoding header field in + // the request's message-headers + // Server should read - but ignore + + httpCh.receiveBody.noBody = !httpCh.receiveBody.isContentDelimited(); + + httpCh.receiveBody.updateCloseOnEnd(); + + /* + * The presence of a message-body in a request is signaled by the + * inclusion of a Content-Length or Transfer-Encoding header field in + * the request's message-headers. A message-body MUST NOT be included + * in a request if the specification of the request method + * (section 5.1.1) does not allow sending an entity-body in requests. + * A server SHOULD read and forward a message-body on any request; if the request method does not include defined semantics for an entity-body, then the message-body SHOULD be ignored when handling the request. + */ + if (!httpCh.receiveBody.isContentDelimited()) { + // No body + httpCh.getIn().close(); + } + + CBuffer valueMB = getMimeHeaders().getHeader("host"); + // Check host header +// if (httpCh.http11 && (valueMB == null)) { +// httpCh.error = true; +// // 400 - Bad request +// httpCh.getResponse().setStatus(400); +// } + } + + + public boolean hasBody() { + return chunked || contentLength >= 0; + } + + /** + * Convert (if necessary) and return the absolute URL that represents the + * resource referenced by this possibly relative URL. If this URL is + * already absolute, return it unchanged. + * + * @param location URL to be (possibly) converted and then returned + * + * @exception IllegalArgumentException if a MalformedURLException is + * thrown when converting the relative URL to an absolute one + */ + public void toAbsolute(String location, CBuffer cb) { + + cb.recycle(); + if (location == null) + return; + + boolean leadingSlash = location.startsWith("/"); + if (leadingSlash || !hasScheme(location)) { + + String scheme = getScheme(); + String name = serverName().toString(); + int port = getServerPort(); + + cb.append(scheme); + cb.append("://", 0, 3); + cb.append(name); + if ((scheme.equals("http") && port != 80) + || (scheme.equals("https") && port != 443)) { + cb.append(':'); + String portS = port + ""; + cb.append(portS); + } + if (!leadingSlash) { + String relativePath = decodedURI().toString(); + int pos = relativePath.lastIndexOf('/'); + relativePath = relativePath.substring(0, pos); + + //String encodedURI = null; + urlEncoding.urlEncode(relativePath, cb, charEncoder); + //encodedURI = urlEncoder.encodeURL(relativePath); + //redirectURLCC.append(encodedURI, 0, encodedURI.length()); + cb.append('/'); + } + + cb.append(location); + } else { + cb.append(location); + } + + } + + /** + * Determine if a URI string has a scheme component. + */ + public static boolean hasScheme(String uri) { + int len = uri.length(); + for(int i=0; i < len ; i++) { + char c = uri.charAt(i); + if(c == ':') { + return i > 0; + } else if(!isSchemeChar(c)) { + return false; + } + } + return false; + } + + /** + * Determine if the character is allowed in the scheme of a URI. + * See RFC 2396, Section 3.1 + */ + private static boolean isSchemeChar(char c) { + return Character.isLetterOrDigit(c) || + c == '+' || c == '-' || c == '.'; + } + + public IOWriter getCharEncoder() { + return charEncoder; + } + + public IOReader getCharDecoder() { + return charDecoder; + } + + public UrlEncoding getUrlEncoding() { + return urlEncoding; + } + + public BBuffer toBytes(CBuffer cb, BBuffer bb) { + if (bb == null) { + bb = BBuffer.allocate(cb.length()); + } + getCharEncoder().encodeAll(cb, bb, "UTF-8"); + return bb; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java new file mode 100644 index 000000000..d4e8c738a --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpResponse.java @@ -0,0 +1,283 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.IOException; +import java.util.HashMap; + +import org.apache.tomcat.lite.io.BBucket; +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.lite.io.IOBuffer; + +public class HttpResponse extends HttpMessage { + + // will not be recycled + public Object nativeResponse; + + protected CBuffer message = CBuffer.newInstance(); + + int status = -1; + + HttpResponse(HttpChannel httpCh) { + super(httpCh); + } + + public void recycle() { + super.recycle(); + message.recycle(); + status = -1; + } + + public void setMessage(String s) { + message.set(filter(s)); + } + + public String getMessage() { + return message.toString(); + } + + public CBuffer getMessageBuffer() { + return message; + } + + byte[] S_200 = new byte[] { '2', '0', '0' }; + + public void setStatus(int i) { + status = i; + } + + public int getStatus() { + if (status >= 0) { + return status; + } + if (getMsgBytes().status().isNull()) { + status = 200; + } else { + try { + status = getMsgBytes().status().getInt(); + } catch(NumberFormatException ex) { + status = 500; + httpCh.log.severe("Invalid status " + getMsgBytes().status()); + } + } + return status; + } + + public void sendHead() throws IOException { + httpCh.sendHeaders(); + } + + /** + * Convert the response to bytes, ready to send. + */ + public void serialize(IOBuffer rawSendBuffers2) throws IOException { + + rawSendBuffers2.append(protocol()).append(' '); + String status = Integer.toString(getStatus()); + rawSendBuffers2.append(status).append(' '); + if (getMessageBuffer().length() > 0) { + rawSendBuffers2.append(getMessage()); + } else { + rawSendBuffers2 + .append(getMessage(getStatus())); + } + rawSendBuffers2.append(BBuffer.CRLF_BYTES); + // Headers + super.serializeHeaders(rawSendBuffers2); + } + + public HttpRequest getRequest() { + return getHttpChannel().getRequest(); + } + + // Http client mode. + protected void processReceivedHeaders() throws IOException { + protocol().set(getMsgBytes().protocol()); + message.set(getMsgBytes().message()); + processMimeHeaders(); + + + // TODO: if protocol == 1.0 and we requested 1.1, downgrade getHttpChannel().pro + int status = 500; + try { + status = getStatus(); + } catch (Throwable t) { + getHttpChannel().log.warning("Invalid status " + getMsgBytes().status() + " " + getMessage()); + } + HttpBody body = (HttpBody) getBody(); + body.noBody = !hasBody(); + + // Will parse 'connection:close', set close on end + getHttpChannel().processConnectionHeader(getMimeHeaders()); + + body.processContentDelimitation(); + + if (body.statusDropsConnection(status)) { + getHttpChannel().closeStreamOnEnd("response status drops connection"); + } + + if (body.isDone()) { + body.close(); + } + + if (!body.isContentDelimited()) { + getHttpChannel().closeStreamOnEnd("not content delimited"); + } + + + } + + /** + * All responses to the HEAD request method MUST NOT include a + * message-body, even though the presence of entity- header fields might + * lead one to believe they do. All 1xx (informational), 204 (no content) + * , and 304 (not modified) responses MUST NOT include a message-body. All + * other responses do include a message-body, although it MAY be of zero + * length. + */ + public boolean hasBody() { + if (httpCh.getRequest().method().equals("HEAD")) { + return false; + } + if (status >= 100 && status < 200) { + return false; + } + // what about (status == 205) ? + if ((status == 204) + || (status == 304)) { + return false; + } + return true; + } + + /** Get the status string associated with a status code. + * No I18N - return the messages defined in the HTTP spec. + * ( the user isn't supposed to see them, this is the last + * thing to translate) + * + * Common messages are cached. + * + */ + private BBucket getMessage( int status ) { + // method from Response. + + // Does HTTP requires/allow international messages or + // are pre-defined? The user doesn't see them most of the time + switch( status ) { + case 200: + return st_200; + case 302: + return st_302; + case 400: + return st_400; + case 404: + return st_404; + } + return stats.get(status); + } + + + static BBucket st_200 = BBuffer.wrapper("OK"); + static BBucket st_302= BBuffer.wrapper("Moved Temporarily"); + static BBucket st_400= BBuffer.wrapper("Bad Request"); + static BBucket st_404= BBuffer.wrapper("Not Found"); + + static HashMap stats = new HashMap(); + private static void addStatus(int stat, String msg) { + stats.put(stat, BBuffer.wrapper(msg)); + } + + static { + addStatus(100, "Continue"); + addStatus(101, "Switching Protocols"); + addStatus(200, "OK"); + addStatus(201, "Created"); + addStatus(202, "Accepted"); + addStatus(203, "Non-Authoritative Information"); + addStatus(204, "No Content"); + addStatus(205, "Reset Content"); + addStatus(206, "Partial Content"); + addStatus(207, "Multi-Status"); + addStatus(300, "Multiple Choices"); + addStatus(301, "Moved Permanently"); + addStatus(302, "Moved Temporarily"); + addStatus(303, "See Other"); + addStatus(304, "Not Modified"); + addStatus(305, "Use Proxy"); + addStatus(307, "Temporary Redirect"); + addStatus(400, "Bad Request"); + addStatus(401, "Unauthorized"); + addStatus(402, "Payment Required"); + addStatus(403, "Forbidden"); + addStatus(404, "Not Found"); + addStatus(405, "Method Not Allowed"); + addStatus(406, "Not Acceptable"); + addStatus(407, "Proxy Authentication Required"); + addStatus(408, "Request Timeout"); + addStatus(409, "Conflict"); + addStatus(410, "Gone"); + addStatus(411, "Length Required"); + addStatus(412, "Precondition Failed"); + addStatus(413, "Request Entity Too Large"); + addStatus(414, "Request-URI Too Long"); + addStatus(415, "Unsupported Media Type"); + addStatus(416, "Requested Range Not Satisfiable"); + addStatus(417, "Expectation Failed"); + addStatus(422, "Unprocessable Entity"); + addStatus(423, "Locked"); + addStatus(424, "Failed Dependency"); + addStatus(500, "Internal Server Error"); + addStatus(501, "Not Implemented"); + addStatus(502, "Bad Gateway"); + addStatus(503, "Service Unavailable"); + addStatus(504, "Gateway Timeout"); + addStatus(505, "HTTP Version Not Supported"); + addStatus(507, "Insufficient Storage"); + + } + + /** + * Filter the specified message string for characters that are sensitive + * in HTML. This avoids potential attacks caused by including JavaScript + * codes in the request URL that is often reported in error messages. + * + * @param message The message string to be filtered + */ + private static String filter(String message) { + + if (message == null) + return (null); + if (message.indexOf('<') < 0 && + message.indexOf('>') < 0 && + message.indexOf('&') < 0 && + message.indexOf('"') < 0) { + return message; + } + + char content[] = new char[message.length()]; + message.getChars(0, message.length(), content, 0); + + StringBuffer result = new StringBuffer(content.length + 50); + for (int i = 0; i < content.length; i++) { + switch (content[i]) { + case '<': + result.append("<"); + break; + case '>': + result.append(">"); + break; + case '&': + result.append("&"); + break; + case '"': + result.append("""); + break; + default: + result.append(content[i]); + } + } + return (result.toString()); + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java new file mode 100644 index 000000000..2d8f91abd --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/HttpWriter.java @@ -0,0 +1,313 @@ +/* + */ +package org.apache.tomcat.lite.http; + +import java.io.IOException; +import java.io.Writer; + +import org.apache.tomcat.lite.io.IOOutputStream; +import org.apache.tomcat.lite.io.IOWriter; + +/** + * Implement character translation and buffering. + * + * The actual buffering happens in the IOBuffer - we translate the + * chars as soon as we get them. + * + * For servlet compat you can set a buffer size and a flush() will happen + * when the number of chars have been written. Note that writes at a lower + * layer can be done and are not counted. + * + * @author Costin Manolache + */ +public class HttpWriter extends Writer { + + public static final String DEFAULT_ENCODING = "ISO-8859-1"; + public static final int DEFAULT_BUFFER_SIZE = 8*1024; + + // ----------------------------------------------------- Instance Variables + HttpMessage message; + + /** + * The byte buffer. + */ + protected IOOutputStream bb; + + int bufferSize = DEFAULT_BUFFER_SIZE; + + /** + * Number of chars written. + */ + protected int wSinceFlush = 0; + + + /** + * Flag which indicates if the output buffer is closed. + */ + protected boolean closed = false; + + /** + * Encoding to use. + * TODO: isn't it redundant ? enc, gotEnc, conv plus the enc in the bb + */ + protected String enc; + + + /** + * Encoder is set. + */ + protected boolean gotEnc = false; + + + /** + * List of encoders. The writer is reused - the encoder mapping + * avoids creating expensive objects. In future it'll contain nio.Charsets + */ + //protected Map encoders = new HashMap(); + + + /** + * Current char to byte converter. TODO: replace with Charset + */ + private IOWriter conv; + + /** + * Suspended flag. All output bytes will be swallowed if this is true. + */ + protected boolean suspended = false; + + + // ----------------------------------------------------------- Constructors + + + /** + * Default constructor. Allocate the buffer with the default buffer size. + * @param out + */ + public HttpWriter(HttpMessage message, IOOutputStream out, + IOWriter conv) { + this.message = message; + bb = out; + this.conv = conv; + } + + // ------------------------------------------------------------- Properties + + + /** + * Is the response output suspended ? + * + * @return suspended flag value + */ + public boolean isSuspended() { + return this.suspended; + } + + + /** + * Set the suspended flag. + * + * @param suspended New suspended flag value + */ + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Recycle the output buffer. + */ + public void recycle() { + wSinceFlush = 0; + bb.recycle(); + closed = false; + suspended = false; + +// if (conv != null) { +// conv.recycle(); +// } + + gotEnc = false; + enc = null; + } + + public void close() + throws IOException { + + if (closed) + return; + if (suspended) + return; + + push(); + closed = true; + + bb.close(); + } + + + /** + * Flush bytes or chars contained in the buffer. + * + * @throws IOException An underlying IOException occurred + */ + public void flush() + throws IOException { + push(); + bb.flush(); // will send the data + wSinceFlush = 0; + } + + /** + * Flush chars to the byte buffer. + */ + public void push() + throws IOException { + + if (suspended) + return; + getConv().push(); + + } + + + private void updateSize(int cnt) throws IOException { + wSinceFlush += cnt; + if (wSinceFlush > bufferSize) { + flush(); + } + } + + public void write(int c) + throws IOException { + if (suspended) + return; + getConv().write(c); + updateSize(1); + } + + + public void write(char c[]) + throws IOException { + write(c, 0, c.length); + } + + + public void write(char c[], int off, int len) + throws IOException { + if (suspended) + return; + getConv().write(c, off, len); + updateSize(len); + } + + + public void write(StringBuffer sb) + throws IOException { + if (suspended) + return; + int len = sb.length(); + getConv().write(sb.toString()); + updateSize(len); + } + + + /** + * Append a string to the buffer + */ + public void write(String s, int off, int len) + throws IOException { + if (suspended) + return; + if (s==null) + s="null"; + getConv().write( s, off, len ); + updateSize(len); + } + + + public void write(String s) + throws IOException { + if (s==null) + s="null"; + write(s, 0, s.length()); + } + + public void println() throws IOException { + write("\n"); + } + + public void println(String s) throws IOException { + write(s); + write("\n"); + } + + public void print(String s) throws IOException { + write(s); + } + + public void checkConverter() + throws IOException { +// if (gotEnc) { +// return; +// } +// if (enc == null) { +// enc = message.getCharacterEncoding(); +// } +// +// gotEnc = true; +// if (enc == null) +// enc = DEFAULT_ENCODING; +// conv = (C2BConverter) encoders.get(enc); +// +// if (conv == null) { +// conv = C2BConverter.newConverter(message.getBodyOutputStream(), +// enc); +// encoders.put(enc, conv); +// +// } + } + + public int getWrittenSinceFlush() { + return wSinceFlush; + } + + + public void setBufferSize(int size) { + if (size > bufferSize) { + bufferSize = size; + } + } + + /** + * Clear any data that was buffered. + */ + public void reset() { + if (conv != null) { + conv.recycle(); + } + wSinceFlush = 0; + gotEnc = false; + enc = null; + bb.reset(); + } + + + public int getBufferSize() { + return bufferSize; + } + + protected IOWriter getConv() throws IOException { + checkConverter(); + return conv; + } + + public void println(CharSequence key) throws IOException { + // TODO: direct + println(key.toString()); + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java new file mode 100644 index 000000000..f0edeaff3 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MappingData.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + +import org.apache.tomcat.lite.io.CBuffer; + + +/** + * Mapping data. + * + * @author Remy Maucherat + */ +public class MappingData { + + public Object context = null; // ServletContextImpl + + public BaseMapper.ContextMapping contextMap; + + public BaseMapper.ServiceMapping service = null; + + public CBuffer contextPath = CBuffer.newInstance(); + public CBuffer requestPath = CBuffer.newInstance(); + public CBuffer wrapperPath = CBuffer.newInstance(); + public CBuffer pathInfo = CBuffer.newInstance(); + + public CBuffer redirectPath = CBuffer.newInstance(); + + // Extension + CBuffer ext = CBuffer.newInstance(); + CBuffer tmpPrefix = CBuffer.newInstance(); + + // Excluding context path, with a '/' added if needed + CBuffer tmpServletPath = CBuffer.newInstance(); + + // Excluding context path, with a '/' added if needed + CBuffer tmpWelcome = CBuffer.newInstance(); + + public void recycle() { + service = null; + context = null; + pathInfo.recycle(); + requestPath.recycle(); + wrapperPath.recycle(); + contextPath.recycle(); + redirectPath.recycle(); + contextMap = null; + } + + + public Object getServiceObject() { + return service == null ? null : service.object; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java new file mode 100644 index 000000000..515c2df59 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/MultiMap.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.CBucket; +import org.apache.tomcat.lite.io.CBuffer; +import org.apache.tomcat.util.buf.CharChunk; + +/** + * Map used to represent headers and parameters ( could be used + * for cookies too ) + * + * It'll avoid garbage collection, like original tomcat classes, + * by converting to chars and strings late. + * + * Not thread safe. + */ +public class MultiMap { + + public static class Entry { + // Wrappers from the head message bytes. + BBuffer nameB; + BBuffer valueB; + + CBuffer key = CBuffer.newInstance(); + private CBuffer value = CBuffer.newInstance(); + + /** + * For the first entry with a given name: list of all + * other entries, including this one, with same name. + * + * For second or more: empty list + */ + public List values = new ArrayList(); + + public void recycle() { + key.recycle(); + value.recycle(); + //next=null; + nameB = null; + valueB = null; + values.clear(); + } + + public CBuffer getName() { + if (key.length() == 0 && nameB != null) { + key.set(nameB); + } + return key; + } + + public CBuffer getValue() { + if (value.length() == 0 && valueB != null) { + value.set(valueB); + } + return value; + } + + /** Important - used by values iterator, returns strings + * from each entry + */ + public String toString() { + return getValue().toString(); + } + + } + + // active entries + protected int count; + + // The key will be converted to lower case + boolean toLower = false; + + // Some may be inactive - up to count. + protected List entries = new ArrayList(); + + // 2 options: convert all header/param names to String + // or use a temp CBuffer to map + Map map = + new HashMap(); + + public void recycle() { + for (int i = 0; i < count; i++) { + Entry entry = entries.get(i); + entry.recycle(); + } + count = 0; + map.clear(); + } + + // ----------- Mutations ------------------------ + + protected Entry newEntry() { + return new Entry(); + } + + /** + * Adds a partially constructed field entry. + * Updates count - but will not affect the map. + */ + private Entry getEntryForAdd() { + Entry entry; + if (count >= entries.size()) { + entry = newEntry(); + entries.add(entry); + } else { + entry = entries.get(count); + } + count++; + return entry; + } + + + /** Create a new named header , return the CBuffer + * container for the new value + */ + public Entry addEntry(CharSequence name ) { + Entry mh = getEntryForAdd(); + mh.getName().append(name); + if (toLower) { + mh.getName().toLower(); + } + updateMap(mh); + return mh; + } + + /** Create a new named header , return the CBuffer + * container for the new value + */ + public Entry addEntry(BBuffer name ) { + Entry mh = getEntryForAdd(); + mh.nameB = name; + if (toLower) { + mh.getName().toLower(); + } + updateMap(mh); + + return mh; + } + + private void updateMap(Entry mh) { + Entry topEntry = map.get(mh.getName()); + + if (topEntry == null) { + map.put(mh.getName(), mh); + mh.values.add(mh); + } else { + topEntry.values.add(mh); + } + } + + + + public void remove(CharSequence key) { + CBucket ckey = key(key); + Entry entry = getEntry(ckey); + if (entry != null) { + map.remove(ckey); + + for (int i = count - 1; i >= 0; i--) { + entry = entries.get(i); + if (entry.getName().equals(key)) { + entry.recycle(); + entries.remove(i); + count--; + } + } + } + } + + // --------------- Key-based access -------------- + CBuffer tmpKey = CBuffer.newInstance(); + + /** + * Finds and returns a header field with the given name. If no such + * field exists, null is returned. If more than one such field is + * in the header, an arbitrary one is returned. + */ + public CBuffer getHeader(String name) { + for (int i = 0; i < count; i++) { + if (entries.get(i).getName().equalsIgnoreCase(name)) { + return entries.get(i).getValue(); + } + } + return null; + } + + private CBucket key(CharSequence key) { + if (key instanceof CBucket) { + CBucket res = (CBucket) key; + if (!toLower || !res.hasUpper()) { + return res; + } + } + tmpKey.recycle(); + tmpKey.append(key); + if (toLower) { + tmpKey.toLower(); + } + return tmpKey; + } + + public Entry getEntry(CharSequence key) { + Entry entry = map.get(key(key)); + return entry; + } + + public Entry getEntry(CBucket buf) { + // lowercase ? + Entry entry = map.get(buf); + return entry; + } + + public Enumeration names() { + return new IteratorEnumerator(map.keySet().iterator()); + } + + // ----------- Index access -------------- + + /** + * Number of entries ( including those with same key + * + * @return + */ + public int size() { + return count; + } + + + public CharSequence getKey(int idx) { + return entries.get(idx).key; + } + + public Entry getEntry(int idx) { + return entries.get(idx); + } + + /** + * Returns the Nth header name, or null if there is no such header. + * This may be used to iterate through all header fields. + */ + public CBuffer getName(int n) { + return n < count ? entries.get(n).getName() : null; + } + + /** + * Returns the Nth header value, or null if there is no such header. + * This may be used to iterate through all header fields. + */ + public CBuffer getValue(int n) { + return n >= 0 && n < count ? entries.get(n).getValue() : null; + } + + // ----------- Helpers -------------- + public void add(CharSequence key, CharSequence value) { + Entry mh = addEntry(key); + mh.value.append(value); + } + + /** Create a new named header , return the CBuffer + * container for the new value + */ + public CBuffer addValue( String name ) { + return addEntry(name).getValue(); + } + + public Entry setEntry( String name ) { + remove(name); + return addEntry(name); + } + + public void set(CharSequence key, CharSequence value) { + remove(key); + add(key, value); + } + + public CBuffer setValue( String name ) { + remove(name); + return addValue(name); + } + + public CBuffer get(CharSequence key) { + Entry entry = getEntry(key); + return (entry == null) ? null : entry.value; + } + + public String getString(CharSequence key) { + Entry entry = getEntry(key); + return (entry == null) ? null : entry.value.toString(); + } + + + // -------------- support classes ---------------- + + public static class IteratorEnumerator implements Enumeration { + private final Iterator keyI; + + public IteratorEnumerator(Iterator iterator) { + this.keyI = iterator; + } + + + public boolean hasMoreElements() { + return keyI.hasNext(); + } + + + public String nextElement() { + return keyI.next().toString(); + } + + } + + public static final Enumeration EMPTY = + new Enumeration() { + + @Override + public boolean hasMoreElements() { + return false; + } + + @Override + public String nextElement() { + return null; + } + + }; + + public MultiMap insensitive() { + toLower = true; + return this; + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java new file mode 100644 index 000000000..c80ef5de9 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/ServerCookie.java @@ -0,0 +1,819 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.lite.http; + +import java.io.Serializable; +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.tomcat.lite.io.BBuffer; +import org.apache.tomcat.lite.io.CBuffer; + + +/** + * Server-side cookie representation. + * Allows recycling and uses MessageBytes as low-level + * representation ( and thus the byte-> char conversion can be delayed + * until we know the charset ). + * + * Tomcat.core uses this recyclable object to represent cookies, + * and the facade will convert it to the external representation. + */ +public class ServerCookie implements Serializable { + + // Version 0 (Netscape) attributes + private BBuffer name = BBuffer.allocate(); + private BBuffer value = BBuffer.allocate(); + + private CBuffer nameC = CBuffer.newInstance(); + + // Expires - Not stored explicitly. Generated from Max-Age (see V1) + private BBuffer path = BBuffer.allocate(); + private BBuffer domain = BBuffer.allocate(); + private boolean secure; + + // Version 1 (RFC2109) attributes + private BBuffer comment = BBuffer.allocate(); + private int maxAge = -1; + private int version = 0; + + // Other fields + private static final String OLD_COOKIE_PATTERN = + "EEE, dd-MMM-yyyy HH:mm:ss z"; + private static final ThreadLocal OLD_COOKIE_FORMAT = + new ThreadLocal() { + protected DateFormat initialValue() { + DateFormat df = + new SimpleDateFormat(OLD_COOKIE_PATTERN, Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + return df; + } + }; + + private static final String ancientDate; + + + static { + ancientDate = OLD_COOKIE_FORMAT.get().format(new Date(10000)); + } + + /** + * If set to true, we parse cookies according to the servlet spec, + */ + public static final boolean STRICT_SERVLET_COMPLIANCE = + Boolean.valueOf(System.getProperty("org.apache.catalina.STRICT_SERVLET_COMPLIANCE", "false")).booleanValue(); + + /** + * If set to false, we don't use the IE6/7 Max-Age/Expires work around + */ + public static final boolean ALWAYS_ADD_EXPIRES = + Boolean.valueOf(System.getProperty("org.apache.tomcat.util.http.ServerCookie.ALWAYS_ADD_EXPIRES", "true")).booleanValue(); + + // Note: Servlet Spec =< 2.5 only refers to Netscape and RFC2109, + // not RFC2965 + + // Version 1 (RFC2965) attributes + // TODO Add support for CommentURL + // Discard - implied by maxAge <0 + // TODO Add support for Port + + public ServerCookie() { + } + + public void recycle() { + path.recycle(); + name.recycle(); + value.recycle(); + comment.recycle(); + maxAge=-1; + path.recycle(); + domain.recycle(); + version=0; + secure=false; + } + + public BBuffer getComment() { + return comment; + } + + public BBuffer getDomain() { + return domain; + } + + public void setMaxAge(int expiry) { + maxAge = expiry; + } + + public int getMaxAge() { + return maxAge; + } + + public BBuffer getPath() { + return path; + } + + public void setSecure(boolean flag) { + secure = flag; + } + + public boolean getSecure() { + return secure; + } + + public BBuffer getName() { + return name; + } + + public BBuffer getValue() { + return value; + } + + public int getVersion() { + return version; + } + + public void setVersion(int v) { + version = v; + } + + + // -------------------- utils -------------------- + + public String toString() { + return "Cookie " + getName() + "=" + getValue() + " ; " + + getVersion() + " " + getPath() + " " + getDomain(); + } + + private static final String tspecials = ",; "; + private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t"; + private static final String tspecials2NoSlash = "()<>@,;:\\\"[]?={} \t"; + + /* + * Tests a string and returns true if the string counts as a + * reserved token in the Java language. + * + * @param value the String to be tested + * + * @return true if the String is a reserved + * token; false if it is not + */ + public static boolean isToken(String value) { + return isToken(value,null); + } + + public static boolean isToken(String value, String literals) { + String tspecials = (literals==null?ServerCookie.tspecials:literals); + if( value==null) return true; + int len = value.length(); + + for (int i = 0; i < len; i++) { + char c = value.charAt(i); + + if (tspecials.indexOf(c) != -1) + return false; + } + return true; + } + + public static boolean containsCTL(String value, int version) { + if( value==null) return false; + int len = value.length(); + for (int i = 0; i < len; i++) { + char c = value.charAt(i); + if (c < 0x20 || c >= 0x7f) { + if (c == 0x09) + continue; //allow horizontal tabs + return true; + } + } + return false; + } + + public static boolean isToken2(String value) { + return isToken2(value,null); + } + + public static boolean isToken2(String value, String literals) { + String tspecials2 = (literals==null?ServerCookie.tspecials2:literals); + if( value==null) return true; + int len = value.length(); + + for (int i = 0; i < len; i++) { + char c = value.charAt(i); + if (tspecials2.indexOf(c) != -1) + return false; + } + return true; + } + + // -------------------- Cookie parsing tools + + + /** + * Return the header name to set the cookie, based on cookie version. + */ + public String getCookieHeaderName() { + return getCookieHeaderName(version); + } + + /** + * Return the header name to set the cookie, based on cookie version. + */ + public static String getCookieHeaderName(int version) { + // TODO Re-enable logging when RFC2965 is implemented + // log( (version==1) ? "Set-Cookie2" : "Set-Cookie"); + if (version == 1) { + // XXX RFC2965 not referenced in Servlet Spec + // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5 + // Set-Cookie2 is supported by Lynx and Opera + // Need to check on later IE and FF releases but for now... + // RFC2109 + return "Set-Cookie"; + // return "Set-Cookie2"; + } else { + // Old Netscape + return "Set-Cookie"; + } + } + + // TODO RFC2965 fields also need to be passed + public static void appendCookieValue( StringBuffer headerBuf, + int version, + String name, + String value, + String path, + String domain, + String comment, + int maxAge, + boolean isSecure, + boolean isHttpOnly) + { + StringBuffer buf = new StringBuffer(); + // Servlet implementation checks name + buf.append( name ); + buf.append("="); + // Servlet implementation does not check anything else + + version = maybeQuote2(version, buf, value,true); + + // Add version 1 specific information + if (version == 1) { + // Version=1 ... required + buf.append ("; Version=1"); + + // Comment=comment + if ( comment!=null ) { + buf.append ("; Comment="); + maybeQuote2(version, buf, comment); + } + } + + // Add domain information, if present + if (domain!=null) { + buf.append("; Domain="); + maybeQuote2(version, buf, domain); + } + + // Max-Age=secs ... or use old "Expires" format + // TODO RFC2965 Discard + if (maxAge >= 0) { + if (version > 0) { + buf.append ("; Max-Age="); + buf.append (maxAge); + } + // IE6, IE7 and possibly other browsers don't understand Max-Age. + // They do understand Expires, even with V1 cookies! + if (version == 0 || ALWAYS_ADD_EXPIRES) { + // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) + buf.append ("; Expires="); + // To expire immediately we need to set the time in past + if (maxAge == 0) + buf.append( ancientDate ); + else + OLD_COOKIE_FORMAT.get().format( + new Date(System.currentTimeMillis() + + maxAge*1000L), + buf, new FieldPosition(0)); + } + } + + // Path=path + if (path!=null) { + buf.append ("; Path="); + if (version==0) { + maybeQuote2(version, buf, path); + } else { + maybeQuote2(version, buf, path, ServerCookie.tspecials2NoSlash, false); + } + } + + // Secure + if (isSecure) { + buf.append ("; Secure"); + } + + // HttpOnly + if (isHttpOnly) { + buf.append("; HttpOnly"); + } + headerBuf.append(buf); + } + + public static boolean alreadyQuoted (String value) { + if (value==null || value.length()==0) return false; + return (value.charAt(0)=='\"' && value.charAt(value.length()-1)=='\"'); + } + + /** + * Quotes values using rules that vary depending on Cookie version. + * @param version + * @param buf + * @param value + */ + public static int maybeQuote2 (int version, StringBuffer buf, String value) { + return maybeQuote2(version,buf,value,false); + } + + public static int maybeQuote2 (int version, StringBuffer buf, String value, boolean allowVersionSwitch) { + return maybeQuote2(version,buf,value,null,allowVersionSwitch); + } + + public static int maybeQuote2 (int version, StringBuffer buf, String value, String literals, boolean allowVersionSwitch) { + if (value==null || value.length()==0) { + buf.append("\"\""); + }else if (containsCTL(value,version)) + throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value"); + else if (alreadyQuoted(value)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value,1,value.length()-1)); + buf.append('"'); + } else if (allowVersionSwitch && (!STRICT_SERVLET_COMPLIANCE) && version==0 && !isToken2(value, literals)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value,0,value.length())); + buf.append('"'); + version = 1; + } else if (version==0 && !isToken(value,literals)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value,0,value.length())); + buf.append('"'); + } else if (version==1 && !isToken2(value,literals)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value,0,value.length())); + buf.append('"'); + }else { + buf.append(value); + } + return version; + } + + + /** + * Escapes any double quotes in the given string. + * + * @param s the input string + * @param beginIndex start index inclusive + * @param endIndex exclusive + * @return The (possibly) escaped string + */ + private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) { + + if (s == null || s.length() == 0 || s.indexOf('"') == -1) { + return s; + } + + StringBuffer b = new StringBuffer(); + for (int i = beginIndex; i < endIndex; i++) { + char c = s.charAt(i); + if (c == '\\' ) { + b.append(c); + //ignore the character after an escape, just append it + if (++i>=endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value."); + b.append(s.charAt(i)); + } else if (c == '"') + b.append('\\').append('"'); + else + b.append(c); + } + + return b.toString(); + } + + /** + * Unescapes any double quotes in the given cookie value. + * + * @param bc The cookie value to modify + */ + public static void unescapeDoubleQuotes(BBuffer bc) { + + if (bc == null || bc.getLength() == 0 || bc.indexOf('"', 0) == -1) { + return; + } + + int src = bc.getStart(); + int end = bc.getEnd(); + int dest = src; + byte[] buffer = bc.array(); + + while (src < end) { + if (buffer[src] == '\\' && src < end && buffer[src+1] == '"') { + src++; + } + buffer[dest] = buffer[src]; + dest ++; + src ++; + } + bc.setEnd(dest); + } + + /* + List of Separator Characters (see isSeparator()) + Excluding the '/' char violates the RFC, but + it looks like a lot of people put '/' + in unquoted values: '/': ; //47 + '\t':9 ' ':32 '\"':34 '\'':39 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60 + '=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125 + */ + public static final char SEPARATORS[] = { '\t', ' ', '\"', '\'', '(', ')', ',', + ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; + + protected static final boolean separators[] = new boolean[128]; + static { + for (int i = 0; i < 128; i++) { + separators[i] = false; + } + for (int i = 0; i < SEPARATORS.length; i++) { + separators[SEPARATORS[i]] = true; + } + } + + /** Add all Cookie found in the headers of a request. + */ + public static void processCookies(List cookies, + List cookiesCache, + HttpMessage.HttpMessageBytes msgBytes ) { + + // process each "cookie" header + for (int i = 0; i < msgBytes.headerCount; i++) { + if (msgBytes.getHeaderName(i).equalsIgnoreCase("Cookie")) { + BBuffer bc = msgBytes.getHeaderValue(i); + if (bc.remaining() == 0) { + continue; + } + processCookieHeader(cookies, cookiesCache, + bc.array(), + bc.getOffset(), + bc.getLength()); + + } + + } + } + + /** + * Returns true if the byte is a separator character as + * defined in RFC2619. Since this is called often, this + * function should be organized with the most probable + * outcomes first. + * JVK + */ + private static final boolean isSeparator(final byte c) { + if (c > 0 && c < 126) + return separators[c]; + else + return false; + } + + /** + * Returns true if the byte is a whitespace character as + * defined in RFC2619 + * JVK + */ + private static final boolean isWhiteSpace(final byte c) { + // This switch statement is slightly slower + // for my vm than the if statement. + // Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-164) + /* + switch (c) { + case ' ':; + case '\t':; + case '\n':; + case '\r':; + case '\f':; + return true; + default:; + return false; + } + */ + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f') + return true; + else + return false; + } + + /** + * Parses a cookie header after the initial "Cookie:" + * [WS][$]token[WS]=[WS](token|QV)[;|,] + * RFC 2965 + * JVK + */ + public static final void processCookieHeader( + List cookies, + List cookiesCache, + byte bytes[], int off, int len){ + if( len<=0 || bytes==null ) return; + int end=off+len; + int pos=off; + int nameStart=0; + int nameEnd=0; + int valueStart=0; + int valueEnd=0; + int version = 0; + ServerCookie sc=null; + boolean isSpecial; + boolean isQuoted; + + while (pos < end) { + isSpecial = false; + isQuoted = false; + + // Skip whitespace and non-token characters (separators) + while (pos < end && + (isSeparator(bytes[pos]) || isWhiteSpace(bytes[pos]))) + {pos++; } + + if (pos >= end) + return; + + // Detect Special cookies + if (bytes[pos] == '$') { + isSpecial = true; + pos++; + } + + // Get the cookie name. This must be a token + valueEnd = valueStart = nameStart = pos; + pos = nameEnd = getTokenEndPosition(bytes,pos,end); + + // Skip whitespace + while (pos < end && isWhiteSpace(bytes[pos])) {pos++; } + + + // Check for an '=' -- This could also be a name-only + // cookie at the end of the cookie header, so if we + // are past the end of the header, but we have a name + // skip to the name-only part. + if (pos < end && bytes[pos] == '=') { + + // Skip whitespace + do { + pos++; + } while (pos < end && isWhiteSpace(bytes[pos])); + + if (pos >= end) + return; + + // Determine what type of value this is, quoted value, + // token, name-only with an '=', or other (bad) + switch (bytes[pos]) { + case '"': // Quoted Value + isQuoted = true; + valueStart=pos + 1; // strip " + // getQuotedValue returns the position before + // at the last qoute. This must be dealt with + // when the bytes are copied into the cookie + valueEnd=getQuotedValueEndPosition(bytes, + valueStart, end); + // We need pos to advance + pos = valueEnd; + // Handles cases where the quoted value is + // unterminated and at the end of the header, + // e.g. [myname="value] + if (pos >= end) + return; + break; + case ';': + case ',': + // Name-only cookie with an '=' after the name token + // This may not be RFC compliant + valueStart = valueEnd = -1; + // The position is OK (On a delimiter) + break; + default: + if (!isSeparator(bytes[pos])) { + // Token + valueStart=pos; + // getToken returns the position at the delimeter + // or other non-token character + valueEnd=getTokenEndPosition(bytes, valueStart, end); + // We need pos to advance + pos = valueEnd; + } else { + // INVALID COOKIE, advance to next delimiter + // The starting character of the cookie value was + // not valid. + //log("Invalid cookie. Value not a token or quoted value"); + while (pos < end && bytes[pos] != ';' && + bytes[pos] != ',') + {pos++; } + pos++; + // Make sure no special avpairs can be attributed to + // the previous cookie by setting the current cookie + // to null + sc = null; + continue; + } + } + } else { + // Name only cookie + valueStart = valueEnd = -1; + pos = nameEnd; + + } + + // We should have an avpair or name-only cookie at this + // point. Perform some basic checks to make sure we are + // in a good state. + + // Skip whitespace + while (pos < end && isWhiteSpace(bytes[pos])) {pos++; } + + + // Make sure that after the cookie we have a separator. This + // is only important if this is not the last cookie pair + while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') { + pos++; + } + + pos++; + + /* + if (nameEnd <= nameStart || valueEnd < valueStart ) { + // Something is wrong, but this may be a case + // of having two ';' characters in a row. + // log("Cookie name/value does not conform to RFC 2965"); + // Advance to next delimiter (ignoring everything else) + while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') + { pos++; }; + pos++; + // Make sure no special cookies can be attributed to + // the previous cookie by setting the current cookie + // to null + sc = null; + continue; + } + */ + + // All checks passed. Add the cookie, start with the + // special avpairs first + if (isSpecial) { + isSpecial = false; + // $Version must be the first avpair in the cookie header + // (sc must be null) + if (equals( "Version", bytes, nameStart, nameEnd) && + sc == null) { + // Set version + if( bytes[valueStart] =='1' && valueEnd == (valueStart+1)) { + version=1; + } else { + // unknown version (Versioning is not very strict) + } + continue; + } + + // We need an active cookie for Path/Port/etc. + if (sc == null) { + continue; + } + + // Domain is more common, so it goes first + if (equals( "Domain", bytes, nameStart, nameEnd)) { + sc.getDomain().setBytes( bytes, + valueStart, + valueEnd-valueStart); + continue; + } + + if (equals( "Path", bytes, nameStart, nameEnd)) { + sc.getPath().setBytes( bytes, + valueStart, + valueEnd-valueStart); + continue; + } + + + if (equals( "Port", bytes, nameStart, nameEnd)) { + // sc.getPort is not currently implemented. + // sc.getPort().setBytes( bytes, + // valueStart, + // valueEnd-valueStart ); + continue; + } + + // Unknown cookie, complain + //log("Unknown Special Cookie"); + + } else { // Normal Cookie + // use a previous value from cache, if any (to avoid GC - tomcat + // legacy ) + if (cookiesCache.size() > cookies.size()) { + sc = cookiesCache.get(cookies.size()); + cookies.add(sc); + } else { + sc = new ServerCookie(); + cookiesCache.add(sc); + cookies.add(sc); + } + sc.setVersion( version ); + sc.getName().append( bytes, nameStart, + nameEnd-nameStart); + + if (valueStart != -1) { // Normal AVPair + sc.getValue().append( bytes, valueStart, + valueEnd-valueStart); + if (isQuoted) { + // We know this is a byte value so this is safe + ServerCookie.unescapeDoubleQuotes( + sc.getValue()); + } + } else { + // Name Only + sc.getValue().recycle(); + } + sc.nameC.recycle(); + sc.nameC.append(sc.getName()); + continue; + } + } + } + + /** + * Given the starting position of a token, this gets the end of the + * token, with no separator characters in between. + * JVK + */ + private static final int getTokenEndPosition(byte bytes[], int off, int end){ + int pos = off; + while (pos < end && !isSeparator(bytes[pos])) {pos++; } + + if (pos > end) + return end; + return pos; + } + + /** + * Given a starting position after an initial quote chracter, this gets + * the position of the end quote. This escapes anything after a '\' char + * JVK RFC 2616 + */ + private static final int getQuotedValueEndPosition(byte bytes[], int off, int end){ + int pos = off; + while (pos < end) { + if (bytes[pos] == '"') { + return pos; + } else if (bytes[pos] == '\\' && pos < (end - 1)) { + pos+=2; + } else { + pos++; + } + } + // Error, we have reached the end of the header w/o a end quote + return end; + } + + + public static boolean equals( String s, byte b[], int start, int end) { + int blen = end-start; + if (b == null || blen != s.length()) { + return false; + } + int boff = start; + for (int i = 0; i < blen; i++) { + if (b[boff++] != s.charAt(i)) { + return false; + } + } + return true; + } + +} + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/http/package.html b/modules/tomcat-lite/java/org/apache/tomcat/lite/http/package.html new file mode 100644 index 000000000..e69de29bb -- 2.11.0