--- /dev/null
+/*
+ * 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<String, Context> 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
+
+
+}
--- /dev/null
+/*
+ * 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,
+ * <code>null</code> 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;
+ }
+
+}
--- /dev/null
+/*
+ */
+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;
+
+}
--- /dev/null
+/* 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
--- /dev/null
+/*
+ */
+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<V> implements Future<V> {
+
+ // 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<V> {
+ public void run(V param);
+ }
+
+ private List<Callback<V>> 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<V> 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;
+ }
+ }
+}
--- /dev/null
+/*
+ */
+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;
+ }
+}
--- /dev/null
+/* 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<HttpChannel> doneLock = new FutureCallbacks<HttpChannel>();
+ ArrayList<IOChannel> filters = new ArrayList<IOChannel>();
+
+ // ---------- 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<HttpChannel> 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<IOChannel> flushLock = new FutureCallbacks<IOChannel>();
+
+ // -- 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<HttpChannel> 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.
+ * <p>
+ * 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
--- /dev/null
+/*
+ */
+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<HttpChannel> httpChannelPool = new ConcurrentLinkedQueue<HttpChannel>();
+
+ 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<IOChannel> connections = new ArrayList<IOChannel>();
+ }
+
+
+ // 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<CharSequence, RemoteServer> hosts = new HashMap<CharSequence,
+ RemoteServer>();
+ 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<CharSequence> 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);
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ */
+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<BBuffer> headerNames = new ArrayList<BBuffer>();
+ List<BBuffer> headerValues = new ArrayList<BBuffer>();
+
+ 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<ServerCookie> cookies;
+ protected ArrayList<ServerCookie> 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<ServerCookie>();
+ cookiesCache = new ArrayList<ServerCookie>();
+ 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<String> getHeaderNames() {
+
+ MultiMap headers = getMimeHeaders();
+ int n = headers.size();
+ ArrayList<String> result = new ArrayList<String>();
+ 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<ServerCookie> 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;
+ }
+
+}
--- /dev/null
+/*
+ */
+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<String, Object> attributes = new HashMap<String, Object>();
+
+ /**
+ * 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<String, Object> 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<String> 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<String, String[]> getParameterMap() {
+ // we could allow 'locking' - I don't think this is
+ // a very useful optimization
+ Map<String, String[]> 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<end );
+ }
+
+// public void processParameters(char 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=CBuffer.indexOf(bytes, nameStart, end, '=' );
+// // Workaround for a&b&c encoding
+// int nameEnd2=CBuffer.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=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<end );
+// }
+
+ private String urlDecode(BBuffer bc, String enc)
+ throws IOException {
+ // Replace %xx
+ urlDecoder.urlDecode(bc, true);
+
+ String result = null;
+ if (enc != null) {
+ result = bc.toString(enc);
+ } else {
+ // Ascii
+
+ CBuffer cc = tmpNameC;
+ cc.recycle();
+ int length = bc.getLength();
+ byte[] bbuf = bc.array();
+ int start = bc.getStart();
+ cc.appendAscii(bbuf, start, length);
+ result = cc.toString();
+ cc.recycle();
+ }
+ return result;
+ }
+
+ private void processParameters( byte bytes[], int start, int len ) {
+ processParameters(bytes, start, len, getEncoding());
+ }
+
+ protected void parseBody() {
+
+ parametersParsed = true;
+ String enc = getCharacterEncoding();
+
+// if (usingInputStream || usingReader)
+// return;
+ if (!getMethod().equalsIgnoreCase("POST"))
+ return;
+
+ String contentType = getContentType();
+ if (contentType == null)
+ contentType = "";
+ int semicolon = contentType.indexOf(';');
+ if (semicolon >= 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 <code>scheme</code> 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;
+ }
+
+}
--- /dev/null
+/*
+ */
+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<Integer,BBucket> stats = new HashMap<Integer, BBucket>();
+ 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());
+ }
+
+}
--- /dev/null
+/*
+ */
+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<String, C2BConverter> 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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<Entry> values = new ArrayList<Entry>();
+
+ 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<Entry> entries = new ArrayList<Entry>();
+
+ // 2 options: convert all header/param names to String
+ // or use a temp CBuffer to map
+ Map<CBuffer, Entry> map =
+ new HashMap<CBuffer, Entry>();
+
+ 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<String> 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<String> {
+ 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<String> EMPTY =
+ new Enumeration<String>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return false;
+ }
+
+ @Override
+ public String nextElement() {
+ return null;
+ }
+
+ };
+
+ public MultiMap insensitive() {
+ toLower = true;
+ return this;
+ }
+
+
+}
--- /dev/null
+/*
+ * 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<DateFormat> OLD_COOKIE_FORMAT =
+ new ThreadLocal<DateFormat>() {
+ 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 <code>String</code> to be tested
+ *
+ * @return <code>true</code> if the <code>String</code> is a reserved
+ * token; <code>false</code> 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<ServerCookie> cookies,
+ List<ServerCookie> 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<ServerCookie> cookies,
+ List<ServerCookie> 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;
+ }
+
+}
+