From: costin Date: Sat, 4 Apr 2009 16:24:34 +0000 (+0000) Subject: First batch, based on sandbox. The main change is adding the ObjectManager to abstrac... X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=6cfe624ce97cca9c2002c88482a39a803359d10f;p=tomcat7.0 First batch, based on sandbox. The main change is adding the ObjectManager to abstract configuration/JMX/integration, and replace some filters with explicit interfaces, as was suggested. This has dependencies on coyote and tomcat-util, and some optional servlets/filters/plugins to provide various features from the spec. git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@761964 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/modules/tomcat-lite/java/org/apache/tomcat/addons/UserAuthentication.java b/modules/tomcat-lite/java/org/apache/tomcat/addons/UserAuthentication.java new file mode 100644 index 000000000..d56556313 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/addons/UserAuthentication.java @@ -0,0 +1,47 @@ +/* + */ +package org.apache.tomcat.addons; + +import java.io.IOException; +import java.security.Principal; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Plugin for user auth. + * + * This interface should support all common forms of auth, + * including Basic, Digest, Form and various other auth + * standards - the plugin has full control over request and + * response. + * + * Container will verify the security constraints on URLs and + * call this for all URLs that have constraints. The plugin can + * either authenticate and return the principal, or change + * the response - redirect, add headers, send content. + * + * Alternative: a simple Filter can do the same, with some conventions + * to support it ( attributes ). + * + * @author Costin Manolache + */ +public interface UserAuthentication { + + /** + * If req has all the info - return the principal. + * Otherwise set the challenge in response. + * + * @param requestedMethod auth method from web.xml. Spec + * complain plugins must support it. + * @throws IOException + */ + public Principal authenticate(HttpServletRequest req, + HttpServletResponse res, + String requestedMethod) throws IOException; + + + public boolean isUserInRole(HttpServletRequest req, + Principal p, + String role); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/addons/UserSessionManager.java b/modules/tomcat-lite/java/org/apache/tomcat/addons/UserSessionManager.java new file mode 100644 index 000000000..5e450e7d5 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/addons/UserSessionManager.java @@ -0,0 +1,45 @@ +/* + */ +package org.apache.tomcat.addons; + +import java.io.IOException; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; + +/** + * Session management plugin. No dependency on tomcat-lite, should + * be possible to add this to tomcat-trunk or other containers. + * + * The container will: + * - extract the session id from request ( via a filter or built-ins ) + * - call this interface when the user makes the related calls in the + * servlet API. + * - provide a context attribute 'context-listeners' with the + * List from web.xml + * + * Implementation of this class must provide HttpSession object + * and implement the spec. + * + */ +public interface UserSessionManager { + + + + HttpSession findSession(String requestedSessionId) throws IOException; + + HttpSession createSession(String requestedSessionId); + + boolean isValid(HttpSession session); + + void access(HttpSession session); + + void endAccess(HttpSession session); + + + void setSessionTimeout(int to); + + void setContext(ServletContext ctx); + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/addons/UserTemplateClassMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/addons/UserTemplateClassMapper.java new file mode 100644 index 000000000..9c696a25c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/addons/UserTemplateClassMapper.java @@ -0,0 +1,39 @@ +/* + */ +package org.apache.tomcat.addons; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +/** + * Mapps a templating name ( like a jsp file ) to a class name + * generated by the template compiler. + * + * This class is needed at runtime to support *.jsp mappings + * or - can be used to support other formats. + * + * The UserTemplateCompiler is only needed to support run-time + * compilation - if ahead-of-time compilation is used + * all generated files can be mapped explicitly with + * . + * + * @author Costin Manolache + */ +public interface UserTemplateClassMapper { + + /** + * Generate and load the proxy corresponding to the template file. + * + */ + public Servlet loadProxy(String jspFile, + ServletContext ctx, + ServletConfig config) throws ServletException; + + /** + * Quick check if the template ( or deps ) has been modified + * and the servlet needs to be regenerated; + */ + public boolean needsReload(String jspFile, Servlet s); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/integration/ObjectManager.java b/modules/tomcat-lite/java/org/apache/tomcat/integration/ObjectManager.java new file mode 100644 index 000000000..88d4c6aac --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/integration/ObjectManager.java @@ -0,0 +1,79 @@ +/* + */ +package org.apache.tomcat.integration; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tomcat is using JMX heavily for monitoring and config - but other + * apps embedding tomcat may have different preferences. There + * is interest to use and better integrate with dependency injection + * frameworks, OSGI. + * + * Tomcat will make call to this class when it creates contexts, + * servlets, connectors - giving a chance to DI frameworks to inject, + * and to JMX to expose the objects. + * + * Tomcat will also call this class when it needs a plugin, allowing + * DI or frameworks to locate the dependency. + * + * @author Costin Manolache + */ +public class ObjectManager { + + /** + * Attribute used to keep a reference to the object manager + * in the context, for the use of servlets. + */ + public static final String ATTRIBUTE = "ObjectManager"; + + /** + * Register a named object with the framework. + * + * For example JMX will expose the object as an MBean. + * + * The framework may inject properties - if it supports that. + */ + public void bind(String name, Object o) { + for (ObjectManagerSpi p : providers) { + p.bind(name, o); + } + } + + /** + * When an object is no longer in use. + */ + public void unbind(String name) { + for (ObjectManagerSpi p : providers) { + p.unbind(name); + } + } + + /** + * Create or get a new object with the given name. + */ + public Object get(String key) { + for (ObjectManagerSpi p : providers) { + Object o = p.get(key); + if (o != null) { + return o; + } + } + return null; + } + + /** + * Helper for typed get. + */ + public Object get(Class c) { + return get(c.getName()); + } + + /** + * ObjectManager delegates to providers. You can have multiple + * providers - for example JMX, DI and OSGI at the same time. + */ + protected List providers = + new ArrayList(); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/integration/ObjectManagerSpi.java b/modules/tomcat-lite/java/org/apache/tomcat/integration/ObjectManagerSpi.java new file mode 100644 index 000000000..3619087e5 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/integration/ObjectManagerSpi.java @@ -0,0 +1,18 @@ +/* + */ +package org.apache.tomcat.integration; + +/** + * Base class for framework-integration plugins. + */ +public abstract class ObjectManagerSpi { + public abstract void bind(String name, Object o); + + public abstract void unbind(String name); + + public abstract Object get(String key); + + public void register(ObjectManager om) { + om.providers.add(this); + } +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/integration/jmx/JmxObjectManagerSpi.java b/modules/tomcat-lite/java/org/apache/tomcat/integration/jmx/JmxObjectManagerSpi.java new file mode 100644 index 000000000..96b134773 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/integration/jmx/JmxObjectManagerSpi.java @@ -0,0 +1,41 @@ +/* + */ +package org.apache.tomcat.integration.jmx; + +import java.util.logging.Logger; + +import org.apache.tomcat.integration.ObjectManagerSpi; +import org.apache.tomcat.util.modeler.Registry; + +/** + * Plugin for integration with JMX. + * + * All objects of interest are registered automatically. + */ +public class JmxObjectManagerSpi extends ObjectManagerSpi { + Registry registry; + Logger log = Logger.getLogger("JmxObjectManager"); + + public JmxObjectManagerSpi() { + registry = Registry.getRegistry(null, null); + } + + public void bind(String name, Object o) { + try { + registry.registerComponent(o, + ":name=\"" + name + "\"", null); + } catch (Exception e) { + log.severe("Error registering" + e); + } + } + + public void unbind(String name) { + registry.unregisterComponent(":name=\"" + name + "\""); + } + + @Override + public Object get(String key) { + return null; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/integration/simple/SimpleObjectManager.java b/modules/tomcat-lite/java/org/apache/tomcat/integration/simple/SimpleObjectManager.java new file mode 100644 index 000000000..5b3398359 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/integration/simple/SimpleObjectManager.java @@ -0,0 +1,214 @@ +/* + */ +package org.apache.tomcat.integration.simple; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; + +import org.apache.tomcat.integration.ObjectManager; +import org.apache.tomcat.integration.ObjectManagerSpi; +import org.apache.tomcat.util.IntrospectionUtils; + +/** + * This is a very small 'dependency injection'/registry poor-man substitute, + * based on old tomcat IntrospectionUtils ( which is based on ant ). + * Alternative would be to just pick one of spring/guice/etc and use it. + * This class is a bit smaller and should be enough for simple use. + * + * How it works: + * - when bound, simple properties are injected in the objects using + * the old IntrospectionUtils, same as in original Tomcat server.xml + * + * - object creation using class name - properties injected as well. + * Similar with how server.xml or ant works. + * + * - it is based on a big Properties file, with command line arguments + * merged in. + * + * Tomcat doesn't require any of the features - they are just used to + * allow configuration in 'default' mode, when no other framework is + * used. + * + * See the Spring example for an alternative. I believe most POJO frameworks + * can be supported. + * + * @author Costin Manolache + */ +public class SimpleObjectManager extends ObjectManagerSpi { + + static Logger log = Logger.getLogger(SimpleObjectManager.class.getName()); + + /** + * Saved CLI arguments. Will be added to the properties. + */ + public static String[] args; + + protected Properties props = new Properties(); + protected Map objects = new HashMap(); + ObjectManager om; + + public SimpleObjectManager() { + } + + public SimpleObjectManager(ObjectManager om) { + register(om); + } + + public void register(ObjectManager om) { + this.om = om; + if (args != null) { + try { + processArgs(args, props); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + super.register(om); + } + + public ObjectManager getObjectManager() { + return om; + } + + public void load(InputStream is) { + try { + props.load(is); + } catch (IOException e) { + throw new RuntimeException("Error loading default config"); + } + } + + public void loadResource(String res) { + InputStream in = this.getClass().getClassLoader() + .getResourceAsStream(res); + load(in); + } + + public Properties getProperties() { + return props; + } + + @Override + public void unbind(String name) { + } + + @Override + public void bind(String name, Object o) { + log.info("Bound: " + name + " " + o); + + // TODO: can I make 'inject' public - Guice seems to + // support this. + inject(name, o); + } + + @Override + public Object get(String key) { + // Use same syntax as Spring props. + String prop = props.getProperty(key + ".(class)"); + if (prop != null) { + Object res = loadClass(prop); + inject(key, res); + return res; + } + + return null; + } + + private void inject(String name, Object o) { + // Simple injection of primitive types + String pref = name + "."; + int prefLen = pref.length(); + + for (String k: props.stringPropertyNames()) { + if (k.startsWith(pref)) { + if (k.endsWith(")")) { + continue; // special + } + String value = props.getProperty(k); + value = IntrospectionUtils.replaceProperties(value, + props, null); + String p = k.substring(prefLen); + int idx = p.indexOf("."); + if (idx > 0) { + // ignore suffix - indexed properties + p = p.substring(0, idx); + } + IntrospectionUtils.setProperty(o, p, value); + log.info("Setting: " + name + " " + k + " " + value); + } + } + // We could do cooler things - inject objects, etc. + } + + private Object loadClass(String className) { + try { + Class c = Class.forName(className); + Object ext = c.newInstance(); + return ext; + } catch (Throwable e) { + e.printStackTrace(); + return null; + } + } + + /** + * Populate properties based on CLI: + * -key value + * --key=value + * + * --config=FILE - load a properties file + * + * @param args + * @param p + * @param meta + * @return everything after the first non arg not starting with '-' + * @throws IOException + */ + public static String[] processArgs(String[] args, Properties props) + throws IOException { + + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.startsWith("--")) { + arg = arg.substring(2); + } else if (arg.startsWith("-")) { + arg = arg.substring(1); + } else { + String [] res = new String[args.length - i]; + System.arraycopy(args, i, res, 0, res.length); + return res; + } + + String name = arg; + int eq = arg.indexOf("="); + String value = null; + if (eq > 0) { + name = arg.substring(0, eq); + value = arg.substring(eq + 1); + } else { + i++; + if (i >= args.length) { + throw new RuntimeException("Missing param " + arg); + } + value = args[i]; + } + + if ("config".equals(arg)) { + props.load(new FileInputStream(value)); + } else { + props.put(name, value); + } + } + return new String[] {}; + } + + public static void setArgs(String[] argv) { + SimpleObjectManager.args = argv; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/CharsetMapperDefault.properties b/modules/tomcat-lite/java/org/apache/tomcat/lite/CharsetMapperDefault.properties new file mode 100644 index 000000000..c73a8fc4c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/CharsetMapperDefault.properties @@ -0,0 +1,2 @@ +en=ISO-8859-1 +fr=ISO-8859-1 diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/Connector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/Connector.java new file mode 100644 index 000000000..d104b6836 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/Connector.java @@ -0,0 +1,42 @@ +/* + */ +package org.apache.tomcat.lite; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.integration.ObjectManager; + +/** + * What we need to plugin a connector. + * + * Currently we have lots of deps on coyote Request, but I plan to + * change this and allow other HTTP implementations - like MINA, the + * experimental async connector, etc. Most important will be + * different support for IO - i.e. a more non-blocking mode. + * We'll probably keep MessageBytes as wrappers for request/res + * properties. + * + * This interface has no dep on coyote. + * + */ +public interface Connector { + + public void setDaemon(boolean b); + + public void start(); + + public void stop(); + + public void finishResponse(HttpServletResponse res) throws IOException; + + public void recycle(HttpServletRequest req, HttpServletResponse res); + + void initRequest(HttpServletRequest req, HttpServletResponse res); + + public void setTomcatLite(TomcatLite tomcatLite); + + public void setObjectManager(ObjectManager objectManager); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ContextPreinitListener.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ContextPreinitListener.java new file mode 100644 index 000000000..c1ddaba89 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ContextPreinitListener.java @@ -0,0 +1,23 @@ +/* + */ +package org.apache.tomcat.lite; + +import javax.servlet.ServletContext; + +/** + * Tomcat-lite specific interface ( could be moved to addons ). + * This class will be called before initialization - implementations + * can add servlets, filters, etc. In particular web.xml parsing + * is done implementing this interface. + * + * On a small server you could remove web.xml support to reduce + * footprint, and either hardcode this class or use properties. + * Same if you already use a framework and you inject settings + * or use framework's registry (OSGI). + * + * @author Costin Manolache + */ +public interface ContextPreinitListener { + + public void preInit(ServletContext ctx); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/FilterChainImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/FilterChainImpl.java new file mode 100644 index 000000000..7d3569b6b --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/FilterChainImpl.java @@ -0,0 +1,168 @@ +/* + * Copyright 2009 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.lite; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/** + * Wraps the list of filters for the current request. One instance + * associated with each RequestImpl, reused. + * + * Populated by the mapper ( WebappFilterMapper for example ), which + * determines the filters for the current request. + * + * Not thread safe. + */ +public final class FilterChainImpl implements FilterChain { + private List filters = new ArrayList(); + + + /** + * The int which is used to maintain the current position + * in the filter chain. + */ + private int pos = 0; + + /** + * The servlet instance to be executed by this chain. + */ + private Servlet servlet = null; + + + private ServletConfigImpl wrapper; + + + public FilterChainImpl() { + super(); + } + + + /** + * Invoke the next filter in this chain, passing the specified request + * and response. If there are no more filters in this chain, invoke + * the service() method of the servlet itself. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet exception occurs + */ + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + + + // Call the next filter if there is one + if (pos < filters.size()) { + FilterConfigImpl filterConfig = filters.get(pos++); + Filter filter = null; + try { + filter = filterConfig.getFilter(); + filter.doFilter(request, response, this); + } catch (IOException e) { + throw e; + } catch (ServletException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + e.printStackTrace(); + throw new ServletException("Throwable", e); + } + return; + } + + // We fell off the end of the chain -- call the servlet instance + try { + if (servlet != null) + servlet.service(request, response); + } catch (IOException e) { + throw e; + } catch (ServletException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new ServletException("Throwable", e); + } + } + + + // -------------------------------------------------------- Package Methods + + + + /** + * Add a filter to the set of filters that will be executed in this chain. + * + * @param filterConfig The FilterConfig for the servlet to be executed + */ + public void addFilter(FilterConfigImpl filterConfig) { + filters.add(filterConfig); + } + + + /** + * Release references to the filters and wrapper executed by this chain. + */ + public void release() { + filters.clear(); + pos = 0; + servlet = null; + } + + + /** + * Set the servlet that will be executed at the end of this chain. + * Set by the mapper filter + */ + public void setServlet(ServletConfigImpl wrapper, Servlet servlet) { + this.wrapper = wrapper; + this.servlet = servlet; + } + + // ------ Getters for information ------------ + + public int getSize() { + return filters.size(); + } + + public FilterConfigImpl getFilter(int i) { + return filters.get(i); + } + + public Servlet getServlet() { + return servlet; + } + + public ServletConfigImpl getServletConfig() { + return wrapper; + } + + public int getPos() { + return pos; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/FilterConfigImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/FilterConfigImpl.java new file mode 100644 index 000000000..80f1e3f62 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/FilterConfigImpl.java @@ -0,0 +1,143 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.apache.tomcat.servlets.util.Enumerator; + + +/** + * A Filter is configured in web.xml by: + * - name - used in mappings + * - className - used to instantiate the filter + * - init params + * - other things not used in the servlet container ( icon, descr, etc ) + * + * Alternatively, in API mode you can pass the actual filter. + * + * @see ServletConfigImpl + */ +public final class FilterConfigImpl implements FilterConfig { + + public FilterConfigImpl(ServletContextImpl context) { + this.context = context; + } + + private ServletContextImpl context = null; + + /** + * The application Filter we are configured for. + */ + private transient Filter filter = null; + + private String filterName; + + private String filterClass; + + private Map initParams; + + public void setData(String filterName, String filterClass, + Map params) { + this.filterName = filterName; + this.filterClass = filterClass; + this.initParams = params; + } + + public void setFilter(Filter f) { + filter = f; + } + + public String getFilterName() { + return filterName; + } + + public String getInitParameter(String name) { + if (initParams == null) return null; + return initParams.get(name); + } + + /** + * Return an Enumeration of the names of the initialization + * parameters for this Filter. + */ + public Enumeration getInitParameterNames() { + if (initParams == null) + return (new Enumerator(new ArrayList())); + else + return (new Enumerator(initParams.keySet())); + } + + + /** + * Return the ServletContext of our associated web application. + */ + public ServletContext getServletContext() { + return context; + } + + /** + * Return the application Filter we are configured for. + */ + public Filter getFilter() throws ClassCastException, ClassNotFoundException, + IllegalAccessException, InstantiationException, ServletException { + + // Return the existing filter instance, if any + if (filter != null) + return filter; + + ClassLoader classLoader = context.getClassLoader(); + + ClassLoader oldCtxClassLoader = + Thread.currentThread().getContextClassLoader(); + if (classLoader != oldCtxClassLoader) { + Thread.currentThread().setContextClassLoader(classLoader); + } + try { + Class clazz = classLoader.loadClass(filterClass); + this.filter = (Filter) clazz.newInstance(); + } finally { + if (classLoader != oldCtxClassLoader) { + Thread.currentThread().setContextClassLoader(oldCtxClassLoader); + } + } + + filter.init(this); + return (this.filter); + } + + + /** + * Release the Filter instance associated with this FilterConfig, + * if there is one. + */ + public void release() { + if (this.filter != null){ + filter.destroy(); + } + this.filter = null; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/Locale2Charset.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/Locale2Charset.java new file mode 100644 index 000000000..0a9d73a6a --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/Locale2Charset.java @@ -0,0 +1,128 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite; + + +import java.io.InputStream; +import java.util.Locale; +import java.util.Properties; + + + +/** + * One instance per Context. Holds the + * + * Utility class that attempts to map from a Locale to the corresponding + * character set to be used for interpreting input text (or generating + * output text) when the Content-Type header does not include one. You + * can customize the behavior of this class by modifying the mapping data + * it loads, or by subclassing it (to change the algorithm) and then using + * your own version for a particular web application. + * + * @author Craig R. McClanahan + */ +public class Locale2Charset { + + + // ---------------------------------------------------- Manifest Constants + + + /** + * Default properties resource name. + */ + public static final String DEFAULT_RESOURCE = + "/org/apache/tomcat/lite/CharsetMapperDefault.properties"; + + + + // ---------------------------------------------------------- Constructors + + + /** + * Construct a new CharsetMapper using the default properties resource. + */ + public Locale2Charset() { + String name = DEFAULT_RESOURCE; + if (defaultMap == null) { // once ! + try { + defaultMap = new Properties(); + InputStream stream = + this.getClass().getResourceAsStream(name); + defaultMap.load(stream); + stream.close(); + } catch (Throwable t) { + throw new IllegalArgumentException(t.toString()); + } + } + map = defaultMap; + } + + + // ---------------------------------------------------- Instance Variables + + + private static Properties defaultMap; // shared for all apps + + /** + * The mapping properties that have been initialized from the specified or + * default properties resource. + */ + private Properties map; + + + // ------------------------------------------------------- Public Methods + + + /** + * Calculate the name of a character set to be assumed, given the specified + * Locale and the absence of a character set specified as part of the + * content type header. + * + * @param locale The locale for which to calculate a character set + */ + public String getCharset(Locale locale) { + // Match full language_country_variant first, then language_country, + // then language only + String charset = map.getProperty(locale.toString()); + if (charset == null) { + charset = map.getProperty(locale.getLanguage() + "_" + + locale.getCountry()); + if (charset == null) { + charset = map.getProperty(locale.getLanguage()); + } + } + return (charset); + } + + + /** + * The deployment descriptor can have a + * locale-encoding-mapping-list element which describes the + * webapp's desired mapping from locale to charset. This method + * gets called when processing the web.xml file for a context + * + * @param locale The locale for a character set + * @param charset The charset to be associated with the locale + */ + public void addCharsetMapping(String locale, String charset) { + if (map == defaultMap) { + // new copy, don't modify original + map = new Properties(defaultMap); + } + map.put(locale, charset); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ParameterMap.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ParameterMap.java new file mode 100644 index 000000000..aa64cbbf6 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ParameterMap.java @@ -0,0 +1,204 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + + +import java.util.HashMap; +import java.util.Map; + + +/** + * Extended implementation of HashMap that includes a + * locked property. This class can be used to safely expose + * Catalina internal parameter map objects to user classes without having + * to clone them in order to avoid modifications. When first created, a + * ParmaeterMap instance is not locked. + * + * @author Craig R. McClanahan + * @version $Revision: 302726 $ $Date: 2004-02-27 06:59:07 -0800 (Fri, 27 Feb 2004) $ + */ + +public final class ParameterMap extends HashMap { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new, empty map with the default initial capacity and + * load factor. + */ + public ParameterMap() { + + super(); + + } + + + /** + * Construct a new, empty map with the specified initial capacity and + * default load factor. + * + * @param initialCapacity The initial capacity of this map + */ + public ParameterMap(int initialCapacity) { + + super(initialCapacity); + + } + + + /** + * Construct a new, empty map with the specified initial capacity and + * load factor. + * + * @param initialCapacity The initial capacity of this map + * @param loadFactor The load factor of this map + */ + public ParameterMap(int initialCapacity, float loadFactor) { + + super(initialCapacity, loadFactor); + + } + + + /** + * Construct a new map with the same mappings as the given map. + * + * @param map Map whose contents are dupliated in the new map + */ + public ParameterMap(Map map) { + + super(map); + + } + + + // ------------------------------------------------------------- Properties + + + /** + * The current lock state of this parameter map. + */ + private boolean locked = false; + + + /** + * Return the locked state of this parameter map. + */ + public boolean isLocked() { + + return (this.locked); + + } + + + /** + * Set the locked state of this parameter map. + * + * @param locked The new locked state + */ + public void setLocked(boolean locked) { + + this.locked = locked; + + } + + + // --------------------------------------------------------- Public Methods + + + + /** + * Remove all mappings from this map. + * + * @exception IllegalStateException if this map is currently locked + */ + public void clear() { + + if (locked) + throw new IllegalStateException + ("parameterMap.locked"); + super.clear(); + + } + + + /** + * Associate the specified value with the specified key in this map. If + * the map previously contained a mapping for this key, the old value is + * replaced. + * + * @param key Key with which the specified value is to be associated + * @param value Value to be associated with the specified key + * + * @return The previous value associated with the specified key, or + * null if there was no mapping for key + * + * @exception IllegalStateException if this map is currently locked + */ + public Object put(Object key, Object value) { + + if (locked) + throw new IllegalStateException + ("parameterMap.locked"); + return (super.put(key, value)); + + } + + + /** + * Copy all of the mappings from the specified map to this one. These + * mappings replace any mappings that this map had for any of the keys + * currently in the specified Map. + * + * @param map Mappings to be stored into this map + * + * @exception IllegalStateException if this map is currently locked + */ + public void putAll(Map map) { + + if (locked) + throw new IllegalStateException + ("parameterMap.locked"); + super.putAll(map); + + } + + + /** + * Remove the mapping for this key from the map if present. + * + * @param key Key whose mapping is to be removed from the map + * + * @return The previous value associated with the specified key, or + * null if there was no mapping for that key + * + * @exception IllegalStateException if this map is currently locked + */ + public Object remove(Object key) { + + if (locked) + throw new IllegalStateException + ("parameterMap.locked"); + return (super.remove(key)); + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/RequestDispatcherImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/RequestDispatcherImpl.java new file mode 100644 index 000000000..6ec239739 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/RequestDispatcherImpl.java @@ -0,0 +1,853 @@ +/* + * 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; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestWrapper; +import javax.servlet.ServletResponse; +import javax.servlet.ServletResponseWrapper; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.mapper.MappingData; + +/** + * + */ +public final class RequestDispatcherImpl implements RequestDispatcher { + /** + * The request attribute under which the original servlet path is stored + * on an forwarded dispatcher request. + */ + public static final String FORWARD_SERVLET_PATH_ATTR = + "javax.servlet.forward.servlet_path"; + + + /** + * The request attribute under which the original query string is stored + * on an forwarded dispatcher request. + */ + public static final String FORWARD_QUERY_STRING_ATTR = + "javax.servlet.forward.query_string"; + + /** + * The request attribute under which the original request URI is stored + * on an forwarded dispatcher request. + */ + public static final String FORWARD_REQUEST_URI_ATTR = + "javax.servlet.forward.request_uri"; + + + /** + * The request attribute under which the original context path is stored + * on an forwarded dispatcher request. + */ + public static final String FORWARD_CONTEXT_PATH_ATTR = + "javax.servlet.forward.context_path"; + + + /** + * The request attribute under which the original path info is stored + * on an forwarded dispatcher request. + */ + public static final String FORWARD_PATH_INFO_ATTR = + "javax.servlet.forward.path_info"; + + /** + * The request attribute under which we store the servlet name on a + * named dispatcher request. + */ + public static final String NAMED_DISPATCHER_ATTR = + "org.apache.catalina.NAMED"; + + /** + * The request attribute under which the request URI of the included + * servlet is stored on an included dispatcher request. + */ + public static final String INCLUDE_REQUEST_URI_ATTR = + "javax.servlet.include.request_uri"; + + + /** + * The request attribute under which the context path of the included + * servlet is stored on an included dispatcher request. + */ + public static final String INCLUDE_CONTEXT_PATH_ATTR = + "javax.servlet.include.context_path"; + + + /** + * The request attribute under which the path info of the included + * servlet is stored on an included dispatcher request. + */ + public static final String INCLUDE_PATH_INFO_ATTR = + "javax.servlet.include.path_info"; + + + /** + * The request attribute under which the servlet path of the included + * servlet is stored on an included dispatcher request. + */ + public static final String INCLUDE_SERVLET_PATH_ATTR = + "javax.servlet.include.servlet_path"; + + + /** + * The request attribute under which the query string of the included + * servlet is stored on an included dispatcher request. + */ + public static final String INCLUDE_QUERY_STRING_ATTR = + "javax.servlet.include.query_string"; + + /** + * The request attribute under which we expose the value of the + * <jsp-file> value associated with this servlet, + * if any. + */ + public static final String JSP_FILE_ATTR = + "org.apache.catalina.jsp_file"; + + + // ----------------------------------------------------- Instance Variables + + private static Logger log = Logger.getLogger(RequestDispatcherImpl.class.getName()); + + private ServletContextImpl ctx = null; + + /** + * The servlet name for a named dispatcher. + */ + private String name = null; + + // Path for a path dispatcher + private String path; + + /** + * MappingData object - per thread for buffering. + */ + private transient ThreadLocal localMappingData = new ThreadLocal(); + + /* + OrigRequest(ServletRequestImpl) -> include/forward * -> this include + + On the path: user-defined RequestWrapper or our ServletRequestWrapper + + include() is called with a RequestWrapper(->...->origRequest) or origRequest + + Based on params, etc -> we wrap the req / response in ServletRequestWrapper, + call filters+servlet. Inside, the req can be wrapped again in + userReqWrapper, and other include called. + + + */ + + /** + * The outermost request that will be passed on to the invoked servlet. + */ + private ServletRequest outerRequest = null; + + /** + * The outermost response that will be passed on to the invoked servlet. + */ + private ServletResponse outerResponse = null; + + /** + * The request wrapper we have created and installed (if any). + */ + private ServletRequest wrapRequest = null; + + /** + * The response wrapper we have created and installed (if any). + */ + private ServletResponse wrapResponse = null; + + // Parameters used when constructing the dispatcvher + /** + * The extra path information for this RequestDispatcher. + */ + private String pathInfo = null; + /** + * The query string parameters for this RequestDispatcher. + */ + private String queryString = null; + /** + * The request URI for this RequestDispatcher. + */ + private String requestURI = null; + /** + * The servlet path for this RequestDispatcher. + */ + private String servletPath = null; + + // + private String origServletPath = null; + + /** + * The Wrapper associated with the resource that will be forwarded to + * or included. + */ + private ServletConfigImpl wrapper = null; + + private Servlet servlet; + + /** Named dispatcher + */ + public RequestDispatcherImpl(ServletConfigImpl wrapper, String name) { + this.wrapper = wrapper; + this.name = name; + this.ctx = (ServletContextImpl) wrapper.getServletContext(); + + } + + public RequestDispatcherImpl(ServletContextImpl ctx, String path) { + this.path = path; + this.ctx = ctx; + } + + + + /** + * Forward this request and response to another resource for processing. + * Any runtime exception, IOException, or ServletException thrown by the + * called servlet will be propogated to the caller. + * + * @param request The servlet request to be forwarded + * @param response The servlet response to be forwarded + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet exception occurs + */ + public void forward(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + // Reset any output that has been buffered, but keep headers/cookies + if (response.isCommitted()) { + throw new IllegalStateException("forward(): response.isComitted()"); + } + + try { + response.resetBuffer(); + } catch (IllegalStateException e) { + throw e; + } + + // Set up to handle the specified request and response + setup(request, response, false); + + // Identify the HTTP-specific request and response objects (if any) + HttpServletRequest hrequest = (HttpServletRequest) request; + + ServletRequestWrapperImpl wrequest = + (ServletRequestWrapperImpl) wrapRequest(); + + + if (name != null) { + wrequest.setRequestURI(hrequest.getRequestURI()); + wrequest.setContextPath(hrequest.getContextPath()); + wrequest.setServletPath(hrequest.getServletPath()); + wrequest.setPathInfo(hrequest.getPathInfo()); + wrequest.setQueryString(hrequest.getQueryString()); + + + } else { // path based + mapPath(); + if (wrapper == null) { + throw new ServletException("Forward not found " + + path); + } + String contextPath = ctx.getContextPath(); + if (hrequest.getAttribute(FORWARD_REQUEST_URI_ATTR) == null) { + wrequest.setAttribute(FORWARD_REQUEST_URI_ATTR, + hrequest.getRequestURI()); + wrequest.setAttribute(FORWARD_CONTEXT_PATH_ATTR, + hrequest.getContextPath()); + wrequest.setAttribute(FORWARD_SERVLET_PATH_ATTR, + hrequest.getServletPath()); + wrequest.setAttribute(FORWARD_PATH_INFO_ATTR, + hrequest.getPathInfo()); + wrequest.setAttribute(FORWARD_QUERY_STRING_ATTR, + hrequest.getQueryString()); + } + + wrequest.setContextPath(contextPath); + wrequest.setRequestURI(requestURI); + wrequest.setServletPath(servletPath); + wrequest.setPathInfo(pathInfo); + if (queryString != null) { + wrequest.setQueryString(queryString); + wrequest.setQueryParams(queryString); + } + } + processRequest(outerRequest, outerResponse); + + wrequest.recycle(); + unwrapRequest(); + + // This is not a real close in order to support error processing +// if ( log.isDebugEnabled() ) +// log.debug(" Disabling the response for futher output"); + + if (response instanceof ServletResponseImpl) { + ((ServletResponseImpl) response).flushBuffer(); + ((ServletResponseImpl) response).setSuspended(true); + } else { + // Servlet SRV.6.2.2. The Resquest/Response may have been wrapped + // and may no longer be instance of RequestFacade + if (log.isLoggable(Level.FINE)){ + log.fine( " The Response is vehiculed using a wrapper: " + + response.getClass().getName() ); + } + + // Close anyway + try { + PrintWriter writer = response.getWriter(); + writer.close(); + } catch (IllegalStateException e) { + try { + ServletOutputStream stream = response.getOutputStream(); + stream.close(); + } catch (IllegalStateException f) { + ; + } catch (IOException f) { + ; + } + } catch (IOException e) { + ; + } + } + } + + + + /** + * Include the response from another resource in the current response. + * Any runtime exception, IOException, or ServletException thrown by the + * called servlet will be propogated to the caller. + * + * @param request The servlet request that is including this one + * @param response The servlet response to be appended to + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet exception occurs + */ + public void include(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + + // Set up to handle the specified request and response + setup(request, response, true); + + // Create a wrapped response to use for this request + // this actually gets inserted somewhere in the chain - it's not + // the last one, but first non-user response + wrapResponse(); + ServletRequestWrapperImpl wrequest = + (ServletRequestWrapperImpl) wrapRequest(); + + + // Handle an HTTP named dispatcher include + if (name != null) { + wrequest.setAttribute(NAMED_DISPATCHER_ATTR, name); + if (servletPath != null) wrequest.setServletPath(servletPath); + wrequest.setAttribute(WebappFilterMapper.DISPATCHER_TYPE_ATTR, + new Integer(WebappFilterMapper.INCLUDE)); + wrequest.setAttribute(WebappFilterMapper.DISPATCHER_REQUEST_PATH_ATTR, + origServletPath); + } else { + mapPath(); + String contextPath = ctx.getContextPath(); + if (requestURI != null) + wrequest.setAttribute(INCLUDE_REQUEST_URI_ATTR, + requestURI); + if (contextPath != null) + wrequest.setAttribute(INCLUDE_CONTEXT_PATH_ATTR, + contextPath); + if (servletPath != null) + wrequest.setAttribute(INCLUDE_SERVLET_PATH_ATTR, + servletPath); + if (pathInfo != null) + wrequest.setAttribute(INCLUDE_PATH_INFO_ATTR, + pathInfo); + if (queryString != null) { + wrequest.setAttribute(INCLUDE_QUERY_STRING_ATTR, + queryString); + wrequest.setQueryParams(queryString); + } + + wrequest.setAttribute(WebappFilterMapper.DISPATCHER_TYPE_ATTR, + new Integer(WebappFilterMapper.INCLUDE)); + wrequest.setAttribute(WebappFilterMapper.DISPATCHER_REQUEST_PATH_ATTR, + origServletPath); + } + + invoke(outerRequest, outerResponse); + + wrequest.recycle(); + unwrapRequest(); + unwrapResponse(); + } + + + // -------------------------------------------------------- Private Methods + + public void mapPath() { + if (path == null || servletPath != null) return; + + // Retrieve the thread local URI, used for mapping + // TODO: recycle RequestDispatcher stack and associated objects + // instead of this object + + // Retrieve the thread local mapping data + MappingData mappingData = (MappingData) localMappingData.get(); + if (mappingData == null) { + mappingData = new MappingData(); + localMappingData.set(mappingData); + } + + // Get query string + int pos = path.indexOf('?'); + if (pos >= 0) { + queryString = path.substring(pos + 1); + } else { + pos = path.length(); + } + + // Map the URI + MessageBytes uriMB = MessageBytes.newInstance(); + CharChunk charBuffer = new CharChunk(); + //mappingData.localURIBytes; + uriMB.recycle(); + //CharChunk uriCC = uriMB.getCharChunk(); + try { + /* + * Ignore any trailing path params (separated by ';') for mapping + * purposes. + * This is sometimes broken - path params can be on any path + * component, not just last. + */ + int semicolon = path.indexOf(';'); + if (pos >= 0 && semicolon > pos) { + semicolon = -1; + } + if (ctx.getContextPath().length() > 1 ) { + charBuffer.append(ctx.getContextPath()); + } + charBuffer.append(path, 0, + semicolon > 0 ? semicolon : pos); + + // Wrap the buffer + uriMB.setChars(charBuffer.getBuffer(), + charBuffer.getOffset(), + charBuffer.getLength()); + + // TODO: make charBuffer part of request or something + ctx.getMapper().map(uriMB, mappingData); + + // at least default wrapper must be returned + + /* + * Append any trailing path params (separated by ';') that were + * ignored for mapping purposes, so that they're reflected in the + * RequestDispatcher's requestURI + */ + if (semicolon > 0) { + // I don't think this will be used in future + charBuffer.append(path, + semicolon, pos - semicolon); + } + } catch (Exception e) { + log.log(Level.SEVERE, "getRequestDispatcher()", e); + } + + wrapper = (ServletConfigImpl) mappingData.wrapper; + servletPath = mappingData.wrapperPath.toString(); + pathInfo = mappingData.pathInfo.toString(); + + mappingData.recycle(); + + } + + + /** + * Prepare the request based on the filter configuration. + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + private void processRequest(ServletRequest request, + ServletResponse response) + throws IOException, ServletException { + Integer disInt = + (Integer) request.getAttribute(WebappFilterMapper.DISPATCHER_TYPE_ATTR); + if (disInt != null) { + if (disInt.intValue() != WebappFilterMapper.ERROR) { + outerRequest.setAttribute + (WebappFilterMapper.DISPATCHER_REQUEST_PATH_ATTR, + origServletPath); + outerRequest.setAttribute + (WebappFilterMapper.DISPATCHER_TYPE_ATTR, + new Integer(WebappFilterMapper.FORWARD)); + } + invoke(outerRequest, response); + } + + } + + + + + /** + * Ask the resource represented by this RequestDispatcher to process + * the associated request, and create (or append to) the associated + * response. + *

+ * IMPLEMENTATION NOTE: This implementation assumes + * that no filters are applied to a forwarded or included resource, + * because they were already done for the original request. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + private void invoke(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + + // Checking to see if the context classloader is the current context + // classloader. If it's not, we're saving it, and setting the context + // classloader to the Context classloader + ClassLoader oldCCL = Thread.currentThread().getContextClassLoader(); + ClassLoader contextClassLoader = ctx.getClassLoader(); + + if (oldCCL != contextClassLoader) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } else { + oldCCL = null; + } + + // Initialize local variables we may need + HttpServletResponse hresponse = (HttpServletResponse) response; + IOException ioException = null; + ServletException servletException = null; + RuntimeException runtimeException = null; + + servletException = allocateServlet(hresponse, servletException); + + // Get the FilterChain Here + WebappFilterMapper factory = + ((ServletContextImpl)wrapper.getServletContext()).getFilterMapper(); + + FilterChainImpl filterChain = factory.createFilterChain(request, + wrapper, + servlet); + + // Call the service() method for the allocated servlet instance + try { + String jspFile = wrapper.getJspFile(); + if (jspFile != null) + request.setAttribute(JSP_FILE_ATTR, jspFile); + else + request.removeAttribute(JSP_FILE_ATTR); + // for includes/forwards + if ((servlet != null) && (filterChain != null)) { + filterChain.doFilter(request, response); + } + } catch (IOException e) { + ctx.getLogger().log(Level.WARNING, "RequestDispatcherImpl error " + + wrapper.getServletName(), e); + ioException = e; + } catch (UnavailableException e) { + ctx.getLogger().log(Level.WARNING, "RequestDispatcherImpl error " + + wrapper.getServletName(), e); + servletException = e; + wrapper.unavailable(e); + } catch (ServletException e) { + servletException = e; + } catch (RuntimeException e) { + ctx.getLogger().log(Level.WARNING, "RequestDispatcherImpl error " + + wrapper.getServletName(), e); + runtimeException = e; + } + request.removeAttribute(JSP_FILE_ATTR); + + // Release the filter chain (if any) for this request + if (filterChain != null) + filterChain.release(); + + servletException = servletDealocate(servletException); + + // Reset the old context class loader + if (oldCCL != null) + Thread.currentThread().setContextClassLoader(oldCCL); + + // Unwrap request/response if needed + unwrapRequest(); + unwrapResponse(); + + // Rethrow an exception if one was thrown by the invoked servlet + if (ioException != null) + throw ioException; + if (servletException != null) + throw servletException; + if (runtimeException != null) + throw runtimeException; + + } + + private ServletException servletDealocate(ServletException servletException) + { + if (servlet != null) { + wrapper.deallocate(servlet); + } + return servletException; + } + + private ServletException allocateServlet(HttpServletResponse hresponse, + ServletException servletException) + throws IOException + { + boolean unavailable = false; + + // Check for the servlet being marked unavailable + if (wrapper.isUnavailable()) { + ctx.getLogger().log(Level.WARNING, "isUnavailable() " + wrapper.getServletName()); + long available = wrapper.getAvailable(); + if ((available > 0L) && (available < Long.MAX_VALUE)) + hresponse.setDateHeader("Retry-After", available); + hresponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + "Unavailable"); // No need to include internal info: wrapper.getServletName(); + unavailable = true; + } + + // Allocate a servlet instance to process this request + try { + if (!unavailable) { + servlet = wrapper.allocate(); + } + } catch (ServletException e) { + ctx.getLogger().log(Level.WARNING, "RequestDispatcher: allocate " + + wrapper.toString()); + servletException = e; + servlet = null; + } catch (Throwable e) { + ctx.getLogger().log(Level.WARNING, "allocate() error " + wrapper.getServletName(), e); + servletException = new ServletException + ("Allocate error " + wrapper.getServletName(), e); + servlet = null; + } + return servletException; + } + + + /** + * Set up to handle the specified request and response + * + * @param request The servlet request specified by the caller + * @param response The servlet response specified by the caller + * @param including Are we performing an include() as opposed to + * a forward()? + */ + private void setup(ServletRequest request, ServletResponse response, + boolean including) { + + this.outerRequest = request; + this.outerResponse = response; + } + + + /** + * Unwrap the request if we have wrapped it. Not sure how it could end + * up in the middle. + */ + private void unwrapRequest() { + if (wrapRequest == null) + return; + + ServletRequest previous = null; + ServletRequest current = outerRequest; + while (current != null) { + // If we run into the container request we are done + if (current instanceof ServletRequestImpl) + break; + + // Remove the current request if it is our wrapper + if (current == wrapRequest) { + ServletRequest next = + ((ServletRequestWrapper) current).getRequest(); + if (previous == null) + outerRequest = next; + else + ((ServletRequestWrapper) previous).setRequest(next); + break; + } + + // Advance to the next request in the chain + previous = current; + current = ((ServletRequestWrapper) current).getRequest(); + } + } + + + /** + * Unwrap the response if we have wrapped it. + */ + private void unwrapResponse() { + if (wrapResponse == null) + return; + + ServletResponse previous = null; + ServletResponse current = outerResponse; + while (current != null) { + // If we run into the container response we are done + if (current instanceof ServletResponseImpl) + break; + + // Remove the current response if it is our wrapper + if (current == wrapResponse) { + ServletResponse next = + ((ServletResponseWrapper) current).getResponse(); + if (previous == null) + outerResponse = next; + else + ((ServletResponseWrapper) previous).setResponse(next); + break; + } + // Advance to the next response in the chain + previous = current; + current = ((ServletResponseWrapper) current).getResponse(); + } + } + + + /** + * Create and return a request wrapper that has been inserted in the + * appropriate spot in the request chain. + */ + private ServletRequest wrapRequest() { + // Locate the request we should insert in front of + ServletRequest previous = null; + ServletRequest current = outerRequest; + while (current != null) { + if (!(current instanceof ServletRequestWrapper)) + break; + if (current instanceof ServletRequestWrapperImpl) + break; + if (current instanceof ServletRequestImpl) + break; + // user-specified + previous = current; + current = ((ServletRequestWrapper) current).getRequest(); + } + // now previous will be a user-specified wrapper, + // and current one of our own wrappers ( deeper in stack ) + // ... current USER_previous USER USER + // previous is null if the top request is ours. + + // Instantiate a new wrapper at this point and insert it in the chain + ServletRequest wrapper = null; + + // Compute a crossContext flag + boolean crossContext = isCrossContext(); + wrapper = + new ServletRequestWrapperImpl((HttpServletRequest) current, + ctx, crossContext); + + if (previous == null) { + // outer becomes the wrapper, includes orig wrapper inside + outerRequest = wrapper; + } else { + // outer remains user-specified sersvlet, delegating to + // our wrapper, which delegates to real request or our wrapper. + ((ServletRequestWrapper) previous).setRequest(wrapper); + } + wrapRequest = wrapper; + return (wrapper); + } + + private boolean isCrossContext() { + boolean crossContext = false; + if ((outerRequest instanceof ServletRequestWrapperImpl) || + (outerRequest instanceof ServletRequestImpl) || + (outerRequest instanceof HttpServletRequest)) { + HttpServletRequest houterRequest = + (HttpServletRequest) outerRequest; + Object contextPath = + houterRequest.getAttribute(INCLUDE_CONTEXT_PATH_ATTR); + if (contextPath == null) { + // Forward + contextPath = houterRequest.getContextPath(); + } + crossContext = !(ctx.getContextPath().equals(contextPath)); + } + return crossContext; + } + + + /** + * Create and return a response wrapper that has been inserted in the + * appropriate spot in the response chain. + * + * Side effect: updates outerResponse, wrapResponse. + * The chain is updated with a wrapper below lowest user wrapper + */ + private ServletResponse wrapResponse() { + // Locate the response we should insert in front of + ServletResponse previous = null; + ServletResponse current = outerResponse; + while (current != null) { + if (!(current instanceof ServletResponseWrapper)) + break; + if (current instanceof ServletResponseImpl) + break; + previous = current; + current = ((ServletResponseWrapper) current).getResponse(); + } + + // Instantiate a new wrapper at this point and insert it in the chain + ServletResponse wrapper = + new ServletResponseIncludeWrapper(current); + + if (previous == null) { + // outer is ours, we can wrap on top + outerResponse = wrapper; + } else { + // outer is user-specified, leave it alone. + // we insert ourself below the lowest user-specified response + ((ServletResponseWrapper) previous).setResponse(wrapper); + } + wrapResponse = wrapper; + return (wrapper); + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletConfigImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletConfigImpl.java new file mode 100644 index 000000000..85be60b5a --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletConfigImpl.java @@ -0,0 +1,809 @@ +/* + * Copyright 1999-2002,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Stack; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.SingleThreadModel; +import javax.servlet.UnavailableException; + +import org.apache.tomcat.addons.UserTemplateClassMapper; +import org.apache.tomcat.servlets.util.Enumerator; +import org.apache.tomcat.util.IntrospectionUtils; + +/** + * Based on Wrapper. + * + * Standard implementation of the Wrapper interface that represents + * an individual servlet definition. No child Containers are allowed, and + * the parent Container must be a Context. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class ServletConfigImpl implements ServletConfig { + + private static Logger log= + Logger.getLogger(ServletConfigImpl.class.getName()); + + private static final String[] DEFAULT_SERVLET_METHODS = new String[] { + "GET", "HEAD", "POST" }; + + // TODO: refactor all 'stm' to separate class (not implemented) + // public static final String SINGLE_THREADED_PROXY = + // "org.apache.tomcat.servlets.jsp.SingleThreadedProxyServlet"; + + + protected Map initParams = new HashMap(); + protected String servletName; + protected String servletClass; + protected String jspFile; + protected int loadOnStartup = -1; + protected String runAs; + protected Map securityRoleRef = new HashMap(); // roleName -> [roleLink] + + /** + * The date and time at which this servlet will become available (in + * milliseconds since the epoch), or zero if the servlet is available. + * If this value equals Long.MAX_VALUE, the unavailability of this + * servlet is considered permanent. + */ + private transient long available = 0L; + + private ServletContextImpl ctx; + + /** + * The (single) initialized instance of this servlet. + */ + private transient Servlet instance = null; + + /** + * Are we unloading our servlet instance at the moment? + */ + private transient boolean unloading = false; + + private Class classClass = null; + + // Support for SingleThreaded + /** + * The count of allocations that are currently active (even if they + * are for the same instance, as will be true on a non-STM servlet). + */ + private transient int countAllocated = 0; + + private transient boolean singleThreadModel = false; + /** + * Stack containing the STM instances. + */ + private transient Stack instancePool = null; + + + // Statistics + private transient long loadTime=0; + private transient int classLoadTime=0; + + // ------------------------------------------------------------- Properties + public ServletConfigImpl(ServletContextImpl ctx, String name, + String classname) { + this.servletName = name; + this.servletClass = classname; + this.ctx = ctx; + ctx.facade.notifyAdd(this); + } + + /** + * Return the available date/time for this servlet, in milliseconds since + * the epoch. If this date/time is Long.MAX_VALUE, it is considered to mean + * that unavailability is permanent and any request for this servlet will return + * an SC_NOT_FOUND error. If this date/time is in the future, any request for + * this servlet will return an SC_SERVICE_UNAVAILABLE error. If it is zero, + * the servlet is currently available. + */ + public long getAvailable() { + return (this.available); + } + + + /** + * Set the available date/time for this servlet, in milliseconds since the + * epoch. If this date/time is Long.MAX_VALUE, it is considered to mean + * that unavailability is permanent and any request for this servlet will return + * an SC_NOT_FOUND error. If this date/time is in the future, any request for + * this servlet will return an SC_SERVICE_UNAVAILABLE error. + * + * @param available The new available date/time + */ + public void setAvailable(long available) { + + long oldAvailable = this.available; + if (available > System.currentTimeMillis()) + this.available = available; + else + this.available = 0L; + + } + + + /** + * Return the number of active allocations of this servlet, even if they + * are all for the same instance (as will be true for servlets that do + * not implement SingleThreadModel. + */ + public int getCountAllocated() { + return (this.countAllocated); + } + + /** + * Return the jsp-file setting for this servlet. + */ + public String getJspFile() { + return jspFile; + } + + public void setJspFile(String s) { + this.jspFile = s; + } + + /** + * Return the load-on-startup order value (negative value means + * load on first call). + */ + public int getLoadOnStartup() { + return loadOnStartup; + } + + /** + * Return the fully qualified servlet class name for this servlet. + */ + public String getServletClass() { + return servletClass; + } + + /** + * Is this servlet currently unavailable? + */ + public boolean isUnavailable() { + if (available == 0L) + return (false); + else if (available <= System.currentTimeMillis()) { + available = 0L; + return (false); + } else + return (true); + + } + + + /** + * Gets the names of the methods supported by the underlying servlet. + * + * This is the same set of methods included in the Allow response header + * in response to an OPTIONS request method processed by the underlying + * servlet. + * + * @return Array of names of the methods supported by the underlying + * servlet + */ + public String[] getServletMethods() throws ServletException { + + Class servletClazz = loadServlet().getClass(); + if (!javax.servlet.http.HttpServlet.class.isAssignableFrom( + servletClazz)) { + return DEFAULT_SERVLET_METHODS; + } + + HashSet allow = new HashSet(); + allow.add("TRACE"); + allow.add("OPTIONS"); + + Method[] methods = getAllDeclaredMethods(servletClazz); + for (int i=0; methods != null && inull + * to mark this servlet as permanently unavailable + */ + public void unavailable(UnavailableException unavailable) { + getServletContext().log("UnavailableException:" + getServletName()); + if (unavailable == null) + setAvailable(Long.MAX_VALUE); + else if (unavailable.isPermanent()) + setAvailable(Long.MAX_VALUE); + else { + int unavailableSeconds = unavailable.getUnavailableSeconds(); + if (unavailableSeconds <= 0) + unavailableSeconds = 60; // Arbitrary default + setAvailable(System.currentTimeMillis() + + (unavailableSeconds * 1000L)); + } + + } + + + /** + * Unload all initialized instances of this servlet, after calling the + * destroy() method for each instance. This can be used, + * for example, prior to shutting down the entire servlet engine, or + * prior to reloading all of the classes from the Loader associated with + * our Loader's repository. + * + * @exception ServletException if an exception is thrown by the + * destroy() method + */ + public synchronized void unload() throws ServletException { + setAvailable(Long.MAX_VALUE); + + // Nothing to do if we have never loaded the instance + if (!singleThreadModel && (instance == null)) + return; + unloading = true; + + // Loaf a while if the current instance is allocated + // (possibly more than once if non-STM) + if (countAllocated > 0) { + int nRetries = 0; + long delay = ctx.getUnloadDelay() / 20; + while ((nRetries < 21) && (countAllocated > 0)) { + if ((nRetries % 10) == 0) { + log.info("Servlet.unload() timeout " + + countAllocated); + } + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + ; + } + nRetries++; + } + } + + ClassLoader oldCtxClassLoader = + Thread.currentThread().getContextClassLoader(); + if (instance != null) { + ClassLoader classLoader = instance.getClass().getClassLoader(); + + PrintStream out = System.out; + // Call the servlet destroy() method + try { + Thread.currentThread().setContextClassLoader(classLoader); + instance.destroy(); + } catch (Throwable t) { + instance = null; + //instancePool = null; + unloading = false; + throw new ServletException("Servlet.destroy() " + + getServletName(), t); + } finally { + // restore the context ClassLoader + Thread.currentThread().setContextClassLoader(oldCtxClassLoader); + } + + // Deregister the destroyed instance + instance = null; + } + if (singleThreadModel && (instancePool != null)) { + try { + ClassLoader classLoader = ctx.getClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + while (!instancePool.isEmpty()) { + ((Servlet) instancePool.pop()).destroy(); + } + } catch (Throwable t) { + instancePool = null; + unloading = false; + throw new ServletException("Servlet.destroy() " + getServletName(), t); + } finally { + // restore the context ClassLoader + Thread.currentThread().setContextClassLoader + (oldCtxClassLoader); + } + instancePool = null; + } + + singleThreadModel = false; + + unloading = false; + } + + + /** + * Return the initialization parameter value for the specified name, + * if any; otherwise return null. + * + * @param name Name of the initialization parameter to retrieve + */ + public String getInitParameter(String name) { + + return (String)initParams.get(name); + + } + + + /** + * Return the set of initialization parameter names defined for this + * servlet. If none are defined, an empty Enumeration is returned. + */ + public Enumeration getInitParameterNames() { + + synchronized (initParams) { + return (new Enumerator(initParams.keySet())); + } + + } + + + /** + * Return the servlet context with which this servlet is associated. + */ + public ServletContext getServletContext() { + return ctx; + } + + + /** + * Return the name of this servlet. + */ + public String getServletName() { + return servletName; + } + +// public long getProcessingTime() { +// return swValve.getProcessingTime(); +// } +// +// public void setProcessingTime(long processingTime) { +// swValve.setProcessingTime(processingTime); +// } +// +// public long getMaxTime() { +// return swValve.getMaxTime(); +// } +// +// public void setMaxTime(long maxTime) { +// swValve.setMaxTime(maxTime); +// } +// +// public long getMinTime() { +// return swValve.getMinTime(); +// } +// +// public void setMinTime(long minTime) { +// swValve.setMinTime(minTime); +// } +// +// public int getRequestCount() { +// return swValve.getRequestCount(); +// } +// +// public void setRequestCount(int requestCount) { +// swValve.setRequestCount(requestCount); +// } +// +// public int getErrorCount() { +// return swValve.getErrorCount(); +// } +// +// public void setErrorCount(int errorCount) { +// swValve.setErrorCount(errorCount); +// } +// +// /** +// * Increment the error count used for monitoring. +// */ +// public void incrementErrorCount(){ +// swValve.setErrorCount(swValve.getErrorCount() + 1); +// } +// +// public long getLoadTime() { +// return loadTime; +// } +// +// public void setLoadTime(long loadTime) { +// this.loadTime = loadTime; +// } +// +// public int getClassLoadTime() { +// return classLoadTime; +// } + + // -------------------------------------------------------- Package Methods + + + // -------------------------------------------------------- Private Methods + + private Method[] getAllDeclaredMethods(Class c) { + + if (c.equals(javax.servlet.http.HttpServlet.class)) { + return null; + } + + Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass()); + + Method[] thisMethods = c.getDeclaredMethods(); + if (thisMethods == null) { + return parentMethods; + } + + if ((parentMethods != null) && (parentMethods.length > 0)) { + Method[] allMethods = + new Method[parentMethods.length + thisMethods.length]; + System.arraycopy(parentMethods, 0, allMethods, 0, + parentMethods.length); + System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, + thisMethods.length); + + thisMethods = allMethods; + } + + return thisMethods; + } + + /** Specify the instance. Avoids the class lookup, disables unloading. + * Use for embedded case, or to control the allocation. + * + * @param servlet + */ + public void setServlet(Servlet servlet) { + instance = servlet; + ctx.getObjectManager().bind("Servlet:" + + ctx.getContextPath() + ":" + getServletName(), + this); + } + + public String getSecurityRoleRef(String role) { + return (String)securityRoleRef.get(role); + } + + public void setSecurityRoleRef(Map securityRoles) { + this.securityRoleRef = securityRoles; + } + + public void setConfig(Map initParams) { + this.initParams = initParams; + } + + public void setLoadOnStartup(int loadOnStartup) { + this.loadOnStartup = loadOnStartup; + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletContextConfig.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletContextConfig.java new file mode 100644 index 000000000..bbb43b877 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletContextConfig.java @@ -0,0 +1,131 @@ +/** + * + */ +package org.apache.tomcat.lite; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * All the data in web.xml should be represented here. + * + * Public fields to make it easy to access it. + * Naming should match the web.xml element name. + * + * @author Costin Manolache + */ +public class ServletContextConfig implements Serializable { + + private static final long serialVersionUID = 1728492145981883124L; + + public String fileName; + public long timestamp; + + public boolean full; + + public String displayName; + + public HashMap contextParam = new HashMap(); + + public HashMap mimeMapping = new HashMap(); // extension -> mime-type + + public ArrayList listenerClass = new ArrayList(); + + public ArrayList welcomeFileList = new ArrayList(); + + // code -> location + public HashMap errorPageCode= new HashMap(); + + // exception -> location + public HashMap errorPageException= new HashMap(); + + public HashMap localeEncodingMapping= new HashMap(); // locale -> encoding + + // public HashMap tagLibs; // uri->location + // jsp-property-group + + // securityConstraint + public ArrayList securityConstraint = new ArrayList(); + + // loginConfig + public String authMethod; + public String realmName; + public String formLoginPage; + public String formErrorPage; + + public ArrayList securityRole = new ArrayList(); + + // envEntry + public ArrayList envEntry = new ArrayList(); + + // ejbRef + // ejbLocalRef + // serviceRef + // resourceRef + // resourceEnvRef + // message-destination + // message-destinationRef + public HashMap filters = new HashMap(); + public HashMap servlets = new HashMap(); + + public int sessionTimeout; + public boolean distributable; + + public HashMap servletMapping = new HashMap(); // url -> servlet + public ArrayList filterMappings = new ArrayList(); + + + public static class FilterData implements Serializable { + private static final long serialVersionUID = -535820271746973166L; + + public HashMap initParams = new HashMap(); + public String filterClass; + public String filterName; + } + + public static class FilterMappingData implements Serializable { + private static final long serialVersionUID = -4533568066713041994L; + public String filterName; + public String urlPattern; + public String servletName; + public ArrayList dispatcher = new ArrayList(); + } + + public static class EnvEntryData implements Serializable { + private static final long serialVersionUID = 7023847615343715257L; + public String envEntryName; + public String envEntryType; + public String envEntryValue; + } + + public static class ServletData implements Serializable { + private static final long serialVersionUID = -3216904178501185930L; + + public ServletData() { + } + public ServletData(String servletName, String servletClass) { + this.servletClass = servletClass; + this.servletName = servletName; + } + + public HashMap initParams = new HashMap(); + public String servletName; + public String servletClass; + public String jspFile; + public int loadOnStartup = -1; + public String runAs; + public HashMap securityRoleRef = new HashMap(); // roleName -> [roleLink] + + } + + public static class SecurityConstraintData implements Serializable { + private static final long serialVersionUID = -4780214921810871769L; + + public ArrayList roleName = new ArrayList(); // auth-constraint/role + + public ArrayList webResourceCollection = new ArrayList(); + public String transportGuarantee; + + } +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletContextImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletContextImpl.java new file mode 100644 index 000000000..0484c07be --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletContextImpl.java @@ -0,0 +1,1428 @@ +/* + * 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; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; + +import org.apache.tomcat.addons.UserSessionManager; +import org.apache.tomcat.integration.ObjectManager; +import org.apache.tomcat.lite.ServletContextConfig.FilterData; +import org.apache.tomcat.lite.ServletContextConfig.FilterMappingData; +import org.apache.tomcat.lite.ServletContextConfig.ServletData; +import org.apache.tomcat.servlets.util.Enumerator; +import org.apache.tomcat.servlets.util.RequestUtil; +import org.apache.tomcat.servlets.util.UrlUtils; +import org.apache.tomcat.util.http.MimeMap; + + +/** + * Context - initialized from web.xml or using APIs. + * + * Initialization order: + * + * - add all listeners + * - add all filters + * - add all servlets + * + * - session parameters + * - + * + * @author Craig R. McClanahan + * @author Remy Maucherat + * @version $Revision: 377994 $ $Date: 2006-02-15 04:37:28 -0800 (Wed, 15 Feb 2006) $ + */ + +public class ServletContextImpl implements ServletContext { + + /** + * Empty collection to serve as the basis for empty enumerations. + */ + private transient static final ArrayList empty = new ArrayList(); + + transient Logger log; + + /** + * Base path - the directory root of the webapp + */ + protected String basePath = null; + + protected String contextPath; + + // All config from web.xml + protected ServletContextConfig contextConfig = new ServletContextConfig(); + + MimeMap contentTypes = new MimeMap(); + + /** + * The context attributes for this context. + */ + protected transient Map attributes = new HashMap(); + + /** + * List of read only attributes for this context. + * In catalina - used to protect workdir att. We trust the app, so no need + * for extra complexity. + */ + //protected transient HashMap readOnlyAttributes = new HashMap(); + + protected transient ArrayList lifecycleListeners = new ArrayList(); + + protected UserSessionManager manager; + + HashMap filters = new HashMap(); + + HashMap servlets = new HashMap(); + + ArrayList securityRoles = new ArrayList(); + + /** Mapper for filters. + */ + protected WebappFilterMapper webappFilterMapper; + + /** Internal mapper for request dispatcher, must have all + * context mappings. + */ + protected WebappServletMapper mapper; + + transient Locale2Charset charsetMapper = new Locale2Charset(); + + transient TomcatLite facade; + + ObjectManager om; + + private String hostname; + + // ------------------------------------------------- ServletContext Methods + public ServletContextImpl() { + } + + public void setTomcat(TomcatLite facade) { + this.facade = facade; + } + + /** + * Registry/framework interface associated with the context. + * Also available as a context attribute. + * @return + */ + public ObjectManager getObjectManager() { + if (om == null) { + om = facade.getObjectManager(); + } + return om; + } + + public void setObjectManager(ObjectManager om) { + this.om = om; + } + + public Locale2Charset getCharsetMapper() { + return charsetMapper; + } + + /** + * Set the context path, starting with "/" - "/" for ROOT + * @param path + */ + public void setContextPath(String path) { + this.contextPath = path; + log = Logger.getLogger("webapp" + path.replace('/', '.')); + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getHostname() { + return hostname; + } + + /** The directory where this app is based. May be null. + * + * @param basePath + */ + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + public ServletContextConfig getContextConfig() { + return contextConfig; + } + + /** The directory where this app is based. + * + * @param basePath + */ + public String getBasePath() { + return basePath; + } + + public String getEncodedPath() { + return null; + } + + + public boolean getCookies() { + return false; + } + + + public ServletContext getServletContext() { + return this; + } + + public List getListeners() { + return lifecycleListeners; + } + + public void addListener(EventListener listener) { + lifecycleListeners.add(listener); + } + + public void removeListener(EventListener listener) { + lifecycleListeners.remove(listener); + } + + + + public Logger getLogger() { + return log; + } + + public long getUnloadDelay() { + return 0; + } + + public ServletConfigImpl getServletConfig(String jsp_servlet_name) { + return (ServletConfigImpl)servlets.get(jsp_servlet_name); + } + + public Map getServletConfigs() { + return servlets; + } + + /** + * Add a servlet to the context. + * Called from processWebAppData() + * + * @param servletConfig + */ + public void addServletConfig(ServletConfigImpl servletConfig) { + servlets.put(servletConfig.getServletName(), servletConfig); + } + + public boolean getPrivileged() { + return false; + } + + + public Map getFilters() { + return filters; + } + + + protected boolean getCrossContext() { + return true; + } + + public void addMimeType(String ext, String type) { + contentTypes.addContentType(ext, type); + } + + public WebappServletMapper getMapper() { + if (mapper == null) { + Object customMapper = getObjectManager().get(WebappServletMapper.class); + if (customMapper == null) { + mapper = new WebappServletMapper(); + } else { + mapper = (WebappServletMapper) customMapper; + } + mapper.setServletContext(this); + } + + return mapper; + } + + public WebappFilterMapper getFilterMapper() { + if (webappFilterMapper == null) { + Object customMapper = getObjectManager().get(WebappFilterMapper.class); + if (customMapper == null) { + webappFilterMapper = new WebappFilterMapper(); + } else { + webappFilterMapper = (WebappFilterMapper) customMapper; + } + webappFilterMapper.setServletContext(this); + } + + return webappFilterMapper ; + } + + public FilterConfigImpl getFilter(String name) { + return (FilterConfigImpl)filters.get(name); + } + + /** + * Return the value of the specified context attribute, if any; + * otherwise return null. + * + * @param name Name of the context attribute to return + */ + public Object getAttribute(String name) { + if ("ObjectManager".equals(name)) { + return om; + } + if ("context-listeners".equals(name)) { + return lifecycleListeners; + } + return (attributes.get(name)); + } + + /** + * Return an enumeration of the names of the context attributes + * associated with this context. + */ + public Enumeration getAttributeNames() { + return new Enumerator(attributes.keySet(), true); + } + + + public void addSecurityRole(String role) { + securityRoles.add(role); + } + + public List getSecurityRoles() { + return securityRoles; + } + + /** + * Return a ServletContext object that corresponds to a + * specified URI on the server. This method allows servlets to gain + * access to the context for various parts of the server, and as needed + * obtain RequestDispatcher objects or resources from the + * context. The given path must be absolute (beginning with a "/"), + * and is interpreted based on our virtual host's document root. + * + * @param uri Absolute URI of a resource on the server + */ + public ServletContext getContext(String uri) { + // TODO: support real uri ( http://host/path ) + // Validate the format of the specified argument + if ((uri == null) || (!uri.startsWith("/"))) + return (null); + + ServletContextImpl child = null; + try { + child = facade.getContext(this, uri); + } catch (IOException e) { + } catch (ServletException e) { + } + + if (child == null) + return (null); + + if (this.getCrossContext()) { + // If crossContext is enabled, can always return the context + return child.getServletContext(); + } else if (child == this) { + // Can still return the current context + return this.getServletContext(); + } else { + // Nothing to return + return (null); + } + } + + + /** + * Return the main path associated with this context. + */ + public String getContextPath() { + return contextPath; + } + + + /** + * Return the value of the specified initialization parameter, or + * null if this parameter does not exist. + * + * @param name Name of the initialization parameter to retrieve + */ + public String getInitParameter(final String name) { + return ((String) contextConfig.contextParam.get(name)); + } + + + /** + * Return the names of the context's initialization parameters, or an + * empty enumeration if the context has no initialization parameters. + */ + public Enumeration getInitParameterNames() { + return (new Enumerator(contextConfig.contextParam.keySet())); + } + + public void setContextParams(Map newParams) { + contextConfig.contextParam = (HashMap) newParams; + } + + /** + * Return the major version of the Java Servlet API that we implement. + */ + public int getMajorVersion() { + return 2; + } + + + /** + * Return the minor version of the Java Servlet API that we implement. + */ + public int getMinorVersion() { + return 4; + } + + + /** + * Return the MIME type of the specified file, or null if + * the MIME type cannot be determined. + * + * @param file Filename for which to identify a MIME type + */ + public String getMimeType(String file) { + return contentTypes.getMimeType(file); + } + + /** + * Return the real path for a given virtual path, if possible; otherwise + * return null. + * + * @param path The path to the desired resource + */ + public String getRealPath(String path) { + if (path == null) { + return null; + } + + File file = new File(basePath, path); + return (file.getAbsolutePath()); + } + + /** + * Return a RequestDispatcher object that acts as a + * wrapper for the named servlet. + * + * @param name Name of the servlet for which a dispatcher is requested + */ + public RequestDispatcher getNamedDispatcher(String name) { + if (name == null) return null; + ServletConfigImpl wrapper = + (ServletConfigImpl) this.getServletConfig(name); + if (wrapper == null) return null; + + return new RequestDispatcherImpl(wrapper, name); + } + + + /** + * Return a RequestDispatcher instance that acts as a + * wrapper for the resource at the given path. The path must begin + * with a "/" and is interpreted as relative to the current context root. + * + * @param path The path to the desired resource. + */ + public RequestDispatcher getRequestDispatcher(String path) { + if (path == null) return null; + + if (!path.startsWith("/")) + throw new IllegalArgumentException(path); + + path = UrlUtils.normalize(path); + if (path == null) return (null); + + + return new RequestDispatcherImpl(this, path); + } + + public RequestDispatcher getRequestDispatcher(String path, + int type, + String dispatcherPath) { + RequestDispatcher dispatcher = getRequestDispatcher(path); + //((RequestDispatcherImpl)dispatcher); + return dispatcher; + } + + ThreadLocal requestDispatcherStack = new ThreadLocal(); + + protected ClassLoader classLoader; + +// protected RequestDispatcherImpl getRequestDispatcher() { +// ArrayList/**/ list = +// (ArrayList)requestDispatcherStack.get(); +// if (list == null) { +// list = new ArrayList(); +// requestDispatcherStack.set(list); +// } +// +// +// return null; +// } + + public void resetDispatcherStack() { + + } + + /** + * Return the URL to the resource that is mapped to a specified path. + * The path must begin with a "/" and is interpreted as relative to the + * current context root. + * + * @param path The path to the desired resource + * + * @exception MalformedURLException if the path is not given + * in the correct form + */ + public URL getResource(String path) + throws MalformedURLException { + + if (path == null || !path.startsWith("/")) { + throw new MalformedURLException("getResource() " + path); + } + + path = UrlUtils.normalize(path); + if (path == null) + return (null); + + String libPath = "/WEB-INF/lib/"; + if ((path.startsWith(libPath)) && (path.endsWith(".jar"))) { + File jarFile = null; + jarFile = new File(basePath, path); + if (jarFile.exists()) { + return jarFile.toURL(); + } else { + return null; + } + } else { + File resFile = new File(basePath + path); + if (resFile.exists()) { + return resFile.toURL(); + } + } + + return (null); + + } + + /** + * Return the requested resource as an InputStream. The + * path must be specified according to the rules described under + * getResource. If no such resource can be identified, + * return null. + * + * @param path The path to the desired resource. + */ + public InputStream getResourceAsStream(String path) { + + path = UrlUtils.normalize(path); + if (path == null) + return (null); + + File resFile = new File(basePath + path); + if (!resFile.exists()) + return null; + + try { + return new FileInputStream(resFile); + } catch (FileNotFoundException e) { + return null; + } + + } + + + /** + * Return a Set containing the resource paths of resources member of the + * specified collection. Each path will be a String starting with + * a "/" character. The returned set is immutable. + * + * @param path Collection path + */ + public Set getResourcePaths(String path) { + + // Validate the path argument + if (path == null) { + return null; + } + if (!path.startsWith("/")) { + throw new IllegalArgumentException("getResourcePaths() " + path); + } + + path = UrlUtils.normalize(path); + if (path == null) + return (null); + + File f = new File(basePath + path); + File[] files = f.listFiles(); + if (files == null) return null; + if (!path.endsWith("/")) { + path = path + "/"; + } + + HashSet result = new HashSet(); + for (int i=0; i < files.length; i++) { + if (files[i].isDirectory() ) { + result.add(path + files[i].getName() + "/"); + } else { + result.add(path + files[i].getName()); + } + } + return result; + } + + + + /** + * Return the name and version of the servlet container. + */ + public String getServerInfo() { + return "Apache Tomcat Lite"; + } + + /** + * @deprecated As of Java Servlet API 2.1, with no direct replacement. + */ + public Servlet getServlet(String name) { + return (null); + } + + + /** + * Return the display name of this web application. + */ + public String getServletContextName() { + return contextConfig.displayName; + } + + + /** + * @deprecated As of Java Servlet API 2.1, with no direct replacement. + */ + public Enumeration getServletNames() { + return (new Enumerator(empty)); + } + + + /** + * @deprecated As of Java Servlet API 2.1, with no direct replacement. + */ + public Enumeration getServlets() { + return (new Enumerator(empty)); + } + + + /** + * Writes the specified message to a servlet log file. + * + * @param message Message to be written + */ + public void log(String message) { + this.getLogger().info(message); + } + + + /** + * Writes the specified exception and message to a servlet log file. + * + * @param exception Exception to be reported + * @param message Message to be written + * + * @deprecated As of Java Servlet API 2.1, use + * log(String, Throwable) instead + */ + public void log(Exception exception, String message) { + this.getLogger().log(Level.INFO, message, exception); + } + + + /** + * Writes the specified message and exception to a servlet log file. + * + * @param message Message to be written + * @param throwable Exception to be reported + */ + public void log(String message, Throwable throwable) { + this.getLogger().log(Level.INFO, message, throwable); + } + + /** + * Remove the context attribute with the specified name, if any. + * + * @param name Name of the context attribute to be removed + */ + public void removeAttribute(String name) { + + Object value = null; + boolean found = false; + + // Remove the specified attribute + // Check for read only attribute + found = attributes.containsKey(name); + if (found) { + value = attributes.get(name); + attributes.remove(name); + } else { + return; + } + + // Notify interested application event listeners + List listeners = this.getListeners(); + if (listeners.size() == 0) + return; + ServletContextAttributeEvent event = null; + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof ServletContextAttributeListener)) + continue; + ServletContextAttributeListener listener = + (ServletContextAttributeListener) listeners.get(i); + try { + if (event == null) { + event = new ServletContextAttributeEvent(this.getServletContext(), + name, value); + + } + listener.attributeRemoved(event); + } catch (Throwable t) { + // FIXME - should we do anything besides log these? + log("ServletContextAttributeListener", t); + } + } + } + + + /** + * Bind the specified value with the specified context attribute name, + * replacing any existing value for that name. + * + * @param name Attribute name to be bound + * @param value New attribute value to be bound + */ + public void setAttribute(String name, Object value) { + // Name cannot be null + if (name == null) + throw new IllegalArgumentException + ("name == null"); + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + Object oldValue = null; + boolean replaced = false; + + // Add or replace the specified attribute + synchronized (attributes) { + // Check for read only attribute + oldValue = attributes.get(name); + if (oldValue != null) + replaced = true; + attributes.put(name, value); + } + + // Notify interested application event listeners + List listeners = this.getListeners(); + if (listeners.size() == 0) + return; + ServletContextAttributeEvent event = null; + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof ServletContextAttributeListener)) + continue; + ServletContextAttributeListener listener = + (ServletContextAttributeListener) listeners.get(i); + try { + if (event == null) { + if (replaced) + event = + new ServletContextAttributeEvent(this.getServletContext(), + name, oldValue); + else + event = + new ServletContextAttributeEvent(this.getServletContext(), + name, value); + + } + if (replaced) { + listener.attributeReplaced(event); + } else { + listener.attributeAdded(event); + } + } catch (Throwable t) { + // FIXME - should we do anything besides log these? + log("ServletContextAttributeListener error", t); + } + } + + } + + /** + * Clear all application-created attributes. + */ + void clearAttributes() { + // Create list of attributes to be removed + ArrayList list = new ArrayList(); + synchronized (attributes) { + Iterator iter = attributes.keySet().iterator(); + while (iter.hasNext()) { + list.add(iter.next()); + } + } + + // Remove application originated attributes + // (read only attributes will be left in place) + Iterator keys = list.iterator(); + while (keys.hasNext()) { + String key = (String) keys.next(); + removeAttribute(key); + } + } + + public void addFilter(String filterName, String filterClass, + Map params) { + FilterConfigImpl fc = new FilterConfigImpl(this); + fc.setData(filterName, filterClass, params); + filters.put(filterName, fc); + } + + public void initFilters() throws ServletException { + Iterator fI = getFilters().values().iterator(); + while (fI.hasNext()) { + FilterConfigImpl fc = (FilterConfigImpl)fI.next(); + try { + fc.getFilter(); // will triger init() + } catch (Throwable e) { + log.log(Level.WARNING, getContextPath() + " Filter.init() " + + fc.getFilterName(), e); + } + + } + } + public void initServlets() throws ServletException { + Iterator fI = getServletConfigs().values().iterator(); + Map/*>*/ onStartup = + new TreeMap/*>*/(); + while (fI.hasNext()) { + ServletConfigImpl fc = (ServletConfigImpl)fI.next(); + if (fc.getLoadOnStartup() > 0 ) { + Integer i = new Integer(fc.getLoadOnStartup()); + List/**/ old = (List)onStartup.get(i); + if (old == null) { + old = new ArrayList/**/(); + onStartup.put(i, old); + } + old.add(fc); + } + } + Iterator keys = onStartup.keySet().iterator(); + while (keys.hasNext()) { + Integer key = (Integer)keys.next(); + List/**/ servlets = (List)onStartup.get(key); + Iterator servletsI = servlets.iterator(); + while (servletsI.hasNext()) { + ServletConfigImpl fc = (ServletConfigImpl) servletsI.next(); + try { + fc.loadServlet(); + } catch (Throwable e) { + log.log(Level.WARNING, "Error initializing " + fc.getServletName(), e); + } + } + } + } + + public void initListeners() throws ServletException { + Iterator fI = contextConfig.listenerClass.iterator(); + while (fI.hasNext()) { + String listenerClass = (String)fI.next(); + try { + Object l = + getClassLoader().loadClass(listenerClass).newInstance(); + lifecycleListeners.add((EventListener) l); + } catch (Throwable e) { + log.log(Level.WARNING, "Error initializing listener " + listenerClass, e); + } + } + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public void addMapping(String path, String name) { + ServletConfigImpl wrapper = getServletConfig(name); + addMapping(path, wrapper); + } + + public void addMapping(String path, ServletConfig wrapper) { + getMapper().addWrapper(getMapper().contextMapElement, path, wrapper); + } + + public void setWelcomeFiles(String[] name) { + getMapper().contextMapElement.welcomeResources = name; + } + + public String[] getWelcomeFiles() { + return getMapper().contextMapElement.welcomeResources; + } + + public void setSessionTimeout(int to) { + getManager().setSessionTimeout(to); + } + + /** + * Initialize the context from the parsed config. + * + * Note that WebAppData is serializable. + */ + public void processWebAppData(ServletContextConfig d) throws ServletException { + this.contextConfig = d; + + Iterator i1 = d.mimeMapping.entrySet().iterator(); + while (i1.hasNext()) { + Entry k = (Entry)i1.next(); + addMimeType((String)k.getKey(), (String)k.getValue()); + } + + String[] wFiles = (String[])d.welcomeFileList.toArray(new String[0]); + if (wFiles.length == 0) { + wFiles = new String[] {"index.html" }; + } + if (basePath != null) { + getMapper().contextMapElement.resources = new File(getBasePath()); + } + setWelcomeFiles(wFiles); + + Iterator i2 = d.filters.values().iterator(); + while (i2.hasNext()) { + FilterData fd = (FilterData)i2.next(); + addFilter(fd.filterName, fd.filterClass, fd.initParams); + } + + Iterator i3 = d.servlets.values().iterator(); + while (i3.hasNext()) { + ServletData sd = (ServletData) i3.next(); + // jsp-file + if (sd.servletClass == null) { + if (sd.jspFile == null) { + log.log(Level.WARNING, "Missing servlet class for " + sd.servletName); + continue; + } + } + + ServletConfigImpl sw = + new ServletConfigImpl(this, sd.servletName, sd.servletClass); + sw.setConfig(sd.initParams); + sw.setJspFile(sd.jspFile); + sw.setLoadOnStartup(sd.loadOnStartup); + //sw.setRunAs(sd.runAs); + sw.setSecurityRoleRef(sd.securityRoleRef); + + addServletConfig(sw); + } + + Iterator i4 = d.servletMapping.entrySet().iterator(); + while (i4.hasNext()) { + Entry/**/ k = (Entry) i4.next(); + addMapping((String) k.getKey(), (String) k.getValue()); + } + + Iterator i5 = d.filterMappings.iterator(); + while (i5.hasNext()) { + FilterMappingData k = (FilterMappingData) i5.next(); + String[] disp = new String[k.dispatcher.size()]; + if (k.urlPattern != null) { + addFilterMapping(k.urlPattern, + k.filterName, + (String[])k.dispatcher.toArray(disp)); + } + if (k.servletName != null) { + addFilterServletMapping(k.servletName, + k.filterName, + (String[])k.dispatcher.toArray(disp)); + } + } + + for (String n: d.localeEncodingMapping.keySet()) { + getCharsetMapper().addCharsetMapping(n, + d.localeEncodingMapping.get(n)); + } + } + + public void addServlet(String servletName, String servletClass, + String jspFile, Map params) { + ServletConfigImpl sc = new ServletConfigImpl(this, servletName, + servletClass); + sc.setJspFile(jspFile); + sc.setConfig(params); + addServletConfig(sc); + } + + public void addServlet(String servletName, Servlet servlet) { + ServletConfigImpl sc = new ServletConfigImpl(this, servletName, null); + sc.setServlet(servlet); + addServletConfig(sc); + } + + public void addServletSec(String serlvetName, String runAs, Map roles) { + // TODO + } + + + + public void addFilterMapping(String path, String filterName, + String[] dispatcher) { + getFilterMapper().addMapping(filterName, + path, null, dispatcher); + + } + + public void addFilterServletMapping(String servlet, + String filterName, + String[] dispatcher) { + getFilterMapper().addMapping(filterName, + null, servlet, + dispatcher); + } + + boolean initDone = false; + + /** + * Called from TomcatLite.init(), required before start. + * + * Will initialize defaults and load web.xml unless webAppData is + * already set and recent. No other processing is done except reading + * the config - you can add or alter it before start() is called. + * + * @throws ServletException + */ + public void init() throws ServletException { + if (initDone) { + return; + } + initDone = true; + // Load global init params from the facade + initEngineDefaults(); + + initTempDir(); + + + // Merge in web.xml - or other config source ( programmatic, etc ) + ContextPreinitListener cfg = + (ContextPreinitListener) getObjectManager().get( + ContextPreinitListener.class); + if (cfg != null) { + cfg.preInit(this); + } + + processWebAppData(contextConfig); + + // if not defined yet: + addDefaultServlets(); + } + + + protected void initTempDir() throws ServletException { + // We need a base path - at least for temp files, req. by spec + if (basePath == null) { + basePath = ("/".equals(contextPath)) ? + facade.getWork().getAbsolutePath() + "/ROOT" : + facade.getWork().getAbsolutePath() + contextPath; + } + + File f = new File(basePath + "/WEB-INF/tmp"); + f.mkdirs(); + setAttribute("javax.servlet.context.tempdir", f); + } + + /** + * Static file handler ( default ) + * *.jsp support + * + */ + protected void addDefaultServlets() throws ServletException { + if (servlets.get("default") == null) { + ServletConfigImpl fileS = new ServletConfigImpl(this, + "default", null); + addServletConfig(fileS); + addMapping("/", fileS); + } + + // *.jsp support + if (servlets.get("jspwildcard") == null) { + ServletConfigImpl fileS = new ServletConfigImpl(this, + "jspwildcard", null); + addServletConfig(fileS); + addMapping("*.jsp", fileS); + } + } + + protected void initEngineDefaults() throws ServletException { + + // TODO: make this customizable, avoid loading it on startup + // Set the class name as default in the addon support + for (String sname: facade.ctxDefaultInitParam.keySet()) { + String path = facade.ctxDefaultInitParam.get(sname); + contextConfig.contextParam.put(sname, path); + } + + for (String sname: facade.preloadServlets.keySet()) { + String sclass = facade.preloadServlets.get(sname); + ServletConfigImpl fileS = new ServletConfigImpl(this, sname, sclass); + addServletConfig(fileS); + } + + for (String sname: facade.preloadMappings.keySet()) { + String path = facade.preloadMappings.get(sname); + ServletConfigImpl servletConfig = getServletConfig(sname); + addMapping(path, servletConfig); + } + } + + + public ArrayList getClasspath(File directory, File classesDir) { + ArrayList res = new ArrayList(); + if (classesDir.isDirectory() && classesDir.exists() && + classesDir.canRead()) { + try { + URL url = classesDir.toURL(); + res.add(url); + } catch (MalformedURLException e) { + } + } + if (!directory.isDirectory() || !directory.exists() + || !directory.canRead()) { + return res; + } + + File[] jars = directory.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".jar"); + } + }); + + for (int j = 0; j < jars.length; j++) { + try { + URL url = jars[j].toURL(); + res.add(url); + } catch (MalformedURLException e) { + } + } + return res; + } + + + public void start() throws ServletException { + String base = getBasePath(); + + ArrayList urls = getClasspath(new File(base + "/WEB-INF/lib"), + new File(base + "/WEB-INF/classes")); + URL[] urlsA = new URL[urls.size()]; + urls.toArray(urlsA); + + URLClassLoader parentLoader = + getEngine().getContextParentLoader(); + + // create a class loader. + // TODO: reimplement special 'deploy' dirs + + /* + Repository ctxRepo = new Repository(); + ctxRepo.setParentClassLoader(parentLoader); + ctxRepo.addURL(urlsA); + repository = ctxRepo; + */ + + classLoader = new URLClassLoader(urlsA, parentLoader); + + // JMX should know about us ( TODO: is it too early ? ) + facade.notifyAdd(this); + + initListeners(); + + List listeners = this.getListeners(); + ServletContextEvent event = null; + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof ServletContextListener)) + continue; + ServletContextListener listener = + (ServletContextListener) listeners.get(i); + if (event == null) { + event = new ServletContextEvent(this); + } + try { + listener.contextInitialized(event); + } catch (Throwable t) { + log.log(Level.WARNING, "Context.init() contextInitialized() error:", t); + } + } + + + initFilters(); + initServlets(); + } + + public UserSessionManager getManager() { + if (manager == null) { + manager = (UserSessionManager) getObjectManager().get( + UserSessionManager.class); + manager.setContext(this); + if (contextConfig.sessionTimeout > 0 ) { + manager.setSessionTimeout(contextConfig.sessionTimeout); + } + } + return manager; + } + + + // TODO: configurable ? init-params + public String getSessionCookieName() { + return "JSESSIONID"; + } + + + + public void destroy() throws ServletException { + // destroy filters + Iterator fI = filters.values().iterator(); + while(fI.hasNext()) { + FilterConfigImpl fc = (FilterConfigImpl) fI.next(); + try { + fc.getFilter().destroy(); + } catch (Exception e) { + e.printStackTrace(); + } + } + // destroy servlets + fI = servlets.values().iterator(); + while(fI.hasNext()) { + ServletConfigImpl fc = (ServletConfigImpl) fI.next(); + try { + fc.unload(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public TomcatLite getEngine() { + return facade; + } + + public String findStatusPage(int status) { + if (contextConfig.errorPageCode.size() == 0) { + return null; + } + if (status == 200) { + return null; + } + + return (String) contextConfig.errorPageCode.get(Integer.toString(status)); + } + + public void handleStatusPage(ServletRequestImpl req, + ServletResponseImpl res, + int status, + String statusPage) { + String message = RequestUtil.filter(res.getMessage()); + if (message == null) + message = ""; + setErrorAttributes(req, status, message); + dispatchError(req, res, statusPage); + } + + protected void setErrorAttributes(ServletRequestImpl req, + int status, + String message) { + req.setAttribute("javax.servlet.error.status_code", + new Integer(status)); + if (req.getWrapper() != null) { + req.setAttribute("javax.servlet.error.servlet_name", + req.getWrapper().servletName); + } + req.setAttribute("javax.servlet.error.request_uri", + req.getRequestURI()); + req.setAttribute("javax.servlet.error.message", + message); + + } + + public void handleError(ServletRequestImpl req, + ServletResponseImpl res, + Throwable t) { + Throwable realError = t; + if (realError instanceof ServletException) { + realError = ((ServletException) realError).getRootCause(); + if (realError == null) { + realError = t; + } + } + //if (realError instanceof ClientAbortException ) { + + String errorPage = findErrorPage(t); + if ((errorPage == null) && (realError != t)) { + errorPage = findErrorPage(realError); + } + + if (errorPage != null) { + setErrorAttributes(req, 500, t.getMessage()); + req.setAttribute("javax.servlet.error.exception", realError); + req.setAttribute("javax.servlet.error.exception_type", + realError.getClass()); + dispatchError(req, res, errorPage); + } else { + log("Unhandled error", t); + if (t instanceof ServletException && + ((ServletException)t).getRootCause() != null) { + log("RootCause:", ((ServletException)t).getRootCause()); + } + if (res.getStatus() < 500) { + res.setStatus(500); + } + } + } + + protected void dispatchError(ServletRequestImpl req, + ServletResponseImpl res, + String errorPage) { + RequestDispatcher rd = + getRequestDispatcher(errorPage); + try { + // will clean up the buffer + rd.forward(req, res); + return; // handled + } catch (ServletException e) { + // TODO + } catch (IOException e) { + // TODO + } + } + + protected String findErrorPage(Throwable exception) { + if (contextConfig.errorPageException.size() == 0) { + return null; + } + if (exception == null) + return (null); + Class clazz = exception.getClass(); + String name = clazz.getName(); + while (!Object.class.equals(clazz)) { + String page = (String)contextConfig.errorPageException.get(name); + if (page != null) + return (page); + clazz = clazz.getSuperclass(); + if (clazz == null) + break; + name = clazz.getName(); + } + return (null); + + } + + @Override + public void addFilter(String filterName, String description, String className, + Map initParameters, + boolean isAsyncSupported) { + } + + @Override + public void addFilterMappingForServletNames( + String filterName, + EnumSet dispatcherTypes, + boolean isMatchAfter, + String... servletNames) { + } + + @Override + public void addFilterMappingForUrlPatterns( + String filterName, + EnumSet dispatcherTypes, + boolean isMatchAfter, + String... urlPatterns) { + } + + @Override + public void addServletMapping(String servletName, String[] urlPatterns) { + } + + @Override + public EnumSet getDefaultSessionTrackingModes() { + return null; + } + + @Override + public EnumSet getEffectiveSessionTrackingModes() { + return null; + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return null; + } + + @Override + public void setSessionCookieConfig(SessionCookieConfig sessionCookieConfig) { + } + + @Override + public void setSessionTrackingModes( + EnumSet sessionTrackingModes) { + } + +} + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletInputStreamImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletInputStreamImpl.java new file mode 100644 index 000000000..5f5d914bc --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletInputStreamImpl.java @@ -0,0 +1,107 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + +import java.io.IOException; +import java.io.InputStream; + +import javax.servlet.ServletInputStream; + + +/** + * Wrapper around MessageReader + * + * @author Remy Maucherat + * @author Jean-Francois Arcand + * @author Costin Manolache + */ +public class ServletInputStreamImpl extends ServletInputStream { + + + // ----------------------------------------------------- Instance Variables + + + protected InputStream ib; + + + // ----------------------------------------------------------- Constructors + + + public ServletInputStreamImpl(InputStream ib) { + this.ib = ib; + } + + // --------------------------------------------- ServletInputStream Methods + + public long skip(long n) + throws IOException { + return ib.skip(n); + } + + public void mark(int readAheadLimit) + { + //try { + ib.mark(readAheadLimit); +// } catch (IOException e) { +// e.printStackTrace(); +// } + } + + + public void reset() + throws IOException { + ib.reset(); + } + + + + public int read() + throws IOException { + return ib.read(); + } + + public int available() throws IOException { + return ib.available(); + } + + public int read(final byte[] b) throws IOException { + return ib.read(b, 0, b.length); + } + + + public int read(final byte[] b, final int off, final int len) + throws IOException { + + return ib.read(b, off, len); + } + + + public int readLine(byte[] b, int off, int len) throws IOException { + return super.readLine(b, off, len); + } + + /** + * Close the stream + * Since we re-cycle, we can't allow the call to super.close() + * which would permantely disable us. + */ + public void close() throws IOException { + ib.close(); + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletOutputStreamImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletOutputStreamImpl.java new file mode 100644 index 000000000..2cd699cf8 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletOutputStreamImpl.java @@ -0,0 +1,119 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + +import java.io.IOException; + +import javax.servlet.ServletOutputStream; + +import org.apache.tomcat.lite.coyote.MessageWriter; + +/** + * Coyote implementation of the servlet output stream. + * + * @author Costin Manolache + * @author Remy Maucherat + */ +public class ServletOutputStreamImpl + extends ServletOutputStream { + + + // ----------------------------------------------------- Instance Variables + + + protected MessageWriter ob; + + + // ----------------------------------------------------------- Constructors + + + public ServletOutputStreamImpl(MessageWriter ob) { + this.ob = ob; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Prevent cloning the facade. + */ + protected Object clone() + throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Clear facade. + */ + void clear() { + ob = null; + } + + + // --------------------------------------------------- OutputStream Methods + + + public void write(int i) + throws IOException { + ob.writeByte(i); + } + + + public void write(byte[] b) + throws IOException { + write(b, 0, b.length); + } + + + public void write(byte[] b, int off, int len) + throws IOException { + ob.write(b, off, len); + } + + + /** + * Will send the buffer to the client. + */ + public void flush() + throws IOException { + ob.flush(); + } + + + public void close() + throws IOException { + ob.close(); + } + + + // -------------------------------------------- ServletOutputStream Methods + + + public void print(String s) + throws IOException { + ob.write(s); + } + + +} + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletReaderImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletReaderImpl.java new file mode 100644 index 000000000..15119e33c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletReaderImpl.java @@ -0,0 +1,181 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; + + + +/** + * Coyote implementation of the buffred reader. + * + * @author Remy Maucherat + */ +public class ServletReaderImpl + extends BufferedReader { + + + // -------------------------------------------------------------- Constants + + + private static final char[] LINE_SEP = { '\r', '\n' }; + private static final int MAX_LINE_LENGTH = 4096; + + + // ----------------------------------------------------- Instance Variables + + + protected Reader ib; + + + protected char[] lineBuffer = null; + + + // ----------------------------------------------------------- Constructors + + + public ServletReaderImpl(Reader ib) { + super(ib, 1); + this.ib = ib; + } + + public void close() + throws IOException { + ib.close(); + } + + + public int read() + throws IOException { + return ib.read(); + } + + + public int read(char[] cbuf) + throws IOException { + return ib.read(cbuf, 0, cbuf.length); + } + + + public int read(char[] cbuf, int off, int len) + throws IOException { + return ib.read(cbuf, off, len); + } + + + public long skip(long n) + throws IOException { + return ib.skip(n); + } + + + public boolean ready() + throws IOException { + return ib.ready(); + } + + + public boolean markSupported() { + return true; + } + + + public void mark(int readAheadLimit) + throws IOException { + ib.mark(readAheadLimit); + } + + + public void reset() + throws IOException { + ib.reset(); + } + + + // TODO: move the readLine functionality to base coyote IO + public String readLine() + throws IOException { + + if (lineBuffer == null) { + lineBuffer = new char[MAX_LINE_LENGTH]; + } + + String result = null; + + int pos = 0; + int end = -1; + int skip = -1; + StringBuffer aggregator = null; + while (end < 0) { + mark(MAX_LINE_LENGTH); + while ((pos < MAX_LINE_LENGTH) && (end < 0)) { + int nRead = read(lineBuffer, pos, MAX_LINE_LENGTH - pos); + if (nRead < 0) { + if (pos == 0) { + return null; + } + end = pos; + skip = pos; + } + for (int i = pos; (i < (pos + nRead)) && (end < 0); i++) { + if (lineBuffer[i] == LINE_SEP[0]) { + end = i; + skip = i + 1; + char nextchar; + if (i == (pos + nRead - 1)) { + nextchar = (char) read(); + } else { + nextchar = lineBuffer[i+1]; + } + if (nextchar == LINE_SEP[1]) { + skip++; + } + } else if (lineBuffer[i] == LINE_SEP[1]) { + end = i; + skip = i + 1; + } + } + if (nRead > 0) { + pos += nRead; + } + } + if (end < 0) { + if (aggregator == null) { + aggregator = new StringBuffer(); + } + aggregator.append(lineBuffer); + pos = 0; + } else { + reset(); + skip(skip); + } + } + + if (aggregator == null) { + result = new String(lineBuffer, 0, end); + } else { + aggregator.append(lineBuffer, 0, end); + result = aggregator.toString(); + } + + return result; + + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletRequestImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletRequestImpl.java new file mode 100644 index 000000000..810b89d15 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletRequestImpl.java @@ -0,0 +1,2510 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.logging.Level; + +import javax.security.auth.Subject; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.apache.coyote.ActionCode; +import org.apache.coyote.Request; +import org.apache.tomcat.addons.UserSessionManager; +import org.apache.tomcat.lite.coyote.MessageReader; +import org.apache.tomcat.servlets.util.Enumerator; +import org.apache.tomcat.servlets.util.LocaleParser; +import org.apache.tomcat.servlets.util.RequestUtil; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.UriNormalizer; +import org.apache.tomcat.util.http.Cookies; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.Parameters; +import org.apache.tomcat.util.http.ServerCookie; +import org.apache.tomcat.util.http.mapper.MappingData; + + +/** + * Wrapper object for the Coyote request. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + * @version $Revision: 325874 $ $Date: 2005-10-17 03:39:15 -0700 (Mon, 17 Oct 2005) $ + */ + +public class ServletRequestImpl implements HttpServletRequest { + + /** + * The request attribute under which we store the array of X509Certificate + * objects representing the certificate chain presented by our client, + * if any. + */ + public static final String CERTIFICATES_ATTR = + "javax.servlet.request.X509Certificate"; + + /** + * The request attribute under which we store the name of the cipher suite + * being used on an SSL connection (as an object of type + * java.lang.String). + */ + public static final String CIPHER_SUITE_ATTR = + "javax.servlet.request.cipher_suite"; + + /** + * Request dispatcher state. + */ + public static final String DISPATCHER_TYPE_ATTR = + "org.apache.catalina.core.DISPATCHER_TYPE"; + + /** + * Request dispatcher path. + */ + public static final String DISPATCHER_REQUEST_PATH_ATTR = + "org.apache.catalina.core.DISPATCHER_REQUEST_PATH"; + + /** + * The servlet context attribute under which we store the class path + * for our application class loader (as an object of type String), + * delimited with the appropriate path delimiter for this platform. + */ + public static final String CLASS_PATH_ATTR = + "org.apache.catalina.jsp_classpath"; + + + /** + * The request attribute under which we forward a Java exception + * (as an object of type Throwable) to an error page. + */ + public static final String EXCEPTION_ATTR = + "javax.servlet.error.exception"; + + + /** + * The request attribute under which we forward the request URI + * (as an object of type String) of the page on which an error occurred. + */ + public static final String EXCEPTION_PAGE_ATTR = + "javax.servlet.error.request_uri"; + + + /** + * The request attribute under which we forward a Java exception type + * (as an object of type Class) to an error page. + */ + public static final String EXCEPTION_TYPE_ATTR = + "javax.servlet.error.exception_type"; + + + /** + * The request attribute under which we forward an HTTP status message + * (as an object of type STring) to an error page. + */ + public static final String ERROR_MESSAGE_ATTR = + "javax.servlet.error.message"; + + + /** + * The request attribute under which we expose the value of the + * <jsp-file> value associated with this servlet, + * if any. + */ + public static final String JSP_FILE_ATTR = + "org.apache.catalina.jsp_file"; + + + /** + * The request attribute under which we store the key size being used for + * this SSL connection (as an object of type java.lang.Integer). + */ + public static final String KEY_SIZE_ATTR = + "javax.servlet.request.key_size"; + + /** + * The request attribute under which we store the session id being used + * for this SSL connection (as an object of type java.lang.String). + */ + public static final String SSL_SESSION_ID_ATTR = + "javax.servlet.request.ssl_session"; + + /** + * The request attribute under which we forward a servlet name to + * an error page. + */ + public static final String SERVLET_NAME_ATTR = + "javax.servlet.error.servlet_name"; + + + /** + * The name of the cookie used to pass the session identifier back + * and forth with the client. + */ + public static final String SESSION_COOKIE_NAME = "JSESSIONID"; + + + /** + * The name of the path parameter used to pass the session identifier + * back and forth with the client. + */ + public static final String SESSION_PARAMETER_NAME = "jsessionid"; + + + /** + * The request attribute under which we forward an HTTP status code + * (as an object of type Integer) to an error page. + */ + public static final String STATUS_CODE_ATTR = + "javax.servlet.error.status_code"; + + + /** + * The subject under which the AccessControlContext is running. + */ + public static final String SUBJECT_ATTR = + "javax.security.auth.subject"; + + + /** + * The servlet context attribute under which we store a temporary + * working directory (as an object of type File) for use by servlets + * within this web application. + */ + public static final String WORK_DIR_ATTR = + "javax.servlet.context.tempdir"; + + protected static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); + + + /** + * The default Locale if none are specified. + */ + protected static Locale defaultLocale = Locale.getDefault(); + + // ApplicationFilterFactory. What's the use ??? + private static Integer REQUEST_INTEGER = new Integer(8); + + /** + * The match string for identifying a session ID parameter. + */ + private static final String match = ";" + SESSION_PARAMETER_NAME + "="; + + /** + * The set of cookies associated with this Request. + */ + protected Cookie[] cookies = null; + + + /** + * The set of SimpleDateFormat formats to use in getDateHeader(). + * + * Notice that because SimpleDateFormat is not thread-safe, we can't + * declare formats[] as a static variable. + */ + protected SimpleDateFormat formats[] = null; + + + /** + * The attributes associated with this Request, keyed by attribute name. + */ + protected HashMap attributes = new HashMap(); + + + /** + * List of read only attributes for this Request. + */ + //private HashMap readOnlyAttributes = new HashMap(); + + + /** + * The preferred Locales assocaited with this Request. + */ + protected ArrayList locales = new ArrayList(); + + + /** + * Authentication type. + */ + protected String authType = null; + + /** + * User principal. + */ + protected Principal userPrincipal = null; + + + /** + * The Subject associated with the current AccessControllerContext + */ + protected transient Subject subject = null; + + + /** + * The current dispatcher type. + */ + protected Object dispatcherType = null; + + + /** + * The associated input buffer. + */ + protected MessageReader inputBuffer = new MessageReader(); + + + /** + * ServletInputStream. + */ + protected ServletInputStreamImpl inputStream = + new ServletInputStreamImpl(inputBuffer.asInputStream()); + + + /** + * Reader. + */ + protected BufferedReader reader = new ServletReaderImpl(inputBuffer); + + + /** + * Using stream flag. + */ + protected boolean usingInputStream = false; + + + /** + * Using writer flag. + */ + protected boolean usingReader = false; + + + /** + * Session parsed flag. + */ + protected boolean sessionParsed = false; + + + /** + * Request parameters parsed flag. + */ + protected boolean parametersParsed = false; + + + /** + * Cookies parsed flag. + */ + protected boolean cookiesParsed = false; + + + /** + * Secure flag. + */ + protected boolean secure = false; + + + /** + * Hash map used in the getParametersMap method. + */ + protected ParameterMap parameterMap = new ParameterMap(); + + + /** + * The currently active session for this request. + */ + protected HttpSession session = null; + + + /** + * The current request dispatcher path. + */ + protected Object requestDispatcherPath = null; + + + /** + * Was the requested session ID received in a cookie? + */ + protected boolean requestedSessionCookie = false; + + + /** + * The requested session ID (if any) for this request. + */ + protected String requestedSessionId = null; + + + /** + * Was the requested session ID received in a URL? + */ + protected boolean requestedSessionURL = false; + + + /** + * Parse locales. + */ + protected boolean localesParsed = false; + + + /** + * Associated context. + */ + protected ServletContextImpl context = null; + + + + // --------------------------------------------------------- Public Methods + + /** + * Filter chain associated with the request. + */ + protected FilterChainImpl filterChain = new FilterChainImpl(); + + /** + * Mapping data. + */ + protected MappingData mappingData = new MappingData(); + + + // -------------------------------------------------------- Request Methods + + /** + * The response with which this request is associated. + */ + protected ServletResponseImpl response = new ServletResponseImpl(); + + /** + * URI byte to char converter (not recycled). + */ + // protected B2CConverter URIConverter = null; + + /** + * Associated wrapper. + */ + protected ServletConfigImpl wrapper = null; + + /** + * Post data buffer. + */ + public final static int CACHED_POST_LEN = 8192; + + public byte[] postData = null; + + + private Request coyoteRequest; + + /** New IO/buffer model + */ + //protected Http11Connection con; + + + public ServletRequestImpl() { + response.setRequest(this); + } + + +// /** +// * Return the Host within which this Request is being processed. +// */ +// public Host getHost() { +// if (getContext() == null) +// return null; +// return (Host) getContext().getParent(); +// //return ((Host) mappingData.host); +// } +// +// +// /** +// * Set the Host within which this Request is being processed. This +// * must be called as soon as the appropriate Host is identified, and +// * before the Request is passed to a context. +// * +// * @param host The newly associated Host +// */ +// public void setHost(Host host) { +// mappingData.host = host; +// } + + /** + * Add a Cookie to the set of Cookies associated with this Request. + * + * @param cookie The new cookie + */ + public void addCookie(Cookie cookie) { + + if (!cookiesParsed) + parseCookies(); + + int size = 0; + if (cookies != null) { + size = cookies.length; + } + + Cookie[] newCookies = new Cookie[size + 1]; + for (int i = 0; i < size; i++) { + newCookies[i] = cookies[i]; + } + newCookies[size] = cookie; + + cookies = newCookies; + + } + + /** + * Add a Header to the set of Headers associated with this Request. + * + * @param name The new header name + * @param value The new header value + */ + public void addHeader(String name, String value) { + // Not used + } + + + /** + * Add a Locale to the set of preferred Locales for this Request. The + * first added Locale will be the first one returned by getLocales(). + * + * @param locale The new preferred Locale + */ + public void addLocale(Locale locale) { + locales.add(locale); + } + + + /** + * Add a parameter name and corresponding set of values to this Request. + * (This is used when restoring the original request on a form based + * login). + * + * @param name Name of this request parameter + * @param values Corresponding values for this request parameter + */ + public void addParameter(String name, String values[]) { + coyoteRequest.getParameters().addParameterValues(name, values); + } + + /** + * Clear the collection of Cookies associated with this Request. + */ + public void clearCookies() { + cookiesParsed = true; + cookies = null; + } + + /** + * Clear the collection of Headers associated with this Request. + */ + public void clearHeaders() { + // Not used + } + + /** + * Clear the collection of Locales associated with this Request. + */ + public void clearLocales() { + locales.clear(); + } + + /** + * Clear the collection of parameters associated with this Request. + */ + public void clearParameters() { + // Not used + } + + + /** + * Create and return a ServletInputStream to read the content + * associated with this Request. + * + * @exception IOException if an input/output error occurs + */ + public ServletInputStream createInputStream() + throws IOException { + return inputStream; + } + + /** + * Perform whatever actions are required to flush and close the input + * stream or reader, in a single operation. + * + * @exception IOException if an input/output error occurs + */ + public void finishRequest() throws IOException { + // The reader and input stream don't need to be closed + } + + + /** + * Return the specified request attribute if it exists; otherwise, return + * null. + * + * @param name Name of the request attribute to return + */ + public Object getAttribute(String name) { + + if (name.equals(ServletRequestImpl.DISPATCHER_TYPE_ATTR)) { + return (dispatcherType == null) + ? REQUEST_INTEGER + : dispatcherType; + } else if (name.equals(ServletRequestImpl.DISPATCHER_REQUEST_PATH_ATTR)) { + return (requestDispatcherPath == null) + ? getRequestPathMB().toString() + : requestDispatcherPath.toString(); + } + + Object attr=attributes.get(name); + + if(attr!=null) + return(attr); + +// attr = reqB.getAttribute(name); +// if(attr != null) +// return attr; +// if( isSSLAttribute(name) ) { +// reqB.action(ActionCode.ACTION_REQ_SSL_ATTRIBUTE, +// reqB); +// attr = reqB.getAttribute(ServletRequestImpl.CERTIFICATES_ATTR); +// if( attr != null) { +// attributes.put(ServletRequestImpl.CERTIFICATES_ATTR, attr); +// } +// attr = reqB.getAttribute(ServletRequestImpl.CIPHER_SUITE_ATTR); +// if(attr != null) { +// attributes.put(ServletRequestImpl.CIPHER_SUITE_ATTR, attr); +// } +// attr = reqB.getAttribute(ServletRequestImpl.KEY_SIZE_ATTR); +// if(attr != null) { +// attributes.put(ServletRequestImpl.KEY_SIZE_ATTR, attr); +// } +// attr = reqB.getAttribute(ServletRequestImpl.SSL_SESSION_ID_ATTR); +// if(attr != null) { +// attributes.put(ServletRequestImpl.SSL_SESSION_ID_ATTR, attr); +// } +// attr = attributes.get(name); +// } + return attr; + } + + /** + * Return the names of all request attributes for this Request, or an + * empty Enumeration if there are none. + */ + public Enumeration getAttributeNames() { + if (isSecure()) { + getAttribute(ServletRequestImpl.CERTIFICATES_ATTR); + } + return new Enumerator(attributes.keySet(), true); + } + + + /** + * Return the authentication type used for this Request. + */ + public String getAuthType() { + return (authType); + } + + + // ------------------------------------------------- Request Public Methods + + + /** + * Return the character encoding for this Request. + */ + public String getCharacterEncoding() { + return (coyoteRequest.getCharacterEncoding()); + } + + + /** + * Return the content length for this Request. + */ + public int getContentLength() { + return (coyoteRequest.getContentLength()); + } + + +// /** +// * Return the object bound with the specified name to the internal notes +// * for this request, or null if no such binding exists. +// * +// * @param name Name of the note to be returned +// */ +// public Object getNote(String name) { +// return (notes.get(name)); +// } +// +// +// /** +// * Return an Iterator containing the String names of all notes bindings +// * that exist for this request. +// */ +// public Iterator getNoteNames() { +// return (notes.keySet().iterator()); +// } +// +// +// /** +// * Remove any object bound to the specified name in the internal notes +// * for this request. +// * +// * @param name Name of the note to be removed +// */ +// public void removeNote(String name) { +// notes.remove(name); +// } +// +// +// /** +// * Bind an object to a specified name in the internal notes associated +// * with this request, replacing any existing binding for this name. +// * +// * @param name Name to which the object should be bound +// * @param value Object to be bound to the specified name +// */ +// public void setNote(String name, Object value) { +// notes.put(name, value); +// } +// + + /** + * Return the content type for this Request. + */ + public String getContentType() { + return (coyoteRequest.getContentType()); + } + + + /** + * Return the Context within which this Request is being processed. + */ + public ServletContextImpl getContext() { + return (this.context); + } + + + /** + * Return the portion of the request URI used to select the Context + * of the Request. + */ + public String getContextPath() { + return (mappingData.contextPath.toString()); + } + + + /** + * Get the context path. + * + * @return the context path + */ + public MessageBytes getContextPathMB() { + return (mappingData.contextPath); + } + + + /** + * Return the set of Cookies received with this Request. + */ + public Cookie[] getCookies() { + + if (!cookiesParsed) + parseCookies(); + + return cookies; + + } + + + /** + * Return the value of the specified date header, if any; otherwise + * return -1. + * + * @param name Name of the requested date header + * + * @exception IllegalArgumentException if the specified header value + * cannot be converted to a date + */ + public long getDateHeader(String name) { + + String value = getHeader(name); + if (value == null) + return (-1L); + if (formats == null) { + formats = new SimpleDateFormat[] { + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) + }; + formats[0].setTimeZone(GMT_ZONE); + formats[1].setTimeZone(GMT_ZONE); + formats[2].setTimeZone(GMT_ZONE); + } + + // Attempt to convert the date header in a variety of formats + long result = FastHttpDateFormat.parseDate(value, formats); + if (result != (-1L)) { + return result; + } + throw new IllegalArgumentException(value); + + } + + + /** + * Get the decoded request URI. + * + * @return the URL decoded request URI + */ + public String getDecodedRequestURI() { + return (coyoteRequest.decodedURI().toString()); + } + + + /** + * Get the decoded request URI. + * + * @return the URL decoded request URI + */ + public MessageBytes getDecodedRequestURIMB() { + return (coyoteRequest.decodedURI()); + } + + + /** + * Get filter chain associated with the request. + */ + public FilterChainImpl getFilterChain() { + return (this.filterChain); + } + + + // ------------------------------------------------- ServletRequest Methods + + /** + * Return the first value of the specified header, if any; otherwise, + * return null + * + * @param name Name of the requested header + */ + public String getHeader(String name) { + return coyoteRequest.getHeader(name); + } + + /** + * Return the names of all headers received with this request. + */ + public Enumeration getHeaderNames() { + return coyoteRequest.getMimeHeaders().names(); + } + + + /** + * Return all of the values of the specified header, if any; otherwise, + * return an empty enumeration. + * + * @param name Name of the requested header + */ + public Enumeration getHeaders(String name) { + return coyoteRequest.getMimeHeaders().values(name); + } + + /** + * Return the servlet input stream for this Request. The default + * implementation returns a servlet input stream created by + * createInputStream(). + * + * @exception IllegalStateException if getReader() has + * already been called for this request + * @exception IOException if an input/output error occurs + */ + public ServletInputStream getInputStream() throws IOException { + + if (usingReader) + throw new IllegalStateException + ("usingReader"); + + usingInputStream = true; + return inputStream; + + } + + + /** + * Return the value of the specified header as an integer, or -1 if there + * is no such header for this request. + * + * @param name Name of the requested header + * + * @exception IllegalArgumentException if the specified header value + * cannot be converted to an integer + */ + public int getIntHeader(String name) { + + String value = getHeader(name); + if (value == null) { + return (-1); + } else { + return (Integer.parseInt(value)); + } + + } + + + /** + * Returns the Internet Protocol (IP) address of the interface on + * which the request was received. + */ + public String getLocalAddr(){ + return coyoteRequest.localAddr().toString(); + } + + + /** + * Return the preferred Locale that the client will accept content in, + * based on the value for the first Accept-Language header + * that was encountered. If the request did not specify a preferred + * language, the server's default Locale is returned. + */ + public Locale getLocale() { + + if (!localesParsed) + parseLocales(); + + if (locales.size() > 0) { + return ((Locale) locales.get(0)); + } else { + return (defaultLocale); + } + + } + + + /** + * Return the set of preferred Locales that the client will accept + * content in, based on the values for any Accept-Language + * headers that were encountered. If the request did not specify a + * preferred language, the server's default Locale is returned. + */ + public Enumeration getLocales() { + + if (!localesParsed) + parseLocales(); + + if (locales.size() > 0) + return (new Enumerator(locales)); + ArrayList results = new ArrayList(); + results.add(defaultLocale); + return (new Enumerator(results)); + + } + + + /** + * Returns the host name of the Internet Protocol (IP) interface on + * which the request was received. + */ + public String getLocalName(){ + return coyoteRequest.localName().toString(); + } + + + /** + * Returns the Internet Protocol (IP) port number of the interface + * on which the request was received. + */ + public int getLocalPort(){ + return coyoteRequest.getLocalPort(); + } + + + /** + * Return mapping data. + */ + public MappingData getMappingData() { + return (mappingData); + } + + + + /** + * Return the HTTP request method used in this Request. + */ + public String getMethod() { + return coyoteRequest.method().toString(); + } + + + /** + * Return the value of the specified request parameter, if any; otherwise, + * return null. If there is more than one value defined, + * return only the first one. + * + * @param name Name of the desired request parameter + */ + public String getParameter(String name) { + + if (!parametersParsed) + parseParameters(); + + return coyoteRequest.getParameters().getParameter(name); + + } + + + /** + * Returns a Map of the parameters of this request. + * Request parameters are extra information sent with the request. + * For HTTP servlets, parameters are contained in the query string + * or posted form data. + * + * @return A Map containing parameter names as keys + * and parameter values as map values. + */ + public Map getParameterMap() { + + if (parameterMap.isLocked()) + return parameterMap; + + Enumeration enumeration = getParameterNames(); + while (enumeration.hasMoreElements()) { + String name = enumeration.nextElement().toString(); + String[] values = getParameterValues(name); + parameterMap.put(name, values); + } + + parameterMap.setLocked(true); + + return parameterMap; + + } + + + /** + * Return the names of all defined request parameters for this request. + */ + public Enumeration getParameterNames() { + + if (!parametersParsed) + parseParameters(); + + return coyoteRequest.getParameters().getParameterNames(); + + } + + + /** + * Return the defined values for the specified request parameter, if any; + * otherwise, return null. + * + * @param name Name of the desired request parameter + */ + public String[] getParameterValues(String name) { + + if (!parametersParsed) + parseParameters(); + + return coyoteRequest.getParameters().getParameterValues(name); + + } + + + /** + * Return the path information associated with this Request. + */ + public String getPathInfo() { + return (mappingData.pathInfo.toString()); + } + + + /** + * Get the path info. + * + * @return the path info + */ + public MessageBytes getPathInfoMB() { + return (mappingData.pathInfo); + } + + + /** + * Return the extra path information for this request, translated + * to a real path. + */ + public String getPathTranslated() { + + if (context == null) + return (null); + + if (getPathInfo() == null) { + return (null); + } else { + return (context.getServletContext().getRealPath(getPathInfo())); + } + + } + + /** + * Return the principal that has been authenticated for this Request. + */ + public Principal getPrincipal() { + return (userPrincipal); + } + + /** + * Return the protocol and version used to make this Request. + */ + public String getProtocol() { + return coyoteRequest.protocol().toString(); + } + + /** + * Return the query string associated with this request. + */ + public String getQueryString() { + String queryString = coyoteRequest.queryString().toString(); + if (queryString == null || queryString.equals("")) { + return (null); + } else { + return queryString; + } + } + + + /** + * Read the Reader wrapping the input stream for this Request. The + * default implementation wraps a BufferedReader around the + * servlet input stream returned by createInputStream(). + * + * @exception IllegalStateException if getInputStream() + * has already been called for this request + * @exception IOException if an input/output error occurs + */ + public BufferedReader getReader() throws IOException { + + if (usingInputStream) + throw new IllegalStateException + ("usingInputStream"); + + usingReader = true; + //inputBuffer.setConverter();// getB2C()); + return reader; + + } + + /** + * Cached list of encoders. + */ + protected HashMap encoders = new HashMap(); + + public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1"; + + /** + * Converter for the encoding associated with the request. + * If encoding is changed - a different encoder will be returned. + * + * Encoders are cached ( per request ) - at least 8K per charset + */ + public B2CConverter getB2C() throws IOException { + String enc = getCharacterEncoding(); + if (enc == null) { + enc = DEFAULT_CHARACTER_ENCODING; + } + B2CConverter conv = + (B2CConverter) encoders.get(enc); + if (conv == null) { + conv = new B2CConverter(enc); + encoders.put(enc, conv); + } + return conv; + } + + /** + * Return the real path of the specified virtual path. + * + * @param path Path to be translated + * + * @deprecated As of version 2.1 of the Java Servlet API, use + * ServletContext.getRealPath(). + */ + public String getRealPath(String path) { + + if (context == null) + return (null); + ServletContext servletContext = context; // .getServletContext(); + if (servletContext == null) + return (null); + else { + try { + return (servletContext.getRealPath(path)); + } catch (IllegalArgumentException e) { + return (null); + } + } + + } + + + /** + * Return the remote IP address making this Request. + */ + public String getRemoteAddr() { + if (coyoteRequest.remoteAddr().isNull()) { + coyoteRequest.action(ActionCode.ACTION_REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest); + } + return coyoteRequest.remoteAddr().toString(); + } + + + /** + * Return the remote host name making this Request. + */ + public String getRemoteHost() { + if (coyoteRequest.remoteHost().isNull()) { + coyoteRequest.action(ActionCode.ACTION_REQ_HOST_ATTRIBUTE, coyoteRequest); + } + return coyoteRequest.remoteHost().toString(); + } + + + /** + * Returns the Internet Protocol (IP) source port of the client + * or last proxy that sent the request. + */ + public int getRemotePort(){ + return coyoteRequest.getRemotePort(); + } + + + /** + * Return the name of the remote user that has been authenticated + * for this Request. + */ + public String getRemoteUser() { + + if (userPrincipal != null) { + return (userPrincipal.getName()); + } else { + return (null); + } + + } + + + /** + * Return the ServletRequest for which this object + * is the facade. This method must be implemented by a subclass. + */ + public HttpServletRequest getRequest() { + return this; + } + + public Request getCoyoteRequest() { + return coyoteRequest; + } + + public void setCoyoteRequest(Request req) { + this.coyoteRequest = req; + inputBuffer.setRequest(req); + } + + + /** + * Return a RequestDispatcher that wraps the resource at the specified + * path, which may be interpreted as relative to the current request path. + * + * @param path Path of the resource to be wrapped + */ + public RequestDispatcher getRequestDispatcher(String path) { + + if (context == null) + return (null); + + // If the path is already context-relative, just pass it through + if (path == null) + return (null); + else if (path.startsWith("/")) + return (context.getRequestDispatcher(path)); + + // Convert a request-relative path to a context-relative one + String servletPath = (String) getAttribute(RequestDispatcherImpl.INCLUDE_SERVLET_PATH_ATTR); + if (servletPath == null) + servletPath = getServletPath(); + + // Add the path info, if there is any + String pathInfo = getPathInfo(); + String requestPath = null; + + if (pathInfo == null) { + requestPath = servletPath; + } else { + requestPath = servletPath + pathInfo; + } + + int pos = requestPath.lastIndexOf('/'); + String relative = null; + if (pos >= 0) { + relative = RequestUtil.normalize + (requestPath.substring(0, pos + 1) + path); + } else { + relative = RequestUtil.normalize(requestPath + path); + } + + return (context.getRequestDispatcher(relative)); + + } + + + /** + * Return the session identifier included in this request, if any. + */ + public String getRequestedSessionId() { + return (requestedSessionId); + } + + + // ---------------------------------------------------- HttpRequest Methods + + + /** + * Get the request path. + * + * @return the request path + */ + public MessageBytes getRequestPathMB() { + return (mappingData.requestPath); + } + + + /** + * Return the request URI for this request. + */ + public String getRequestURI() { + return coyoteRequest.requestURI().toString(); + } + + /** + */ + public void setRequestURI(String uri) { + coyoteRequest.decodedURI().setString(uri); + try { + UriNormalizer.decodeRequest(coyoteRequest.decodedURI(), + coyoteRequest.requestURI(), coyoteRequest.getURLDecoder()); + } catch(IOException ioe) { + ioe.printStackTrace(); + return; + } + } + + + + /** + * Reconstructs the URL the client used to make the request. + * The returned URL contains a protocol, server name, port + * number, and server path, but it does not include query + * string parameters. + *

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

+ * This method is useful for creating redirect messages and + * for reporting errors. + * + * @return A StringBuffer object containing the + * reconstructed URL + */ + public StringBuffer getRequestURL() { + + StringBuffer url = new StringBuffer(); + String scheme = getScheme(); + int port = getServerPort(); + if (port < 0) + port = 80; // Work around java.net.URL bug + + url.append(scheme); + url.append("://"); + url.append(getServerName()); + if ((scheme.equals("http") && (port != 80)) + || (scheme.equals("https") && (port != 443))) { + url.append(':'); + url.append(port); + } + url.append(getRequestURI()); + + return (url); + + } + + + /** + * Return the Response with which this Request is associated. + */ + public ServletResponseImpl getResponse() { + return (this.response); + } + + + /** + * Return the scheme used to make this Request. + */ + public String getScheme() { + String scheme = coyoteRequest.scheme().toString(); + if (scheme == null) { + scheme = (isSecure() ? "https" : "http"); + } + return scheme; + } + + + /** + * Return the server name responding to this Request. + */ + public String getServerName() { + return (coyoteRequest.serverName().toString()); + } + + + /** + * Return the server port responding to this Request. + */ + public int getServerPort() { + return (coyoteRequest.getServerPort()); + } + + + /** + * Return the portion of the request URI used to select the servlet + * that will process this request. + */ + public String getServletPath() { + return (mappingData.wrapperPath.toString()); + } + + + /** + * Get the servlet path. + * + * @return the servlet path + */ + public MessageBytes getServletPathMB() { + return (mappingData.wrapperPath); + } + + + + /** + * Return the input stream associated with this Request. + */ + public InputStream getStream() { + return inputStream; + } + + + /** + * Return the principal that has been authenticated for this Request. + */ + public Principal getUserPrincipal() { + return userPrincipal; + } + + + /** + * Return the Wrapper within which this Request is being processed. + */ + public ServletConfigImpl getWrapper() { + return (this.wrapper); + } + + + /** + * Return true if the session identifier included in this + * request came from a cookie. + */ + public boolean isRequestedSessionIdFromCookie() { + + if (requestedSessionId != null) + return (requestedSessionCookie); + else + return (false); + + } + + + /** + * Return true if the session identifier included in this + * request came from the request URI. + * + * @deprecated As of Version 2.1 of the Java Servlet API, use + * isRequestedSessionIdFromURL() instead. + */ + public boolean isRequestedSessionIdFromUrl() { + return (isRequestedSessionIdFromURL()); + } + + + /** + * Return true if the session identifier included in this + * request came from the request URI. + */ + public boolean isRequestedSessionIdFromURL() { + + if (requestedSessionId != null) + return (requestedSessionURL); + else + return (false); + + } + + + /** + * Return true if the session identifier included in this + * request identifies a valid session. + */ + public boolean isRequestedSessionIdValid() { + + if (requestedSessionId == null) + return (false); + if (context == null) + return (false); + UserSessionManager manager = context.getManager(); + if (manager == null) + return (false); + HttpSession session = null; + try { + session = manager.findSession(requestedSessionId); + } catch (IOException e) { + session = null; + } + if ((session != null) && manager.isValid(session)) + return (true); + else + return (false); + + } + + /** + * Was this request received on a secure connection? + */ + public boolean isSecure() { + return (secure); + } + + + /** + * Return true if the authenticated user principal + * possesses the specified role name. + * + * @param role Role name to be validated + */ + public boolean isUserInRole(String role) { + // Have we got an authenticated principal at all? + Principal userPrincipal = getPrincipal(); + if (userPrincipal == null) + return (false); + + // Identify the Realm we will use for checking role assignmenets + if (context == null) + return (false); + + // Check for a role alias defined in a element + if (wrapper != null) { + String realRole = wrapper.getSecurityRoleRef(role); + if (realRole != null) { + role = realRole; + } + } + + if (role.equals(userPrincipal.getName())) { + return true; + } + + // TODO: check !!!! + // Check for a role defined directly as a + return false; + } + + /** + * Release all object references, and initialize instance variables, in + * preparation for reuse of this object. + */ + public void recycle() { + + wrapper = null; + + dispatcherType = null; + requestDispatcherPath = null; + + authType = null; + inputBuffer.recycle(); + usingInputStream = false; + usingReader = false; + userPrincipal = null; + subject = null; + sessionParsed = false; + parametersParsed = false; + cookiesParsed = false; + locales.clear(); + localesParsed = false; + secure = false; + + attributes.clear(); + //notes.clear(); + cookies = null; + + if (session != null) { + context.getManager().endAccess(session); + } + context = null; + session = null; + requestedSessionCookie = false; + requestedSessionId = null; + requestedSessionURL = false; + + parameterMap.setLocked(false); + parameterMap.clear(); + + mappingData.recycle(); + + } + + + /** + * Remove the specified request attribute if it exists. + * + * @param name Name of the request attribute to remove + */ + public void removeAttribute(String name) { + Object value = null; + boolean found = false; + + // Remove the specified attribute + // Check for read only attribute + // requests are per thread so synchronization unnecessary +// if (readOnlyAttributes.containsKey(name)) { +// return; +// } + found = attributes.containsKey(name); + if (found) { + value = attributes.get(name); + attributes.remove(name); + } else { + return; + } + + // Notify interested application event listeners + List listeners = context.getListeners(); + if (listeners.size() == 0) + return; + ServletRequestAttributeEvent event = null; + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof ServletRequestAttributeListener)) + continue; + ServletRequestAttributeListener listener = + (ServletRequestAttributeListener) listeners.get(i); + try { + if (event == null) { + event = + new ServletRequestAttributeEvent(context.getServletContext(), + getRequest(), name, value); + } + listener.attributeRemoved(event); + } catch (Throwable t) { + context.getLogger().log(Level.WARNING, "ServletRequestAttributeListner.attributeRemoved()", t); + // Error valve will pick this execption up and display it to user + attributes.put( ServletRequestImpl.EXCEPTION_ATTR, t ); + } + } + } + + + /** + * Set the specified request attribute to the specified value. + * + * @param name Name of the request attribute to set + * @param value The associated value + */ + public void setAttribute(String name, Object value) { + + // Name cannot be null + if (name == null) + throw new IllegalArgumentException + ("setAttribute() name == null"); + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + if (name.equals(ServletRequestImpl.DISPATCHER_TYPE_ATTR)) { + dispatcherType = value; + return; + } else if (name.equals(ServletRequestImpl.DISPATCHER_REQUEST_PATH_ATTR)) { + requestDispatcherPath = value; + return; + } + + Object oldValue = null; + boolean replaced = false; + + // Add or replace the specified attribute + // Check for read only attribute + // requests are per thread so synchronization unnecessary +// if (readOnlyAttributes.containsKey(name)) { +// return; +// } + + oldValue = attributes.put(name, value); + if (oldValue != null) { + replaced = true; + } + + // Pass special attributes to the native layer +// if (name.startsWith("org.apache.tomcat.")) { +// reqB.setAttribute(name, value); +// } +// + // Notify interested application event listeners + List listeners = context.getListeners(); + if (listeners.size() == 0) + return; + ServletRequestAttributeEvent event = null; + + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof ServletRequestAttributeListener)) + continue; + ServletRequestAttributeListener listener = + (ServletRequestAttributeListener) listeners.get(i); + try { + if (event == null) { + if (replaced) + event = + new ServletRequestAttributeEvent(context.getServletContext(), + getRequest(), name, oldValue); + else + event = + new ServletRequestAttributeEvent(context.getServletContext(), + getRequest(), name, value); + } + if (replaced) { + listener.attributeReplaced(event); + } else { + listener.attributeAdded(event); + } + } catch (Throwable t) { + context.getLogger().log(Level.WARNING, "ServletRequestAttributeListener error", t); + // Error valve will pick this execption up and display it to user + attributes.put( ServletRequestImpl.EXCEPTION_ATTR, t ); + } + } + } + + + // --------------------------------------------- HttpServletRequest Methods + + + /** + * Set the authentication type used for this request, if any; otherwise + * set the type to null. Typical values are "BASIC", + * "DIGEST", or "SSL". + * + * @param type The authentication type used + */ + public void setAuthType(String type) { + this.authType = type; + } + + + /** + * Overrides the name of the character encoding used in the body of + * this request. This method must be called prior to reading request + * parameters or reading input using getReader(). + * + * @param enc The character encoding to be used + * + * @exception UnsupportedEncodingException if the specified encoding + * is not supported + * + * @since Servlet 2.3 + */ + public void setCharacterEncoding(String enc) + throws UnsupportedEncodingException { + + // Ensure that the specified encoding is valid + byte buffer[] = new byte[1]; + buffer[0] = (byte) 'a'; + String dummy = new String(buffer, enc); + + // Save the validated encoding + coyoteRequest.setCharacterEncoding(enc); + + } + + +// public void setConnection(Http11Connection con) { +// this.con = con; +// //reqB.messageWriter.setConnection(con); +// //inputBuffer.setRequest(req); +// } + + + /** + * Set the content length associated with this Request. + * + * @param length The new content length + */ + public void setContentLength(int length) { + // Not used + } + + + /** + * Set the content type (and optionally the character encoding) + * associated with this Request. For example, + * text/html; charset=ISO-8859-4. + * + * @param type The new content type + */ + public void setContentType(String type) { + // Not used + } + + + /** + * Set the Context within which this Request is being processed. This + * must be called as soon as the appropriate Context is identified, because + * it identifies the value to be returned by getContextPath(), + * and thus enables parsing of the request URI. + * + * @param context The newly associated Context + */ + public void setContext(ServletContextImpl context) { + this.context = context; + } + + + /** + * Set the context path for this Request. This will normally be called + * when the associated Context is mapping the Request to a particular + * Wrapper. + * + * @param path The context path + */ + public void setContextPath(String path) { + + if (path == null) { + mappingData.contextPath.setString(""); + } else { + mappingData.contextPath.setString(path); + } + + } + + + /** + * Set the set of cookies recieved with this Request. + */ + public void setCookies(Cookie[] cookies) { + + this.cookies = cookies; + + } + + + /** + * Set the decoded request URI. + * + * @param uri The decoded request URI + */ + public void setDecodedRequestURI(String uri) { + // Not used + } + + + /** + * Set the HTTP request method used for this Request. + * + * @param method The request method + */ + public void setMethod(String method) { + coyoteRequest.method().setString(method); + } + + + /** + * Set the path information for this Request. This will normally be called + * when the associated Context is mapping the Request to a particular + * Wrapper. + * + * @param path The path information + */ + public void setPathInfo(String path) { + mappingData.pathInfo.setString(path); + } + + + /** + * Set the protocol name and version associated with this Request. + * + * @param protocol Protocol name and version + */ + public void setProtocol(String protocol) { + // Not used + } + + + /** + * Set the query string for this Request. This will normally be called + * by the HTTP Connector, when it parses the request headers. + * + * @param query The query string + */ + public void setQueryString(String query) { + // Not used + } + + + /** + * Set the IP address of the remote client associated with this Request. + * + * @param remoteAddr The remote IP address + */ + public void setRemoteAddr(String remoteAddr) { + // Not used + } + + + /** + * Set the fully qualified name of the remote client associated with this + * Request. + * + * @param remoteHost The remote host name + */ + public void setRemoteHost(String remoteHost) { + // Not used + } + + + /** + * Set a flag indicating whether or not the requested session ID for this + * request came in through a cookie. This is normally called by the + * HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionCookie(boolean flag) { + + this.requestedSessionCookie = flag; + + } + + + /** + * Set the requested session ID for this request. This is normally called + * by the HTTP Connector, when it parses the request headers. + * + * @param id The new session id + */ + public void setRequestedSessionId(String id) { + + this.requestedSessionId = id; + + } + + + /** + * Set a flag indicating whether or not the requested session ID for this + * request came in through a URL. This is normally called by the + * HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionURL(boolean flag) { + + this.requestedSessionURL = flag; + + } + + + /** + * Set the name of the scheme associated with this request. Typical values + * are http, https, and ftp. + * + * @param scheme The scheme + */ + public void setScheme(String scheme) { + // Not used + } + + + /** + * Set the value to be returned by isSecure() + * for this Request. + * + * @param secure The new isSecure value + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + + /** + * Set the name of the server (virtual host) to process this request. + * + * @param name The server name + */ + public void setServerName(String name) { + coyoteRequest.serverName().setString(name); + } + + + /** + * Set the port number of the server to process this request. + * + * @param port The server port + */ + public void setServerPort(int port) { + coyoteRequest.setServerPort(port); + } + + + /** + * Set the servlet path for this Request. This will normally be called + * when the associated Context is mapping the Request to a particular + * Wrapper. + * + * @param path The servlet path + */ + public void setServletPath(String path) { + if (path != null) + mappingData.wrapperPath.setString(path); + } + + + /** + * Set the input stream associated with this Request. + * + * @param stream The new input stream + */ + public void setStream(InputStream stream) { + // Ignore + } + + + /** + * Set the Principal who has been authenticated for this Request. This + * value is also used to calculate the value to be returned by the + * getRemoteUser() method. + * + * @param principal The user Principal + */ + public void setUserPrincipal(Principal principal) { + + if (System.getSecurityManager() != null){ + HttpSession session = getSession(false); + if ( (subject != null) && + (!subject.getPrincipals().contains(principal)) ){ + subject.getPrincipals().add(principal); + } else if (session != null && + session.getAttribute(ServletRequestImpl.SUBJECT_ATTR) == null) { + subject = new Subject(); + subject.getPrincipals().add(principal); + } + if (session != null){ + session.setAttribute(ServletRequestImpl.SUBJECT_ATTR, subject); + } + } + + this.userPrincipal = principal; + } + + + /** + * Set the Wrapper within which this Request is being processed. This + * must be called as soon as the appropriate Wrapper is identified, and + * before the Request is ultimately passed to an application servlet. + * @param wrapper The newly associated Wrapper + */ + public void setWrapper(ServletConfigImpl wrapper) { + this.wrapper = wrapper; + } + + + public String toString() { + return coyoteRequest.requestURI().toString(); + } + + + /** + * Configures the given JSESSIONID cookie. + * + * @param cookie The JSESSIONID cookie to be configured + */ + protected void configureSessionCookie(Cookie cookie) { + cookie.setMaxAge(-1); + String contextPath = null; + if (//!connector.getEmptySessionPath() && + (getContext() != null)) { + contextPath = getContext().getEncodedPath(); + } + if ((contextPath != null) && (contextPath.length() > 0)) { + cookie.setPath(contextPath); + } else { + cookie.setPath("/"); + } + if (isSecure()) { + cookie.setSecure(true); + } + } + + + /** + * Return the session associated with this Request, creating one + * if necessary. + */ + public HttpSession getSession() { + return getSession(true); + } + + + public HttpSession getSession(boolean create) { + + // There cannot be a session if no context has been assigned yet + if (context == null) + return (null); + + + // Return the requested session if it exists and is valid + UserSessionManager manager = null; + if (context != null) + manager = context.getManager(); + if (manager == null) + return (null); // Sessions are not supported + + // Return the current session if it exists and is valid + if ((session != null) && !manager.isValid(session)) + session = null; + if (session != null) + return (session); + + + if (requestedSessionId != null) { + try { + session = manager.findSession(requestedSessionId); + } catch (IOException e) { + session = null; + } + if ((session != null) && !manager.isValid(session)) + session = null; + if (session != null) { + manager.access(session); + return (session); + } + } + + // Create a new session if requested and the response is not committed + if (!create) + return (null); + if ((context != null) && (response != null) && + context.getCookies() && + getResponse().isCommitted()) { + throw new IllegalStateException + ("isCommited()"); + } + + // Attempt to reuse session id if one was submitted in a cookie + // Do not reuse the session id if it is from a URL, to prevent possible + // phishing attacks + if (// connector.getEmptySessionPath() && + isRequestedSessionIdFromCookie()) { + session = manager.createSession(getRequestedSessionId()); + } else { + session = manager.createSession(null); + } + + // Creating a new session cookie based on that session + if ((session != null) && (getContext() != null) + && getContext().getCookies()) { + Cookie cookie = new Cookie(ServletRequestImpl.SESSION_COOKIE_NAME, + session.getId()); + configureSessionCookie(cookie); + response.addCookie(cookie); + } + + if (session != null) { + manager.access(session); + return (session); + } else { + return (null); + } + + } + + + /** + * Return the URI converter. + */ +// protected B2CConverter getURIConverter() { +// return URIConverter; +// } +// + + // ------------------------------------------------------ Protected Methods + + + /** + * Parse cookies. + */ + protected void parseCookies() { + + cookiesParsed = true; + + Cookies serverCookies = coyoteRequest.getCookies(); + int count = serverCookies.getCookieCount(); + if (count <= 0) + return; + + cookies = new Cookie[count]; + + int idx=0; + for (int i = 0; i < count; i++) { + ServerCookie scookie = serverCookies.getCookie(i); + try { + Cookie cookie = new Cookie(scookie.getName().toString(), + scookie.getValue().toString()); + cookie.setPath(scookie.getPath().toString()); + cookie.setVersion(scookie.getVersion()); + String domain = scookie.getDomain().toString(); + if (domain != null) { + cookie.setDomain(scookie.getDomain().toString()); + } + cookies[idx++] = cookie; + } catch(IllegalArgumentException e) { + // Ignore bad cookie + } + } + if( idx < count ) { + Cookie [] ncookies = new Cookie[idx]; + System.arraycopy(cookies, 0, ncookies, 0, idx); + cookies = ncookies; + } + + } + + /** + * Parse request locales. + */ + protected void parseLocales() { + + localesParsed = true; + + Enumeration values = getHeaders("accept-language"); + + while (values.hasMoreElements()) { + String value = values.nextElement().toString(); + parseLocalesHeader(value); + } + + } + + /** + * Parse accept-language header value. + */ + protected void parseLocalesHeader(String value) { + + TreeMap locales = new LocaleParser().parseLocale(value); + // Process the quality values in highest->lowest order (due to + // negating the Double value when creating the key) + Iterator keys = locales.keySet().iterator(); + while (keys.hasNext()) { + Double key = (Double) keys.next(); + ArrayList list = (ArrayList) locales.get(key); + Iterator values = list.iterator(); + while (values.hasNext()) { + Locale locale = (Locale) values.next(); + addLocale(locale); + } + } + + } + + /** + * Parse request parameters. + */ + protected void parseParameters() { + + parametersParsed = true; + + Parameters parameters = coyoteRequest.getParameters(); + + // getCharacterEncoding() may have been overridden to search for + // hidden form field containing request encoding + String enc = getCharacterEncoding(); + +// 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); +// } + } + + parameters.handleQueryParameters(); + + 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) { +// int maxPostSize = connector.getMaxPostSize(); +// if ((maxPostSize > 0) && (len > maxPostSize)) { +// context.getLogger().info +// (sm.getString("reqB.postTooLarge")); +// throw new IllegalStateException("Post too large"); +// } + 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) { + parameters.processParameters(formData, 0, len); + } + } catch (Throwable t) { + ; // Ignore + } + } + + } + + + /** + * Parse session id in URL. Done in request for performance. + * TODO: should be done in manager + */ + protected void parseSessionCookiesId() { + String sessionCookieName = context.getSessionCookieName(); + + // Parse session id from cookies + Cookies serverCookies = coyoteRequest.getCookies(); + int count = serverCookies.getCookieCount(); + if (count <= 0) + return; + + for (int i = 0; i < count; i++) { + ServerCookie scookie = serverCookies.getCookie(i); + if (scookie.getName().equals(sessionCookieName)) { + // Override anything requested in the URL + if (!isRequestedSessionIdFromCookie()) { + // Accept only the first session id cookie + //scookie.getValue().convertToAscii(); + + setRequestedSessionId + (scookie.getValue().toString()); + setRequestedSessionCookie(true); + setRequestedSessionURL(false); + } else { + if (!isRequestedSessionIdValid()) { + // Replace the session id until one is valid + //scookie.getValue().convertToAscii(); + setRequestedSessionId + (scookie.getValue().toString()); + } + } + } + } + } + + /** + * Parse session id in URL. + */ + protected void parseSessionId() { + Request req = coyoteRequest; + ServletRequestImpl request = this; + ByteChunk uriBC = req.requestURI().getByteChunk(); + int semicolon = uriBC.indexOf(match, 0, match.length(), 0); + + if (semicolon > 0) { + + // Parse session ID, and extract it from the decoded request URI + int start = uriBC.getStart(); + int end = uriBC.getEnd(); + + int sessionIdStart = semicolon + match.length(); + int semicolon2 = uriBC.indexOf(';', sessionIdStart); + if (semicolon2 >= 0) { + request.setRequestedSessionId + (new String(uriBC.getBuffer(), start + sessionIdStart, + semicolon2 - sessionIdStart)); + // Extract session ID from request URI + byte[] buf = uriBC.getBuffer(); + for (int i = 0; i < end - start - semicolon2; i++) { + buf[start + semicolon + i] + = buf[start + i + semicolon2]; + } + uriBC.setBytes(buf, start, end - start - semicolon2 + semicolon); + } else { + request.setRequestedSessionId + (new String(uriBC.getBuffer(), start + sessionIdStart, + (end - start) - sessionIdStart)); + uriBC.setEnd(start + semicolon); + } + request.setRequestedSessionURL(true); + + } else { + request.setRequestedSessionId(null); + request.setRequestedSessionURL(false); + } + + } + + + /** + * Read post body in an array. + */ + protected int readPostBody(byte body[], int len) + throws IOException { + + int offset = 0; + do { + int inputLen = getStream().read(body, offset, len - offset); + if (inputLen <= 0) { + return offset; + } + offset += inputLen; + } while ((len - offset) > 0); + return len; + + } + + + /** + * Test if a given name is one of the special Servlet-spec SSL attributes. + */ + static boolean isSSLAttribute(String name) { + return ServletRequestImpl.CERTIFICATES_ATTR.equals(name) || + ServletRequestImpl.CIPHER_SUITE_ATTR.equals(name) || + ServletRequestImpl.KEY_SIZE_ATTR.equals(name) || + ServletRequestImpl.SSL_SESSION_ID_ATTR.equals(name); + } + + + @Override + public void addAsyncListener(AsyncListener listener) { + } + + + @Override + public void addAsyncListener(AsyncListener listener, + ServletRequest servletRequest, + ServletResponse servletResponse) { + } + + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + + @Override + public ServletContext getServletContext() { + return null; + } + + + @Override + public boolean isAsyncStarted() { + return false; + } + + + @Override + public boolean isAsyncSupported() { + return false; + } + + + @Override + public void setAsyncTimeout(long timeout) { + } + + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, + ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletRequestWrapperImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletRequestWrapperImpl.java new file mode 100644 index 000000000..af400d8ca --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletRequestWrapperImpl.java @@ -0,0 +1,943 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpSession; + +import org.apache.tomcat.addons.UserSessionManager; +import org.apache.tomcat.servlets.util.Enumerator; +import org.apache.tomcat.servlets.util.RequestUtil; + + +/** + * Wrapper around a javax.servlet.http.HttpServletRequest + * that transforms an application request object (which might be the original + * one passed to a servlet, or might be based on the 2.3 + * javax.servlet.http.HttpServletRequestWrapper class) + * back into an internal org.apache.catalina.HttpRequest. + *

+ * WARNING: Due to Java's lack of support for multiple + * inheritance, all of the logic in ApplicationRequest is + * duplicated in ApplicationHttpRequest. Make sure that you + * keep these two classes in synchronization when making changes! + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class ServletRequestWrapperImpl extends HttpServletRequestWrapper { + + + // ------------------------------------------------------- Static Variables + + + /** + * The set of attribute names that are special for request dispatchers. + */ + protected static final String specials[] = + { RequestDispatcherImpl.INCLUDE_REQUEST_URI_ATTR, + RequestDispatcherImpl.INCLUDE_CONTEXT_PATH_ATTR, + RequestDispatcherImpl.INCLUDE_SERVLET_PATH_ATTR, + RequestDispatcherImpl.INCLUDE_PATH_INFO_ATTR, + RequestDispatcherImpl.INCLUDE_QUERY_STRING_ATTR, + RequestDispatcherImpl.FORWARD_REQUEST_URI_ATTR, + RequestDispatcherImpl.FORWARD_CONTEXT_PATH_ATTR, + RequestDispatcherImpl.FORWARD_SERVLET_PATH_ATTR, + RequestDispatcherImpl.FORWARD_PATH_INFO_ATTR, + RequestDispatcherImpl.FORWARD_QUERY_STRING_ATTR }; + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new wrapped request around the specified servlet request. + * + * @param request The servlet request being wrapped + */ + public ServletRequestWrapperImpl(HttpServletRequest request, + ServletContextImpl context, + boolean crossContext) { + + super(request); + this.context = context; + this.crossContext = crossContext; + setRequest(request); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The context for this request. + */ + protected ServletContextImpl context = null; + + + /** + * The context path for this request. + */ + protected String contextPath = null; + + + /** + * If this request is cross context, since this changes session accesss + * behavior. + */ + protected boolean crossContext = false; + + + /** + * The current dispatcher type. + */ + protected Object dispatcherType = null; + + + /** + * Descriptive information about this implementation. + */ + protected static final String info = + "org.apache.catalina.core.ApplicationHttpRequest/1.0"; + + + /** + * The request parameters for this request. This is initialized from the + * wrapped request, but updates are allowed. + */ + protected Map parameters = null; + + + /** + * Have the parameters for this request already been parsed? + */ + private boolean parsedParams = false; + + + /** + * The path information for this request. + */ + protected String pathInfo = null; + + + /** + * The query parameters for the current request. + */ + private String queryParamString = null; + + + /** + * The query string for this request. + */ + protected String queryString = null; + + + /** + * The current request dispatcher path. + */ + protected Object requestDispatcherPath = null; + + + /** + * The request URI for this request. + */ + protected String requestURI = null; + + + /** + * The servlet path for this request. + */ + protected String servletPath = null; + + + /** + * The currently active session for this request. + */ + protected HttpSession session = null; + + + /** + * Special attributes. + */ + protected Object[] specialAttributes = new Object[specials.length]; + + + // ------------------------------------------------- ServletRequest Methods + + + /** + * Override the getAttribute() method of the wrapped request. + * + * @param name Name of the attribute to retrieve + */ + public Object getAttribute(String name) { + + if (name.equals(ServletRequestImpl.DISPATCHER_TYPE_ATTR)) { + return dispatcherType; + } else if (name.equals(ServletRequestImpl.DISPATCHER_REQUEST_PATH_ATTR)) { + if ( requestDispatcherPath != null ){ + return requestDispatcherPath.toString(); + } else { + return null; + } + } + + int pos = getSpecial(name); + if (pos == -1) { + return getRequest().getAttribute(name); + } else { + if ((specialAttributes[pos] == null) + && (specialAttributes[5] == null) && (pos >= 5)) { + // If it's a forward special attribute, and null, it means this + // is an include, so we check the wrapped request since + // the request could have been forwarded before the include + return getRequest().getAttribute(name); + } else { + return specialAttributes[pos]; + } + } + + } + + + /** + * Override the getAttributeNames() method of the wrapped + * request. + */ + public Enumeration getAttributeNames() { + return (new AttributeNamesEnumerator()); + } + + + /** + * Override the removeAttribute() method of the + * wrapped request. + * + * @param name Name of the attribute to remove + */ + public void removeAttribute(String name) { + + if (!removeSpecial(name)) + getRequest().removeAttribute(name); + + } + + + /** + * Override the setAttribute() method of the + * wrapped request. + * + * @param name Name of the attribute to set + * @param value Value of the attribute to set + */ + public void setAttribute(String name, Object value) { + + if (name.equals(ServletRequestImpl.DISPATCHER_TYPE_ATTR)) { + dispatcherType = value; + return; + } else if (name.equals(ServletRequestImpl.DISPATCHER_REQUEST_PATH_ATTR)) { + requestDispatcherPath = value; + return; + } + + if (!setSpecial(name, value)) { + getRequest().setAttribute(name, value); + } + + } + + + /** + * Return a RequestDispatcher that wraps the resource at the specified + * path, which may be interpreted as relative to the current request path. + * + * @param path Path of the resource to be wrapped + */ + public RequestDispatcher getRequestDispatcher(String path) { + + if (context == null) + return (null); + + // If the path is already context-relative, just pass it through + if (path == null) + return (null); + else if (path.startsWith("/")) + return (context.getServletContext().getRequestDispatcher(path)); + + // Convert a request-relative path to a context-relative one + String servletPath = + (String) getAttribute(RequestDispatcherImpl.INCLUDE_SERVLET_PATH_ATTR); + if (servletPath == null) + servletPath = getServletPath(); + + // Add the path info, if there is any + String pathInfo = getPathInfo(); + String requestPath = null; + + if (pathInfo == null) { + requestPath = servletPath; + } else { + requestPath = servletPath + pathInfo; + } + + int pos = requestPath.lastIndexOf('/'); + String relative = null; + if (pos >= 0) { + relative = RequestUtil.normalize + (requestPath.substring(0, pos + 1) + path); + } else { + relative = RequestUtil.normalize(requestPath + path); + } + + return (context.getServletContext().getRequestDispatcher(relative)); + + } + + + // --------------------------------------------- HttpServletRequest Methods + + + /** + * Override the getContextPath() method of the wrapped + * request. + */ + public String getContextPath() { + + return (this.contextPath); + + } + + + /** + * Override the getParameter() method of the wrapped request. + * + * @param name Name of the requested parameter + */ + public String getParameter(String name) { + + parseParameters(); + + Object value = parameters.get(name); + if (value == null) + return (null); + else if (value instanceof String[]) + return (((String[]) value)[0]); + else if (value instanceof String) + return ((String) value); + else + return (value.toString()); + + } + + + /** + * Override the getParameterMap() method of the + * wrapped request. + */ + public Map getParameterMap() { + + parseParameters(); + return (parameters); + + } + + + /** + * Override the getParameterNames() method of the + * wrapped request. + */ + public Enumeration getParameterNames() { + + parseParameters(); + return (new Enumerator(parameters.keySet())); + + } + + + /** + * Override the getParameterValues() method of the + * wrapped request. + * + * @param name Name of the requested parameter + */ + public String[] getParameterValues(String name) { + + parseParameters(); + Object value = parameters.get(name); + if (value == null) + return ((String[]) null); + else if (value instanceof String[]) + return ((String[]) value); + else if (value instanceof String) { + String values[] = new String[1]; + values[0] = (String) value; + return (values); + } else { + String values[] = new String[1]; + values[0] = value.toString(); + return (values); + } + + } + + + /** + * Override the getPathInfo() method of the wrapped request. + */ + public String getPathInfo() { + + return (this.pathInfo); + + } + + + /** + * Override the getQueryString() method of the wrapped + * request. + */ + public String getQueryString() { + + return (this.queryString); + + } + + + /** + * Override the getRequestURI() method of the wrapped + * request. + */ + public String getRequestURI() { + + return (this.requestURI); + + } + + + /** + * Override the getRequestURL() method of the wrapped + * request. + */ + public StringBuffer getRequestURL() { + + StringBuffer url = new StringBuffer(); + String scheme = getScheme(); + int port = getServerPort(); + if (port < 0) + port = 80; // Work around java.net.URL bug + + url.append(scheme); + url.append("://"); + url.append(getServerName()); + if ((scheme.equals("http") && (port != 80)) + || (scheme.equals("https") && (port != 443))) { + url.append(':'); + url.append(port); + } + url.append(getRequestURI()); + + return (url); + + } + + + /** + * Override the getServletPath() method of the wrapped + * request. + */ + public String getServletPath() { + + return (this.servletPath); + + } + + + /** + * Return the session associated with this Request, creating one + * if necessary. + */ + public HttpSession getSession() { + return (getSession(true)); + } + + + /** + * Return the session associated with this Request, creating one + * if necessary and requested. + * + * @param create Create a new session if one does not exist + */ + public HttpSession getSession(boolean create) { + + if (crossContext) { + + // There cannot be a session if no context has been assigned yet + if (context == null) + return (null); + UserSessionManager manager = context.getManager(); + // Return the current session if it exists and is valid + if (session != null && manager.isValid(session)) { + return session; + } + + HttpSession other = super.getSession(false); + if (create && (other == null)) { + // First create a session in the first context: the problem is + // that the top level request is the only one which can + // create the cookie safely + other = super.getSession(true); + } + if (other != null) { + HttpSession localSession = null; + try { + localSession = + manager.findSession(other.getId()); + } catch (IOException e) { + // Ignore + } + if (localSession == null && create) { + localSession = + context.getManager().createSession(other.getId()); + } + if (localSession != null) { + context.getManager().access(localSession); + session = localSession; + return session; + } + } + return null; + + } else { + return super.getSession(create); + } + + } + + + /** + * Returns true if the request specifies a JSESSIONID that is valid within + * the context of this ApplicationHttpRequest, false otherwise. + * + * @return true if the request specifies a JSESSIONID that is valid within + * the context of this ApplicationHttpRequest, false otherwise. + */ + public boolean isRequestedSessionIdValid() { + + if (crossContext) { + + String requestedSessionId = getRequestedSessionId(); + if (requestedSessionId == null) + return (false); + if (context == null) + return (false); + UserSessionManager manager = context.getManager(); + if (manager == null) + return (false); + HttpSession session = null; + try { + session = manager.findSession(requestedSessionId); + } catch (IOException e) { + session = null; + } + if ((session != null) && manager.isValid(session)) { + return (true); + } else { + return (false); + } + + } else { + return super.isRequestedSessionIdValid(); + } + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Recycle this request + */ + public void recycle() { + if (session != null) { + context.getManager().endAccess(session); + } + } + + + /** + * Return descriptive information about this implementation. + */ + public String getInfo() { + + return (info); + + } + + + /** + * Perform a shallow copy of the specified Map, and return the result. + * + * @param orig Origin Map to be copied + */ + Map copyMap(Map orig) { + + if (orig == null) + return (new HashMap()); + HashMap dest = new HashMap(); + Iterator keys = orig.keySet().iterator(); + while (keys.hasNext()) { + String key = (String) keys.next(); + dest.put(key, orig.get(key)); + } + return (dest); + + } + + + /** + * Set the context path for this request. + * + * @param contextPath The new context path + */ + void setContextPath(String contextPath) { + + this.contextPath = contextPath; + + } + + + /** + * Set the path information for this request. + * + * @param pathInfo The new path info + */ + void setPathInfo(String pathInfo) { + + this.pathInfo = pathInfo; + + } + + + /** + * Set the query string for this request. + * + * @param queryString The new query string + */ + void setQueryString(String queryString) { + + this.queryString = queryString; + + } + + + /** + * Set the request that we are wrapping. + * + * @param request The new wrapped request + */ + void setRequest(HttpServletRequest request) { + + super.setRequest(request); + + // Initialize the attributes for this request + dispatcherType = request.getAttribute(ServletRequestImpl.DISPATCHER_TYPE_ATTR); + requestDispatcherPath = + request.getAttribute(ServletRequestImpl.DISPATCHER_REQUEST_PATH_ATTR); + + // Initialize the path elements for this request + contextPath = request.getContextPath(); + pathInfo = request.getPathInfo(); + queryString = request.getQueryString(); + requestURI = request.getRequestURI(); + servletPath = request.getServletPath(); + + } + + + /** + * Set the request URI for this request. + * + * @param requestURI The new request URI + */ + void setRequestURI(String requestURI) { + + this.requestURI = requestURI; + + } + + + /** + * Set the servlet path for this request. + * + * @param servletPath The new servlet path + */ + void setServletPath(String servletPath) { + + this.servletPath = servletPath; + + } + + + /** + * Parses the parameters of this request. + * + * If parameters are present in both the query string and the request + * content, they are merged. + */ + void parseParameters() { + + if (parsedParams) { + return; + } + + parameters = new HashMap(); + parameters = copyMap(getRequest().getParameterMap()); + mergeParameters(); + parsedParams = true; + } + + + /** + * Save query parameters for this request. + * + * @param queryString The query string containing parameters for this + * request + */ + void setQueryParams(String queryString) { + this.queryParamString = queryString; + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Is this attribute name one of the special ones that is added only for + * included servlets? + * + * @param name Attribute name to be tested + */ + protected boolean isSpecial(String name) { + + for (int i = 0; i < specials.length; i++) { + if (specials[i].equals(name)) + return (true); + } + return (false); + + } + + + /** + * Get a special attribute. + * + * @return the special attribute pos, or -1 if it is not a special + * attribute + */ + protected int getSpecial(String name) { + for (int i = 0; i < specials.length; i++) { + if (specials[i].equals(name)) { + return (i); + } + } + return (-1); + } + + + /** + * Set a special attribute. + * + * @return true if the attribute was a special attribute, false otherwise + */ + protected boolean setSpecial(String name, Object value) { + for (int i = 0; i < specials.length; i++) { + if (specials[i].equals(name)) { + specialAttributes[i] = value; + return (true); + } + } + return (false); + } + + + /** + * Remove a special attribute. + * + * @return true if the attribute was a special attribute, false otherwise + */ + protected boolean removeSpecial(String name) { + for (int i = 0; i < specials.length; i++) { + if (specials[i].equals(name)) { + specialAttributes[i] = null; + return (true); + } + } + return (false); + } + + + /** + * Merge the two sets of parameter values into a single String array. + * + * @param values1 First set of values + * @param values2 Second set of values + */ + protected String[] mergeValues(Object values1, Object values2) { + + ArrayList results = new ArrayList(); + + if (values1 == null) + ; + else if (values1 instanceof String) + results.add(values1); + else if (values1 instanceof String[]) { + String values[] = (String[]) values1; + for (int i = 0; i < values.length; i++) + results.add(values[i]); + } else + results.add(values1.toString()); + + if (values2 == null) + ; + else if (values2 instanceof String) + results.add(values2); + else if (values2 instanceof String[]) { + String values[] = (String[]) values2; + for (int i = 0; i < values.length; i++) + results.add(values[i]); + } else + results.add(values2.toString()); + + String values[] = new String[results.size()]; + return ((String[]) results.toArray(values)); + + } + + + // ------------------------------------------------------ Private Methods + + + /** + * Merge the parameters from the saved query parameter string (if any), and + * the parameters already present on this request (if any), such that the + * parameter values from the query string show up first if there are + * duplicate parameter names. + */ + private void mergeParameters() { + + if ((queryParamString == null) || (queryParamString.length() < 1)) + return; + + HashMap queryParameters = new HashMap(); + String encoding = getCharacterEncoding(); + if (encoding == null) + encoding = "ISO-8859-1"; + try { + RequestUtil.parseParameters + (queryParameters, queryParamString, encoding); + } catch (Exception e) { + ; + } + Iterator keys = parameters.keySet().iterator(); + while (keys.hasNext()) { + String key = (String) keys.next(); + Object value = queryParameters.get(key); + if (value == null) { + queryParameters.put(key, parameters.get(key)); + continue; + } + queryParameters.put + (key, mergeValues(value, parameters.get(key))); + } + parameters = queryParameters; + + } + + + // ----------------------------------- AttributeNamesEnumerator Inner Class + + + /** + * Utility class used to expose the special attributes as being available + * as request attributes. + */ + protected class AttributeNamesEnumerator implements Enumeration { + + protected int pos = -1; + protected int last = -1; + protected Enumeration parentEnumeration = null; + protected String next = null; + + public AttributeNamesEnumerator() { + parentEnumeration = getRequest().getAttributeNames(); + for (int i = 0; i < specialAttributes.length; i++) { + if (getAttribute(specials[i]) != null) { + last = i; + } + } + } + + public boolean hasMoreElements() { + return ((pos != last) || (next != null) + || ((next = findNext()) != null)); + } + + public Object nextElement() { + if (pos != last) { + for (int i = pos + 1; i <= last; i++) { + if (getAttribute(specials[i]) != null) { + pos = i; + return (specials[i]); + } + } + } + String result = next; + if (next != null) { + next = findNext(); + } else { + throw new NoSuchElementException(); + } + return result; + } + + protected String findNext() { + String result = null; + while ((result == null) && (parentEnumeration.hasMoreElements())) { + String current = (String) parentEnumeration.nextElement(); + if (!isSpecial(current)) { + result = current; + } + } + return result; + } + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletResponseImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletResponseImpl.java new file mode 100644 index 000000000..42e1802f1 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletResponseImpl.java @@ -0,0 +1,1449 @@ +/* + * 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; + + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Locale; +import java.util.TimeZone; +import java.util.Vector; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.coyote.Response; +import org.apache.tomcat.lite.coyote.MessageWriter; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.UEncoder; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.ServerCookie; + +/** + * Wrapper object for the Coyote response. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + * @version $Revision: 371866 $ $Date: 2006-01-24 00:52:54 -0800 (Tue, 24 Jan 2006) $ + */ + +public class ServletResponseImpl + implements HttpServletResponse { + + /** + * Format for http response header date field + * From DateTool + */ + public static final String HTTP_RESPONSE_DATE_HEADER = + "EEE, dd MMM yyyy HH:mm:ss zzz"; + + // ----------------------------------------------------------- Constructors + + private Response resB; + + ServletResponseImpl() { + urlEncoder.addSafeCharacter('/'); + } + + + // ----------------------------------------------------- Class Variables + + + /** + * Descriptive information about this Response implementation. + */ + protected static final String info = + "org.apache.tomcat.lite/1.0"; + + + // ----------------------------------------------------- Instance Variables + + /** + * The date format we will use for creating date headers. + */ + protected SimpleDateFormat format = null; + + + // ------------------------------------------------------------- Properties + + + /** + * Set the Connector through which this Request was received. + * + * @param connector The new connector + */ + public void setConnector() { + // default size to size of one ajp-packet + outputBuffer = MessageWriter.getWriter(req.getCoyoteRequest(), getCoyoteResponse(), 4096); + outputStream = new ServletOutputStreamImpl(outputBuffer); + } + + /** + * Coyote response. + */ + //protected org.apache.coyote.Response coyoteResponse; + + + /** + * The associated output buffer. + */ + protected MessageWriter outputBuffer; + + + /** + * The associated output stream. + */ + protected ServletOutputStreamImpl outputStream; + + + /** + * The associated writer. + */ + protected PrintWriter writer; + + + /** + * The application commit flag. + */ + protected boolean appCommitted = false; + + + /** + * The included flag. + */ + protected boolean included = false; + + + /** + * The characterEncoding flag + */ + private boolean isCharacterEncodingSet = false; + + /** + * The error flag. + */ + protected boolean error = false; + + + /** + * The set of Cookies associated with this Response. + */ + protected ArrayList cookies = new ArrayList(); + + + /** + * Using output stream flag. + */ + protected boolean usingOutputStream = false; + + + /** + * Using writer flag. + */ + protected boolean usingWriter = false; + + + /** + * URL encoder. + */ + protected UEncoder urlEncoder = new UEncoder(); + + + /** + * Recyclable buffer to hold the redirect URL. + */ + protected CharChunk redirectURLCC = new CharChunk(1024); + + + // --------------------------------------------------------- Public Methods + + + /** + * Release all object references, and initialize instance variables, in + * preparation for reuse of this object. + */ + public void recycle() { + + outputBuffer.recycle(); + usingOutputStream = false; + usingWriter = false; + appCommitted = false; + included = false; + error = false; + isCharacterEncodingSet = false; + + cookies.clear(); + + outputBuffer.recycle(); + + } + + + // ------------------------------------------------------- Response Methods + + + /** + * Return the number of bytes actually written to the output stream. + */ + public int getContentCount() { + return outputBuffer.getContentWritten(); + } + + + /** + * Set the application commit flag. + * + * @param appCommitted The new application committed flag value + */ + public void setAppCommitted(boolean appCommitted) { + this.appCommitted = appCommitted; + } + + + /** + * Application commit flag accessor. + */ + public boolean isAppCommitted() { + return (this.appCommitted || isCommitted() || isSuspended() + || ((getContentLength() > 0) + && (getContentCount() >= getContentLength()))); + } + + + /** + * Return the "processing inside an include" flag. + */ + public boolean getIncluded() { + return included; + } + + + /** + * Set the "processing inside an include" flag. + * + * @param included true if we are currently inside a + * RequestDispatcher.include(), else false + */ + public void setIncluded(boolean included) { + this.included = included; + } + + + /** + * Return descriptive information about this Response implementation and + * the corresponding version number, in the format + * <description>/<version>. + */ + public String getInfo() { + return (info); + } + + + /** + * The request with which this response is associated. + */ + protected ServletRequestImpl req = null; + + /** + * Return the Request with which this Response is associated. + */ + public ServletRequestImpl getRequest() { + return (this.req); + } + + /** + * Set the Request with which this Response is associated. + * + * @param request The new associated request + */ + public void setRequest(ServletRequestImpl request) { + this.req = (ServletRequestImpl) request; + } + + + /** + * Return the output stream associated with this Response. + */ + public OutputStream getStream() { + if (outputStream == null) { + outputStream = new ServletOutputStreamImpl(outputBuffer); + } + return outputStream; + } + + + /** + * Set the output stream associated with this Response. + * + * @param stream The new output stream + */ + public void setStream(OutputStream stream) { + // This method is evil + } + + + /** + * Set the suspended flag. + * + * @param suspended The new suspended flag value + */ + public void setSuspended(boolean suspended) throws IOException { + //coyoteResponse.setCommitted(true); + flushBuffer(); + outputBuffer.setSuspended(suspended); + } + + + /** + * Suspended flag accessor. + */ + public boolean isSuspended() { + return outputBuffer.isSuspended(); + } + + + /** + * Set the error flag. + */ + public void setError() { + error = true; + } + + + /** + * Error flag accessor. + */ + public boolean isError() { + return error; + } + + + /** + * Create and return a ServletOutputStream to write the content + * associated with this Response. + * + * @exception IOException if an input/output error occurs + */ + public ServletOutputStream createOutputStream() + throws IOException { + // Probably useless + if (outputStream == null) { + outputStream = new ServletOutputStreamImpl(outputBuffer); + } + return outputStream; + } + + + /** + * Return the content length that was set or calculated for this Response. + */ + public int getContentLength() { + return getCoyoteResponse().getContentLength(); + } + + + /** + * Return the content type that was set or calculated for this response, + * or null if no content type was set. + */ + public String getContentType() { + return getCoyoteResponse().getContentType(); + } + + + /** + * Return a PrintWriter that can be used to render error messages, + * regardless of whether a stream or writer has already been acquired. + * + * @return Writer which can be used for error reports. If the response is + * not an error report returned using sendError or triggered by an + * unexpected exception thrown during the servlet processing + * (and only in that case), null will be returned if the response stream + * has already been used. + * + * @exception IOException if an input/output error occurs + */ + public PrintWriter getReporter() throws IOException { + if (outputBuffer.isNew()) { + outputBuffer.checkConverter(); + if (writer == null) { + writer = new ServletWriterImpl(outputBuffer); + } + return writer; + } else { + return null; + } + } + + + // ------------------------------------------------ ServletResponse Methods + + + /** + * Flush the buffer and commit this response. + * + * @exception IOException if an input/output error occurs + */ + public void flushBuffer() + throws IOException { + outputBuffer.flush(); + } + + + /** + * Return the actual buffer size used for this Response. + */ + public int getBufferSize() { + return outputBuffer.getBufferSize(); + } + + + /** + * Return the character encoding used for this Response. + */ + public String getCharacterEncoding() { + return (getCoyoteResponse().getCharacterEncoding()); + } + + + /** + * Return the servlet output stream associated with this Response. + * + * @exception IllegalStateException if getWriter has + * already been called for this response + * @exception IOException if an input/output error occurs + */ + public ServletOutputStream getOutputStream() + throws IOException { + + if (usingWriter) + throw new IllegalStateException + ("usingWriter"); + + usingOutputStream = true; + if (outputStream == null) { + outputStream = new ServletOutputStreamImpl(outputBuffer); + } + return outputStream; + + } + + + /** + * Return the Locale assigned to this response. + */ + public Locale getLocale() { + return (getCoyoteResponse().getLocale()); + } + + + /** + * Return the writer associated with this Response. + * + * @exception IllegalStateException if getOutputStream has + * already been called for this response + * @exception IOException if an input/output error occurs + */ + public PrintWriter getWriter() + throws IOException { + + if (usingOutputStream) + throw new IllegalStateException + ("usingOutputStream"); + + /* + * If the response's character encoding has not been specified as + * described in getCharacterEncoding (i.e., the method + * just returns the default value ISO-8859-1), + * getWriter updates it to ISO-8859-1 + * (with the effect that a subsequent call to getContentType() will + * include a charset=ISO-8859-1 component which will also be + * reflected in the Content-Type response header, thereby satisfying + * the Servlet spec requirement that containers must communicate the + * character encoding used for the servlet response's writer to the + * client). + */ + setCharacterEncoding(getCharacterEncoding()); + + usingWriter = true; + outputBuffer.checkConverter(); + if (writer == null) { + writer = new ServletWriterImpl(outputBuffer); + } + return writer; + + } + + + /** + * Has the output of this response already been committed? + */ + public boolean isCommitted() { + return (getCoyoteResponse().isCommitted()); + } + + + /** + * Clear any content written to the buffer. + * + * @exception IllegalStateException if this response has already + * been committed + */ + public void reset() { + + if (included) + return; // Ignore any call from an included servlet + + getCoyoteResponse().reset(); + //req.con.reset(); + outputBuffer.reset(); + } + + + /** + * Reset the data buffer but not any status or header information. + * + * @exception IllegalStateException if the response has already + * been committed + */ + public void resetBuffer() { + + if (isCommitted()) + throw new IllegalStateException("isCommitted"); + + outputBuffer.reset(); + + } + + + /** + * Set the buffer size to be used for this Response. + * + * @param size The new buffer size + * + * @exception IllegalStateException if this method is called after + * output has been committed for this response + */ + public void setBufferSize(int size) { + + if (isCommitted() || !outputBuffer.isNew()) + throw new IllegalStateException + ("isCommitted || !isNew"); + + outputBuffer.setBufferSize(size); + + } + + + /** + * Set the content length (in bytes) for this Response. + * Ignored for writers if non-ISO-8859-1 encoding ( we could add more + * encodings that are constant. + */ + public void setContentLength(int length) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + // writers can use variable-length encoding. + if (usingWriter && !"ISO-8859-1".equals(getCharacterEncoding())) { + return; + } + getCoyoteResponse().setContentLength(length); + + } + + + /** + * Set the content type for this Response. + * + * @param type The new content type + */ + public void setContentType(String type) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + // Ignore charset if getWriter() has already been called + if (usingWriter) { + if (type != null) { + int index = type.indexOf(";"); + if (index != -1) { + type = type.substring(0, index); + } + } + } + + getCoyoteResponse().setContentType(type); + + // Check to see if content type contains charset + if (type != null) { + int index = type.indexOf(";"); + if (index != -1) { + int len = type.length(); + index++; + while (index < len && Character.isSpace(type.charAt(index))) { + index++; + } + if (index+7 < 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) == '=') { + isCharacterEncodingSet = true; + } + } + } + } + + + /* + * Overrides the name of the character encoding used in the body + * of the request. This method must be called prior to reading + * request parameters or reading input using getReader(). + * + * @param charset String containing the name of the chararacter encoding. + */ + public void setCharacterEncoding(String charset) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + // Ignore any call made after the getWriter has been invoked + // The default should be used + if (usingWriter) + return; + + getCoyoteResponse().setCharacterEncoding(charset); + isCharacterEncodingSet = true; + } + + + + /** + * Set the Locale that is appropriate for this response, including + * setting the appropriate character encoding. + * + * @param locale The new locale + */ + public void setLocale(Locale locale) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + getCoyoteResponse().setLocale(locale); + + // Ignore any call made after the getWriter has been invoked. + // The default should be used + if (usingWriter) + return; + + if (isCharacterEncodingSet) { + return; + } + + Locale2Charset cm = req.getContext().getCharsetMapper(); + String charset = cm.getCharset( locale ); + if ( charset != null ){ + getCoyoteResponse().setCharacterEncoding(charset); + } + + } + + + // --------------------------------------------------- HttpResponse Methods + + + /** + * Return an array of all cookies set for this response, or + * a zero-length array if no cookies have been set. + */ + public Cookie[] getCookies() { + return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()])); + } + + + /** + * Return the value for the specified header, or null if this + * header has not been set. If more than one value was added for this + * name, only the first is returned; use getHeaderValues() to retrieve all + * of them. + * + * @param name Header name to look up + */ + public String getHeader(String name) { + return getCoyoteResponse().getMimeHeaders().getHeader(name); + } + + + /** + * Return an array of all the header names set for this response, or + * a zero-length array if no headers have been set. + */ + public String[] getHeaderNames() { + + MimeHeaders headers = getCoyoteResponse().getMimeHeaders(); + int n = headers.size(); + String[] result = new String[n]; + for (int i = 0; i < n; i++) { + result[i] = headers.getName(i).toString(); + } + return result; + + } + + + /** + * Return an array of all the header values associated with the + * specified header name, or an zero-length array if there are no such + * header values. + * + * @param name Header name to look up + */ + public String[] getHeaderValues(String name) { + + Enumeration enumeration = getCoyoteResponse().getMimeHeaders().values(name); + Vector result = new Vector(); + while (enumeration.hasMoreElements()) { + result.addElement(enumeration.nextElement()); + } + String[] resultArray = new String[result.size()]; + result.copyInto(resultArray); + return resultArray; + + } + + + /** + * Return the error message that was set with sendError() + * for this Response. + */ + public String getMessage() { + return getCoyoteResponse().getMessage(); + } + + + /** + * Return the HTTP status code associated with this Response. + */ + public int getStatus() { + return getCoyoteResponse().getStatus(); + } + + + /** + * Reset this response, and specify the values for the HTTP status code + * and corresponding message. + * + * @exception IllegalStateException if this response has already been + * committed + */ + public void reset(int status, String message) { + reset(); + setStatus(status, message); + } + + + // -------------------------------------------- HttpServletResponse Methods + + + /** + * Add the specified Cookie to those that will be included with + * this Response. + * + * @param cookie Cookie to be added + */ + public void addCookie(final Cookie cookie) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + cookies.add(cookie); + + final StringBuffer sb = new StringBuffer(); + ServerCookie.appendCookieValue + (sb, cookie.getVersion(), cookie.getName(), cookie.getValue(), + cookie.getPath(), cookie.getDomain(), cookie.getComment(), + cookie.getMaxAge(), cookie.getSecure(), false); + + // the header name is Set-Cookie for both "old" and v.1 ( RFC2109 ) + // RFC2965 is not supported by browsers and the Servlet spec + // asks for 2109. + addHeader("Set-Cookie", sb.toString()); + + } + + + /** + * Add the specified date header to the specified value. + * + * @param name Name of the header to set + * @param value Date value to be set + */ + public void addDateHeader(String name, long value) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) { + return; + } + + if (format == null) { + format = new SimpleDateFormat(HTTP_RESPONSE_DATE_HEADER, + Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + addHeader(name, FastHttpDateFormat.formatDate(value, format)); + + } + + + /** + * Add the specified header to the specified value. + * + * @param name Name of the header to set + * @param value Value to be set + */ + public void addHeader(String name, String value) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + getCoyoteResponse().addHeader(name, value); + + } + + + /** + * Add the specified integer header to the specified value. + * + * @param name Name of the header to set + * @param value Integer value to be set + */ + public void addIntHeader(String name, int value) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + addHeader(name, "" + value); + + } + + + /** + * Has the specified header been set already in this response? + * + * @param name Name of the header to check + */ + public boolean containsHeader(String name) { + // Need special handling for Content-Type and Content-Length due to + // special handling of these in coyoteResponse + char cc=name.charAt(0); + if(cc=='C' || cc=='c') { + if(name.equalsIgnoreCase("Content-Type")) { + // Will return null if this has not been set + return (getCoyoteResponse().getContentType() != null); + } + if(name.equalsIgnoreCase("Content-Length")) { + // -1 means not known and is not sent to client + return (getCoyoteResponse().getContentLengthLong() != -1); + } + } + + return getCoyoteResponse().containsHeader(name); + } + + + /** + * Encode the session identifier associated with this response + * into the specified redirect URL, if necessary. + * + * @param url URL to be encoded + */ + public String encodeRedirectURL(String url) { + + if (isEncodeable(toAbsolute(url))) { + return (toEncoded(url, req.getSession().getId())); + } else { + return (url); + } + + } + + + /** + * Encode the session identifier associated with this response + * into the specified redirect URL, if necessary. + * + * @param url URL to be encoded + * + * @deprecated As of Version 2.1 of the Java Servlet API, use + * encodeRedirectURL() instead. + */ + public String encodeRedirectUrl(String url) { + return (encodeRedirectURL(url)); + } + + + /** + * Encode the session identifier associated with this response + * into the specified URL, if necessary. + * + * @param url URL to be encoded + */ + public String encodeURL(String url) { + + String absolute = toAbsolute(url); + if (isEncodeable(absolute)) { + // W3c spec clearly said + if (url.equalsIgnoreCase("")){ + url = absolute; + } + return (toEncoded(url, req.getSession().getId())); + } else { + return (url); + } + + } + + + /** + * Encode the session identifier associated with this response + * into the specified URL, if necessary. + * + * @param url URL to be encoded + * + * @deprecated As of Version 2.1 of the Java Servlet API, use + * encodeURL() instead. + */ + public String encodeUrl(String url) { + return (encodeURL(url)); + } + + + /** + * Send an acknowledgment of a request. + * + * @exception IOException if an input/output error occurs + */ + public void sendAcknowledgement() + throws IOException { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + getCoyoteResponse().acknowledge(); + + } + + + /** + * Send an error response with the specified status and a + * default message. + * + * @param status HTTP status code to send + * + * @exception IllegalStateException if this response has + * already been committed + * @exception IOException if an input/output error occurs + */ + public void sendError(int status) + throws IOException { + sendError(status, null); + } + + + /** + * Send an error response with the specified status and message. + * + * @param status HTTP status code to send + * @param message Corresponding message to send + * + * @exception IllegalStateException if this response has + * already been committed + * @exception IOException if an input/output error occurs + */ + public void sendError(int status, String message) + throws IOException { + + if (isCommitted()) + throw new IllegalStateException + ("isCommitted"); + + // Ignore any call from an included servlet + if (included) + return; + + setError(); + + getCoyoteResponse().setStatus(status); + getCoyoteResponse().setMessage(message); + + // Clear any data content that has been buffered + resetBuffer(); + + // Cause the response to be finished (from the application perspective) + String statusPage = req.getContext().findStatusPage(status); + + if (statusPage != null) { + req.getContext().handleStatusPage(req, this, status, statusPage); + } else { + // Send a default message body. + // TODO: maybe other mechanism to customize default. + defaultStatusPage(status, message); + } + setSuspended(true); + } + + /** + * Default handler for status code != 200 + */ + void defaultStatusPage(int status, String message) + throws IOException { + setContentType("text/html"); + if (status > 400 && status < 600) { + if (getOutputBuffer().getBytesWritten() == 0) { + getOutputBuffer().write("

Status: " + + status + "

Message: " + message + + "

"); + getOutputBuffer().flush(); + } + } + } + + + + /** + * Send a temporary redirect to the specified redirect location URL. + * + * @param location Location URL to redirect to + * + * @exception IllegalStateException if this response has + * already been committed + * @exception IOException if an input/output error occurs + */ + public void sendRedirect(String location) + throws IOException { + + if (isCommitted()) + throw new IllegalStateException + ("isCommitted"); + + // Ignore any call from an included servlet + if (included) + return; + + // Clear any data content that has been buffered + resetBuffer(); + + // Generate a temporary redirect to the specified location + try { + String absolute = toAbsolute(location); + setStatus(SC_FOUND); + setHeader("Location", absolute); + } catch (IllegalArgumentException e) { + setStatus(SC_NOT_FOUND); + } + + // Cause the response to be finished (from the application perspective) + setSuspended(true); + + } + + + /** + * Set the specified date header to the specified value. + * + * @param name Name of the header to set + * @param value Date value to be set + */ + public void setDateHeader(String name, long value) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) { + return; + } + + if (format == null) { + format = new SimpleDateFormat(HTTP_RESPONSE_DATE_HEADER, + Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + setHeader(name, FastHttpDateFormat.formatDate(value, format)); + + } + + + /** + * Set the specified header to the specified value. + * + * @param name Name of the header to set + * @param value Value to be set + */ + public void setHeader(String name, String value) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + getCoyoteResponse().setHeader(name, value); + + } + + + /** + * Set the specified integer header to the specified value. + * + * @param name Name of the header to set + * @param value Integer value to be set + */ + public void setIntHeader(String name, int value) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + setHeader(name, "" + value); + + } + + + /** + * Set the HTTP status to be returned with this response. + * + * @param status The new HTTP status + */ + public void setStatus(int status) { + setStatus(status, null); + } + + + /** + * Set the HTTP status and message to be returned with this response. + * + * @param status The new HTTP status + * @param message The associated text message + * + * @deprecated As of Version 2.1 of the Java Servlet API, this method + * has been deprecated due to the ambiguous meaning of the message + * parameter. + */ + public void setStatus(int status, String message) { + + if (isCommitted()) + return; + + // Ignore any call from an included servlet + if (included) + return; + + getCoyoteResponse().setStatus(status); + getCoyoteResponse().setMessage(message); + + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Return true if the specified URL should be encoded with + * a session identifier. This will be true if all of the following + * conditions are met: + *
    + *
  • The request we are responding to asked for a valid session + *
  • The requested session ID was not received via a cookie + *
  • The specified URL points back to somewhere within the web + * application that is responding to this request + *
+ * + * @param location Absolute URL to be validated + */ + protected boolean isEncodeable(final String location) { + + if (location == null) + return (false); + + // Is this an intra-document reference? + if (location.startsWith("#")) + return (false); + + // Are we in a valid session that is not using cookies? + final ServletRequestImpl hreq = req; + final HttpSession session = hreq.getSession(false); + if (session == null) + return (false); + if (hreq.isRequestedSessionIdFromCookie()) + return (false); + + // Is this a valid absolute URL? + URL url = null; + try { + url = new URL(location); + } catch (MalformedURLException e) { + return (false); + } + + // Does this URL match down to (and including) the context path? + if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) + return (false); + if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) + return (false); + int serverPort = hreq.getServerPort(); + if (serverPort == -1) { + if ("https".equals(hreq.getScheme())) + serverPort = 443; + else + serverPort = 80; + } + int urlPort = url.getPort(); + if (urlPort == -1) { + if ("https".equals(url.getProtocol())) + urlPort = 443; + else + urlPort = 80; + } + if (serverPort != urlPort) + return (false); + + String contextPath = req.getContext().getContextPath(); + if (contextPath != null) { + String file = url.getFile(); + if ((file == null) || !file.startsWith(contextPath)) + return (false); + if( file.indexOf(";jsessionid=" + session.getId()) >= 0 ) + return (false); + } + + // This URL belongs to our web application, so it is encodeable + return (true); + + } + + + /** + * 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 + */ + private String toAbsolute(String location) { + + if (location == null) + return (location); + + boolean leadingSlash = location.startsWith("/"); + + if (leadingSlash || !hasScheme(location)) { + + redirectURLCC.recycle(); + + String scheme = req.getScheme(); + String name = req.getServerName(); + int port = req.getServerPort(); + + try { + redirectURLCC.append(scheme, 0, scheme.length()); + redirectURLCC.append("://", 0, 3); + redirectURLCC.append(name, 0, name.length()); + if ((scheme.equals("http") && port != 80) + || (scheme.equals("https") && port != 443)) { + redirectURLCC.append(':'); + String portS = port + ""; + redirectURLCC.append(portS, 0, portS.length()); + } + if (!leadingSlash) { + String relativePath = req.getDecodedRequestURI(); + int pos = relativePath.lastIndexOf('/'); + relativePath = relativePath.substring(0, pos); + + String encodedURI = null; + encodedURI = urlEncoder.encodeURL(relativePath); + redirectURLCC.append(encodedURI, 0, encodedURI.length()); + redirectURLCC.append('/'); + } + redirectURLCC.append(location, 0, location.length()); + } catch (IOException e) { + IllegalArgumentException iae = + new IllegalArgumentException(location); + iae.initCause(e); + throw iae; + } + + return redirectURLCC.toString(); + + } else { + + return (location); + + } + + } + + + /** + * Determine if a URI string has a scheme component. + */ + private 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 == '.'; + } + + + /** + * Return the specified URL with the specified session identifier + * suitably encoded. + * + * @param url URL to be encoded with the session id + * @param sessionId Session id to be included in the encoded URL + */ + protected String toEncoded(String url, String sessionId) { + + if ((url == null) || (sessionId == null)) + return (url); + + String path = url; + String query = ""; + String anchor = ""; + int question = url.indexOf('?'); + if (question >= 0) { + path = url.substring(0, question); + query = url.substring(question); + } + int pound = path.indexOf('#'); + if (pound >= 0) { + anchor = path.substring(pound); + path = path.substring(0, pound); + } + StringBuffer sb = new StringBuffer(path); + if( sb.length() > 0 ) { // jsessionid can't be first. + sb.append(";jsessionid="); + sb.append(sessionId); + } + sb.append(anchor); + sb.append(query); + return (sb.toString()); + + } + + + public int getBytesWritten() { + return outputBuffer.getBytesWritten(); + } + + public MessageWriter getOutputBuffer() { + return outputBuffer; + } + + public CharSequence getResponseHeader(String name) { + MessageBytes v = getCoyoteResponse().getMimeHeaders().getValue(name); + return (v == null) ? null : v.toString(); + } + + + public Response getCoyoteResponse() { + return resB; + } + + + public void setCoyoteResponse(Response resB) { + this.resB = resB; + } + + +} + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletResponseIncludeWrapper.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletResponseIncludeWrapper.java new file mode 100644 index 000000000..6f8b26077 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletResponseIncludeWrapper.java @@ -0,0 +1,116 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + + +import java.io.IOException; +import java.util.Locale; + +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + + +/** + * Wrapper around the response object received as parameter to + * RequestDispatcher.include(). + * + * @author Costin Manolache + */ +public class ServletResponseIncludeWrapper extends HttpServletResponseWrapper { + public ServletResponseIncludeWrapper(ServletResponse current) { + super((HttpServletResponse) current); + } + + // Not overriden: + /* + public boolean containsHeader(String name) + public String encodeRedirectUrl(String url) + public String encodeRedirectURL(String url) + public String encodeUrl(String url) + public String encodeURL(String url) + public void flushBuffer() throws IOException + public int getBufferSize() + public String getCharacterEncoding() + public String getContentType() + public Locale getLocale() + public ServletOutputStream getOutputStream() throws IOException + public ServletResponse getResponse() + public PrintWriter getWriter() throws IOException + public boolean isCommitted() + public void resetBuffer() + public void setCharacterEncoding(String charset) + public void setResponse(ServletResponse response) + */ + + public void reset() { + if (getResponse().isCommitted()) + getResponse().reset(); + else + throw new IllegalStateException(); + } + + public void setContentLength(int len) { + } + + public void setContentType(String type) { + } + + public void setLocale(Locale loc) { + } + + public void setBufferSize(int size) { + } + + public void addCookie(Cookie cookie) { + } + + public void addDateHeader(String name, long value) { + } + + public void addHeader(String name, String value) { + } + + public void addIntHeader(String name, int value) { + } + + public void sendError(int sc) throws IOException { + } + + public void sendError(int sc, String msg) throws IOException { + } + + public void sendRedirect(String location) throws IOException { + } + + public void setDateHeader(String name, long value) { + } + + public void setHeader(String name, String value) { + } + + public void setIntHeader(String name, int value) { + } + + public void setStatus(int sc) { + } + + public void setStatus(int sc, String msg) { + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletWriterImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletWriterImpl.java new file mode 100644 index 000000000..a27d69301 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/ServletWriterImpl.java @@ -0,0 +1,289 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; + +/** + * Coyote implementation of the servlet writer. + * + * @author Remy Maucherat + */ +public class ServletWriterImpl + extends PrintWriter { + + + // -------------------------------------------------------------- Constants + + + private static final char[] LINE_SEP = { '\r', '\n' }; + + + // ----------------------------------------------------- Instance Variables + + + protected Writer ob; + protected boolean error = false; + + + // ----------------------------------------------------------- Constructors + + + public ServletWriterImpl(Writer ob) { + super(ob); + this.ob = ob; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Prevent cloning the facade. + */ + protected Object clone() + throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Clear facade. + */ + void clear() { + ob = null; + } + + + /** + * Recycle. + */ + void recycle() { + error = false; + } + + + // --------------------------------------------------------- Writer Methods + + + public void flush() { + + if (error) + return; + + try { + ob.flush(); + } catch (IOException e) { + error = true; + } + + } + + + public void close() { + + // We don't close the PrintWriter - super() is not called, + // so the stream can be reused. We close ob. + try { + ob.close(); + } catch (IOException ex ) { + ; + } + error = false; + + } + + + public boolean checkError() { + flush(); + return error; + } + + + public void write(int c) { + + if (error) + return; + + try { + ob.write(c); + } catch (IOException e) { + error = true; + } + + } + + + public void write(char buf[], int off, int len) { + + if (error) + return; + + try { + ob.write(buf, off, len); + } catch (IOException e) { + error = true; + } + + } + + + public void write(char buf[]) { + write(buf, 0, buf.length); + } + + + public void write(String s, int off, int len) { + + if (error) + return; + + try { + ob.write(s, off, len); + } catch (IOException e) { + error = true; + } + + } + + + public void write(String s) { + write(s, 0, s.length()); + } + + + // ---------------------------------------------------- PrintWriter Methods + + + public void print(boolean b) { + if (b) { + write("true"); + } else { + write("false"); + } + } + + + public void print(char c) { + write(c); + } + + + public void print(int i) { + write(String.valueOf(i)); + } + + + public void print(long l) { + write(String.valueOf(l)); + } + + + public void print(float f) { + write(String.valueOf(f)); + } + + + public void print(double d) { + write(String.valueOf(d)); + } + + + public void print(char s[]) { + write(s); + } + + + public void print(String s) { + if (s == null) { + s = "null"; + } + write(s); + } + + + public void print(Object obj) { + write(String.valueOf(obj)); + } + + + public void println() { + write(LINE_SEP); + } + + + public void println(boolean b) { + print(b); + println(); + } + + + public void println(char c) { + print(c); + println(); + } + + + public void println(int i) { + print(i); + println(); + } + + + public void println(long l) { + print(l); + println(); + } + + + public void println(float f) { + print(f); + println(); + } + + + public void println(double d) { + print(d); + println(); + } + + + public void println(char c[]) { + print(c); + println(); + } + + + public void println(String s) { + print(s); + println(); + } + + + public void println(Object o) { + print(o); + println(); + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/TomcatLite.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/TomcatLite.java new file mode 100644 index 000000000..5ddd9cde2 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/TomcatLite.java @@ -0,0 +1,642 @@ +/* + * 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; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.integration.ObjectManager; +import org.apache.tomcat.integration.simple.SimpleObjectManager; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.UriNormalizer; +import org.apache.tomcat.util.http.mapper.MappingData; + +/** + * Simpler, lower footprint serlvet engine. + * + * Uses ObjectManager to integate with an embedding app + * - the object names it uses: + * + * Internal objects created by Tomcat and registered for management + * and injection: + * - Servlet:CONTEXT_PATH:SERVLETNAME - a ServletWrapper + * - ServletContext:CONTEXT_PATH + * - ProtocolHandler:ep-PORT - coyote ProtocolHandler + * - CoyoteServer:CoyoteServer-PORT + * - CoyoteAdapter:PATH - for the coyote used Adapter + * - TomcatLite - this object. + * - Connector - the connector object + * + * Plugins to be constructed by framework ( defaults set in initDefaults ): + * - UserSessionManager + * - UserTemplateClassMapper + * - ContextPreinitListener + * - Connector + * - WebappServletMapper + * - WebappFilterMapper + * - default-servlet + * - jspwildcard-servlet + * - Foo-servlet - servlet named Foo + * + * + * @author Costin Manolache + */ +public class TomcatLite implements Runnable { + + private String serverDirName; + private File workDir; + + // all contexts - hostMapper knows about hostnames and how they are mapped. + // this shouldn't be needed if we want to delegate ctx management + private ArrayList contexts = new ArrayList(); + + URLClassLoader contextParentLoader; + + // Discovered or default Host/Context mapper + Filter hostMapper; + + // Servlets to preload in each context, configurable from CLI or API + Map preloadServlets = new HashMap(); + Map preloadMappings = new HashMap(); + + Map ctxDefaultInitParam = new HashMap(); + + Connector coyoteAdapter; + + ObjectManager om; + + static String SERVLETS_PACKAGE = "org.apache.tomcat.servlets"; + + + protected boolean daemon = false; + + public TomcatLite() { + } + + public TomcatLite(ObjectManager om) { + this.setObjectManager(om); + } + + // --------------- start/stop --------------- + + /** + * Return the object manager associated with this tomcat. + * If none set, create a minimal one with the default + * values. + */ + public ObjectManager getObjectManager() { + if (om == null) { + // Defaults. + om = new ObjectManager(); + SimpleObjectManager props = new SimpleObjectManager(om); + // Init defaults. If using a custom OM, you should register + // at the default objects as well. + props.loadResource("org/apache/tomcat/lite/config.properties"); + } + return om; + } + + public void setObjectManager(ObjectManager om) { + this.om = om; + } + + public List/**/ getWebapps() { + return contexts; + } + + public URLClassLoader getContextParentLoader() { + if (contextParentLoader == null) { + + ClassLoader parent = this.getClass().getClassLoader(); + contextParentLoader = new URLClassLoader(new URL[] {}, + parent); + + /*if (engineRepo == null) { + engineRepo = new Repository(); + engineRepo.setParentClassLoader(parent); + } + + contextParentLoader = + engineRepo.getClassLoader(); + */ + } + return contextParentLoader; + } + + public void start() throws IOException { + long t0 = System.currentTimeMillis(); + + // start all contexts + // init all contexts + Iterator i1 = contexts.iterator(); + while (i1.hasNext()) { + ServletContextImpl ctx = (ServletContextImpl) i1.next(); + try { + ctx.start(); + } catch (Throwable e) { + e.printStackTrace(); + } + } + long t1 = System.currentTimeMillis(); + System.err.println("Engine.start() " + (t1-t0)); + } + + + /** + * Add a context - used for IntrospectionUtils. + * + * ContextPath:ContextBaseDir + */ + public void setContext(String c) throws ServletException { + String[] pathDir = c.split(":", 2); + addServletContext("", pathDir[1], pathDir[0]); + } + + public void setServletContexts(List c) throws ServletException { + for (ServletContext ctx: c) { + addServletContext((ServletContextImpl) ctx); + } + } + + public void setPreload(String servletNameClass) { + String[] nv = servletNameClass.split(":"); + preloadServlets.put(nv[0], nv[1]); + } + + public void addPreload(String servletName, String servletClassName) { + preloadServlets.put(servletName, servletClassName); + } + + public void setDefaultInitParam(String nameValue) { + String[] nv = nameValue.split(":"); + ctxDefaultInitParam.put(nv[0], nv[1]); + } + + public void addDefaultInitParam(String name, String value) { + ctxDefaultInitParam.put(name, value); + } + + public void setPreloadMappings(String servletPath) { + String[] nv = servletPath.split(":"); + preloadMappings.put(nv[0], nv[1]); + } + + public void addPreloadMapping(String servletName, String path) { + preloadMappings.put(servletName, path); + } + + public void stop() { + Iterator i1 = contexts.iterator(); + while (i1.hasNext()) { + ServletContextImpl ctx = (ServletContextImpl) i1.next(); + try { + ctx.destroy(); + } catch (Throwable e) { + e.printStackTrace(); + } + } + stopConnector(); + } + + // -------------- Context add/remove -------------- + + public static String[] DEFAULT_WELCOME = { "index.html" }; + + public void addServletContext(ServletContextImpl ctx) throws ServletException { + ctx.setTomcat(this); + if (hostMapper == null) { + hostMapper = new WebappContextMapper(); + } + + ((WebappContextMapper) hostMapper).addHost(ctx.getHostname(), null); + ((WebappContextMapper) hostMapper).addContext(ctx.getHostname(), + ctx); + + contexts.add(ctx); + + getObjectManager().bind("ServletContext:" + ctx.getContextPath(), + ctx); + + } + + /** + * Add a context. + * + * web.xml will be read as part of init, and the initialization will be + * part of start or lazy. + * + * @param hostname - "" + * if default host, or string to be matched with Host header + * @param basePath = directory where the webapp is installed + * @param path - + * context path, "/" for root, "/examples", etc + * @return a servlet context + * @throws ServletException + */ + public ServletContext addServletContext(String hostname, + String basePath, + String path) + throws ServletException + { + ServletContextImpl ctx = new ServletContextImpl(); + ctx.setContextPath(path); + ctx.setBasePath(basePath); + addServletContext(ctx); + return ctx; + } + + public void removeServletContext(ServletContext sctx) + throws ServletException + { + ServletContextImpl ctx = (ServletContextImpl) sctx; + // TODO: destroy all servlets and filters + // TODO: clean up any other reference to the context or its loader + notifyRemove(ctx); + } + + + /** + * Required for ServletContext.getContext(uri); + * @throws ServletException + * @throws IOException + */ + public ServletContextImpl getContext(ServletContextImpl impl, String uri) + throws IOException, ServletException { + // Create a request - needs to be simplified + ServletRequestImpl req = createMessage(impl, impl.contextPath, uri); + hostMapper.doFilter(req, null, null); + return req.getContext(); + } + + public ServletResponseImpl service(ServletRequestImpl req) throws IOException, Exception { + ServletResponseImpl res = req.getResponse(); + service(req, res); + endRequest(req, res); + return res; + } + + public void service(ServletRequestImpl req, ServletResponseImpl res) + throws Exception, IOException { + // parse the session id from URI + req.parseSessionId(); + + try { + UriNormalizer.decodeRequest(req.getCoyoteRequest().decodedURI(), + req.getCoyoteRequest().requestURI(), + req.getCoyoteRequest().getURLDecoder()); + } catch(IOException ioe) { + res.setStatus(400); + return; + } + + MappingData mapRes = req.getMappingData(); + try { + // TODO: make hostMapper configurable, implement interface, + // simple to set on ROOT context + hostMapper.doFilter(req, null, null); + + + ServletContextImpl ctx = (ServletContextImpl)mapRes.context; + if( ctx == null ) { + // TODO: 404 + res.setStatus(404); + return; + } + req.setContext(ctx); + + // bind class loader + Thread.currentThread().setContextClassLoader(ctx.getClassLoader()); + + WebappServletMapper mapper = ctx.getMapper(); + mapper.map(req.getCoyoteRequest().decodedURI(), mapRes); + + // Possible redirect + MessageBytes redirectPathMB = mapRes.redirectPath; + if (!redirectPathMB.isNull()) { + String redirectPath = res.urlEncoder.encodeURL(redirectPathMB.toString()); + String query = req.getQueryString(); + if (req.isRequestedSessionIdFromURL()) { + // This is not optimal, but as this is not very common, it + // shouldn't matter + redirectPath = redirectPath + ";" + ServletRequestImpl.SESSION_PARAMETER_NAME + "=" + + req.getRequestedSessionId(); + } + if (query != null) { + // This is not optimal, but as this is not very common, it + // shouldn't matter + redirectPath = redirectPath + "?" + query; + } + res.sendRedirect(redirectPath); + return; + } + + req.parseSessionCookiesId(); + + ServletConfigImpl h=(ServletConfigImpl)mapRes.wrapper; + if (h != null) { + req.setWrapper((ServletConfigImpl)mapRes.wrapper); + serviceServlet(ctx, req, res, h, mapRes ); + // send the response... + + //res.flushBuffer(); + + // Recycle the wrapper request and response + //req.recycle(); + //res.recycle(); + } + } finally { + if(mapRes != null ) + mapRes.recycle(); + } + } + + /** Coyote / mapper adapter. Result of the mapper. + * + * This replaces the valve chain, the path is: + * 1. coyote calls mapper -> result Adapter + * 2. service is called. Additional filters are set on the wrapper. + * @param mapRes + */ + private void serviceServlet(ServletContextImpl ctx, + ServletRequestImpl req, + ServletResponseImpl res, + ServletConfigImpl servletConfig, + MappingData mapRes) + throws IOException { + Servlet servlet = null; + try { + if (servletConfig.isUnavailable()) { + handleUnavailable(res, servletConfig); + return; + } + try { + servlet = servletConfig.allocate(); + } catch(ServletException ex) { + handleUnavailable(res, servletConfig); + } + WebappFilterMapper filterMap = ctx.getFilterMapper(); + FilterChainImpl chain = + filterMap.createFilterChain(req, servletConfig, servlet); + + try { + if (chain == null) { + if (servlet != null) { + servlet.service(req, res); + } else { + System.err.println("No servlet " + req.getRequestURI()); + res.sendError(404); + } + } else { + chain.doFilter(req, res); + } + } catch(UnavailableException ex) { + servletConfig.unavailable(ex); + handleUnavailable(res, servletConfig); + return; + } + + // servlet completed without exception. Check status + int status = res.getStatus(); + if (status != 200 && !res.isCommitted()) { + String statusPage = ctx.findStatusPage(status); + + if (statusPage != null) { + ctx.handleStatusPage(req, res, status, statusPage); + } else { + // Send a default message body. + // TODO: maybe other mechanism to customize default. + res.defaultStatusPage(status, res.getMessage()); + } + } + } catch (Throwable t) { + ctx.handleError(req, res, t); + } finally { + if (servlet != null) { + servletConfig.deallocate(servlet); + } + } + } + + private void handleUnavailable(ServletResponseImpl response, + ServletConfigImpl servletConfig) + throws IOException { + long available = servletConfig.getAvailable(); + if ((available > 0L) && (available < Long.MAX_VALUE)) + response.setDateHeader("Retry-After", available); + // TODO: handle via error pages ! + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + "Service unavailable"); + } + + + // ------------ Notifications for JMX ---------------- + + void notifyAdd(Object o) { + } + + void notifyRemove(Object o) { + } + + public void setServerDir(String dir) { + this.serverDirName = dir; + } + + public File getWork() { + if (workDir == null) { + if (serverDirName == null) { + serverDirName = "./"; + } + File rootDirFile = new File(serverDirName); + workDir = new File(rootDirFile, "tomcat-work"); + if (workDir.exists()) { + workDir.mkdirs(); + } + } + return workDir; + } + + /** + * Init + * + * @throws ServletException + * @throws IOException + */ + public void init() throws ServletException, IOException { + getObjectManager().bind("TomcatLite", this); + if (contexts.size() == 0) { + setContext("/:./webapps/ROOT"); + } + getConnector().setObjectManager(getObjectManager()); + Iterator i1 = contexts.iterator(); + while (i1.hasNext()) { + ServletContextImpl ctx = (ServletContextImpl) i1.next(); + try { + ctx.init(); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + /** + * Initialize an webapp and add it to the server. + * - load web.xml + * - call + * + * @param rootDir + * @param path + * @param deployServlet + */ + public void init(String rootDir, String path) + throws ServletException, IOException { + + long t0 = System.currentTimeMillis(); + + ServletContextImpl ctx = + (ServletContextImpl)addServletContext(null, + rootDir, + path); + ctx.init(); + + long t1 = System.currentTimeMillis(); + + // At this point all config is loaded. Contexts are not yet init() + // - this will happen on start. + System.err.println("Context.init() " + path + " " + (t1-t0)); + } + + /** + * Get an empty request/response pair ( response available + * as getResponse() ). Optional set input and output buffers. + * + * This can be used for a connector-less interface to tomcat lite. + * + * TODO: make it independent of coyote ! + */ + + public ServletRequestImpl createMessage() { + ServletRequestImpl req = new ServletRequestImpl(); + ServletResponseImpl res = req.getResponse(); + + getConnector().initRequest(req, res); + + req.getCoyoteRequest().method().setString("GET"); + req.getCoyoteRequest().protocol().setString("HTTP/1.1"); + + return req; + } + + /** + * Used internally for mapping. + */ + private ServletRequestImpl createMessage(ServletContextImpl deployCtx, + String ctxPath, + String reqPath) { + ServletRequestImpl req = createMessage(); + req.setContextPath(ctxPath); + req.setContext(deployCtx); + req.setRequestURI(ctxPath + reqPath); + return req; + } + + + /** + * Set a global filter that will be used for context mapping. + * + * The filter will get a request, with requestUri and hostname set. + * + * It needs to compute the context path and set it as an attribute. + * + * Advanced features may include on-demand loading of webapps, large scale + * virtual hosting, etc. + */ + public void setContextMapper(Filter hostMapper2) { + this.hostMapper = hostMapper2; + } + + public void endRequest(ServletRequestImpl req, + ServletResponseImpl res) throws IOException { + res.outputBuffer.flush(); + res.getCoyoteResponse().finish(); + } + + public Connector getConnector() { + if (coyoteAdapter == null) { + coyoteAdapter = (Connector) getObjectManager().get(Connector.class); + setConnector(coyoteAdapter); + } + return coyoteAdapter; + } + + public void setConnector(Connector c) { + coyoteAdapter = c; + coyoteAdapter.setTomcatLite(this); + getObjectManager().bind("Connector", coyoteAdapter); + } + + + public void setDaemon(boolean d) { + getConnector(); + if (coyoteAdapter != null) { + coyoteAdapter.setDaemon(d); + } + } + + public void startConnector() { + getConnector(); + if (coyoteAdapter != null) { + coyoteAdapter.start(); + } + } + + public void stopConnector() { + getConnector(); + if (coyoteAdapter != null) { + coyoteAdapter.stop(); + } + } + + public void run() { + try { + execute(); + } catch (ServletException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void execute() throws ServletException, IOException { + init(); + start(); + startConnector(); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappContextMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappContextMapper.java new file mode 100644 index 000000000..7fc2c311c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappContextMapper.java @@ -0,0 +1,132 @@ +package org.apache.tomcat.lite; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.mapper.MappingData; + +/** + * This handles host and context mapping. + * + * The default implementation for tomcat lite is very limitted - + * no support for virtual hosts, no support for contexts deeper than + * 1 level. The intention is to override this with more advanced mappers. + * + * With 'ConfigurableHosts' interface it is possible for a smart + * mapper to load/unload virtual hosts at runtime, maybe from a + * database. It should be possible to use databases to store huge number + * of hosts or webapps. + * + */ +public class WebappContextMapper implements Filter { + + ServletContext rootContext; + Map contexts = new HashMap(); + + public WebappContextMapper() { + } + + public void addHost(String name, String[] aliases) { + } + + /** + * Add a new Context to an existing Host. + * + * @param hostName Virtual host name this context belongs to + * @param contextPath Context path + * @param context Context object + * @param welcomeResources Welcome files defined for this context + * @param resources Static resources of the context + */ + public void addContext(String hostName, + ServletContext context) + throws ServletException + { + String path = context.getContextPath(); + if (path.lastIndexOf("/") > 0) { + throw new ServletException("Base context mapper supports only one level"); + } + if ("/".equals(path)) { + rootContext = context; + } + MessageBytes mb = MessageBytes.newInstance(); + mb.setChars(path.toCharArray(), 0, path.length()); + contexts.put(mb, context); + } + + + /** + * 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) + throws ServletException { + if ("/".equals(path)) { + rootContext = null; + } + contexts.remove(path); + } + + /** + * Map the specified URI. + */ + private void mapContext(ServletRequestImpl req) + throws IOException, ServletException { + MessageBytes uriMB = req.getDecodedRequestURIMB(); + MappingData mappingData = req.getMappingData(); + uriMB.toChars(); + CharChunk uri = uriMB.getCharChunk(); + + + if (uri.length() < 2 || contexts.size() == 0) { + mappingData.context = rootContext; + if (rootContext != null) { + mappingData.contextPath.setString(rootContext.getContextPath()); + } + return; + } + + int nextSlash = uri.indexOf('/', 1); + if (nextSlash == -1) { + nextSlash = uri.length(); + } + mappingData.contextPath.setChars(uri.getChars(), 0, nextSlash); + ServletContext servletContext = contexts.get(mappingData.contextPath); + + if (servletContext != null) { + mappingData.context = servletContext; + } else { + mappingData.context = rootContext; + if (rootContext != null) { + mappingData.contextPath.setString(rootContext.getContextPath()); + } + } + } + + public void init(FilterConfig filterConfig) throws ServletException { + } + + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) + throws IOException, ServletException { + ServletRequestImpl req = (ServletRequestImpl)request; + mapContext(req); + } + + public void destroy() { + } +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappFilterMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappFilterMapper.java new file mode 100644 index 000000000..78847a346 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappFilterMapper.java @@ -0,0 +1,529 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.lite; + + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.apache.tomcat.servlets.util.RequestUtil; + +/** + * First filter after the context and servlet are mapped. It will add + * web.xml-defined filters. + * + * costin: This is another mapping - done in RequestDispatcher or initial + * mapping. + * Also: StandardHostValve - sets attribute for error pages, + * StandardWrapperValve - mapping per invocation + * + * @author Greg Murray + * @author Remy Maucherat + */ +public class WebappFilterMapper implements Filter { + + + // -------------------------------------------------------------- Constants + + + public static final int ERROR = 1; + public static final Integer ERROR_INTEGER = new Integer(ERROR); + public static final int FORWARD = 2; + public static final Integer FORWARD_INTEGER = new Integer(FORWARD); + public static final int INCLUDE = 4; + public static final Integer INCLUDE_INTEGER = new Integer(INCLUDE); + public static final int REQUEST = 8; + public static final Integer REQUEST_INTEGER = new Integer(REQUEST); + + /** + * Request dispatcher state. + */ + public static final String DISPATCHER_TYPE_ATTR = + "org.apache.catalina.core.DISPATCHER_TYPE"; + + /** + * Request dispatcher path. + */ + public static final String DISPATCHER_REQUEST_PATH_ATTR = + "org.apache.catalina.core.DISPATCHER_REQUEST_PATH"; + + + // ----------------------------------------------------------- Constructors + ServletContextImpl servletContext; + + public WebappFilterMapper() { + } + + public WebappFilterMapper(ServletContextImpl impl) { + servletContext = impl; + } + + public void setServletContext(ServletContextImpl sc) { + servletContext = sc; + } + + // --------------------------------------------------------- Public Methods + + ArrayList filterMaps = new ArrayList(); + + public void addMapping(String filterName, + String url, + String servletName, + String type[]) { + FilterMap map = new FilterMap(); + map.setURLPattern(url); + map.setFilterName(filterName); + map.setServletName(servletName); + filterMaps.add(map); + } + + /** + * Construct and return a FilterChain implementation that will wrap the + * execution of the specified servlet instance. If we should not execute + * a filter chain at all, return null. + * + * @param request The servlet request we are processing + * @param servlet The servlet instance to be wrapped + */ + public FilterChainImpl createFilterChain(ServletRequest request, + ServletConfigImpl wrapper, + Servlet servlet) { + + // If there is no servlet to execute, return null + if (servlet == null) + return (null); + + // get the dispatcher type + int dispatcher = -1; + if (request.getAttribute(DISPATCHER_TYPE_ATTR) != null) { + Integer dispatcherInt = + (Integer) request.getAttribute(DISPATCHER_TYPE_ATTR); + dispatcher = dispatcherInt.intValue(); + } + String requestPath = null; + Object attribute = request.getAttribute(DISPATCHER_REQUEST_PATH_ATTR); + + if (attribute != null){ + requestPath = attribute.toString(); + } + + HttpServletRequest hreq = null; + if (request instanceof HttpServletRequest) + hreq = (HttpServletRequest)request; + + // Create and initialize a filter chain object + FilterChainImpl filterChain = null; + if ((request instanceof ServletRequestImpl)) { + ServletRequestImpl req = (ServletRequestImpl) request; + filterChain = (FilterChainImpl) req.getFilterChain(); + filterChain.release(); + } else { + // Security: Do not recycle + filterChain = new FilterChainImpl(); + } + + filterChain.setServlet(wrapper, servlet); + + // If there are no filter mappings, we are done + if ((filterMaps.size() == 0)) + return (filterChain); + + // Acquire the information we will need to match filter mappings + String servletName = wrapper.getServletName(); + + int n = 0; + + // TODO(costin): optimize: separate in 2 lists, one for url-mapped, one for + // servlet-name. Maybe even separate list for dispatcher and + // non-dispatcher + + // TODO(costin): optimize: set the FilterConfig in the FilterMap, to + // avoid second hash lookup + + // Add the relevant path-mapped filters to this filter chain + for (int i = 0; i < filterMaps.size(); i++) { + FilterMap filterMap = (FilterMap)filterMaps.get(i); + if (!matchDispatcher(filterMap ,dispatcher)) { + continue; + } + if (!matchFiltersURL(filterMap, requestPath)) + continue; + FilterConfigImpl filterConfig = + servletContext.getFilter(filterMap.getFilterName()); + if (filterConfig == null) { + // FIXME - log configuration problem + continue; + } + filterChain.addFilter(filterConfig); + n++; + } + + // Add filters that match on servlet name second + for (int i = 0; i < filterMaps.size(); i++) { + FilterMap filterMap = (FilterMap)filterMaps.get(i); + if (!matchDispatcher(filterMap ,dispatcher)) { + continue; + } + if (!matchFiltersServlet(filterMap, servletName)) + continue; + FilterConfigImpl filterConfig = + servletContext.getFilter(filterMap.getFilterName()); + if (filterConfig == null) { + ; // FIXME - log configuration problem + continue; + } + filterChain.addFilter(filterConfig); + n++; + } + + // Return the completed filter chain + return (filterChain); + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Return true if the context-relative request path + * matches the requirements of the specified filter mapping; + * otherwise, return null. + * + * @param filterMap Filter mapping being checked + * @param requestPath Context-relative request path of this request + */ + private boolean matchFiltersURL(FilterMap filterMap, String requestPath) { + + if (requestPath == null) + return (false); + + // Match on context relative request path + String testPath = filterMap.getURLPattern(); + if (testPath == null) + return (false); + + // Case 1 - Exact Match + if (testPath.equals(requestPath)) + return (true); + + // Case 2 - Path Match ("/.../*") + if (testPath.equals("/*")) + return (true); + if (testPath.endsWith("/*")) { + if (testPath.regionMatches(0, requestPath, 0, + testPath.length() - 2)) { + if (requestPath.length() == (testPath.length() - 2)) { + return (true); + } else if ('/' == requestPath.charAt(testPath.length() - 2)) { + return (true); + } + } + return (false); + } + + // Case 3 - Extension Match + if (testPath.startsWith("*.")) { + int slash = requestPath.lastIndexOf('/'); + int period = requestPath.lastIndexOf('.'); + if ((slash >= 0) && (period > slash) + && (period != requestPath.length() - 1) + && ((requestPath.length() - period) + == (testPath.length() - 1))) { + return (testPath.regionMatches(2, requestPath, period + 1, + testPath.length() - 2)); + } + } + + // Case 4 - "Default" Match + return (false); // NOTE - Not relevant for selecting filters + + } + + + /** + * Return true if the specified servlet name matches + * the requirements of the specified filter mapping; otherwise + * return false. + * + * @param filterMap Filter mapping being checked + * @param servletName Servlet name being checked + */ + private boolean matchFiltersServlet(FilterMap filterMap, + String servletName) { + + if (servletName == null) { + return (false); + } else { + if (servletName.equals(filterMap.getServletName())) { + return (true); + } else { + return false; + } + } + + } + + + /** + * Convienience method which returns true if the dispatcher type + * matches the dispatcher types specified in the FilterMap + */ + private boolean matchDispatcher(FilterMap filterMap, int dispatcher) { + switch (dispatcher) { + case FORWARD : { + if (filterMap.getDispatcherMapping() == FilterMap.FORWARD || + filterMap.getDispatcherMapping() == FilterMap.FORWARD_ERROR || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_ERROR_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_FORWARD_INCLUDE) { + return true; + } + break; + } + case INCLUDE : { + if (filterMap.getDispatcherMapping() == FilterMap.INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_ERROR || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_ERROR_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_FORWARD_INCLUDE) { + return true; + } + break; + } + case REQUEST : { + if (filterMap.getDispatcherMapping() == FilterMap.REQUEST || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_FORWARD_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD_INCLUDE) { + return true; + } + break; + } + case ERROR : { + if (filterMap.getDispatcherMapping() == FilterMap.ERROR || + filterMap.getDispatcherMapping() == FilterMap.FORWARD_ERROR || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_ERROR || + filterMap.getDispatcherMapping() == FilterMap.INCLUDE_ERROR_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_FORWARD_INCLUDE || + filterMap.getDispatcherMapping() == FilterMap.REQUEST_ERROR_INCLUDE) { + return true; + } + break; + } + } + return false; + } + + + // -------------------- Map elements ----------------------- + + public static class FilterMap implements Serializable { + + + // ------------------------------------------------------------- Properties + + + /** + * The name of this filter to be executed when this mapping matches + * a particular request. + */ + + public static final int ERROR = 1; + public static final int FORWARD = 2; + public static final int FORWARD_ERROR =3; + public static final int INCLUDE = 4; + public static final int INCLUDE_ERROR = 5; + public static final int INCLUDE_ERROR_FORWARD =6; + public static final int INCLUDE_FORWARD = 7; + public static final int REQUEST = 8; + public static final int REQUEST_ERROR = 9; + public static final int REQUEST_ERROR_FORWARD = 10; + public static final int REQUEST_ERROR_FORWARD_INCLUDE = 11; + public static final int REQUEST_ERROR_INCLUDE = 12; + public static final int REQUEST_FORWARD = 13; + public static final int REQUEST_INCLUDE = 14; + public static final int REQUEST_FORWARD_INCLUDE= 15; + + // represents nothing having been set. This will be seen + // as equal to a REQUEST + private static final int NOT_SET = -1; + + private int dispatcherMapping=NOT_SET; + + private String filterName = null; + + /** + * The URL pattern this mapping matches. + */ + private String urlPattern = null; + + /** + * The servlet name this mapping matches. + */ + private String servletName = null; + + + + public String getFilterName() { + return (this.filterName); + } + + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + + public String getServletName() { + return (this.servletName); + } + + public void setServletName(String servletName) { + this.servletName = servletName; + } + + + public String getURLPattern() { + return (this.urlPattern); + } + + public void setURLPattern(String urlPattern) { + this.urlPattern = RequestUtil.URLDecode(urlPattern); + } + + /** + * + * This method will be used to set the current state of the FilterMap + * representing the state of when filters should be applied: + * + * ERROR + * FORWARD + * FORWARD_ERROR + * INCLUDE + * INCLUDE_ERROR + * INCLUDE_ERROR_FORWARD + * REQUEST + * REQUEST_ERROR + * REQUEST_ERROR_INCLUDE + * REQUEST_ERROR_FORWARD_INCLUDE + * REQUEST_INCLUDE + * REQUEST_FORWARD, + * REQUEST_FORWARD_INCLUDE + * + */ + public void setDispatcher(String dispatcherString) { + String dispatcher = dispatcherString.toUpperCase(); + + if (dispatcher.equals("FORWARD")) { + + // apply FORWARD to the global dispatcherMapping. + switch (dispatcherMapping) { + case NOT_SET : dispatcherMapping = FORWARD; break; + case ERROR : dispatcherMapping = FORWARD_ERROR; break; + case INCLUDE : dispatcherMapping = INCLUDE_FORWARD; break; + case INCLUDE_ERROR : dispatcherMapping = INCLUDE_ERROR_FORWARD; break; + case REQUEST : dispatcherMapping = REQUEST_FORWARD; break; + case REQUEST_ERROR : dispatcherMapping = REQUEST_ERROR_FORWARD; break; + case REQUEST_ERROR_INCLUDE : dispatcherMapping = REQUEST_ERROR_FORWARD_INCLUDE; break; + case REQUEST_INCLUDE : dispatcherMapping = REQUEST_FORWARD_INCLUDE; break; + } + } else if (dispatcher.equals("INCLUDE")) { + // apply INCLUDE to the global dispatcherMapping. + switch (dispatcherMapping) { + case NOT_SET : dispatcherMapping = INCLUDE; break; + case ERROR : dispatcherMapping = INCLUDE_ERROR; break; + case FORWARD : dispatcherMapping = INCLUDE_FORWARD; break; + case FORWARD_ERROR : dispatcherMapping = INCLUDE_ERROR_FORWARD; break; + case REQUEST : dispatcherMapping = REQUEST_INCLUDE; break; + case REQUEST_ERROR : dispatcherMapping = REQUEST_ERROR_INCLUDE; break; + case REQUEST_ERROR_FORWARD : dispatcherMapping = REQUEST_ERROR_FORWARD_INCLUDE; break; + case REQUEST_FORWARD : dispatcherMapping = REQUEST_FORWARD_INCLUDE; break; + } + } else if (dispatcher.equals("REQUEST")) { + // apply REQUEST to the global dispatcherMapping. + switch (dispatcherMapping) { + case NOT_SET : dispatcherMapping = REQUEST; break; + case ERROR : dispatcherMapping = REQUEST_ERROR; break; + case FORWARD : dispatcherMapping = REQUEST_FORWARD; break; + case FORWARD_ERROR : dispatcherMapping = REQUEST_ERROR_FORWARD; break; + case INCLUDE : dispatcherMapping = REQUEST_INCLUDE; break; + case INCLUDE_ERROR : dispatcherMapping = REQUEST_ERROR_INCLUDE; break; + case INCLUDE_FORWARD : dispatcherMapping = REQUEST_FORWARD_INCLUDE; break; + case INCLUDE_ERROR_FORWARD : dispatcherMapping = REQUEST_ERROR_FORWARD_INCLUDE; break; + } + } else if (dispatcher.equals("ERROR")) { + // apply ERROR to the global dispatcherMapping. + switch (dispatcherMapping) { + case NOT_SET : dispatcherMapping = ERROR; break; + case FORWARD : dispatcherMapping = FORWARD_ERROR; break; + case INCLUDE : dispatcherMapping = INCLUDE_ERROR; break; + case INCLUDE_FORWARD : dispatcherMapping = INCLUDE_ERROR_FORWARD; break; + case REQUEST : dispatcherMapping = REQUEST_ERROR; break; + case REQUEST_INCLUDE : dispatcherMapping = REQUEST_ERROR_INCLUDE; break; + case REQUEST_FORWARD : dispatcherMapping = REQUEST_ERROR_FORWARD; break; + case REQUEST_FORWARD_INCLUDE : dispatcherMapping = REQUEST_ERROR_FORWARD_INCLUDE; break; + } + } + } + + public int getDispatcherMapping() { + // per the SRV.6.2.5 absence of any dispatcher elements is + // equivelant to a REQUEST value + if (dispatcherMapping == NOT_SET) return REQUEST; + else return dispatcherMapping; + } + + } + + + public void init(FilterConfig filterConfig) throws ServletException { + } + + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) + throws IOException, ServletException { + } + + + public void destroy() { + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappServletMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappServletMapper.java new file mode 100644 index 000000000..a6eb28438 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/WebappServletMapper.java @@ -0,0 +1,883 @@ +/* + * Copyright 1999-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite; + +import java.io.File; +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.tomcat.util.buf.Ascii; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.mapper.MappingData; + +/** + * Mapper, which implements the servlet API mapping rules (which are derived + * from the HTTP rules). + * + * Based on catalina mapper - but simplified. All host and context mappings + * is done in HostMapper - this is just dealing with web.xml. + * + * For corner cases ( very large number of rules, dynamic rules, etc ) you + * can override the mapper for a context with a class extending this. + * + * TODO: remove, use coyote-level mapper or user-space + */ +public class WebappServletMapper implements Filter { + + /** + * Context associated with this wrapper, used for wrapper mapping. + */ + public ContextMapElement contextMapElement = new ContextMapElement(); + + + // --------------------------------------------------------- Public Methods + public WebappServletMapper() { + } + + public void setServletContext(ServletContextImpl impl) { + contextMapElement.object = impl; + contextMapElement.name = impl.getContextPath(); + } + + + /** Set context, used for wrapper mapping (request dispatcher). + * + * @param welcomeResources Welcome files defined for this context + * @param resources Static resources of the context + */ + public void setContext(String path, String[] welcomeResources, + File resources) { + contextMapElement.name = path; + contextMapElement.welcomeResources = welcomeResources; + contextMapElement.resources = resources; + } + + + /** + * 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); + } + + + public void addWrapper(ContextMapElement 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(ContextMapElement context, String path, Object wrapper, + boolean jspWildCard) { + + synchronized (context) { + WrapperMapElement newWrapper = new WrapperMapElement(); + newWrapper.object = wrapper; + newWrapper.jspWildCard = jspWildCard; + if (path.endsWith("/*")) { + // Wildcard wrapper + newWrapper.name = path.substring(0, path.length() - 2); + WrapperMapElement[] oldWrappers = context.wildcardWrappers; + WrapperMapElement[] newWrappers = + new WrapperMapElement[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); + WrapperMapElement[] oldWrappers = context.extensionWrappers; + WrapperMapElement[] newWrappers = + new WrapperMapElement[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; + WrapperMapElement[] oldWrappers = context.exactWrappers; + WrapperMapElement[] newWrappers = + new WrapperMapElement[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.exactWrappers = newWrappers; + } + } + } + } + + + /** + * Remove a wrapper from the context associated with this wrapper. + * + * @param path Wrapper mapping + */ + public void removeWrapper(String path) { + removeWrapper(contextMapElement, path); + } + + + protected void removeWrapper(ContextMapElement context, String path) { + synchronized (context) { + if (path.endsWith("/*")) { + // Wildcard wrapper + String name = path.substring(0, path.length() - 2); + WrapperMapElement[] oldWrappers = context.wildcardWrappers; + WrapperMapElement[] newWrappers = + new WrapperMapElement[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); + WrapperMapElement[] oldWrappers = context.extensionWrappers; + WrapperMapElement[] newWrappers = + new WrapperMapElement[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; + WrapperMapElement[] oldWrappers = context.exactWrappers; + WrapperMapElement[] newWrappers = + new WrapperMapElement[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + context.exactWrappers = newWrappers; + } + } + } + } + + /** + * 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(MessageBytes uri, MappingData mappingData) + throws Exception { + + uri.toChars(); + CharChunk uricc = uri.getCharChunk(); + //uricc.setLimit(-1); + internalMapWrapper(contextMapElement, uricc, mappingData); + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Wrapper mapping. + */ + private final void internalMapWrapper(ContextMapElement context, + CharChunk path, + MappingData mappingData) + throws Exception { + + int pathOffset = path.getOffset(); + int pathEnd = path.getEnd(); + int servletPath = pathOffset; + boolean noServletPath = false; + + int length = context.name.length(); + if (length == 1) length--; + if (length != (pathEnd - pathOffset)) { + servletPath = pathOffset + length; + } else { + noServletPath = true; + // What is this doing ??? + path.append('/'); + pathOffset = path.getOffset(); + pathEnd = path.getEnd(); + servletPath = pathOffset+length; + } + + path.setOffset(servletPath); + + // Rule 1 -- Exact Match + WrapperMapElement[] exactWrappers = context.exactWrappers; + internalMapExactWrapper(exactWrappers, path, mappingData); + + // Rule 2 -- Prefix Match + boolean checkJspWelcomeFiles = false; + WrapperMapElement[] wildcardWrappers = context.wildcardWrappers; + if (mappingData.wrapper == null) { + internalMapWildcardWrapper(wildcardWrappers, context.nesting, + path, mappingData); + if (mappingData.wrapper != null && mappingData.jspWildCard) { + char[] buf = path.getBuffer(); + if (buf[pathEnd - 1] == '/') { + /* + * 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.wrapper = null; + checkJspWelcomeFiles = true; + } else { + // See Bugzilla 27704 + mappingData.wrapperPath.setChars(buf, path.getStart(), + path.getLength()); + mappingData.pathInfo.recycle(); + } + } + } + + if(mappingData.wrapper == null && noServletPath) { + // The path is empty, redirect to "/" + mappingData.redirectPath.setChars + (path.getBuffer(), pathOffset, pathEnd); + path.setEnd(pathEnd - 1); + return; + } + + // Rule 3 -- Extension Match + WrapperMapElement[] extensionWrappers = context.extensionWrappers; + if (mappingData.wrapper == null && !checkJspWelcomeFiles) { + internalMapExtensionWrapper(extensionWrappers, path, mappingData); + } + + File file = null; + // Rule 4 -- Welcome resources processing for servlets + if (mappingData.wrapper == null) { + boolean checkWelcomeFiles = checkJspWelcomeFiles; + if (!checkWelcomeFiles) { + char[] buf = path.getBuffer(); + checkWelcomeFiles = (buf[pathEnd - 1] == '/'); + } + if (checkWelcomeFiles) { + for (int i = 0; (i < context.welcomeResources.length) + && (mappingData.wrapper == null); i++) { + path.setOffset(pathOffset); + path.setEnd(pathEnd); + path.append(context.welcomeResources[i], 0, + context.welcomeResources[i].length()); + path.setOffset(servletPath); + + // Rule 4a -- Welcome resources processing for exact macth + internalMapExactWrapper(exactWrappers, path, mappingData); + + // Rule 4b -- Welcome resources processing for prefix match + if (mappingData.wrapper == null) { + internalMapWildcardWrapper + (wildcardWrappers, context.nesting, + path, mappingData); + } + + // Rule 4c -- Welcome resources processing + // for physical folder + if (mappingData.wrapper == null + && context.resources != null) { + // Default servlet: check if it's file or dir to apply + // welcome files rules. + // TODO: Save the File in attributes, + // to avoid duplication in DefaultServlet. + + String pathStr = path.toString(); + file = new File(context.resources, pathStr); + if (file.exists() && !(file.isDirectory()) ) { + + internalMapExtensionWrapper(extensionWrappers, + path, mappingData); + if (mappingData.wrapper == null + && context.defaultWrapper != null) { + mappingData.wrapper = + context.defaultWrapper.object; + mappingData.requestPath.setChars + (path.getBuffer(), path.getStart(), + path.getLength()); + mappingData.wrapperPath.setChars + (path.getBuffer(), path.getStart(), + path.getLength()); + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } + } + } + + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + + } + + + // Rule 7 -- Default servlet + if (mappingData.wrapper == null && !checkJspWelcomeFiles) { + if (context.defaultWrapper != null) { + mappingData.wrapper = context.defaultWrapper.object; + mappingData.requestPath.setChars + (path.getBuffer(), path.getStart(), path.getLength()); + mappingData.wrapperPath.setChars + (path.getBuffer(), path.getStart(), path.getLength()); + } + // Redirection to a folder + char[] buf = path.getBuffer(); + if (context.resources != null && buf[pathEnd -1 ] != '/') { + String pathStr = path.toString(); + file = new File( context.resources, pathStr); + if (file.exists() && file.isDirectory()) { + // Note: this mutates the path: do not do any processing + // after this (since we set the redirectPath, there + // shouldn't be any) + path.setOffset(pathOffset); + path.append('/'); + mappingData.redirectPath.setChars + (path.getBuffer(), path.getStart(), path.getLength()); + } else { + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } + } + + path.setOffset(pathOffset); + path.setEnd(pathEnd); + } + + + /** + * Exact mapping. + */ + private final void internalMapExactWrapper + (WrapperMapElement[] wrappers, CharChunk path, MappingData mappingData) { + int pos = find(wrappers, path); + if ((pos != -1) && (path.equals(wrappers[pos].name))) { + mappingData.requestPath.setString(wrappers[pos].name); + mappingData.wrapperPath.setString(wrappers[pos].name); + mappingData.wrapper = wrappers[pos].object; + } + } + + + /** + * Wildcard mapping. + */ + private final void internalMapWildcardWrapper + (WrapperMapElement[] wrappers, int nesting, CharChunk path, + MappingData mappingData) { + + int pathEnd = path.getEnd(); + int pathOffset = path.getOffset(); + + int lastSlash = -1; + int length = -1; + int pos = find(wrappers, path); + if (pos != -1) { + boolean found = false; + while (pos >= 0) { + if (path.startsWith(wrappers[pos].name)) { + length = wrappers[pos].name.length(); + if (path.getLength() == length) { + found = true; + break; + } else if (path.startsWithIgnoreCase("/", length)) { + found = true; + break; + } + } + if (lastSlash == -1) { + lastSlash = nthSlash(path, nesting + 1); + } else { + lastSlash = lastSlash(path); + } + path.setEnd(lastSlash); + pos = find(wrappers, path); + } + path.setEnd(pathEnd); + if (found) { + mappingData.wrapperPath.setString(wrappers[pos].name); + if (path.getLength() > length) { + mappingData.pathInfo.setChars + (path.getBuffer(), + path.getOffset() + length, + path.getLength() - length); + } + mappingData.requestPath.setChars + (path.getBuffer(), path.getOffset(), path.getLength()); + mappingData.wrapper = wrappers[pos].object; + mappingData.jspWildCard = wrappers[pos].jspWildCard; + } + } + } + + + /** + * Extension mappings. + */ + private final void internalMapExtensionWrapper + (WrapperMapElement[] wrappers, CharChunk path, MappingData mappingData) { + char[] buf = path.getBuffer(); + int pathEnd = path.getEnd(); + int servletPath = path.getOffset(); + int slash = -1; + for (int i = pathEnd - 1; i >= servletPath; i--) { + if (buf[i] == '/') { + slash = i; + break; + } + } + if (slash == -1 ) slash = 0; + if (slash >= 0) { + int period = -1; + for (int i = pathEnd - 1; i > slash; i--) { + if (buf[i] == '.') { + period = i; + break; + } + } + if (period >= 0) { + path.setOffset(period + 1); + path.setEnd(pathEnd); + int pos = find(wrappers, path); + if ((pos != -1) + && (path.equals(wrappers[pos].name))) { + mappingData.wrapperPath.setChars + (buf, servletPath, pathEnd - servletPath); + mappingData.requestPath.setChars + (buf, servletPath, pathEnd - servletPath); + mappingData.wrapper = wrappers[pos].object; + } + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + } + } + + + /** + * 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. + */ + public static final int find(MapElement[] map, CharChunk name) { + return find(map, name, name.getStart(), name.getEnd()); + } + + + /** + * 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(MapElement[] map, CharChunk name, + int start, int end) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, start, end, map[0].name) < 0 ) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) / 2; + int result = compare(name, start, end, map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, start, end, 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(MapElement[] map, CharChunk name) { + return findIgnoreCase(map, name, name.getStart(), name.getEnd()); + } + + + /** + * 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(MapElement[] map, CharChunk name, + int start, int end) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + if (compareIgnoreCase(name, start, end, map[0].name) < 0 ) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) / 2; + int result = compareIgnoreCase(name, start, end, map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compareIgnoreCase(name, start, end, 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. + */ + public static final int find(MapElement[] 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; + } + } + } + + } + + + /** + * Compare given char chunk with String. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + private static final int compare(CharChunk name, int start, int end, + String compareTo) { + int result = 0; + char[] c = name.getBuffer(); + int len = compareTo.length(); + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (c[i + start] > compareTo.charAt(i)) { + result = 1; + } else if (c[i + start] < compareTo.charAt(i)) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length() > (end - start)) { + result = -1; + } else if (compareTo.length() < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Compare given char chunk with String ignoring case. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + private static final int compareIgnoreCase(CharChunk name, int start, int end, + String compareTo) { + int result = 0; + char[] c = name.getBuffer(); + int len = compareTo.length(); + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (Ascii.toLower(c[i + start]) > Ascii.toLower(compareTo.charAt(i))) { + result = 1; + } else if (Ascii.toLower(c[i + start]) < Ascii.toLower(compareTo.charAt(i))) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length() > (end - start)) { + result = -1; + } else if (compareTo.length() < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Find the position of the last slash in the given char chunk. + */ + public static final int lastSlash(CharChunk name) { + + char[] c = name.getBuffer(); + int end = name.getEnd(); + int start = name.getStart(); + int pos = end; + + while (pos > start) { + if (c[--pos] == '/') { + break; + } + } + + return (pos); + + } + + + /** + * Find the position of the nth slash, in the given char chunk. + */ + public static final int nthSlash(CharChunk name, int n) { + + char[] c = name.getBuffer(); + int end = name.getEnd(); + int start = name.getStart(); + int pos = start; + int count = 0; + + while (pos < end) { + if ((c[pos++] == '/') && ((++count) == n)) { + pos--; + break; + } + } + + return (pos); + + } + + + /** + * Return the slash count in a given string. + */ + public 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. + */ + public static final boolean insertMap + (MapElement[] oldMap, MapElement[] newMap, MapElement 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. + */ + public static final boolean removeMap + (MapElement[] oldMap, MapElement[] 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 abstract class MapElement { + /** hostname or path + */ + public String name = null; + public Object object = null; + + public String toString() { + return "MapElement: \"" + name +"\""; + } + } + + + // ---------------------------------------------------- Context Inner Class + + + public static final class ContextMapElement + extends MapElement { + + public String[] welcomeResources = new String[0]; + public File resources = null; + public WrapperMapElement defaultWrapper = null; + public WrapperMapElement[] exactWrappers = new WrapperMapElement[0]; + public WrapperMapElement[] wildcardWrappers = new WrapperMapElement[0]; + public WrapperMapElement[] extensionWrappers = new WrapperMapElement[0]; + public int nesting = 0; + + public String toString() { + return "ContextMapElement {" + + "name: \"" + name + + "\"\nnesting: \"" + nesting + + "\"\n}"; + } + } + + + // ---------------------------------------------------- Wrapper Inner Class + + + public static class WrapperMapElement + extends MapElement { + public boolean jspWildCard = false; + } + + + public void destroy() { + } + + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, + ServletException { + } + + + public void init(FilterConfig filterConfig) throws ServletException { + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/cli/Main.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/cli/Main.java new file mode 100644 index 000000000..3abe1f482 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/cli/Main.java @@ -0,0 +1,59 @@ +/* + * 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.cli; + +import org.apache.tomcat.integration.ObjectManager; +import org.apache.tomcat.integration.jmx.JmxObjectManagerSpi; +import org.apache.tomcat.integration.simple.SimpleObjectManager; +import org.apache.tomcat.lite.TomcatLite; +import org.apache.tomcat.lite.coyote.CoyoteHttp; + +/** + * Run tomcat lite, mostly for demo. In normal use you would + * embed it in an app, or use a small customized launcher. + * + * With no arguments, it'll load only the root context / from + * ./webapps/ROOT + * + * Most configuration can be done using web.xml files, few settings + * can be set by flags. + * + * @author Costin Manolache + */ +public class Main { + + public static void main(String args[]) + throws Exception { + + if (args.length == 0) { + System.err.println("Please specify at least one webapp."); + System.err.println("Example:"); + System.err.println("-context /:webapps/ROOT -port 9999"); + } + + // Enable CLI processing + SimpleObjectManager.setArgs(args); + + TomcatLite lite = new TomcatLite(); + ObjectManager om = lite.getObjectManager(); + + // add JMX support + new JmxObjectManagerSpi().register(om); + + lite.run(); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/config.properties b/modules/tomcat-lite/java/org/apache/tomcat/lite/config.properties new file mode 100644 index 000000000..c6e43e1d3 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/config.properties @@ -0,0 +1,28 @@ +# Support for PropertiesSpi - the 'dummy' framework used for tomcat-lite +# If tomcat is used with a proper framework, you need to bind and configure +# those objects in the framework. + +# --- Class names for required plugin interfaces --- + +org.apache.tomcat.lite.WebappServletMapper.(class)=org.apache.tomcat.lite.WebappServletMapper +org.apache.tomcat.lite.WebappFilterMapper.(class)=org.apache.tomcat.lite.WebappFilterMapper + +# Sessions +org.apache.tomcat.addons.UserSessionManager.(class)=org.apache.tomcat.servlets.session.SimpleSessionManager + +# *.jsp support +org.apache.tomcat.addons.UserTemplateClassMapper.(class)=org.apache.tomcat.servlets.jsp.JasperCompilerTemplateClassMapper + +# Loader for web.xml - you can have your own custom class using a more efficient +# or hardcoded. +org.apache.tomcat.lite.ContextPreinitListener.(class)=org.apache.tomcat.lite.webxml.TomcatLiteWebXmlConfig + +# Connector class +org.apache.tomcat.lite.Connector.(class)=org.apache.tomcat.lite.coyote.CoyoteHttp + +# --- Other required settings --- + +# Customize default and *.jsp mappings +default-servlet.(class)=org.apache.tomcat.servlets.file.WebdavServlet +jspwildcard-servlet.(class)=org.apache.tomcat.servlets.jsp.WildcardTemplateServlet + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/ClientAbortException.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/ClientAbortException.java new file mode 100644 index 000000000..db61fd38c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/ClientAbortException.java @@ -0,0 +1,143 @@ +/* + * 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.coyote; + +import java.io.IOException; + +/** + * Wrap an IOException identifying it as being caused by an abort + * of a request by a remote client. + * + * @author Glenn L. Nielsen + * @version $Revision: 304063 $ $Date: 2005-08-18 06:25:18 -0700 (Thu, 18 Aug 2005) $ + */ + +public final class ClientAbortException extends IOException { + + + //------------------------------------------------------------ Constructors + + + /** + * Construct a new ClientAbortException with no other information. + */ + public ClientAbortException() { + + this(null, null); + + } + + + /** + * Construct a new ClientAbortException for the specified message. + * + * @param message Message describing this exception + */ + public ClientAbortException(String message) { + + this(message, null); + + } + + + /** + * Construct a new ClientAbortException for the specified throwable. + * + * @param throwable Throwable that caused this exception + */ + public ClientAbortException(Throwable throwable) { + + this(null, throwable); + + } + + + /** + * Construct a new ClientAbortException for the specified message + * and throwable. + * + * @param message Message describing this exception + * @param throwable Throwable that caused this exception + */ + public ClientAbortException(String message, Throwable throwable) { + + super(); + this.message = message; + this.throwable = throwable; + + } + + + //------------------------------------------------------ Instance Variables + + + /** + * The error message passed to our constructor (if any) + */ + protected String message = null; + + + /** + * The underlying exception or error passed to our constructor (if any) + */ + protected Throwable throwable = null; + + + //---------------------------------------------------------- Public Methods + + + /** + * Returns the message associated with this exception, if any. + */ + public String getMessage() { + + return (message); + + } + + + /** + * Returns the cause that caused this exception, if any. + */ + public Throwable getCause() { + + return (throwable); + + } + + + /** + * Return a formatted string that describes this exception. + */ + public String toString() { + + StringBuffer sb = new StringBuffer("ClientAbortException: "); + if (message != null) { + sb.append(message); + if (throwable != null) { + sb.append(": "); + } + } + if (throwable != null) { + sb.append(throwable.toString()); + } + return (sb.toString()); + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteHttp.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteHttp.java new file mode 100644 index 000000000..722eca1d0 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteHttp.java @@ -0,0 +1,174 @@ +/* + */ +package org.apache.tomcat.lite.coyote; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.coyote.ActionCode; +import org.apache.coyote.ActionHook; +import org.apache.coyote.Adapter; +import org.apache.coyote.Request; +import org.apache.coyote.Response; +import org.apache.tomcat.integration.ObjectManager; +import org.apache.tomcat.lite.Connector; +import org.apache.tomcat.lite.ServletRequestImpl; +import org.apache.tomcat.lite.ServletResponseImpl; +import org.apache.tomcat.lite.TomcatLite; +import org.apache.tomcat.util.net.SocketStatus; + +public class CoyoteHttp implements Adapter, Connector { + + //private TomcatLite lite; + CoyoteServer coyote; + private TomcatLite lite; + + public CoyoteHttp() { + } + + + @Override + public void finishResponse(HttpServletResponse res) throws IOException { + ((ServletResponseImpl) res).getCoyoteResponse().finish(); + } + + + public void recycle(HttpServletRequest req, HttpServletResponse res) { + + } + + public void setPort(int port) { + if (getConnectors() != null) { + coyote.setPort(port); + } + } + + @Override + public void setDaemon(boolean b) { + if (getConnectors() != null) { + coyote.setDaemon(b); + } + } + + + @Override + public void start() { + if (getConnectors() != null) { + try { + coyote.init(); + coyote.start(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + + + @Override + public void stop() { + if (coyote != null) { + try { + coyote.stop(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + @Override + public void initRequest(HttpServletRequest hreq, HttpServletResponse hres) { + ServletRequestImpl req = (ServletRequestImpl) hreq; + ServletResponseImpl res = (ServletResponseImpl) hres; + + Request creq = new Request(); + res.setCoyoteResponse(new Response()); + res.getCoyoteResponse().setRequest(creq); + res.getCoyoteResponse().setHook(new ActionHook() { + public void action(ActionCode actionCode, + Object param) { + } + }); + + req.setCoyoteRequest(creq); + + res.setConnector(); + + } + + // Coyote-specific hooking. + // This could be moved out to a separate class, TomcatLite can + // work without it. + public CoyoteServer getConnectors() { + if (coyote == null) { + coyote = new CoyoteServer(); + coyote.addAdapter("/", this); + } + return coyote; + } + + public void setConnectors(CoyoteServer server) { + this.coyote = server; + coyote.addAdapter("/", this); + } + + @Override + public void service(Request req, Response res) throws Exception { + // find the facades + ServletRequestImpl sreq = (ServletRequestImpl) req.getNote(CoyoteServer.ADAPTER_REQ_NOTE); + ServletResponseImpl sres = (ServletResponseImpl) res.getNote(CoyoteServer.ADAPTER_RES_NOTE); + if (sreq == null) { + sreq = new ServletRequestImpl(); + sres = sreq.getResponse(); + + sreq.setCoyoteRequest(req); + sres.setCoyoteResponse(res); + + req.setNote(CoyoteServer.ADAPTER_REQ_NOTE, sreq); + res.setNote(CoyoteServer.ADAPTER_RES_NOTE, sres); + + sres.setConnector(); + + } + + lite.service(sreq, sres); + + if (res.getNote(CoyoteServer.COMET_RES_NOTE) == null) { + if (!sres.isCommitted()) { + res.sendHeaders(); + } + sres.getOutputBuffer().flush(); + res.finish(); + + sreq.recycle(); + sres.recycle(); + } + } + + // ---- Coyote --- + + @Override + public boolean event(Request req, Response res, SocketStatus status) + throws Exception { + return false; + } + + + public void setTomcatLite(TomcatLite lite) { + this.lite = lite; + } + + + @Override + public void setObjectManager(ObjectManager objectManager) { + getConnectors(); + coyote.setObjectManager(objectManager); + } + + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteServer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteServer.java new file mode 100644 index 000000000..17c1e62b5 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteServer.java @@ -0,0 +1,188 @@ +/* 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.coyote; + +import java.io.IOException; + +import org.apache.coyote.Adapter; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.http11.Http11NioProtocol; +import org.apache.tomcat.integration.ObjectManager; + + +/** + * Simple example of embeding coyote servlet. + * + */ +public class CoyoteServer { + protected int port = 8800; + protected boolean daemon = false; + + /** + * Note indicating the response is COMET. + */ + public static final int COMET_RES_NOTE = 2; + public static final int COMET_REQ_NOTE = 2; + + public static final int ADAPTER_RES_NOTE = 1; + public static final int ADAPTER_REQ_NOTE = 1; + + protected ProtocolHandler proto; + + protected Adapter adapter = new MapperAdapter(); + protected int maxThreads = 20; + boolean started = false; + boolean async = false; // use old nio connector + + protected ObjectManager om; + + public CoyoteServer() { + } + + public void setObjectManager(ObjectManager om) { + this.om = om; + } + + /** + * Add an adapter. If more than the 'default' adapter is + * added, a MapperAdapter will be inserted. + * + * @param path Use "/" for the default. + * @param adapter + */ + public void addAdapter(String path, Adapter added) { + if ("/".equals(path)) { + ((MapperAdapter) adapter).setDefaultAdapter(added); + } else { + ((MapperAdapter) adapter).getMapper().addWrapper(path, added); + } + } + + /** + */ + public void run() { + try { + init(); + start(); + } catch(IOException ex) { + ex.printStackTrace(); + } + } + + public void setDaemon(boolean b) { + daemon = b; + } + + public void init() { + //JdkLoggerConfig.loadCustom(); + om.bind("CoyoteServer:" + "CoyoteServer-" + port, + this); + om.bind("CoyoteAdapter", adapter); + } + + protected void initAdapters() { + if (proto == null) { + addProtocolHandler(port, daemon); + } + // adapter = ... + // Adapter secondaryadapter = ... + //registry.registerComponent(secondaryadapter, ":name=adapter", null); + } + + public void stop() throws Exception { + if (!started) { + return; + } + proto.destroy(); + started = false; + } + + /** + * Simple CLI support - arg is a path:className pair. + */ + public void setAdapter(String arg) { + String[] pathClass = arg.split(":", 2); + try { + Class c = Class.forName(pathClass[1]); + Adapter a = (Adapter) c.newInstance(); + addAdapter(pathClass[0],a); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + public void setPort(int port) { + if (proto != null) { + proto.setAttribute("port", Integer.toString(port)); + } + this.port = port; + } + + public void setConnector(ProtocolHandler h) { + this.proto = h; + h.setAttribute("port", Integer.toString(port)); + + om.bind("ProtocolHandler:" + "ep-" + port, proto); + } + + public void addProtocolHandler(int port, boolean daemon) { + Http11NioProtocol proto = new Http11NioProtocol(); + proto.setCompression("on"); + proto.setCompressionMinSize(32); + proto.setPort(port); + proto.getEndpoint().setDaemon(daemon); + CoyoteServer server = this; + server.setConnector(proto); + server.setPort(port); + server.setDaemon(daemon); + } + + public void addProtocolHandler(ProtocolHandler proto, + int port, boolean daemon) { + CoyoteServer server = this; + server.setConnector(proto); + server.setPort(port); + server.setDaemon(daemon); + } + + + + public void start() throws IOException { + try { + if (started) { + return; + } + initAdapters(); + + // not required - should run fine without a connector. + if (proto != null) { + proto.setAdapter(adapter); + + proto.init(); + proto.start(); + } + + started = true; + } catch (Throwable e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public boolean getStarted() { + return started; + } +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteUtils.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteUtils.java new file mode 100644 index 000000000..e97a450b7 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/CoyoteUtils.java @@ -0,0 +1,107 @@ +/* + */ +package org.apache.tomcat.lite.coyote; + +import java.io.IOException; + +import org.apache.coyote.Request; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.mapper.MappingData; + +public class CoyoteUtils { + static ByteChunk space = new ByteChunk(1); + static ByteChunk col = new ByteChunk(1); + static ByteChunk crlf = new ByteChunk(2); + static { + try { + space.append(' '); + col.append(':'); + crlf.append('\r'); + crlf.append('\n'); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public static String getContextPath(Request request) { + MappingData md = MapperAdapter.getMappingData(request); + String ctxPath = (md.contextPath.isNull()) ? "/" : + md.contextPath.toString(); + return ctxPath; + } + + public static String getPathInfo(Request request) { + MappingData md = MapperAdapter.getMappingData(request); + String ctxPath = (md.pathInfo.isNull()) ? "/" : + md.pathInfo.toString(); + return ctxPath; + } + public static String getServletPath(Request request) { + MappingData md = MapperAdapter.getMappingData(request); + String ctxPath = (md.wrapperPath.isNull()) ? "/" : + md.wrapperPath.toString(); + return ctxPath; + } + + // TODO: collate all notes in a signle file + static int READER_NOTE = 5; + + public static MessageReader getReader(Request req) { + MessageReader r = (MessageReader) req.getNote(READER_NOTE); + if (r == null) { + r = new MessageReader(); + r.setRequest(req); + req.setNote(READER_NOTE, r); + } + return r; + } + + /** + * Convert the request to bytes, ready to send. + */ + public static void serializeRequest(Request req, + ByteChunk reqBuf) throws IOException { + req.method().toBytes(); + if (!req.unparsedURI().isNull()) { + req.unparsedURI().toBytes(); + } + req.protocol().toBytes(); + + reqBuf.append(req.method().getByteChunk()); + reqBuf.append(space); + if (req.unparsedURI().isNull()) { + req.requestURI().toBytes(); + + reqBuf.append(req.requestURI().getByteChunk()); + } else { + reqBuf.append(req.unparsedURI().getByteChunk()); + } + reqBuf.append(space); + reqBuf.append(req.protocol().getByteChunk()); + reqBuf.append(crlf); + // Headers + MimeHeaders mimeHeaders = req.getMimeHeaders(); + boolean hasHost = false; + for (int i = 0; i < mimeHeaders.size(); i++) { + MessageBytes name = mimeHeaders.getName(i); + name.toBytes(); + reqBuf.append(name.getByteChunk()); + if (name.equalsIgnoreCase("host")) { + hasHost = true; + } + reqBuf.append(col); + mimeHeaders.getValue(i).toBytes(); + reqBuf.append(mimeHeaders.getValue(i).getByteChunk()); + reqBuf.append(crlf); + } + if (!hasHost) { + reqBuf.append("Host: localhost\r\n".getBytes(), 0, 17); + } + reqBuf.append(crlf); + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MapperAdapter.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MapperAdapter.java new file mode 100644 index 000000000..78ad37f39 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MapperAdapter.java @@ -0,0 +1,139 @@ +/* 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.coyote; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.coyote.Adapter; +import org.apache.coyote.Request; +import org.apache.coyote.Response; +import org.apache.tomcat.util.buf.UriNormalizer; +import org.apache.tomcat.util.http.mapper.BaseMapper; +import org.apache.tomcat.util.http.mapper.MappingData; +import org.apache.tomcat.util.net.SocketStatus; + +/** + * + */ +public class MapperAdapter implements Adapter { + + private BaseMapper mapper=new BaseMapper(); + + static Logger log = Logger.getLogger("Mapper"); + static final int MAP_NOTE = 4; + + public MapperAdapter() { + mapper.setDefaultHostName("localhost"); + mapper.setContext("", new String[] {"index.html"}, + null); + } + + public MapperAdapter(BaseMapper mapper2) { + mapper = mapper2; + } + + public static MappingData getMappingData(Request req) { + MappingData md = (MappingData) req.getNote(MAP_NOTE); + if (md == null) { + md = new MappingData(); + req.setNote(MAP_NOTE, md); + } + return md; + } + + /** + * Copy an array of bytes to a different position. Used during + * normalization. + */ + public 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 void service(Request req, final Response res) + throws Exception { + long t0 = System.currentTimeMillis(); + try { + // compute decodedURI - not done by connector + UriNormalizer.decodeRequest(req.decodedURI(), req.requestURI(), req.getURLDecoder()); + MappingData mapRes = getMappingData(req); + mapRes.recycle(); + + mapper.map(req.requestURI(), mapRes); + + Adapter h=(Adapter)mapRes.wrapper; + + if (h != null) { + log.info(">>>>>>>> START: " + req.method() + " " + + req.decodedURI() + " " + + h.getClass().getSimpleName()); + h.service( req, res ); + } else { + res.setStatus(404); + } + } catch(IOException ex) { + throw ex; + } catch( Throwable t ) { + t.printStackTrace(); + } finally { + long t1 = System.currentTimeMillis(); + + log.info("<<<<<<<< DONE: " + req.method() + " " + + req.decodedURI() + " " + + res.getStatus() + " " + + (t1 - t0)); + + // Final processing + // TODO: only if not commet, this doesn't work with the + // other connectors since we don't have the info + // TODO: add this note in the nio/apr connectors + // TODO: play nice with TomcatLite, other adapters that flush/close + if (res.getNote(CoyoteServer.COMET_RES_NOTE) == null) { + MessageWriter mw = MessageWriter.getWriter(req, res, 0); + mw.flush(); + mw.recycle(); + MessageReader reader = CoyoteUtils.getReader(req); + reader.recycle(); + res.finish(); + + req.recycle(); + res.recycle(); + } + } + } + + public BaseMapper getMapper() { + return mapper; + } + + public void setDefaultAdapter(Adapter adapter) { + mapper.addWrapper("/", adapter); + } + + public boolean event(Request req, Response res, boolean error) throws Exception { + // TODO Auto-generated method stub + return false; + } + + public boolean event(Request req, Response res, SocketStatus status) + throws Exception { + return false; + } + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MessageReader.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MessageReader.java new file mode 100644 index 000000000..5215e5d90 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MessageReader.java @@ -0,0 +1,519 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; + +import org.apache.coyote.Request; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.CharChunk; + +/** + * Refactored from catalina.connector.InputBuffer. Renamed to avoid conflict + * with coyote class. + * + * TODO: move to coyote package. + */ + +/** + * The buffer used by Tomcat request. This is a derivative of the Tomcat 3.3 + * OutputBuffer, adapted to handle input instead of output. This allows + * complete recycling of the facade objects (the ServletInputStream and the + * BufferedReader). + * + * @author Remy Maucherat + */ +public class MessageReader extends Reader + implements ByteChunk.ByteInputChannel, CharChunk.CharInputChannel, + CharChunk.CharOutputChannel { + + + // -------------------------------------------------------------- Constants + + + public static final String DEFAULT_ENCODING = + org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING; + public static final int DEFAULT_BUFFER_SIZE = 8*1024; + + // The buffer can be used for byte[] and char[] reading + // ( this is needed to support ServletInputStream and BufferedReader ) + public final int INITIAL_STATE = 0; + public final int CHAR_STATE = 1; + public final int BYTE_STATE = 2; + + + // ----------------------------------------------------- Instance Variables + + + /** + * The byte buffer. More data may be added to it while reading. + */ + private ByteChunk bb; + + + /** + * The chunk buffer, will be filled in from the bb. + */ + private CharChunk cb; + + + /** + * State of the output buffer. + */ + private int state = 0; + + + /** + * Number of bytes read. + */ + private int bytesRead = 0; + + + /** + * Number of chars read. + */ + private int charsRead = 0; + + + /** + * Flag which indicates if the input buffer is closed. + */ + private boolean closed = false; + + /** + * Encoding to use. + */ + private String enc; + + + /** + * Encoder is set. + */ + private boolean gotEnc = false; + + + /** + * Cached encoders. + */ + protected HashMap encoders = + new HashMap(); + + + /** + * Current byte to char converter. + */ + protected B2CConverter conv; + + + /** + * Associated Coyote request. + */ + private Request coyoteRequest; + + + /** + * Buffer position. + */ + private int markPos = -1; + + + /** + * Buffer size. + */ + private int size = -1; + + + // ----------------------------------------------------------- Constructors + + + /** + * Default constructor. Allocate the buffer with the default buffer size. + */ + public MessageReader() { + this(DEFAULT_BUFFER_SIZE); + } + + + /** + * Alternate constructor which allows specifying the initial buffer size. + * + * @param size Buffer size to use + */ + public MessageReader(int size) { + this.size = size; + bb = new ByteChunk(size); + bb.setLimit(size); + bb.setByteInputChannel(this); + cb = new CharChunk(size); + cb.setLimit(size); + cb.setOptimizedWrite(false); + cb.setCharInputChannel(this); + cb.setCharOutputChannel(this); + } + + + // ------------------------------------------------------------- Properties + + + /** + * Associated Coyote request. + * + * @param coyoteRequest Associated Coyote request + */ + public void setRequest(Request coyoteRequest) { + this.coyoteRequest = coyoteRequest; + } + + + /** + * Get associated Coyote request. + * + * @return the associated Coyote request + */ + public Request getRequest() { + return this.coyoteRequest; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Recycle the output buffer. + */ + public void recycle() { + + state = INITIAL_STATE; + bytesRead = 0; + charsRead = 0; + + // If usage of mark made the buffer too big, reallocate it + if (cb.getChars().length > size) { + cb = new CharChunk(size); + cb.setLimit(size); + cb.setCharInputChannel(this); + cb.setCharOutputChannel(this); + } else { + cb.recycle(); + } + markPos = -1; + bb.recycle(); + closed = false; + + if (conv != null) { + conv.recycle(); + } + + gotEnc = false; + enc = null; + + } + + + /** + * Close the input buffer. + * + * @throws IOException An underlying IOException occurred + */ + public void close() + throws IOException { + closed = true; + } + + + public int available() + throws IOException { + if (state == BYTE_STATE) { + return bb.getLength(); + } else if (state == CHAR_STATE) { + return cb.getLength(); + } else { + return 0; + } + } + + + // ------------------------------------------------- Bytes Handling Methods + + + /** + * Reads new bytes in the byte chunk. + * + * @param cbuf Byte buffer to be written to the response + * @param off Offset + * @param len Length + * + * @throws IOException An underlying IOException occurred + */ + public int realReadBytes(byte cbuf[], int off, int len) + throws IOException { + + if (closed) + return -1; + if (coyoteRequest == null) + return -1; + + state = BYTE_STATE; + + int result = coyoteRequest.doRead(bb); + + return result; + + } + + + public int readByte() + throws IOException { + return bb.substract(); + } + + + public int read(byte[] b, int off, int len) + throws IOException { + return bb.substract(b, off, len); + } + + + // ------------------------------------------------- Chars Handling Methods + + + /** + * Since the converter will use append, it is possible to get chars to + * be removed from the buffer for "writing". Since the chars have already + * been read before, they are ignored. If a mark was set, then the + * mark is lost. + */ + public void realWriteChars(char c[], int off, int len) + throws IOException { + markPos = -1; + } + + + public void setEncoding(String s) { + enc = s; + } + + /** + * Called when a read(char[]) operation is lacking data. It will read + * bytes. + */ + public int realReadChars(char cbuf[], int off, int len) + throws IOException { + + if (!gotEnc) + setConverter(); + + if (bb.getLength() <= 0) { + int nRead = realReadBytes(bb.getBytes(), 0, bb.getBytes().length); + if (nRead < 0) { + return -1; + } + } + + if (markPos == -1) { + cb.setOffset(0); + cb.setEnd(0); + } + + conv.convert(bb, cb, -1); + bb.setOffset(bb.getEnd()); + state = CHAR_STATE; + + return cb.getLength(); + + } + + + public int read() + throws IOException { + return cb.substract(); + } + + + public int read(char[] cbuf) + throws IOException { + return read(cbuf, 0, cbuf.length); + } + + + public int read(char[] cbuf, int off, int len) + throws IOException { + return cb.substract(cbuf, off, len); + } + + + public long skip(long n) + throws IOException { + + if (n < 0) { + throw new IllegalArgumentException(); + } + + long nRead = 0; + while (nRead < n) { + if (cb.getLength() >= n) { + cb.setOffset(cb.getStart() + (int) n); + nRead = n; + } else { + nRead += cb.getLength(); + cb.setOffset(cb.getEnd()); + int toRead = 0; + if (cb.getChars().length < (n - nRead)) { + toRead = cb.getChars().length; + } else { + toRead = (int) (n - nRead); + } + int nb = realReadChars(cb.getChars(), 0, toRead); + if (nb < 0) + break; + } + } + + return nRead; + + } + + + public boolean ready() + throws IOException { + return (cb.getLength() > 0); + } + + + public boolean markSupported() { + return true; + } + + + public void mark(int readAheadLimit) + throws IOException { + if (cb.getLength() <= 0) { + cb.setOffset(0); + cb.setEnd(0); + } else { + if ((cb.getBuffer().length > (2 * size)) + && (cb.getLength()) < (cb.getStart())) { + System.arraycopy(cb.getBuffer(), cb.getStart(), + cb.getBuffer(), 0, cb.getLength()); + cb.setEnd(cb.getLength()); + cb.setOffset(0); + } + } + int offset = readAheadLimit; + if (offset < size) { + offset = size; + } + cb.setLimit(cb.getStart() + offset); + markPos = cb.getStart(); + } + + + public void reset() + throws IOException { + if (state == CHAR_STATE) { + if (markPos < 0) { + cb.recycle(); + markPos = -1; + throw new IOException(); + } else { + cb.setOffset(markPos); + } + } else { + bb.recycle(); + } + } + + + protected void setConverter() + throws IOException { + if (coyoteRequest != null) + enc = coyoteRequest.getCharacterEncoding(); + + gotEnc = true; + if (enc == null) + enc = DEFAULT_ENCODING; + conv = (B2CConverter) encoders.get(enc); + if (conv == null) { + conv = new B2CConverter(enc); + encoders.put(enc, conv); + } + } + + public class MRInputStream extends InputStream { + public long skip(long n) + throws IOException { + return MessageReader.this.skip(n); + } + + public void mark(int readAheadLimit) + { + try { + MessageReader.this.mark(readAheadLimit); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + public void reset() + throws IOException { + MessageReader.this.reset(); + } + + + + public int read() + throws IOException { + return MessageReader.this.readByte(); + } + + public int available() throws IOException { + return MessageReader.this.available(); + } + + public int read(final byte[] b) throws IOException { + return MessageReader.this.read(b, 0, b.length); + } + + + public int read(final byte[] b, final int off, final int len) + throws IOException { + + return MessageReader.this.read(b, off, len); + } + + + /** + * Close the stream + * Since we re-cycle, we can't allow the call to super.close() + * which would permantely disable us. + */ + public void close() throws IOException { + MessageReader.this.close(); + } + } + + MRInputStream is = new MRInputStream(); + + public InputStream asInputStream() { + return is; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MessageWriter.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MessageWriter.java new file mode 100644 index 000000000..f53e53446 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/coyote/MessageWriter.java @@ -0,0 +1,694 @@ +/* + * 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.coyote; + + +import java.io.IOException; +import java.io.Writer; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; + +import org.apache.coyote.ActionCode; +import org.apache.coyote.Request; +import org.apache.coyote.Response; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.C2BConverter; +import org.apache.tomcat.util.buf.CharChunk; + +/* + * Refactoring: original code in catalina.connector. + * - renamed to OutputWriter to avoid confusion with coyote OutputBuffer + * - + * TODO: move it to coyote, add Response.getWriter + * + */ + +/** + * The buffer used by Tomcat response. This is a derivative of the Tomcat 3.3 + * OutputBuffer, with the removal of some of the state handling (which in + * Coyote is mostly the Processor's responsability). + * + * @author Costin Manolache + * @author Remy Maucherat + */ +public class MessageWriter extends Writer + implements ByteChunk.ByteOutputChannel, CharChunk.CharOutputChannel { + + // used in getWriter, until a method is added to res. + private static final int WRITER_NOTE = 3; + + // -------------------------------------------------------------- Constants + + + public static final String DEFAULT_ENCODING = + org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING; + public static final int DEFAULT_BUFFER_SIZE = 8*1024; + + + // The buffer can be used for byte[] and char[] writing + // ( this is needed to support ServletOutputStream and for + // efficient implementations of templating systems ) + public final int INITIAL_STATE = 0; + public final int CHAR_STATE = 1; + public final int BYTE_STATE = 2; + + + // ----------------------------------------------------- Instance Variables + + + /** + * The byte buffer. + */ + private ByteChunk bb; + + + /** + * The chunk buffer. + */ + private CharChunk cb; + + + /** + * State of the output buffer. + */ + private int state = 0; + + + /** + * Number of bytes written. + */ + private int bytesWritten = 0; + + + /** + * Number of chars written. + */ + private int charsWritten = 0; + + + /** + * Flag which indicates if the output buffer is closed. + */ + private boolean closed = false; + + + /** + * Do a flush on the next operation. + */ + private boolean doFlush = false; + + + /** + * Byte chunk used to output bytes. This is just used to wrap the byte[] + * to match the coyote OutputBuffer interface + */ + private ByteChunk outputChunk = new ByteChunk(); + + + /** + * Encoding to use. + * TODO: isn't it redundant ? enc, gotEnc, conv plus the enc in the bb + */ + private String enc; + + + /** + * Encoder is set. + */ + private 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 HashMap encoders = new HashMap(); + + + /** + * Current char to byte converter. TODO: replace with Charset + */ + protected C2BConverter conv; + + + /** + * Associated Coyote response. + */ + private Response coyoteResponse; + + + /** + * Suspended flag. All output bytes will be swallowed if this is true. + */ + private boolean suspended = false; + + + // ----------------------------------------------------------- Constructors + + + /** + * Default constructor. Allocate the buffer with the default buffer size. + */ + public MessageWriter() { + + this(DEFAULT_BUFFER_SIZE); + + } + + + /** + * Alternate constructor which allows specifying the initial buffer size. + * + * @param size Buffer size to use + */ + public MessageWriter(int size) { + + bb = new ByteChunk(size); + bb.setLimit(size); + bb.setByteOutputChannel(this); + cb = new CharChunk(size); + cb.setCharOutputChannel(this); + cb.setLimit(size); + + } + + + // ------------------------------------------------------------- Properties + + + /** + * Associated Coyote response. + * + * @param coyoteResponse Associated Coyote response + */ + public void setResponse(Response coyoteResponse) { + this.coyoteResponse = coyoteResponse; + } + + + /** + * Get associated Coyote response. + * + * @return the associated Coyote response + */ + public Response getResponse() { + return this.coyoteResponse; + } + + + /** + * 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() { + + state = INITIAL_STATE; + bytesWritten = 0; + charsWritten = 0; + + cb.recycle(); + bb.recycle(); + closed = false; + suspended = false; + + if (conv!= null) { + conv.recycle(); + } + + gotEnc = false; + enc = null; + + } + + + /** + * Close the output buffer. This tries to calculate the response size if + * the response has not been committed yet. + * + * @throws IOException An underlying IOException occurred + */ + public void close() + throws IOException { + + if (closed) + return; + if (suspended) + return; + + if ((!coyoteResponse.isCommitted()) + && (coyoteResponse.getContentLengthLong() == -1)) { + // Flushing the char buffer + if (state == CHAR_STATE) { + cb.flushBuffer(); + state = BYTE_STATE; + } + // If this didn't cause a commit of the response, the final content + // length can be calculated + if (!coyoteResponse.isCommitted()) { + coyoteResponse.setContentLength(bb.getLength()); + } + } + + doFlush(false); + closed = true; + + coyoteResponse.finish(); + + } + + + /** + * Flush bytes or chars contained in the buffer. + * + * @throws IOException An underlying IOException occurred + */ + public void flush() + throws IOException { + doFlush(true); + } + + + /** + * Flush bytes or chars contained in the buffer. + * + * @throws IOException An underlying IOException occurred + */ + protected void doFlush(boolean realFlush) + throws IOException { + + if (suspended) + return; + + doFlush = true; + if (state == CHAR_STATE) { + cb.flushBuffer(); + bb.flushBuffer(); + state = BYTE_STATE; + } else if (state == BYTE_STATE) { + bb.flushBuffer(); + } else if (state == INITIAL_STATE) { + // If the buffers are empty, commit the response header + coyoteResponse.sendHeaders(); + } + doFlush = false; + + if (realFlush) { + coyoteResponse.action(ActionCode.ACTION_CLIENT_FLUSH, + coyoteResponse); + // If some exception occurred earlier, or if some IOE occurred + // here, notify the servlet with an IOE + if (coyoteResponse.isExceptionPresent()) { + throw new ClientAbortException + (coyoteResponse.getErrorException()); + } + } + + } + + + // ------------------------------------------------- Bytes Handling Methods + + + /** + * Sends the buffer data to the client output, checking the + * state of Response and calling the right interceptors. + * + * @param buf Byte buffer to be written to the response + * @param off Offset + * @param cnt Length + * + * @throws IOException An underlying IOException occurred + */ + public void realWriteBytes(byte buf[], int off, int cnt) + throws IOException { + + if (closed) + return; + if (coyoteResponse == null) + return; + + // If we really have something to write + if (cnt > 0) { + // real write to the adapter + outputChunk.setBytes(buf, off, cnt); + try { + coyoteResponse.doWrite(outputChunk); + } catch (IOException e) { + // An IOException on a write is almost always due to + // the remote client aborting the request. Wrap this + // so that it can be handled better by the error dispatcher. + throw new ClientAbortException(e); + } + } + + } + + + public void write(byte b[], int off, int len) throws IOException { + + if (suspended) + return; + + if (state == CHAR_STATE) + cb.flushBuffer(); + state = BYTE_STATE; + writeBytes(b, off, len); + + } + + + private void writeBytes(byte b[], int off, int len) + throws IOException { + + if (closed) + return; + + bb.append(b, off, len); + bytesWritten += len; + + // if called from within flush(), then immediately flush + // remaining bytes + if (doFlush) { + bb.flushBuffer(); + } + + } + + + public void writeByte(int b) + throws IOException { + + if (suspended) + return; + + if (state == CHAR_STATE) + cb.flushBuffer(); + state = BYTE_STATE; + + bb.append( (byte)b ); + bytesWritten++; + + } + + + // ------------------------------------------------- Chars Handling Methods + + + public void write(int c) + throws IOException { + + if (suspended) + return; + + state = CHAR_STATE; + + cb.append((char) c); + charsWritten++; + + } + + + public void write(char c[]) + throws IOException { + + if (suspended) + return; + + write(c, 0, c.length); + + } + + + public void write(char c[], int off, int len) + throws IOException { + + if (suspended) + return; + + state = CHAR_STATE; + + cb.append(c, off, len); + charsWritten += len; + + } + + + public void write(StringBuffer sb) + throws IOException { + + if (suspended) + return; + + state = CHAR_STATE; + + int len = sb.length(); + charsWritten += len; + cb.append(sb); + + } + + + /** + * Append a string to the buffer + */ + public void write(String s, int off, int len) + throws IOException { + + if (suspended) + return; + + state=CHAR_STATE; + + charsWritten += len; + if (s==null) + s="null"; + cb.append( s, off, len ); + + } + + + public void write(String s) + throws IOException { + + if (suspended) + return; + + state = CHAR_STATE; + 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 flushChars() + throws IOException { + + cb.flushBuffer(); + state = BYTE_STATE; + + } + + + public boolean flushCharsNeeded() { + return state == CHAR_STATE; + } + + + public void setEncoding(String s) { + enc = s; + } + + + public void realWriteChars(char c[], int off, int len) + throws IOException { + + if (!gotEnc) + setConverter(); + + conv.convert(c, off, len); + conv.flushBuffer(); // ??? + + } + + + public void checkConverter() + throws IOException { + + if (!gotEnc) + setConverter(); + + } + + + protected void setConverter() + throws IOException { + + if (coyoteResponse != null) + enc = coyoteResponse.getCharacterEncoding(); + + gotEnc = true; + if (enc == null) + enc = DEFAULT_ENCODING; + conv = (C2BConverter) encoders.get(enc); + if (conv == null) { + + if (System.getSecurityManager() != null){ + try{ + conv = (C2BConverter)AccessController.doPrivileged( + new PrivilegedExceptionAction(){ + + public Object run() throws IOException{ + return new C2BConverter(bb, enc); + } + + } + ); + }catch(PrivilegedActionException ex){ + Exception e = ex.getException(); + if (e instanceof IOException) + throw (IOException)e; + } + } else { + conv = new C2BConverter(bb, enc); + } + + encoders.put(enc, conv); + + } + } + + + // -------------------- BufferedOutputStream compatibility + + + /** + * Real write - this buffer will be sent to the client + */ + public void flushBytes() + throws IOException { + + bb.flushBuffer(); + + } + + + public int getBytesWritten() { + return bytesWritten; + } + + + public int getCharsWritten() { + return charsWritten; + } + + + public int getContentWritten() { + return bytesWritten + charsWritten; + } + + + /** + * True if this buffer hasn't been used ( since recycle() ) - + * i.e. no chars or bytes have been added to the buffer. + */ + public boolean isNew() { + return (bytesWritten == 0) && (charsWritten == 0); + } + + + public void setBufferSize(int size) { + if (size > bb.getLimit()) {// ?????? + bb.setLimit(size); + } + } + + + public void reset() { + + //count=0; + bb.recycle(); + bytesWritten = 0; + cb.recycle(); + charsWritten = 0; + gotEnc = false; + enc = null; + state = INITIAL_STATE; + } + + + public int getBufferSize() { + return bb.getLimit(); + } + + + public static MessageWriter getWriter(Request req, Response res, int size) + { + MessageWriter out=(MessageWriter)req.getNote(MessageWriter.WRITER_NOTE); + if( out == null ) { + if( size<=0 ) { + out=new MessageWriter(); + } else { + out=new MessageWriter(size); + } + out.setResponse(res); + req.setNote(MessageWriter.WRITER_NOTE, out ); + } + return out; + } + + public ByteChunk getByteBuffer() { + return outputChunk; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/TomcatLiteWebXmlConfig.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/TomcatLiteWebXmlConfig.java new file mode 100644 index 000000000..dff9e3479 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/TomcatLiteWebXmlConfig.java @@ -0,0 +1,64 @@ +/* + */ +package org.apache.tomcat.lite.webxml; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.apache.tomcat.lite.ServletContextImpl; +import org.apache.tomcat.lite.ContextPreinitListener; + +/** + * Default configurator - parse web.xml, init the context. + * + * Will be executed first - if set as the default config addon. + * + * Possible extensions: + * - read from a .ser file instead of web.xml + * - custom code for manual/extra config + * - read from a central repo + * + * @author Costin Manolache + */ +public class TomcatLiteWebXmlConfig implements ContextPreinitListener { + + protected void readWebXml(ServletContextImpl ctx, + String base) throws ServletException { + // TODO: .ser, reloading, etc +// if (contextConfig != null && contextConfig.fileName != null) { +// // TODO: this should move to deploy - if not set, there is no point +// File f = new File(contextConfig.fileName); +// if (f.exists()) { +// if (f.lastModified() > contextConfig.timestamp + 1000) { +// log("Reloading web.xml"); +// contextConfig = null; +// } +// } else { +// log("Old web.xml"); +// contextConfig = null; +// } +// } + if (base != null) { + WebXml webXml = new WebXml(ctx.getContextConfig()); + webXml.readWebXml(base); + } + } + + @Override + public void preInit(ServletContext ctx) { + ServletContextImpl servletContext = + (ServletContextImpl) ctx; + + String base = servletContext.getBasePath(); + if (base == null) { + return; // nothing we can do + } + try { + readWebXml(servletContext, base); + } catch (ServletException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebAnnotation.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebAnnotation.java new file mode 100644 index 000000000..cecaf60a8 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebAnnotation.java @@ -0,0 +1,152 @@ +package org.apache.tomcat.lite.webxml; + +import java.util.Iterator; +import java.util.List; + +import javax.annotation.security.DeclareRoles; +import javax.annotation.security.RunAs; + +import org.apache.tomcat.lite.ServletContextConfig; +import org.apache.tomcat.lite.ServletContextConfig.FilterData; +import org.apache.tomcat.lite.ServletContextConfig.ServletData; + + + + +/** + * Based on catalina.WebAnnotationSet + * + * Supports: + * @DeclaresRoles - on Servlet class - web-app/security-role/role-name + * @RunAs - on Servlet class - web-app/servlet/run-as + * + * + * No support for jndi @Resources, @Resource + * No @InjectionComplete callback annotation + * + * No support for @EJB, @WebServiceRef + * + * @author costin + * @author Fabien Carrion + */ +public class WebAnnotation { + + /** + * Process the annotations on a context. + */ + public static void loadApplicationAnnotations(ServletContextConfig context, ClassLoader classLoader) { + loadApplicationListenerAnnotations(context, classLoader); + loadApplicationFilterAnnotations(context, classLoader); + loadApplicationServletAnnotations(context, classLoader); + } + + + // -------------------------------------------------------- protected Methods + + + /** + * Process the annotations for the listeners. + */ + static void loadApplicationListenerAnnotations(ServletContextConfig context, ClassLoader classLoader) { + List applicationListeners = context.listenerClass; + for (int i = 0; i < applicationListeners.size(); i++) { + loadClassAnnotation(context, (String)applicationListeners.get(i), classLoader); + } + } + + + /** + * Process the annotations for the filters. + */ + static void loadApplicationFilterAnnotations(ServletContextConfig context, ClassLoader classLoader) { + Iterator i1 = context.filters.values().iterator(); + while (i1.hasNext()) { + FilterData fc = (FilterData) i1.next(); + loadClassAnnotation(context, fc.filterClass, classLoader); + } + } + + + /** + * Process the annotations for the servlets. + * @param classLoader + */ + static void loadApplicationServletAnnotations(ServletContextConfig context, ClassLoader classLoader) { + Class classClass = null; + + + Iterator i1 = context.servlets.values().iterator(); + while (i1.hasNext()) { + ServletData sd = (ServletData) i1.next(); + if (sd.servletClass == null) { + continue; + } + + try { + classClass = classLoader.loadClass(sd.servletClass); + } catch (ClassNotFoundException e) { + // We do nothing + } catch (NoClassDefFoundError e) { + // We do nothing + } + + if (classClass == null) { + continue; + } + + loadClassAnnotation(context, classClass); + /* Process RunAs annotation which can be only on servlets. + * Ref JSR 250, equivalent to the run-as element in + * the deployment descriptor + */ + if (classClass.isAnnotationPresent(RunAs.class)) { + RunAs annotation = (RunAs) + classClass.getAnnotation(RunAs.class); + sd.runAs = annotation.value(); + } + } + } + + /** + * Process the annotations on a context for a given className. + */ + static void loadClassAnnotation(ServletContextConfig context, + String classClass2, ClassLoader classLoader) { + + Class classClass = null; + + try { + classClass = classLoader.loadClass(classClass2); + } catch (ClassNotFoundException e) { + // We do nothing + } catch (NoClassDefFoundError e) { + // We do nothing + } + + if (classClass == null) { + return; + } + loadClassAnnotation(context, classClass); + } + + static void loadClassAnnotation(ServletContextConfig context, + Class classClass) { + + /* Process DeclareRoles annotation. + * Ref JSR 250, equivalent to the security-role element in + * the deployment descriptor + */ + if (classClass.isAnnotationPresent(DeclareRoles.class)) { + DeclareRoles annotation = (DeclareRoles) + classClass.getAnnotation(DeclareRoles.class); + for (int i = 0; annotation.value() != null && + i < annotation.value().length; i++) { + context.securityRole.add(annotation.value()[i]); + } + } + + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebResourceCollectionData.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebResourceCollectionData.java new file mode 100644 index 000000000..94fa2a41c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebResourceCollectionData.java @@ -0,0 +1,13 @@ +/** + * + */ +package org.apache.tomcat.lite.webxml; + +import java.io.Serializable; +import java.util.ArrayList; + +public class WebResourceCollectionData implements Serializable { + public String webResourceName; + public ArrayList urlPattern = new ArrayList(); + public ArrayList httpMethod = new ArrayList(); +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebXml.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebXml.java new file mode 100644 index 000000000..65a3fc119 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/webxml/WebXml.java @@ -0,0 +1,384 @@ +/* + */ +package org.apache.tomcat.lite.webxml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.servlet.ServletException; + +import org.apache.tomcat.lite.ServletContextConfig; +import org.apache.tomcat.lite.ServletContextConfig.EnvEntryData; +import org.apache.tomcat.lite.ServletContextConfig.FilterData; +import org.apache.tomcat.lite.ServletContextConfig.FilterMappingData; +import org.apache.tomcat.lite.ServletContextConfig.SecurityConstraintData; +import org.apache.tomcat.lite.ServletContextConfig.ServletData; +import org.apache.tomcat.util.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +/** + * General-purpose utility to process an web.xml file. Result + * is a tree of objects starting with WebAppData. + * + * TODO: allow writting of web.xml, allow modification ( preserving + * comments ) + * + * @author costin + */ +public class WebXml { + ServletContextConfig d; + + public WebXml(ServletContextConfig cfg) { + d = cfg; + } + + /** + * Serialize the data, for caching. + */ + public void saveWebAppData(String fileName) throws IOException { + ObjectOutputStream oos = + new ObjectOutputStream(new FileOutputStream(fileName)); + oos.writeObject(d); + oos.close(); + } + + public ServletContextConfig getWebAppData() { + return d; + } + + public void readWebXml(String baseDir) throws ServletException { + try { + File webXmlFile = new File( baseDir + "/WEB-INF/web.xml"); + if (!webXmlFile.exists()) { + return; + } + d.fileName = webXmlFile.getCanonicalPath(); + d.timestamp = webXmlFile.lastModified(); + + FileInputStream fileInputStream = new FileInputStream(webXmlFile); + readWebXml(fileInputStream); + } catch (Exception e) { + e.printStackTrace(); + throw new ServletException(e); + } + } + + public void readWebXml(InputStream fileInputStream) + throws ServletException { + try { + + Document document = DomUtil.readXml(fileInputStream); + Node webappNode = DomUtil.getChild(document, "web-app"); + + String fullS = DomUtil.getAttribute(webappNode, "full"); + if (fullS != null && fullS.equalsIgnoreCase("true")) { + d.full = true; + } + + d.displayName = DomUtil.getAttribute(webappNode, "display-name"); + + // Process each child of web-app + Node confNode = DomUtil.getChild(webappNode, "filter"); + while (confNode != null ) { + processFilter(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "filter-mapping"); + while (confNode != null ) { + processFilterMapping(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "context-param"); + while (confNode != null ) { + String n = DomUtil.getChildContent(confNode, "param-name").trim(); + String v = DomUtil.getChildContent(confNode, "param-value").trim(); + d.contextParam.put(n, v); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "mime-mapping"); + while (confNode != null ) { + String n = DomUtil.getChildContent(confNode, "extension"); + String t = DomUtil.getChildContent(confNode, "mime-type"); + d.mimeMapping.put(n, t); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "error-page"); + while (confNode != null ) { + processErrorPage(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "jsp-config"); + while (confNode != null ) { + processJspConfig(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "servlet"); + while (confNode != null ) { + processServlet(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "servlet-mapping"); + while (confNode != null ) { + processServletMapping(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "listener"); + while (confNode != null ) { + String lClass = DomUtil.getChildContent(confNode, "listener-class"); + d.listenerClass.add(lClass); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "security-constraint"); + while (confNode != null ) { + processSecurityConstraint(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "login-config"); + while (confNode != null ) { + processLoginConfig(confNode); + confNode = DomUtil.getNext(confNode); + if (confNode != null) + throw new ServletException("Multiple login-config"); + } + + confNode = DomUtil.getChild(webappNode, "session-config"); + while (confNode != null ) { + String n = DomUtil.getChildContent(confNode, "session-timeout"); + int stout = Integer.parseInt(n); + d.sessionTimeout = stout; + confNode = DomUtil.getNext(confNode); + if (confNode != null) + throw new ServletException("Multiple session-config"); + } + + confNode = DomUtil.getChild(webappNode, "welcome-file-list"); + while (confNode != null ) { + Node wf = DomUtil.getChild(confNode, "welcome-file"); + while (wf != null) { + String file = DomUtil.getContent(wf); + d.welcomeFileList.add(file); + wf = DomUtil.getNext(wf); + } + // more sections ? + confNode = DomUtil.getNext(confNode); + } + + // Not supported right now - TODO: collect, have jndi plugin + confNode = DomUtil.getChild(webappNode, "env-entry"); + while (confNode != null ) { + processEnvEntry(confNode); + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(webappNode, "locale-encoding-mapping-list"); + while (confNode != null ) { + confNode = DomUtil.getNext(confNode); + String n = DomUtil.getChildContent(confNode, "locale"); + String t = DomUtil.getChildContent(confNode, "encoding"); + d.localeEncodingMapping.put(n, t); + } + + confNode = DomUtil.getChild(webappNode, "distributable"); + while (confNode != null ) { + d.distributable = true; + confNode = DomUtil.getNext(confNode); + } + + confNode = DomUtil.getChild(confNode, "security-role"); + while (confNode != null ) { + String n = DomUtil.getChildContent(confNode, "role-name"); + d.securityRole.add(n); + confNode = DomUtil.getNext(confNode); + } + + } catch (Exception e) { + e.printStackTrace(); + throw new ServletException(e); + } + } + + private void processJspConfig(Node confNode) { + Node tagLib = DomUtil.getChild(confNode, "taglib"); + while (tagLib != null) { + String uri = DomUtil.getChildContent(tagLib, "taglib-uri"); + String l = DomUtil.getChildContent(tagLib, "taglib-location"); + //d.tagLibs.put(uri, l); + tagLib = DomUtil.getNext(tagLib); + } + + tagLib = DomUtil.getChild(confNode, "jsp-property-group"); + while (tagLib != null) { + // That would be the job of the JSP servlet to process. + tagLib = DomUtil.getNext(tagLib); + } + } + + private void processEnvEntry(Node confNode) { + EnvEntryData ed = new EnvEntryData(); + ed.envEntryName = DomUtil.getChildContent(confNode,"env-entry-name"); + ed.envEntryType = DomUtil.getChildContent(confNode,"env-entry-type"); + ed.envEntryValue = DomUtil.getChildContent(confNode,"env-entry-value"); + d.envEntry.add(ed); + } + + private void processLoginConfig(Node confNode) { + d.authMethod = DomUtil.getChildContent(confNode,"auth-method"); + d.realmName = DomUtil.getChildContent(confNode,"auth-method"); + Node formNode = DomUtil.getChild(confNode, "form-login-config"); + if (formNode != null) { + d.formLoginPage = DomUtil.getChildContent(formNode,"form-login-page"); + d.formErrorPage = DomUtil.getChildContent(formNode,"form-error-page"); + } + } + + private void processSecurityConstraint(Node confNode) { + SecurityConstraintData sd = new SecurityConstraintData(); + Node cn = DomUtil.getChild(confNode, "web-resource-collection"); + while (cn != null) { + WebResourceCollectionData wrd = new WebResourceCollectionData(); + wrd.webResourceName = DomUtil.getChildContent(cn, "web-resource-name"); + Node scn = DomUtil.getChild(cn,"url-pattern"); + while (scn != null) { + wrd.urlPattern.add(DomUtil.getContent(scn)); + scn = DomUtil.getNext(scn); + } + scn = DomUtil.getChild(cn,"http-method"); + while (scn != null) { + wrd.httpMethod.add(DomUtil.getContent(scn)); + scn = DomUtil.getNext(scn); + } + cn = DomUtil.getNext(cn); + } + d.securityConstraint.add(sd); + } + + private void processErrorPage(Node confNode) { + String name = DomUtil.getChildContent(confNode,"location"); + String c = DomUtil.getChildContent(confNode,"error-code"); + String t = DomUtil.getChildContent(confNode,"exception-type"); + if (c != null) { + d.errorPageCode.put(c, name); + } + if (t != null) { + d.errorPageException.put(t, name); + } + } + + private void processServlet(Node confNode) throws ServletException { + ServletData sd = new ServletData(); + + sd.servletName = DomUtil.getChildContent(confNode,"servlet-name"); + sd.servletClass = DomUtil.getChildContent(confNode,"servlet-class"); + sd.jspFile = DomUtil.getChildContent(confNode,"jsp-file"); + + processInitParams(confNode, sd.initParams); + + d.servlets.put( sd.servletName, sd ); + + String los = DomUtil.getChildContent(confNode, "load-on-startup"); + if (los != null ) { + sd.loadOnStartup = Integer.parseInt(los); + } + + Node sn = DomUtil.getChild(confNode, "security-role-ref"); + while (sn != null ) { + String roleName = DomUtil.getChildContent(sn, "role-name"); + String roleLink = DomUtil.getChildContent(sn, "role-link"); + if (roleLink == null) { + sd.securityRoleRef.put(roleName, ""); + } else { + sd.securityRoleRef.put(roleName, roleLink); + } + sn = DomUtil.getNext(sn); + } + } + + private void processInitParams(Node confNode, HashMap initParams) { + Node initN = DomUtil.getChild(confNode, "init-param"); + while (initN != null ) { + String n = DomUtil.getChildContent(initN, "param-name"); + String v = DomUtil.getChildContent(initN, "param-value"); + initParams.put(n, v); + initN = DomUtil.getNext(initN); + } + } + + private void processServletMapping(Node confNode) { + String name = DomUtil.getChildContent(confNode,"servlet-name"); + Node dataN = DomUtil.getChild(confNode, "url-pattern"); + while (dataN != null) { + String path = DomUtil.getContent(dataN).trim(); + dataN = DomUtil.getNext(dataN); + + if (! (path.startsWith("/") || path.startsWith("*"))) { + // backward compat + path = "/" + path; + } + d.servletMapping.put(path, name); + } + } + + private void processFilterMapping(Node confNode) { + String filterName = DomUtil.getChildContent(confNode,"filter-name"); + // multiple + ArrayList dispatchers = new ArrayList(); + Node dataN = DomUtil.getChild(confNode, "dispatcher"); + while (dataN != null ) { + String d = DomUtil.getContent(dataN); + dispatchers.add(d); + dataN = DomUtil.getNext(dataN); + } + + // Multiple url-pattern and servlet-name in one + // mapping rule. Need to be applied in order. + dataN = DomUtil.getChild(confNode, "url-pattern"); + while (dataN != null ) { + FilterMappingData fm = new FilterMappingData(); + fm.filterName = filterName; + fm.dispatcher = dispatchers; + String path = DomUtil.getContent(dataN); + dataN = DomUtil.getNext(dataN); + fm.urlPattern = path; + d.filterMappings.add(fm); + } + dataN = DomUtil.getChild(confNode, "servlet-name"); + while (dataN != null ) { + FilterMappingData fm = new FilterMappingData(); + fm.filterName = filterName; + fm.dispatcher = dispatchers; + String sn = DomUtil.getContent(dataN); + dataN = DomUtil.getNext(dataN); + fm.servletName = sn; + d.filterMappings.add(fm); + } + } + + private void processFilter(Node confNode) { + String name = DomUtil.getChildContent(confNode,"filter-name"); + String sclass = DomUtil.getChildContent(confNode,"filter-class"); + + FilterData fd = new FilterData(); + processInitParams(confNode, fd.initParams); + fd.filterName = name; + fd.filterClass = sclass; + d.filters.put(name, fd); + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/CopyUtils.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/CopyUtils.java new file mode 100644 index 000000000..ebe966d90 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/CopyUtils.java @@ -0,0 +1,257 @@ +package org.apache.tomcat.servlets.file; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; + +import javax.servlet.ServletOutputStream; + +public class CopyUtils { + protected static int input = 2048; + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param resourceInfo The resource info + * @param writer The writer to write to + * + * @exception IOException if an input/output error occurs + */ + public static void copy(InputStream is, + PrintWriter writer, + String fileEncoding) + throws IOException { + + IOException exception = null; + + InputStream resourceInputStream = is; + + Reader reader; + if (fileEncoding == null) { + reader = new InputStreamReader(resourceInputStream); + } else { + reader = new InputStreamReader(resourceInputStream, + fileEncoding); + } + + // Copy the input stream to the output stream + exception = copyRange(reader, writer); + + // Clean up the reader + try { + reader.close(); + } catch (Throwable t) { + ; + } + + // Rethrow any exception that has occurred + if (exception != null) + throw exception; + + } + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param resourceInfo The resource information + * @param ostream The output stream to write to + * + * @exception IOException if an input/output error occurs + */ + public static void copy(InputStream is, OutputStream ostream) + throws IOException { + + IOException exception = null; + InputStream resourceInputStream = null; + + resourceInputStream = is; + + InputStream istream = new BufferedInputStream + (resourceInputStream, input); + + // Copy the input stream to the output stream + exception = CopyUtils.copyRange(istream, ostream); + + // Clean up the input stream + try { + istream.close(); + } catch (Throwable t) { + ; + } + + // Rethrow any exception that has occurred + if (exception != null) + throw exception; + } + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param istream The input stream to read from + * @param ostream The output stream to write to + * @return Exception which occurred during processing + */ + public static IOException copyRange(InputStream istream, + OutputStream ostream) { + + // Copy the input stream to the output stream + IOException exception = null; + byte buffer[] = new byte[input]; + int len = buffer.length; + while (true) { + try { + len = istream.read(buffer); + if (len == -1) + break; + ostream.write(buffer, 0, len); + } catch (IOException e) { + exception = e; + len = -1; + break; + } + } + return exception; + + } + + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param reader The reader to read from + * @param writer The writer to write to + * @return Exception which occurred during processing + */ + public static IOException copyRange(Reader reader, PrintWriter writer) { + + // Copy the input stream to the output stream + IOException exception = null; + char buffer[] = new char[input]; + int len = buffer.length; + while (true) { + try { + len = reader.read(buffer); + if (len == -1) + break; + writer.write(buffer, 0, len); + } catch (IOException e) { + exception = e; + len = -1; + break; + } + } + return exception; + + } + + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param istream The input stream to read from + * @param ostream The output stream to write to + * @param start Start of the range which will be copied + * @param end End of the range which will be copied + * @return Exception which occurred during processing + */ + public static IOException copyRange(InputStream istream, + ServletOutputStream ostream, + long start, long end) { + + try { + istream.skip(start); + } catch (IOException e) { + return e; + } + + IOException exception = null; + long bytesToRead = end - start + 1; + + byte buffer[] = new byte[input]; + int len = buffer.length; + while ( (bytesToRead > 0) && (len >= buffer.length)) { + try { + len = istream.read(buffer); + if (bytesToRead >= len) { + ostream.write(buffer, 0, len); + bytesToRead -= len; + } else { + ostream.write(buffer, 0, (int) bytesToRead); + bytesToRead = 0; + } + } catch (IOException e) { + exception = e; + len = -1; + } + if (len < buffer.length) + break; + } + + return exception; + + } + + + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param reader The reader to read from + * @param writer The writer to write to + * @param start Start of the range which will be copied + * @param end End of the range which will be copied + * @return Exception which occurred during processing + */ + public static IOException copyRange(Reader reader, PrintWriter writer, + long start, long end) { + + try { + reader.skip(start); + } catch (IOException e) { + return e; + } + + IOException exception = null; + long bytesToRead = end - start + 1; + + char buffer[] = new char[input]; + int len = buffer.length; + while ( (bytesToRead > 0) && (len >= buffer.length)) { + try { + len = reader.read(buffer); + if (bytesToRead >= len) { + writer.write(buffer, 0, len); + bytesToRead -= len; + } else { + writer.write(buffer, 0, (int) bytesToRead); + bytesToRead = 0; + } + } catch (IOException e) { + exception = e; + len = -1; + } + if (len < buffer.length) + break; + } + + return exception; + + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/DefaultServlet.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/DefaultServlet.java new file mode 100644 index 000000000..d9986b9ba --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/DefaultServlet.java @@ -0,0 +1,1019 @@ +/* + * Copyright 1999,2004-2006 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.file; + + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.Reader; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.servlets.util.Range; + +/** + * The default resource-serving servlet for most web applications, + * used to serve static resources such as HTML pages and images. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + * @author Costin Manolache + */ +public class DefaultServlet extends HttpServlet { + + + // ----------------------------------------------------- Instance Variables + + + /** + * Should we generate directory listings? + */ + protected boolean listings = true; + + /** + * Array containing the safe characters set. + */ + protected static URLEncoder urlEncoder; + + /** + * Allow a readme file to be included. + */ + protected String readmeFile = null; + + // TODO: find a better default + /** + * The input buffer size to use when serving resources. + */ + protected static int input = 2048; + + /** + * The output buffer size to use when serving resources. + */ + protected int output = 2048; + + /** + * File encoding to be used when reading static files. If none is specified + * the platform default is used. + */ + protected String fileEncoding = null; + + static ThreadLocal formatTL = new ThreadLocal(); + + Dir2Html dir2Html = new Dir2Html(); + /** + * Full range marker. + */ + protected static ArrayList FULL = new ArrayList(); + + // Context base dir + protected File basePath; + protected String basePathName; + + // ----------------------------------------------------- Static Initializer + + + /** + * GMT timezone - all HTTP dates are on GMT + */ + static { + urlEncoder = new URLEncoder(); + urlEncoder.addSafeCharacter('-'); + urlEncoder.addSafeCharacter('_'); + urlEncoder.addSafeCharacter('.'); + urlEncoder.addSafeCharacter('*'); + urlEncoder.addSafeCharacter('/'); + } + + + /** + * MIME multipart separation string + */ + protected static final String mimeSeparation = "TOMCAT_MIME_BOUNDARY"; + + + // --------------------------------------------------------- Public Methods + + + /** + * Finalize this servlet. + */ + public void destroy() { + } + + + /** + * Initialize this servlet. + */ + public void init() throws ServletException { + String realPath = getServletContext().getRealPath("/"); + basePath = new File(realPath); + try { + basePathName = basePath.getCanonicalPath(); + } catch (IOException e) { + basePathName = basePath.getAbsolutePath(); + } + + // Set our properties from the initialization parameters + String value = null; + try { + value = getServletConfig().getInitParameter("input"); + input = Integer.parseInt(value); + } catch (Throwable t) { + ; + } + try { + value = getServletConfig().getInitParameter("listings"); + if (value != null ) + listings = (new Boolean(value)).booleanValue(); + } catch (Throwable t) { + ; + } + try { + value = getServletConfig().getInitParameter("output"); + output = Integer.parseInt(value); + } catch (Throwable t) { + ; + } + fileEncoding = getServletConfig().getInitParameter("fileEncoding"); + + readmeFile = getServletConfig().getInitParameter("readmeFile"); + + + // Sanity check on the specified buffer sizes + if (input < 256) + input = 256; + if (output < 256) + output = 256; + + } + + public void loadDefaultMime() throws IOException { + File mimeF = new File("/etc/mime.types"); + boolean loaded =false; + if (!mimeF.exists()) { + loaded =true; + loadMimeFile( new FileInputStream(mimeF)); + } + mimeF = new File("/etc/httpd/mime.types"); + if (mimeF.exists()) { + loaded =true; + loadMimeFile( new FileInputStream(mimeF)); + } + if (!loaded) { + throw new IOException("mime.types not found"); + } + } + + public void loadMimeFile(InputStream is) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line = null; + while((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() == 0) continue; + if (line.startsWith("#")) continue; + String[] parts = line.split("\\w+"); + String type = parts[0]; + for (int i=1; i < parts.length; i++) { + String ext = parts[i]; + if (!ext.equals("")) { + addMimeType(type, ext); + System.err.println(type + " = " + ext); + } else { + System.err.println("XXX " + ext); + } + } + } + + } + + private void addMimeType(String type, String ext) { + + } + // ------------------------------------------------------ Protected Methods + + public static final String INCLUDE_REQUEST_URI_ATTR = + "javax.servlet.include.request_uri"; + public static final String INCLUDE_SERVLET_PATH_ATTR = + "javax.servlet.include.servlet_path"; + public static final String INCLUDE_PATH_INFO_ATTR = + "javax.servlet.include.path_info"; + public static final String INCLUDE_CONTEXT_PATH_ATTR = + "javax.servlet.include.context_path"; + + + /** + * Return the relative path associated with this servlet. + * Multiple sources are used - include attribute, servlet path, etc + * + * @param request The servlet request we are processing + */ + public static String getRelativePath(HttpServletRequest request) { + + // Are we being processed by a RequestDispatcher.include()? + if (request.getAttribute(INCLUDE_REQUEST_URI_ATTR) != null) { + String result = (String) request.getAttribute( + INCLUDE_PATH_INFO_ATTR); + if (result == null) + result = (String) request.getAttribute( + INCLUDE_SERVLET_PATH_ATTR); + if ((result == null) || (result.equals(""))) + result = "/"; + return (result); + } + + // No, extract the desired path directly from the request + // For 'default' servlet, the path info contains the path + // if this is mapped to serve a subset of the files - we + // need both + + String result = request.getPathInfo(); + if (result == null) { + result = request.getServletPath(); + } else { + result = request.getServletPath() + result; + } + if ((result == null) || (result.equals(""))) { + result = "/"; + } + return (result); + } + + protected void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + serveResource(request, response, true); + + } + + + protected void doHead(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + serveResource(request, response, false); + + } + + + protected void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + // TODO: not allowed ? + doGet(request, response); + } + + + + + /** + * Check if the conditions specified in the optional If headers are + * satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resourceAttributes The resource information + * @return boolean true if the resource meets all the specified conditions, + * and false if any of the conditions is not satisfied, in which case + * request processing is stopped + */ + protected boolean checkIfHeaders(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + + return checkIfMatch(request, response, resourceAttributes) + && checkIfModifiedSince(request, response, resourceAttributes) + && checkIfNoneMatch(request, response, resourceAttributes) + && checkIfUnmodifiedSince(request, response, resourceAttributes); + + } + + + /** + * Get the ETag associated with a file. + * + * @param resourceAttributes The resource information + */ + protected String getETag(File resourceAttributes) { + return "W/\"" + resourceAttributes.length() + "-" + + resourceAttributes.lastModified() + "\""; + } + + LRUFileCache fileCache; + + public void setFileCacheSize(int size) { + if (fileCache == null) { + fileCache = new LRUFileCache(); + } + fileCache.cacheSize = size; + } + + public int getFileCacheSize() { + if (fileCache ==null ) return 0; + return fileCache.cacheSize; + } + + static class LRUFileCache extends LinkedHashMap { + int cacheSize; + public LRUFileCache() { + } + protected boolean removeEldestEntity(Map.Entry eldest) { + return size() > cacheSize; + } + } + + + /** + * Serve the specified resource, optionally including the data content. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param content Should the content be included? + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + protected void serveResource(HttpServletRequest request, + HttpServletResponse response, + boolean content) + throws IOException, ServletException { + + // Identify the requested resource path - checks include attributes + String path = getRelativePath(request); + + // TODO: Check the file cache to avoid a bunch of FS accesses. + + File resFile = new File(basePath, path); + + if (!resFile.exists()) { + send404(request, response); + return; + } + + boolean isDir = resFile.isDirectory(); + + if (isDir) { + getServletContext(); + // Skip directory listings if we have been configured to + // suppress them + if (!listings) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + request.getRequestURI()); + return; + } + dir2Html.renderDir(request, response, resFile, fileEncoding, content, + path); + + return; + } + + // If the resource is not a collection, and the resource path + // ends with "/" or "\", return NOT FOUND + if (path.endsWith("/") || (path.endsWith("\\"))) { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute( + INCLUDE_REQUEST_URI_ATTR); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } + response.sendError(HttpServletResponse.SC_NOT_FOUND, + requestUri); + return; + } + + // Check if the conditions specified in the optional If headers are + // satisfied. + + // Checking If headers. The method will generate the + boolean included = + (request.getAttribute(INCLUDE_CONTEXT_PATH_ATTR) != null); + if (!included + && !checkIfHeaders(request, response, resFile)) { + return; + } + + + // Find content type. + String contentType = getServletContext().getMimeType(resFile.getName()); + + long contentLength = -1L; + + // ETag header + response.setHeader("ETag", getETag(resFile)); + + // TODO: remove the sync, optimize - it's from ResourceAttribute + String lastModifiedHttp = lastModifiedHttp(resFile); + + // Last-Modified header + response.setHeader("Last-Modified", lastModifiedHttp); + + // Get content length + contentLength = resFile.length(); + // Special case for zero length files, which would cause a + // (silent) ISE when setting the output buffer size + if (contentLength == 0L) { + content = false; + } + + ServletOutputStream ostream = null; + PrintWriter writer = null; + + if (content) { + + // Trying to retrieve the servlet output stream + + try { + ostream = response.getOutputStream(); + } catch (IllegalStateException e) { + // If it fails, we try to get a Writer instead if we're + // trying to serve a text file + if ( (contentType == null) + || (contentType.startsWith("text")) ) { + writer = response.getWriter(); + } else { + throw e; + } + } + } + + // Parse range specifier + + ArrayList ranges = parseRange(request, response, resFile); + + if ( ( ((ranges == null) || (ranges.isEmpty())) + && (request.getHeader("Range") == null) ) + || (ranges == FULL) ) { + + processFullFile(response, content, resFile, contentType, + contentLength, ostream, writer); + + } else { + + if ((ranges == null) || (ranges.isEmpty())) + return; + + // Partial content response. + + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + if (ranges.size() == 1) { + + processSingleRange(response, content, resFile, contentType, + ostream, writer, ranges); + + } else { + + processMultiRange(response, content, resFile, contentType, + ostream, writer, ranges); + + } + + } + + } + + + private void processMultiRange(HttpServletResponse response, + boolean content, + File resFile, + String contentType, + ServletOutputStream ostream, + PrintWriter writer, + ArrayList ranges) throws IOException { + response.setContentType("multipart/byteranges; boundary=" + + mimeSeparation); + + if (content) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + copyRanges(resFile, ostream, ranges.iterator(), + contentType); + } else { + copyRanges(resFile, writer, ranges.iterator(), + contentType); + } + } + } + + + private void processSingleRange(HttpServletResponse response, boolean content, File resFile, String contentType, ServletOutputStream ostream, PrintWriter writer, ArrayList ranges) throws IOException { + Range range = (Range) ranges.get(0); + response.addHeader("Content-Range", "bytes " + + range.start + + "-" + range.end + "/" + + range.length); + long length = range.end - range.start + 1; + if (length < Integer.MAX_VALUE) { + response.setContentLength((int) length); + } else { + // Set the content-length as String to be able to use a long + response.setHeader("content-length", "" + length); + } + + if (contentType != null) { + response.setContentType(contentType); + } + + if (content) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + FileCopyUtils.copy(resFile, ostream, range); + } else { + FileCopyUtils.copy(resFile, writer, range, fileEncoding); + } + } + } + + + private void processFullFile(HttpServletResponse response, boolean content, File resFile, String contentType, long contentLength, ServletOutputStream ostream, PrintWriter writer) throws IOException { + // Set the appropriate output headers + if (contentType != null) { + response.setContentType(contentType); + } + if ((contentLength >= 0)) { + if (contentLength < Integer.MAX_VALUE) { + response.setContentLength((int) contentLength); + } else { + // Set the content-length as String to be able to use a long + response.setHeader("content-length", "" + contentLength); + } + } + + // Copy the input stream to our output stream (if requested) + if (content) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + FileCopyUtils.copy(resFile, ostream); + } else { + FileCopyUtils.copy(resFile, writer, fileEncoding); + } + } + } + + public static String lastModifiedHttp(File resFile) { + String lastModifiedHttp = null; + SimpleDateFormat format = (SimpleDateFormat)formatTL.get(); + if (format == null) { + format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", + Locale.US); + formatTL.set(format); + } + lastModifiedHttp = format.format(new Date(resFile.lastModified())); + return lastModifiedHttp; + } + + + protected static void send404(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute( + INCLUDE_REQUEST_URI_ATTR); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } else { + // We're included, and the response.sendError() below is going + // to be ignored by the resource that is including us. + // Therefore, the only way we can let the including resource + // know is by including warning message in response + response.getWriter().write("Not found"); + // skip the URI - just to be safe. + } + + response.sendError(HttpServletResponse.SC_NOT_FOUND, + requestUri); + return; + } + + + + /** + * Parse the range header. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @return Vector of ranges + */ + protected ArrayList parseRange(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + // if it has an IfRange and the file is newer, retur FULL + ArrayList result = processIfRange(request, resourceAttributes); + if ( result != null ) return result; + + long fileLength = resourceAttributes.length(); + if (fileLength == 0) + return null; + + // Retrieving the range header (if any is specified + String rangeHeader = request.getHeader("Range"); + + if (rangeHeader == null) + return null; + // bytes is the only range unit supported (and I don't see the point + // of adding new ones). + if (!rangeHeader.startsWith("bytes")) { + return sendRangeNotSatisfiable(response, fileLength); + } + + rangeHeader = rangeHeader.substring(6); + + // Vector which will contain all the ranges which are successfully + // parsed. + result = Range.parseRanges(fileLength, rangeHeader); + if (result == null) { + sendRangeNotSatisfiable(response, fileLength); + } + return result; + } + + + private ArrayList sendRangeNotSatisfiable(HttpServletResponse response, + long fileLength) + throws IOException { + response.addHeader("Content-Range", "bytes */" + fileLength); + response.sendError + (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return null; + } + + + + private ArrayList processIfRange(HttpServletRequest request, + File resourceAttributes) { + // Checking If-Range + String headerValue = request.getHeader("If-Range"); + + if (headerValue != null) { + + long headerValueTime = (-1L); + try { + headerValueTime = request.getDateHeader("If-Range"); + } catch (Exception e) { + ; + } + + String eTag = getETag(resourceAttributes); + long lastModified = resourceAttributes.lastModified(); + + if (headerValueTime == (-1L)) { + + // If the ETag the client gave does not match the entity + // etag, then the entire entity is returned. + if (!eTag.equals(headerValue.trim())) + return FULL; + + } else { + + // If the timestamp of the entity the client got is older than + // the last modification date of the entity, the entire entity + // is returned. + if (lastModified > (headerValueTime + 1000)) + return FULL; + + } + + } + return null; + } + + + // -------------------------------------------------------- protected Methods + + + /** + * Check if the if-match condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resourceInfo File object + * @return boolean true if the resource meets the specified condition, + * and false if the condition is not satisfied, in which case request + * processing is stopped + */ + protected boolean checkIfMatch(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + + String eTag = getETag(resourceAttributes); + String headerValue = request.getHeader("If-Match"); + if (headerValue != null) { + if (headerValue.indexOf('*') == -1) { + + StringTokenizer commaTokenizer = new StringTokenizer + (headerValue, ","); + boolean conditionSatisfied = false; + + while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) { + String currentToken = commaTokenizer.nextToken(); + if (currentToken.trim().equals(eTag)) + conditionSatisfied = true; + } + + // If none of the given ETags match, 412 Precodition failed is + // sent back + if (!conditionSatisfied) { + response.sendError + (HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + + } + } + return true; + + } + + + /** + * Check if the if-modified-since condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resourceInfo File object + * @return boolean true if the resource meets the specified condition, + * and false if the condition is not satisfied, in which case request + * processing is stopped + */ + protected boolean checkIfModifiedSince(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + try { + long headerValue = request.getDateHeader("If-Modified-Since"); + long lastModified = resourceAttributes.lastModified(); + if (headerValue != -1) { + + // If an If-None-Match header has been specified, if modified since + // is ignored. + if ((request.getHeader("If-None-Match") == null) + && (lastModified <= headerValue + 1000)) { + // The entity has not been modified since the date + // specified by the client. This is not an error case. + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return false; + } + } + } catch(IllegalArgumentException illegalArgument) { + return true; + } + return true; + + } + + + /** + * Check if the if-none-match condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resourceInfo File object + * @return boolean true if the resource meets the specified condition, + * and false if the condition is not satisfied, in which case request + * processing is stopped + */ + protected boolean checkIfNoneMatch(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + + String eTag = getETag(resourceAttributes); + String headerValue = request.getHeader("If-None-Match"); + if (headerValue != null) { + + boolean conditionSatisfied = false; + + if (!headerValue.equals("*")) { + + StringTokenizer commaTokenizer = + new StringTokenizer(headerValue, ","); + + while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) { + String currentToken = commaTokenizer.nextToken(); + if (currentToken.trim().equals(eTag)) + conditionSatisfied = true; + } + + } else { + conditionSatisfied = true; + } + + if (conditionSatisfied) { + + // For GET and HEAD, we should respond with + // 304 Not Modified. + // For every other method, 412 Precondition Failed is sent + // back. + if ( ("GET".equals(request.getMethod())) + || ("HEAD".equals(request.getMethod())) ) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return false; + } else { + response.sendError + (HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + } + } + return true; + + } + + + /** + * Check if the if-unmodified-since condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resourceInfo File object + * @return boolean true if the resource meets the specified condition, + * and false if the condition is not satisfied, in which case request + * processing is stopped + */ + protected boolean checkIfUnmodifiedSince(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + try { + long lastModified = resourceAttributes.lastModified(); + long headerValue = request.getDateHeader("If-Unmodified-Since"); + if (headerValue != -1) { + if ( lastModified > (headerValue + 1000)) { + // The entity has not been modified since the date + // specified by the client. This is not an error case. + response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + } + } catch(IllegalArgumentException illegalArgument) { + return true; + } + return true; + + } + + + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param resourceInfo The ResourceInfo object + * @param ostream The output stream to write to + * @param ranges Enumeration of the ranges the client wanted to retrieve + * @param contentType Content type of the resource + * @exception IOException if an input/output error occurs + */ + protected void copyRanges(File cacheEntry, ServletOutputStream ostream, + Iterator ranges, String contentType) + throws IOException { + + IOException exception = null; + + while ( (exception == null) && (ranges.hasNext()) ) { + + InputStream resourceInputStream = new FileInputStream(cacheEntry); + InputStream istream = + new BufferedInputStream(resourceInputStream, input); + + Range currentRange = (Range) ranges.next(); + + // Writing MIME header. + ostream.println(); + ostream.println("--" + mimeSeparation); + if (contentType != null) + ostream.println("Content-Type: " + contentType); + ostream.println("Content-Range: bytes " + currentRange.start + + "-" + currentRange.end + "/" + + currentRange.length); + ostream.println(); + + // Printing content + exception = CopyUtils.copyRange(istream, ostream, currentRange.start, + currentRange.end); + + try { + istream.close(); + } catch (Throwable t) { + ; + } + + } + + ostream.println(); + ostream.print("--" + mimeSeparation + "--"); + + // Rethrow any exception that has occurred + if (exception != null) + throw exception; + + } + + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param resourceInfo The ResourceInfo object + * @param writer The writer to write to + * @param ranges Enumeration of the ranges the client wanted to retrieve + * @param contentType Content type of the resource + * @exception IOException if an input/output error occurs + */ + protected void copyRanges(File cacheEntry, PrintWriter writer, + Iterator ranges, String contentType) + throws IOException { + + IOException exception = null; + + // quite inefficient - why not sort and open once + while ( (exception == null) && (ranges.hasNext()) ) { + + InputStream resourceInputStream = new FileInputStream(cacheEntry); + + Reader reader; + if (fileEncoding == null) { + reader = new InputStreamReader(resourceInputStream); + } else { + reader = new InputStreamReader(resourceInputStream, + fileEncoding); + } + + Range currentRange = (Range) ranges.next(); + + // Writing MIME header. + writer.println(); + writer.println("--" + mimeSeparation); + if (contentType != null) + writer.println("Content-Type: " + contentType); + writer.println("Content-Range: bytes " + currentRange.start + + "-" + currentRange.end + "/" + + currentRange.length); + writer.println(); + + // Printing content + exception = CopyUtils.copyRange(reader, writer, currentRange.start, + currentRange.end); + + try { + reader.close(); + } catch (Throwable t) { + ; + } + + } + writer.println(); + writer.print("--" + mimeSeparation + "--"); + + // Rethrow any exception that has occurred + if (exception != null) + throw exception; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/Dir2Html.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/Dir2Html.java new file mode 100644 index 000000000..0c8eeb9cf --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/Dir2Html.java @@ -0,0 +1,495 @@ +/* + * 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.servlets.file; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handles directory listing + */ +public class Dir2Html extends HttpServlet { + + /** + * Array containing the safe characters set. + */ + protected static URLEncoder urlEncoder; + + /** + * Allow a readme file to be included. + */ + protected String readmeFile = null; + + // TODO: find a better default + /** + * The input buffer size to use when serving resources. + */ + protected int input = 2048; + + /** + * The output buffer size to use when serving resources. + */ + protected int output = 2048; + + /** + * File encoding to be used when reading static files. If none is specified + * the platform default is used. + */ + protected String fileEncoding = null; + + ThreadLocal formatTL = new ThreadLocal(); + + /** + * Full range marker. + */ + protected static ArrayList FULL = new ArrayList(); + + // Context base dir + protected File basePath; + protected String basePathName; + + public static final String TOMCAT_CSS = + "H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " + + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " + + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " + + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " + + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " + + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" + + "A {color : black;}" + + "A.name {color : black;}" + + "HR {color : #525D76;}"; + + // ----------------------------------------------------- Static Initializer + + + /** + * GMT timezone - all HTTP dates are on GMT + */ + static { + urlEncoder = new URLEncoder(); + urlEncoder.addSafeCharacter('-'); + urlEncoder.addSafeCharacter('_'); + urlEncoder.addSafeCharacter('.'); + urlEncoder.addSafeCharacter('*'); + urlEncoder.addSafeCharacter('/'); + } + + + /** + * MIME multipart separation string + */ + protected static final String mimeSeparation = "TOMCAT_MIME_BOUNDARY"; + + + // --------------------------------------------------------- Public Methods + + + /** + * Finalize this servlet. + */ + public void destroy() { + } + + + /** + * Initialize this servlet. + */ + public void init() throws ServletException { + } + + protected void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + + // Serve the requested resource, including the data content + //serveResource(request, response, true); + + } + + + protected void doHead(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + + // Serve the requested resource, without the data content + //serveResource(request, response, false); + + } + + + protected void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + doGet(request, response); + } + + + + + + /** + * Serve the specified resource, optionally including the data content. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param content Should the content be included? + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + protected void serveResource(HttpServletRequest request, + HttpServletResponse response, + boolean content, + String relativePath) + throws IOException, ServletException { + + // Identify the requested resource path - checks include attributes + String path = DefaultServlet.getRelativePath(request); + + // TODO: Check the file cache to avoid a bunch of FS accesses. + + File resFile = new File(basePath, path); + + if (!resFile.exists()) { + DefaultServlet.send404(request, response); + return; + } + + boolean isDir = resFile.isDirectory(); + + if (isDir) { + renderDir(request, response, resFile,"UTF=8", content, + relativePath); + return; + } + + } + + + + // ----------------- Directory rendering -------------------- + + // Just basic HTML rendering - extend or replace for xslt + + public void renderDir(HttpServletRequest request, + HttpServletResponse response, + File resFile, + String fileEncoding, + boolean content, + String relativePath) throws IOException { + + String contentType = "text/html;charset=" + fileEncoding; + + ServletOutputStream ostream = null; + PrintWriter writer = null; + + if (content) { + // Trying to retrieve the servlet output stream + try { + ostream = response.getOutputStream(); + } catch (IllegalStateException e) { + // If it fails, we try to get a Writer instead if we're + // trying to serve a text file + if ( (contentType == null) + || (contentType.startsWith("text")) ) { + writer = response.getWriter(); + } else { + throw e; + } + } + + } + + // Set the appropriate output headers + response.setContentType(contentType); + + InputStream renderResult = null; + + if (content) { + // Serve the directory browser + renderResult = + render(request.getContextPath(), resFile, relativePath); + } + + + // Copy the input stream to our output stream (if requested) + if (content) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + CopyUtils.copy(renderResult, ostream); + } else { + CopyUtils.copy(renderResult, writer, fileEncoding); + } + } + + + } + + + /** + * URL rewriter. + * + * @param path Path which has to be rewiten + */ + protected String rewriteUrl(String path) { + return urlEncoder.encode( path ); + } + + + + /** + * Decide which way to render. HTML or XML. + */ + protected InputStream render(String contextPath, File cacheEntry, + String relativePath) { + return renderHtml(contextPath, cacheEntry, relativePath); + } + + + /** + * Return an InputStream to an HTML representation of the contents + * of this directory. + * + * @param contextPath Context path to which our internal paths are + * relative + */ + protected InputStream renderHtml(String contextPath, File cacheEntry, + String relativePath) { + + String dirName = cacheEntry.getName(); + + // Number of characters to trim from the beginnings of filenames +// int trim = relativePath.length(); +// if (!relativePath.endsWith("/")) +// trim += 1; +// if (relativePAth.equals("/")) +// trim = 1; + + // Prepare a writer to a buffered area + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + OutputStreamWriter osWriter = null; + try { + osWriter = new OutputStreamWriter(stream, "UTF8"); + } catch (Exception e) { + // Should never happen + osWriter = new OutputStreamWriter(stream); + } + PrintWriter writer = new PrintWriter(osWriter); + + StringBuffer sb = new StringBuffer(); + + // rewriteUrl(contextPath) is expensive. cache result for later reuse + String rewrittenContextPath = rewriteUrl(contextPath); + + // TODO: use a template for localization / customization + + // Render the page header + sb.append("\r\n"); + sb.append("\r\n"); + sb.append(""); + sb.append(dirName); + sb.append("\r\n"); + sb.append(" "); + sb.append("\r\n"); + sb.append(""); + sb.append("

"); + sb.append(dirName); + sb.append("

"); + + sb.append("
"); + + + sb.append("\r\n"); + + // Render the column headings + sb.append("\r\n"); + sb.append("\r\n"); + sb.append("\r\n"); + sb.append("\r\n"); + sb.append(""); + boolean shade = false; + + // Render the link to our parent (if required) + String parentDirectory = relativePath; + if (parentDirectory.endsWith("/")) { + parentDirectory = + parentDirectory.substring(0, parentDirectory.length() - 1); + } + int slash = parentDirectory.lastIndexOf('/'); + if (slash >= 0) { + String parent = relativePath.substring(0, slash); + sb.append("\r\n"); + shade = true; + } + + + // Render the directory entries within this directory + String[] files = cacheEntry.list(); + for (int i=0; i\r\n"); + shade = !shade; + + sb.append("\r\n"); + + sb.append("\r\n"); + + sb.append("\r\n"); + + sb.append("\r\n"); + } + + + // Render the page footer + sb.append("
"); + sb.append("Name"); + sb.append(""); + sb.append("Size"); + sb.append(""); + sb.append("Last Modified"); + sb.append("
  \r\n"); + //sb.append(""); + sb.append(".."); + //sb.append(""); + sb.append("
  \r\n"); + sb.append(""); + sb.append(trimmed); + if (isDir) + sb.append("/"); + sb.append(""); + if (isDir) + sb.append(" "); + else + displaySize(sb,childCacheEntry.length()); + sb.append(""); + sb.append(DefaultServlet.lastModifiedHttp(childCacheEntry)); + sb.append("
\r\n"); + + sb.append("
"); + + String readme = getReadme(cacheEntry); + if (readme!=null) { + sb.append(readme); + sb.append("
"); + } + + sb.append("\r\n"); + sb.append("\r\n"); + + // Return an input stream to the underlying bytes + writer.write(sb.toString()); + writer.flush(); + return (new ByteArrayInputStream(stream.toByteArray())); + + } + + /** + * Display the size of a file. + */ + protected void displaySize(StringBuffer buf, long filesize) { + + long leftside = filesize / 1024; + long rightside = (filesize % 1024) / 103; // makes 1 digit + if (leftside == 0 && rightside == 0 && filesize != 0) + rightside = 1; + buf.append(leftside).append(".").append(rightside); + buf.append(" KB"); + } + + /** + * Get the readme file as a string. + */ + protected String getReadme(File directory) { + if (readmeFile!=null) { + try { + File rf = new File(directory, readmeFile); + + if (rf.exists()) { + StringWriter buffer = new StringWriter(); + InputStream is = new FileInputStream(rf); + CopyUtils.copyRange(new InputStreamReader(is), + new PrintWriter(buffer)); + + return buffer.toString(); + } + } catch(Throwable e) { + ; /* Should only be IOException or NamingException + * can be ignored + */ + } + } + return null; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/FileCopyUtils.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/FileCopyUtils.java new file mode 100644 index 000000000..d3be53ae1 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/FileCopyUtils.java @@ -0,0 +1,112 @@ +package org.apache.tomcat.servlets.file; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; + +import javax.servlet.ServletOutputStream; + +import org.apache.tomcat.servlets.util.Range; + + +/** Like CopyUtils, but with File as source. + * + * This has the potential to be optimized with JNI + */ +public class FileCopyUtils { + protected static int input = 2048; + + public static void copy(File f, OutputStream ostream) + throws IOException { + CopyUtils.copy(new FileInputStream(f), ostream); + } + + + public static void copy(File cacheEntry, + PrintWriter writer, + String fileEncoding) + throws IOException { + CopyUtils.copy(new FileInputStream(cacheEntry), writer, fileEncoding); + } + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param resourceInfo The ResourceInfo object + * @param ostream The output stream to write to + * @param range Range the client wanted to retrieve + * @exception IOException if an input/output error occurs + */ + public static void copy(File cacheEntry, ServletOutputStream ostream, + Range range) + throws IOException { + + IOException exception = null; + + InputStream resourceInputStream = new FileInputStream(cacheEntry); + InputStream istream = + new BufferedInputStream(resourceInputStream, input); + exception = CopyUtils.copyRange(istream, ostream, range.start, range.end); + + // Clean up the input stream + try { + istream.close(); + } catch (Throwable t) { + ; + } + + // Rethrow any exception that has occurred + if (exception != null) + throw exception; + + } + + /** + * Copy the contents of the specified input stream to the specified + * output stream, and ensure that both streams are closed before returning + * (even in the face of an exception). + * + * @param resourceInfo The ResourceInfo object + * @param writer The writer to write to + * @param range Range the client wanted to retrieve + * @exception IOException if an input/output error occurs + */ + public static void copy(File cacheEntry, PrintWriter writer, + Range range, String fileEncoding) + throws IOException { + + IOException exception = null; + + InputStream resourceInputStream = new FileInputStream(cacheEntry); + + Reader reader; + if (fileEncoding == null) { + reader = new InputStreamReader(resourceInputStream); + } else { + reader = new InputStreamReader(resourceInputStream, + fileEncoding); + } + + exception = CopyUtils.copyRange(reader, writer, range.start, range.end); + + // Clean up the input stream + try { + reader.close(); + } catch (Throwable t) { + ; + } + + // Rethrow any exception that has occurred + if (exception != null) + throw exception; + + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/MD5Encoder.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/MD5Encoder.java new file mode 100644 index 000000000..3f544fca3 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/MD5Encoder.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.servlets.file; + + +/** + * Encode an MD5 digest into a String. + *

+ * The 128 bit MD5 hash is converted into a 32 character long String. + * Each character of the String is the hexadecimal representation of 4 bits + * of the digest. + * + * @author Remy Maucherat + */ + +public final class MD5Encoder { + + + // ----------------------------------------------------- Instance Variables + + + private static final char[] hexadecimal = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f'}; + + + // --------------------------------------------------------- Public Methods + + + /** + * Encodes the 128 bit (16 bytes) MD5 into a 32 character String. + * + * @param binaryData Array containing the digest + * @return Encoded MD5, or null if encoding failed + */ + public String encode( byte[] binaryData ) { + + if (binaryData.length != 16) + return null; + + char[] buffer = new char[32]; + + for (int i=0; i<16; i++) { + int low = (int) (binaryData[i] & 0x0f); + int high = (int) ((binaryData[i] & 0xf0) >> 4); + buffer[i*2] = hexadecimal[high]; + buffer[i*2 + 1] = hexadecimal[low]; + } + + return new String(buffer); + + } + + +} + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/URLEncoder.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/URLEncoder.java new file mode 100644 index 000000000..64fc962f5 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/URLEncoder.java @@ -0,0 +1,100 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.servlets.file; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.BitSet; + +/** + * + * This class is very similar to the java.net.URLEncoder class. + * + * Unfortunately, with java.net.URLEncoder there is no way to specify to the + * java.net.URLEncoder which characters should NOT be encoded. + * + * This code was moved from DefaultServlet.java + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class URLEncoder { + protected static final char[] hexadecimal = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F'}; + + //Array containing the safe characters set. + protected BitSet safeCharacters = new BitSet(256); + + public URLEncoder() { + for (char i = 'a'; i <= 'z'; i++) { + addSafeCharacter(i); + } + for (char i = 'A'; i <= 'Z'; i++) { + addSafeCharacter(i); + } + for (char i = '0'; i <= '9'; i++) { + addSafeCharacter(i); + } + } + + public void addSafeCharacter( char c ) { + safeCharacters.set( c ); + } + + public String encode( String path ) { + int maxBytesPerChar = 10; + int caseDiff = ('a' - 'A'); + StringBuffer rewrittenPath = new StringBuffer(path.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(buf, "UTF8"); + } catch (Exception e) { + e.printStackTrace(); + writer = new OutputStreamWriter(buf); + } + + for (int i = 0; i < path.length(); i++) { + int c = (int) path.charAt(i); + if (safeCharacters.get(c)) { + rewrittenPath.append((char)c); + } else { + // convert to external encoding before hex conversion + try { + writer.write((char)c); + writer.flush(); + } catch(IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (int j = 0; j < ba.length; j++) { + // Converting each byte in the buffer + byte toEncode = ba[j]; + rewrittenPath.append('%'); + int low = (int) (toEncode & 0x0f); + int high = (int) ((toEncode & 0xf0) >> 4); + rewrittenPath.append(hexadecimal[high]); + rewrittenPath.append(hexadecimal[low]); + } + buf.reset(); + } + } + return rewrittenPath.toString(); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/WebdavServlet.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/WebdavServlet.java new file mode 100644 index 000000000..93563e6ce --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/WebdavServlet.java @@ -0,0 +1,1840 @@ +/* + * 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.servlets.file; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Stack; +import java.util.TimeZone; +import java.util.Vector; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.tomcat.servlets.util.Range; +import org.apache.tomcat.servlets.util.RequestUtil; +import org.apache.tomcat.servlets.util.UrlUtils; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + + + +/** + * Servlet which adds support for WebDAV level 1. All the basic HTTP requests + * are handled by the DefaultServlet. + * + * Based on Catalina WebdavServlet, with following changes: + * - removed the JNDI abstraction, use File instead + * - removed WebDAV 2 support ( moved to Webdav2Servlet ) - i.e. no locks + * supported + * - simplified and cleaned up the code + * + * @author Remy Maucherat + * @author Costin Manolache + */ +public class WebdavServlet extends DefaultServlet { + + + // -------------------------------------------------------------- Constants + + protected static final String METHOD_PROPFIND = "PROPFIND"; + protected static final String METHOD_PROPPATCH = "PROPPATCH"; + protected static final String METHOD_MKCOL = "MKCOL"; + protected static final String METHOD_COPY = "COPY"; + protected static final String METHOD_DELETE = "DELETE"; + protected static final String METHOD_MOVE = "MOVE"; + protected static final String METHOD_LOCK = "LOCK"; + protected static final String METHOD_UNLOCK = "UNLOCK"; + + + /** + * Default depth is infite. + */ + protected static final int INFINITY = 3; // To limit tree browsing a bit + + + /** + * PROPFIND - Specify a property mask. + */ + protected static final int FIND_BY_PROPERTY = 0; + + + /** + * PROPFIND - Display all properties. + */ + protected static final int FIND_ALL_PROP = 1; + + + /** + * PROPFIND - Return property names. + */ + protected static final int FIND_PROPERTY_NAMES = 2; + + /** + * Default namespace. + */ + protected static final String DEFAULT_NAMESPACE = "DAV:"; + + + /** + * Simple date format for the creation date ISO representation (partial). + * TODO: ThreadLocal + */ + protected static final SimpleDateFormat creationDateFormat = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + + static { + creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + // TODO: replace it with writeRole - who is enabled to write + protected boolean readOnly = false; + + /** + * Repository of the lock-null resources. + *

+ * Key : path of the collection containing the lock-null resource
+ * Value : Vector of lock-null resource which are members of the + * collection. Each element of the Vector is the path associated with + * the lock-null resource. + */ + protected Hashtable lockNullResources = new Hashtable(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Initialize this servlet. + */ + public void init() + throws ServletException { + + super.init(); + + try { + String value = getServletConfig().getInitParameter("readonly"); + if (value != null) + readOnly = (new Boolean(value)).booleanValue(); + } catch (Throwable t) { + ; + } + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Handles the special WebDAV methods. + */ + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String method = req.getMethod(); + + if (method.equals(METHOD_PROPFIND)) { + doPropfind(req, resp); + } else if (method.equals(METHOD_PROPPATCH)) { + doProppatch(req, resp); + } else if (method.equals(METHOD_MKCOL)) { + doMkcol(req, resp); + } else if (method.equals(METHOD_COPY)) { + doCopy(req, resp); + } else if (method.equals(METHOD_MOVE)) { + doMove(req, resp); + } else if (method.equals(METHOD_LOCK)) { + doLock(req, resp); + } else if (method.equals(METHOD_UNLOCK)) { + doUnlock(req, resp); + } else if (method.equals(METHOD_DELETE)) { + doDelete(req, resp); + } else { + // DefaultServlet processing - GET, HEAD, POST + super.service(req, resp); + } + + } + + /** + * Check if the conditions specified in the optional If headers are + * satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resourceAttributes The resource information + * @return boolean true if the resource meets all the specified conditions, + * and false if any of the conditions is not satisfied, in which case + * request processing is stopped + */ + protected boolean checkIfHeaders(HttpServletRequest request, + HttpServletResponse response, + File resourceAttributes) + throws IOException { + + if (!super.checkIfHeaders(request, response, resourceAttributes)) + return false; + + // TODO : Checking the WebDAV If header + return true; + + } + + + /** + * OPTIONS Method. + * + * @param req The request + * @param resp The response + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.addHeader("DAV", "1"); // And not: ,2"); + + StringBuffer methodsAllowed = determineMethodsAllowed(basePath, + req); + resp.addHeader("Allow", methodsAllowed.toString()); + resp.addHeader("MS-Author-Via", "DAV"); + } + + // ------------------ PROPFIND -------------------- + + /** + * PROPFIND Method. + */ + protected void doPropfind(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String path = getRelativePath(req); + if (path.endsWith("/")) + path = path.substring(0, path.length() - 1); + + if ((path.toUpperCase().startsWith("/WEB-INF")) || + (path.toUpperCase().startsWith("/META-INF"))) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + // Propfind depth + int depth = getDepth(req); + + File object = new File(basePath, path); + + if (!object.exists()) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, path); + return; + } + + // Properties which are to be displayed. + Vector properties = new Vector(); + int type = getPropfindType(req, properties); + + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + resp.setContentType("text/xml; charset=UTF-8"); + + // Create multistatus object + XMLWriter generatedXML = new XMLWriter(resp.getWriter()); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement(null, "multistatus" + + generateNamespaceDeclarations(), + XMLWriter.OPENING); + + if (depth == 0) { + parseProperties(req, generatedXML, path, type, + properties); + } else { + propfindRecurse(req, path, depth, properties, + type, generatedXML); + } + + generatedXML.writeElement(null, "multistatus", + XMLWriter.CLOSING); + generatedXML.sendData(); + } + + + private void propfindRecurse(HttpServletRequest req, String path, int depth, Vector properties, int type, XMLWriter generatedXML) throws IOException { + File object; + // The stack always contains the object of the current level + Stack stack = new Stack(); + stack.push(path); + + // Stack of the objects one level below + Stack stackBelow = new Stack(); + + while ((!stack.isEmpty()) && (depth >= 0)) { + String currentPath = (String) stack.pop(); + parseProperties(req, generatedXML, currentPath, + type, properties); + + object = new File(basePath,currentPath); + if (!object.exists()) { + continue; + } + + if ((object.isDirectory()) && (depth > 0)) { + + File[] files = object.listFiles(); + for (int i=0; i < files.length; i++) { + String newPath = currentPath; + if (!(newPath.endsWith("/"))) + newPath += "/"; + newPath += files[i].getName(); + stackBelow.push(newPath); + } + } + + if (stack.isEmpty()) { + depth--; + stack = stackBelow; + stackBelow = new Stack(); + } + + generatedXML.sendData(); + } + } + + + private int getPropfindType(HttpServletRequest req, Vector properties) throws ServletException { + // Propfind type + int type = FIND_ALL_PROP; + + DocumentBuilder documentBuilder = getDocumentBuilder(); + try { + Document document = documentBuilder.parse + (new InputSource(req.getInputStream())); + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + NodeList childList = rootElement.getChildNodes(); + + for (int i=0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + if (currentNode.getNodeName().endsWith("prop")) { + type = FIND_BY_PROPERTY; + Node propNode = currentNode; + NodeList childListPN = propNode.getChildNodes(); + for (int iPN=0; iPN < childListPN.getLength(); iPN++) { + Node currentNodePN = childListPN.item(iPN); + switch (currentNodePN.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + String nodeName = currentNodePN.getNodeName(); + String propertyName = null; + if (nodeName.indexOf(':') != -1) { + propertyName = nodeName.substring + (nodeName.indexOf(':') + 1); + } else { + propertyName = nodeName; + } + // href is a live property which is handled differently + properties.addElement(propertyName); + break; + } + } + } + if (currentNode.getNodeName().endsWith("propname")) { + type = FIND_PROPERTY_NAMES; + } + if (currentNode.getNodeName().endsWith("allprop")) { + type = FIND_ALL_PROP; + } + break; + } + } + } catch(Exception e) { + // Most likely there was no content : we use the defaults. + // TODO : Enhance that ! + } + return type; + } + + + private int getDepth(HttpServletRequest req) { + int depth = INFINITY; + String depthStr = req.getHeader("Depth"); + + if (depthStr == null) { + depth = INFINITY; + } else { + if (depthStr.equals("0")) { + depth = 0; + } else if (depthStr.equals("1")) { + depth = 1; + } else if (depthStr.equals("infinity")) { + depth = INFINITY; + } + } + return depth; + } + + + /** + * URL rewriter. + * + * @param path Path which has to be rewiten + */ + protected String rewriteUrl(String path) { + return urlEncoder.encode( path ); + } + + + /** + * Propfind helper method. + * + * @param req The servlet request + * @param resources Resources object associated with this context + * @param out XML response to the Propfind request + * @param path Path of the current resource + * @param type Propfind type + * @param propertiesVector If the propfind type is find properties by + * name, then this Vector contains those properties + */ + protected void parseProperties(HttpServletRequest req, + XMLWriter out, + String path, int type, + Vector propertiesVector) { + + // Exclude any resource in the /WEB-INF and /META-INF subdirectories + // (the "toUpperCase()" avoids problems on Windows systems) + if (path.toUpperCase().startsWith("/WEB-INF") || + path.toUpperCase().startsWith("/META-INF")) + return; + + File cacheEntry = new File(basePath, path); + + out.writeElement(null, "response", XMLWriter.OPENING); + String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + " " + + WebdavStatus.getStatusText + (WebdavStatus.SC_OK)); + + // Generating href element + out.writeElement(null, "href", XMLWriter.OPENING); + + String href = req.getContextPath();// + req.getServletPath() + + // req.getPathInfo(); + + // ??? + if ((href.endsWith("/")) && (path.startsWith("/"))) + href += path.substring(1); + else + href += path; + if ((cacheEntry.isDirectory()) && (!href.endsWith("/"))) + href += "/"; + + out.writeText(rewriteUrl(href)); + + out.writeElement(null, "href", XMLWriter.CLOSING); + + String resourceName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash != -1) + resourceName = resourceName.substring(lastSlash + 1); + + switch (type) { + + case FIND_ALL_PROP : + + out.writeElement(null, "propstat", XMLWriter.OPENING); + out.writeElement(null, "prop", XMLWriter.OPENING); + + out.writeProperty + (null, "creationdate", + getISOCreationDate(cacheEntry.lastModified())); + out.writeElement(null, "displayname", XMLWriter.OPENING); + out.writeData(resourceName); + out.writeElement(null, "displayname", XMLWriter.CLOSING); + if (!cacheEntry.isDirectory()) { + out.writeProperty(null, "getlastmodified", + FastHttpDateFormat.formatDate(cacheEntry.lastModified(), + null)); + out.writeProperty(null, "getcontentlength", + String.valueOf(cacheEntry.length())); + String contentType = + getServletContext().getMimeType(cacheEntry.getName()); + if (contentType != null) { + out.writeProperty(null, "getcontenttype", contentType); + } + out.writeProperty(null, "getetag", getETag(cacheEntry)); + out.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT); + } else { + out.writeElement(null, "resourcetype", XMLWriter.OPENING); + out.writeElement(null, "collection", XMLWriter.NO_CONTENT); + out.writeElement(null, "resourcetype", XMLWriter.CLOSING); + } + + out.writeProperty(null, "source", ""); + out.writeElement(null, "prop", XMLWriter.CLOSING); + out.writeElement(null, "status", XMLWriter.OPENING); + out.writeText(status); + out.writeElement(null, "status", XMLWriter.CLOSING); + out.writeElement(null, "propstat", XMLWriter.CLOSING); + break; + + case FIND_PROPERTY_NAMES : + out.writeElement(null, "propstat", XMLWriter.OPENING); + out.writeElement(null, "prop", XMLWriter.OPENING); + out.writeElement(null, "creationdate", XMLWriter.NO_CONTENT); + out.writeElement(null, "displayname", XMLWriter.NO_CONTENT); + if (! cacheEntry.isDirectory()) { + out.writeElement(null, "getcontentlanguage", + XMLWriter.NO_CONTENT); + out.writeElement(null, "getcontentlength", + XMLWriter.NO_CONTENT); + out.writeElement(null, "getcontenttype", + XMLWriter.NO_CONTENT); + out.writeElement(null, "getetag", + XMLWriter.NO_CONTENT); + out.writeElement(null, "getlastmodified", + XMLWriter.NO_CONTENT); + } + out.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT); + out.writeElement(null, "source", XMLWriter.NO_CONTENT); + out.writeElement(null, "lockdiscovery", XMLWriter.NO_CONTENT); + + out.writeElement(null, "prop", XMLWriter.CLOSING); + out.writeElement(null, "status", XMLWriter.OPENING); + out.writeText(status); + out.writeElement(null, "status", XMLWriter.CLOSING); + out.writeElement(null, "propstat", XMLWriter.CLOSING); + + break; + + case FIND_BY_PROPERTY : + + Vector propertiesNotFound = new Vector(); + + // Parse the list of properties + out.writeElement(null, "propstat", XMLWriter.OPENING); + out.writeElement(null, "prop", XMLWriter.OPENING); + + genPropertiesFound(out, propertiesVector, cacheEntry, + resourceName, propertiesNotFound); + + out.writeElement(null, "prop", XMLWriter.CLOSING); + out.writeElement(null, "status", XMLWriter.OPENING); + out.writeText(status); + out.writeElement(null, "status", XMLWriter.CLOSING); + out.writeElement(null, "propstat", XMLWriter.CLOSING); + + Enumeration propertiesNotFoundList = propertiesNotFound.elements(); + + if (propertiesNotFoundList.hasMoreElements()) { + status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + + " " + WebdavStatus.getStatusText + (WebdavStatus.SC_NOT_FOUND)); + + out.writeElement(null, "propstat", XMLWriter.OPENING); + out.writeElement(null, "prop", XMLWriter.OPENING); + while (propertiesNotFoundList.hasMoreElements()) { + out.writeElement + (null, (String) propertiesNotFoundList.nextElement(), + XMLWriter.NO_CONTENT); + } + out.writeElement(null, "prop", XMLWriter.CLOSING); + out.writeElement(null, "status", XMLWriter.OPENING); + out.writeText(status); + out.writeElement(null, "status", XMLWriter.CLOSING); + out.writeElement(null, "propstat", XMLWriter.CLOSING); + } + break; + + } + + out.writeElement(null, "response", XMLWriter.CLOSING); + } + + + private void genPropertiesFound(XMLWriter out, Vector propertiesVector, + File cacheEntry, String resourceName, + Vector propertiesNotFound) { + Enumeration properties = propertiesVector.elements(); + + while (properties.hasMoreElements()) { + String property = (String) properties.nextElement(); + if (property.equals("creationdate")) { + out.writeProperty(null, "creationdate", + getISOCreationDate(cacheEntry.lastModified())); + } else if (property.equals("displayname")) { + out.writeElement(null, "displayname", XMLWriter.OPENING); + out.writeData(resourceName); + out.writeElement(null, "displayname", XMLWriter.CLOSING); + } else if (property.equals("getcontentlanguage")) { + if (cacheEntry.isDirectory()) { + propertiesNotFound.addElement(property); + } else { + out.writeElement(null, "getcontentlanguage", + XMLWriter.NO_CONTENT); + } + } else if (property.equals("getcontentlength")) { + if (cacheEntry.isDirectory()) { + propertiesNotFound.addElement(property); + } else { + out.writeProperty(null, "getcontentlength", + (String.valueOf(cacheEntry.length()))); + } + } else if (property.equals("getcontenttype")) { + if (cacheEntry.isDirectory()) { + propertiesNotFound.addElement(property); + } else { + out.writeProperty(null, "getcontenttype", + getServletContext().getMimeType(cacheEntry.getName())); + } + } else if (property.equals("getetag")) { + if (cacheEntry.isDirectory()) { + propertiesNotFound.addElement(property); + } else { + out.writeProperty(null, "getetag", getETag(cacheEntry)); + } + } else if (property.equals("getlastmodified")) { + if (cacheEntry.isDirectory()) { + propertiesNotFound.addElement(property); + } else { + out.writeProperty(null, "getlastmodified", + FastHttpDateFormat + .formatDate(cacheEntry.lastModified(), null)); + } + } else if (property.equals("resourcetype")) { + if (cacheEntry.isDirectory()) { + out.writeElement(null, "resourcetype", + XMLWriter.OPENING); + out.writeElement(null, "collection", + XMLWriter.NO_CONTENT); + out.writeElement(null, "resourcetype", + XMLWriter.CLOSING); + } else { + out.writeElement(null, "resourcetype", + XMLWriter.NO_CONTENT); + } + } else if (property.equals("source")) { + out.writeProperty(null, "source", ""); + } else if (property.equals("supportedlock")) { + // TODO: hook for Webdav2 + propertiesNotFound.addElement(property); + } else if (property.equals("lockdiscovery")) { + propertiesNotFound.addElement(property); + } else { + propertiesNotFound.addElement(property); + } + + } + } + + // ------------------ PROPPATCH -------------------- + + /** + * PROPPATCH Method. + */ + protected void doProppatch(HttpServletRequest req, + HttpServletResponse resp) + throws ServletException, IOException { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + + // ------------------ MKCOL -------------------- + + /** + * MKCOL Method. + */ + protected void doMkcol(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + + if ((path.toUpperCase().startsWith("/WEB-INF")) || + (path.toUpperCase().startsWith("/META-INF"))) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + File object = new File(basePath,path); + + // Can't create a collection if a resource already exists at the given + // path + if (object.exists()) { + // Get allowed methods + StringBuffer methodsAllowed = determineMethodsAllowed(basePath, + req); + + resp.addHeader("Allow", methodsAllowed.toString()); + + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + return; + } + + if (req.getInputStream().available() > 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + try { + Document document = documentBuilder.parse + (new InputSource(req.getInputStream())); + // TODO : Process this request body + resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED); + return; + + } catch(SAXException saxe) { + // Parse error - assume invalid content + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return; + } + } + + boolean result = true; + + File newDir = new File(basePath, path); + result = newDir.mkdir(); + + if (!result) { + resp.sendError(WebdavStatus.SC_CONFLICT, + WebdavStatus.getStatusText + (WebdavStatus.SC_CONFLICT)); + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + + } + + /** + * DELETE Method. + */ + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + deleteResource(req, resp); + + } + + + /** + * This is not part of DefaultServlet - all PUT and write operations + * should be handled by webdav servlet, which allows more control + */ + protected void doPut(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + if (readOnly) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + String path = getRelativePath(req); + + if (path.indexOf("..") >= 0) { + // not supported, too dangerous + // what else to escape ? + resp.setStatus(404); + return; + } + + File resFile = new File(basePath, path); + boolean exists = resFile.exists(); + + // extra check + if (!resFile.getCanonicalPath().startsWith(basePathName)) { + //log.info("File outside basedir " + basePathS + " " + f); + resp.setStatus(404); + return; + } + + + boolean result = true; + + // Temp. content file used to support partial PUT + //File contentFile = null; + + String rangeHeader = req.getHeader("Content-Range"); + Range range = null; + if ( rangeHeader != null ) { + // we have a header, but has bad value. + // original catalina code has a bug I think + range = Range.parseContentRange(rangeHeader); + if (range == null) { // can't parse + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + } + + InputStream resourceInputStream = null; + + // Append data specified in ranges to existing content for this + // resource - create a temp. file on the local filesystem to + // perform this operation + // Assume just one range is specified for now + if (range != null) { + //contentFile = executePartialPut(req, range, path); + //resourceInputStream = new FileInputStream(contentFile); + resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); + return; + } else { + resourceInputStream = req.getInputStream(); + } + + try { + // will override + FileOutputStream fos = new FileOutputStream(resFile); + CopyUtils.copy(resourceInputStream, fos); + } catch(IOException e) { + result = false; + } + + if (result) { + if (exists) { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } else { + resp.setStatus(HttpServletResponse.SC_CREATED); + } + } else { + resp.sendError(HttpServletResponse.SC_CONFLICT); + } + + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + + + /** + * Handle a partial PUT. New content specified in request is appended to + * existing content in oldRevisionContent (if present). This code does + * not support simultaneous partial updates to the same resource. + */ +// protected File executePartialPut(HttpServletRequest req, Range range, +// String path) +// throws IOException { +// +// // Append data specified in ranges to existing content for this +// // resource - create a temp. file on the local filesystem to +// // perform this operation +// File tempDir = (File) getServletContext().getAttribute +// ("javax.servlet.context.tempdir"); +// // Convert all '/' characters to '.' in resourcePath +// String convertedResourcePath = path.replace('/', '.'); +// File contentFile = new File(tempDir, convertedResourcePath); +// if (contentFile.createNewFile()) { +// // Clean up contentFile when Tomcat is terminated +// contentFile.deleteOnExit(); +// } +// +// RandomAccessFile randAccessContentFile = +// new RandomAccessFile(contentFile, "rw"); +// +// Resource oldResource = null; +// try { +// Object obj = resources.lookup(path); +// if (obj instanceof Resource) +// oldResource = (Resource) obj; +// } catch (NamingException e) { +// } +// +// // Copy data in oldRevisionContent to contentFile +// if (oldResource != null) { +// BufferedInputStream bufOldRevStream = +// new BufferedInputStream(oldResource.streamContent(), +// BUFFER_SIZE); +// +// int numBytesRead; +// byte[] copyBuffer = new byte[BUFFER_SIZE]; +// while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) { +// randAccessContentFile.write(copyBuffer, 0, numBytesRead); +// } +// +// bufOldRevStream.close(); +// } +// +// randAccessContentFile.setLength(range.length); +// +// // Append data in request input stream to contentFile +// randAccessContentFile.seek(range.start); +// int numBytesRead; +// byte[] transferBuffer = new byte[BUFFER_SIZE]; +// BufferedInputStream requestBufInStream = +// new BufferedInputStream(req.getInputStream(), BUFFER_SIZE); +// while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) { +// randAccessContentFile.write(transferBuffer, 0, numBytesRead); +// } +// randAccessContentFile.close(); +// requestBufInStream.close(); +// +// return contentFile; +// +// } + + + /** + * COPY Method. + */ + protected void doCopy(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + copyResource(req, resp); + } + + + /** + * MOVE Method. + */ + protected void doMove(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + + if (copyResource(req, resp)) { + deleteResource(path, req, resp, false); + } + + } + + + /** + * LOCK Method. + */ + protected void doLock(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED); + } + + /** + * UNLOCK Method. + */ + protected void doUnlock(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED); + } + + // -------------------------------------------------------- protected Methods + + /** + * Generate the namespace declarations. + */ + protected String generateNamespaceDeclarations() { + return " xmlns=\"" + DEFAULT_NAMESPACE + "\""; + } + + + /** + * Check to see if a resource is currently write locked. The method + * will look at the "If" header to make sure the client + * has give the appropriate lock tokens. + * + * @param req Servlet request + * @return boolean true if the resource is locked (and no appropriate + * lock token has been found for at least one of the non-shared locks which + * are present on the resource). + */ + protected boolean isLocked(HttpServletRequest req) { + return false; + } + + /** + * Check to see if a resource is currently write locked. + * + * @param path Path of the resource + * @param ifHeader "If" HTTP header which was included in the request + * @return boolean true if the resource is locked (and no appropriate + * lock token has been found for at least one of the non-shared locks which + * are present on the resource). + */ + protected boolean isLocked(String path, String ifHeader) { + return false; + } + + + /** + * Copy a resource. + * + * @param req Servlet request + * @param resp Servlet response + * @return boolean true if the copy is successful + */ + protected boolean copyResource(HttpServletRequest req, + HttpServletResponse resp) + throws ServletException, IOException { + + // Parsing destination header + + String destinationPath = req.getHeader("Destination"); + + if (destinationPath == null) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return false; + } + + // Remove url encoding from destination + destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8"); + + destinationPath = removeDestinationPrefix(req, destinationPath); + + // Normalise destination path (remove '.' and '..') + destinationPath = UrlUtils.normalize(destinationPath); + + String contextPath = req.getContextPath(); + if ((contextPath != null) && + (destinationPath.startsWith(contextPath))) { + destinationPath = destinationPath.substring(contextPath.length()); + } + + String pathInfo = req.getPathInfo(); + if (pathInfo != null) { + String servletPath = req.getServletPath(); + if ((servletPath != null) && + (destinationPath.startsWith(servletPath))) { + destinationPath = destinationPath.substring(servletPath.length()); + } + } + + + if ((destinationPath.toUpperCase().startsWith("/WEB-INF")) || + (destinationPath.toUpperCase().startsWith("/META-INF"))) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + String path = getRelativePath(req); + + if ((path.toUpperCase().startsWith("/WEB-INF")) || + (path.toUpperCase().startsWith("/META-INF"))) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + if (destinationPath.equals(path)) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + // Parsing overwrite header + + boolean overwrite = true; + String overwriteHeader = req.getHeader("Overwrite"); + + if (overwriteHeader != null) { + if (overwriteHeader.equalsIgnoreCase("T")) { + overwrite = true; + } else { + overwrite = false; + } + } + + // Overwriting the destination + + boolean exists = true; + File f1 = new File(basePath, destinationPath); + if (!f1.exists()) { + exists = false; + } + + if (overwrite) { + // Delete destination resource, if it exists + if (exists) { + if (!deleteResource(destinationPath, req, resp, true)) { + return false; + } + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + } else { + // If the destination exists, then it's a conflict + if (exists) { + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + return false; + } + } + + // Copying source to destination + + Hashtable errorList = new Hashtable(); + + boolean result = copyResource(basePath, errorList, + path, destinationPath); + + if ((!result) || (!errorList.isEmpty())) { + sendReport(req, resp, errorList); + return false; + } + + // Removing any lock-null resource which would be present at + // the destination path + lockNullResources.remove(destinationPath); + return true; + + } + + + private String removeDestinationPrefix(HttpServletRequest req, + String destinationPath) { + int protocolIndex = destinationPath.indexOf("://"); + if (protocolIndex >= 0) { + // if the Destination URL contains the protocol, we can safely + // trim everything upto the first "/" character after "://" + int firstSeparator = + destinationPath.indexOf("/", protocolIndex + 4); + if (firstSeparator < 0) { + destinationPath = "/"; + } else { + destinationPath = destinationPath.substring(firstSeparator); + } + } else { + String hostName = req.getServerName(); + if ((hostName != null) && (destinationPath.startsWith(hostName))) { + destinationPath = destinationPath.substring(hostName.length()); + } + + int portIndex = destinationPath.indexOf(":"); + if (portIndex >= 0) { + destinationPath = destinationPath.substring(portIndex); + } + + if (destinationPath.startsWith(":")) { + int firstSeparator = destinationPath.indexOf("/"); + if (firstSeparator < 0) { + destinationPath = "/"; + } else { + destinationPath = + destinationPath.substring(firstSeparator); + } + } + } + return destinationPath; + } + + + /** + * Copy a collection. + * + * @param resources Resources implementation to be used + * @param errorList Hashtable containing the list of errors which occurred + * during the copy operation + * @param source Path of the resource to be copied + * @param dest Destination path + */ + protected boolean copyResource(File resources, Hashtable errorList, + String source, String dest) { + + File object = new File(basePath, source); + File destF = new File(basePath, dest); + + if (object.isDirectory()) { + + boolean done = destF.mkdirs(); + if (!done) { + errorList.put + (dest, new Integer(WebdavStatus.SC_CONFLICT)); + return false; + } + + File[] enumeration = object.listFiles(); + for (int i=0; i 0) { + creationDateValue.append("+"); + } + if (offset != 0) { + if (offset < 10) + creationDateValue.append("0"); + creationDateValue.append(offset + ":00"); + } else { + creationDateValue.append("Z"); + } + */ + return creationDateValue.toString(); + } + + /** + * Determines the methods normally allowed for the resource. + * + */ + protected StringBuffer determineMethodsAllowed(File basePath, + HttpServletRequest req) { + + StringBuffer methodsAllowed = new StringBuffer(); + String path = getRelativePath(req); + File object = new File(basePath, path); + if (!object.exists()) { + methodsAllowed.append("OPTIONS, MKCOL, PUT"); + return methodsAllowed; + } + methodsAllowed.append("OPTIONS, GET, HEAD, POST, DELETE, TRACE"); + methodsAllowed.append(", PROPPATCH, COPY, MOVE, PROPFIND"); + + if (object.isDirectory()) { + methodsAllowed.append(", PUT"); + } + + return methodsAllowed; + } +} + + +// -------------------------------------------------------- WebdavStatus Class + + +/** + * Wraps the HttpServletResponse class to abstract the + * specific protocol used. To support other protocols + * we would only need to modify this class and the + * WebDavRetCode classes. + * + * @author Marc Eaddy + * @version 1.0, 16 Nov 1997 + */ +class WebdavStatus { + + + // ----------------------------------------------------- Instance Variables + + + /** + * This Hashtable contains the mapping of HTTP and WebDAV + * status codes to descriptive text. This is a static + * variable. + */ + protected static Hashtable mapStatusCodes = new Hashtable(); + + + // ------------------------------------------------------ HTTP Status Codes + + + /** + * Status code (200) indicating the request succeeded normally. + */ + public static final int SC_OK = HttpServletResponse.SC_OK; + + + /** + * Status code (201) indicating the request succeeded and created + * a new resource on the server. + */ + public static final int SC_CREATED = HttpServletResponse.SC_CREATED; + + + /** + * Status code (202) indicating that a request was accepted for + * processing, but was not completed. + */ + public static final int SC_ACCEPTED = HttpServletResponse.SC_ACCEPTED; + + + /** + * Status code (204) indicating that the request succeeded but that + * there was no new information to return. + */ + public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT; + + + /** + * Status code (301) indicating that the resource has permanently + * moved to a new location, and that future references should use a + * new URI with their requests. + */ + public static final int SC_MOVED_PERMANENTLY = + HttpServletResponse.SC_MOVED_PERMANENTLY; + + + /** + * Status code (302) indicating that the resource has temporarily + * moved to another location, but that future references should + * still use the original URI to access the resource. + */ + public static final int SC_MOVED_TEMPORARILY = + HttpServletResponse.SC_MOVED_TEMPORARILY; + + + /** + * Status code (304) indicating that a conditional GET operation + * found that the resource was available and not modified. + */ + public static final int SC_NOT_MODIFIED = + HttpServletResponse.SC_NOT_MODIFIED; + + + /** + * Status code (400) indicating the request sent by the client was + * syntactically incorrect. + */ + public static final int SC_BAD_REQUEST = + HttpServletResponse.SC_BAD_REQUEST; + + + /** + * Status code (401) indicating that the request requires HTTP + * authentication. + */ + public static final int SC_UNAUTHORIZED = + HttpServletResponse.SC_UNAUTHORIZED; + + + /** + * Status code (403) indicating the server understood the request + * but refused to fulfill it. + */ + public static final int SC_FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; + + + /** + * Status code (404) indicating that the requested resource is not + * available. + */ + public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND; + + + /** + * Status code (500) indicating an error inside the HTTP service + * which prevented it from fulfilling the request. + */ + public static final int SC_INTERNAL_SERVER_ERROR = + HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + + + /** + * Status code (501) indicating the HTTP service does not support + * the functionality needed to fulfill the request. + */ + public static final int SC_NOT_IMPLEMENTED = + HttpServletResponse.SC_NOT_IMPLEMENTED; + + + /** + * Status code (502) indicating that the HTTP server received an + * invalid response from a server it consulted when acting as a + * proxy or gateway. + */ + public static final int SC_BAD_GATEWAY = + HttpServletResponse.SC_BAD_GATEWAY; + + + /** + * Status code (503) indicating that the HTTP service is + * temporarily overloaded, and unable to handle the request. + */ + public static final int SC_SERVICE_UNAVAILABLE = + HttpServletResponse.SC_SERVICE_UNAVAILABLE; + + + /** + * Status code (100) indicating the client may continue with + * its request. This interim response is used to inform the + * client that the initial part of the request has been + * received and has not yet been rejected by the server. + */ + public static final int SC_CONTINUE = 100; + + + /** + * Status code (405) indicating the method specified is not + * allowed for the resource. + */ + public static final int SC_METHOD_NOT_ALLOWED = 405; + + + /** + * Status code (409) indicating that the request could not be + * completed due to a conflict with the current state of the + * resource. + */ + public static final int SC_CONFLICT = 409; + + + /** + * Status code (412) indicating the precondition given in one + * or more of the request-header fields evaluated to false + * when it was tested on the server. + */ + public static final int SC_PRECONDITION_FAILED = 412; + + + /** + * Status code (413) indicating the server is refusing to + * process a request because the request entity is larger + * than the server is willing or able to process. + */ + public static final int SC_REQUEST_TOO_LONG = 413; + + + /** + * Status code (415) indicating the server is refusing to service + * the request because the entity of the request is in a format + * not supported by the requested resource for the requested + * method. + */ + public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415; + + + // -------------------------------------------- Extended WebDav status code + + + /** + * Status code (207) indicating that the response requires + * providing status for multiple independent operations. + */ + public static final int SC_MULTI_STATUS = 207; + // This one colides with HTTP 1.1 + // "207 Parital Update OK" + + + /** + * Status code (418) indicating the entity body submitted with + * the PATCH method was not understood by the resource. + */ + public static final int SC_UNPROCESSABLE_ENTITY = 418; + // This one colides with HTTP 1.1 + // "418 Reauthentication Required" + + + /** + * Status code (419) indicating that the resource does not have + * sufficient space to record the state of the resource after the + * execution of this method. + */ + public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419; + // This one colides with HTTP 1.1 + // "419 Proxy Reauthentication Required" + + + /** + * Status code (420) indicating the method was not executed on + * a particular resource within its scope because some part of + * the method's execution failed causing the entire method to be + * aborted. + */ + public static final int SC_METHOD_FAILURE = 420; + + + /** + * Status code (423) indicating the destination resource of a + * method is locked, and either the request did not contain a + * valid Lock-Info header, or the Lock-Info header identifies + * a lock held by another principal. + */ + public static final int SC_LOCKED = 423; + + + // ------------------------------------------------------------ Initializer + + + static { + // HTTP 1.0 tatus Code + addStatusCodeMap(SC_OK, "OK"); + addStatusCodeMap(SC_CREATED, "Created"); + addStatusCodeMap(SC_ACCEPTED, "Accepted"); + addStatusCodeMap(SC_NO_CONTENT, "No Content"); + addStatusCodeMap(SC_MOVED_PERMANENTLY, "Moved Permanently"); + addStatusCodeMap(SC_MOVED_TEMPORARILY, "Moved Temporarily"); + addStatusCodeMap(SC_NOT_MODIFIED, "Not Modified"); + addStatusCodeMap(SC_BAD_REQUEST, "Bad Request"); + addStatusCodeMap(SC_UNAUTHORIZED, "Unauthorized"); + addStatusCodeMap(SC_FORBIDDEN, "Forbidden"); + addStatusCodeMap(SC_NOT_FOUND, "Not Found"); + addStatusCodeMap(SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); + addStatusCodeMap(SC_NOT_IMPLEMENTED, "Not Implemented"); + addStatusCodeMap(SC_BAD_GATEWAY, "Bad Gateway"); + addStatusCodeMap(SC_SERVICE_UNAVAILABLE, "Service Unavailable"); + addStatusCodeMap(SC_CONTINUE, "Continue"); + addStatusCodeMap(SC_METHOD_NOT_ALLOWED, "Method Not Allowed"); + addStatusCodeMap(SC_CONFLICT, "Conflict"); + addStatusCodeMap(SC_PRECONDITION_FAILED, "Precondition Failed"); + addStatusCodeMap(SC_REQUEST_TOO_LONG, "Request Too Long"); + addStatusCodeMap(SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"); + // WebDav Status Codes + addStatusCodeMap(SC_MULTI_STATUS, "Multi-Status"); + addStatusCodeMap(SC_UNPROCESSABLE_ENTITY, "Unprocessable Entity"); + addStatusCodeMap(SC_INSUFFICIENT_SPACE_ON_RESOURCE, + "Insufficient Space On Resource"); + addStatusCodeMap(SC_METHOD_FAILURE, "Method Failure"); + addStatusCodeMap(SC_LOCKED, "Locked"); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Returns the HTTP status text for the HTTP or WebDav status code + * specified by looking it up in the static mapping. This is a + * static function. + * + * @param nHttpStatusCode [IN] HTTP or WebDAV status code + * @return A string with a short descriptive phrase for the + * HTTP status code (e.g., "OK"). + */ + public static String getStatusText(int nHttpStatusCode) { + Integer intKey = new Integer(nHttpStatusCode); + + if (!mapStatusCodes.containsKey(intKey)) { + return ""; + } else { + return (String) mapStatusCodes.get(intKey); + } + } + + + // -------------------------------------------------------- protected Methods + + + /** + * Adds a new status code -> status text mapping. This is a static + * method because the mapping is a static variable. + * + * @param nKey [IN] HTTP or WebDAV status code + * @param strVal [IN] HTTP status text + */ + protected static void addStatusCodeMap(int nKey, String strVal) { + mapStatusCodes.put(new Integer(nKey), strVal); + } + +}; + diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/XMLWriter.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/XMLWriter.java new file mode 100644 index 000000000..46652dc9d --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/file/XMLWriter.java @@ -0,0 +1,243 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.servlets.file; + +import java.io.IOException; +import java.io.Writer; + +/** + * XMLWriter helper class. + * + * @author Remy Maucherat + */ +public class XMLWriter { + + + // -------------------------------------------------------------- Constants + + + /** + * Opening tag. + */ + public static final int OPENING = 0; + + + /** + * Closing tag. + */ + public static final int CLOSING = 1; + + + /** + * Element with no content. + */ + public static final int NO_CONTENT = 2; + + + // ----------------------------------------------------- Instance Variables + + + /** + * Buffer. + */ + protected StringBuffer buffer = new StringBuffer(); + + + /** + * Writer. + */ + protected Writer writer = null; + + + // ----------------------------------------------------------- Constructors + + + /** + * Constructor. + */ + public XMLWriter() { + } + + + /** + * Constructor. + */ + public XMLWriter(Writer writer) { + this.writer = writer; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Retrieve generated XML. + * + * @return String containing the generated XML + */ + public String toString() { + return buffer.toString(); + } + + + /** + * Write property to the XML. + * + * @param namespace Namespace + * @param namespaceInfo Namespace info + * @param name Property name + * @param value Property value + */ + public void writeProperty(String namespace, String namespaceInfo, + String name, String value) { + writeElement(namespace, namespaceInfo, name, OPENING); + buffer.append(value); + writeElement(namespace, namespaceInfo, name, CLOSING); + + } + + + /** + * Write property to the XML. + * + * @param namespace Namespace + * @param name Property name + * @param value Property value + */ + public void writeProperty(String namespace, String name, String value) { + writeElement(namespace, name, OPENING); + buffer.append(value); + writeElement(namespace, name, CLOSING); + } + + + /** + * Write property to the XML. + * + * @param namespace Namespace + * @param name Property name + */ + public void writeProperty(String namespace, String name) { + writeElement(namespace, name, NO_CONTENT); + } + + + /** + * Write an element. + * + * @param name Element name + * @param namespace Namespace abbreviation + * @param type Element type + */ + public void writeElement(String namespace, String name, int type) { + writeElement(namespace, null, name, type); + } + + + /** + * Write an element. + * + * @param namespace Namespace abbreviation + * @param namespaceInfo Namespace info + * @param name Element name + * @param type Element type + */ + public void writeElement(String namespace, String namespaceInfo, + String name, int type) { + if ((namespace != null) && (namespace.length() > 0)) { + switch (type) { + case OPENING: + if (namespaceInfo != null) { + buffer.append("<" + namespace + ":" + name + " xmlns:" + + namespace + "=\"" + + namespaceInfo + "\">"); + } else { + buffer.append("<" + namespace + ":" + name + ">"); + } + break; + case CLOSING: + buffer.append("\n"); + break; + case NO_CONTENT: + default: + if (namespaceInfo != null) { + buffer.append("<" + namespace + ":" + name + " xmlns:" + + namespace + "=\"" + + namespaceInfo + "\"/>"); + } else { + buffer.append("<" + namespace + ":" + name + "/>"); + } + break; + } + } else { + switch (type) { + case OPENING: + buffer.append("<" + name + ">"); + break; + case CLOSING: + buffer.append("\n"); + break; + case NO_CONTENT: + default: + buffer.append("<" + name + "/>"); + break; + } + } + } + + + /** + * Write text. + * + * @param text Text to append + */ + public void writeText(String text) { + buffer.append(text); + } + + + /** + * Write data. + * + * @param data Data to append + */ + public void writeData(String data) { + buffer.append(""); + } + + + /** + * Write XML Header. + */ + public void writeXMLHeader() { + buffer.append("\n"); + } + + + /** + * Send data and reinitializes buffer. + */ + public void sendData() + throws IOException { + if (writer != null) { + writer.write(buffer.toString()); + buffer = new StringBuffer(); + } + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/JasperCompilerTemplateClassMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/JasperCompilerTemplateClassMapper.java new file mode 100644 index 000000000..29c9d8cef --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/JasperCompilerTemplateClassMapper.java @@ -0,0 +1,173 @@ +/* + * 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.servlets.jsp; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import javax.naming.NamingException; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.apache.InstanceManager; +import org.apache.jasper.EmbeddedServletOptions; +import org.apache.jasper.JasperException; +import org.apache.jasper.JspC; +import org.apache.jasper.Options; +import org.apache.jasper.compiler.JspRuntimeContext; +import org.apache.jasper.servlet.JspServletWrapper; + +/** + * The actual compiler. Maps and compile a jsp-file to a class. + */ +public class JasperCompilerTemplateClassMapper + extends SimpleTemplateClassMapper { + + public void init(ServletConfig config) { + this.config = config; + ServletContext context = config.getServletContext(); + context.setAttribute(InstanceManager.class.getName(), + new InstanceManager() { + + public void destroyInstance(Object arg0) + throws IllegalAccessException, + InvocationTargetException { + } + + public Object newInstance(String arg0) + throws IllegalAccessException, + InvocationTargetException, NamingException, + InstantiationException, ClassNotFoundException { + return newInstance(arg0, + this.getClass().getClassLoader()); + } + + public void newInstance(Object o) + throws IllegalAccessException, + InvocationTargetException, NamingException { + } + + public Object newInstance(String className, + ClassLoader classLoader) + throws IllegalAccessException, + InvocationTargetException, NamingException, + InstantiationException, ClassNotFoundException { + Class clazz = classLoader.loadClass(className); + return clazz.newInstance(); + } + + }); + // Initialize the JSP Runtime Context + options = new EmbeddedServletOptions(config, context); + + rctxt = new JspRuntimeContext(context, options); + String basePath = context.getRealPath("/"); + File f = new File(basePath + "/WEB-INF/classes"); + f.mkdirs(); + //fileS.initParams.put("scratchdir", f.getAbsolutePath()); + // if load-on-startup: allow other servlets to find us + + + } + + private Options options; + private JspRuntimeContext rctxt; + private ServletConfig config; + + public boolean needsReload(String jspFile, Servlet s) { + JspServletWrapper wrapper = + (JspServletWrapper) rctxt.getWrapper(jspFile); + // TODO: extract outdate info, compilation date, etc + return false; + } + + protected Servlet compileAndInitPage(ServletContext ctx, + String jspUri, + ServletConfig cfg) + throws ServletException { + try { + if (config == null) { + init(cfg); + } + JspServletWrapper wrapper = + (JspServletWrapper) rctxt.getWrapper(jspUri); + if (wrapper == null) { + synchronized(this) { + wrapper = (JspServletWrapper) rctxt.getWrapper(jspUri); + if (wrapper == null) { + // Check if the requested JSP page exists, to avoid + // creating unnecessary directories and files. + if (null == ctx.getResource(jspUri)) { + return null; + } + //boolean isErrorPage = exception != null; + wrapper = new JspServletWrapper(cfg, options, jspUri, + false, rctxt); + rctxt.addWrapper(jspUri,wrapper); + } + } + } + + wrapper.getJspEngineContext().compile(); + return wrapper.getServlet(); + } catch (IOException ex) { + throw new ServletException(ex); + } + } + + /** + * + * Do the compilation - without JspServletWrapper + * + * Options: + * - jasper.jar in classpath, we do Class.forName for main() + * - TODO: exec jasper.sh ( or any other script set in params ) + * - TODO: redirect to a different servlet + * + * Not used right - see previous method for a safer approach + * + * @param ctx + * @param jspPath + */ + public void compileJspDirect(ServletContext ctx, String jspPath) { + //ServletContextImpl ctx = (ServletContextImpl)sctx; + // Params to pass to jspc: + // classpath + // webapp base dir + String baseDir = ctx.getRealPath("/"); + // jsp path ( rel. base dir ) + + JspC jspc = new JspC(); + jspc.setUriroot(baseDir); + jspc.setTrimSpaces(false); + jspc.setPoolingEnabled(true); + jspc.setErrorOnUseBeanInvalidClassAttribute(false); + jspc.setClassDebugInfo(true); + jspc.setCaching(true); + jspc.setSmapDumped(true); + + try { + jspc.execute(); + } catch (JasperException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/JspFileTemplateServlet.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/JspFileTemplateServlet.java new file mode 100644 index 000000000..85c1cc023 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/JspFileTemplateServlet.java @@ -0,0 +1,90 @@ +/* + * 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.servlets.jsp; + +import java.io.IOException; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.addons.UserTemplateClassMapper; +import org.apache.tomcat.integration.ObjectManager; + +/** + * Support for config. + * + * A servlet can be configured with a jsp-file instead of a class. This can + * be translated into a regular servlet, with JspFileTemplateServlet class and + * the jsp-file as init parameter. + * + * This servlet is not jsp specific - you can put any templating file in the + * jsp-file config ( if you can ignore the jsp in the name of the attribute ). + * The file will be converted to a servlet by a custom addon ( jasper in most + * cases ) + * + * @author Costin Manolache + */ +public class JspFileTemplateServlet extends HttpServlet { + + String jspFile; + Servlet realJspServlet; + + UserTemplateClassMapper mapper; + + /** + * If called from a servlet, compile the servlet and init it. + */ + public void init(ServletConfig config) throws ServletException { + super.init(config); + // Support + String jspFileParam = config.getInitParameter("jsp-file"); + // TODO: use extension to find the right UserTemplateClassMapper. + ObjectManager om = + (ObjectManager) config.getServletContext().getAttribute(ObjectManager.ATTRIBUTE); + mapper = + (UserTemplateClassMapper) om.get( + UserTemplateClassMapper.class); + if (mapper == null) { + mapper = new SimpleTemplateClassMapper(); + } + + if (jspFile == null && jspFileParam != null) { + jspFile = jspFileParam; + } + // exception will be thrown if not set properly + realJspServlet = mapper.loadProxy(jspFile, getServletContext(), + config); + realJspServlet.init(config); + } + + public void setJspFile(String jspFile) { + this.jspFile = jspFile; + } + + protected void service(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException + { + // TODO: support reload + + // realJspServlet will be set - init will fail otherwise. + realJspServlet.service(req, res); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/PreCompileFilter.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/PreCompileFilter.java new file mode 100644 index 000000000..06497ddd9 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/PreCompileFilter.java @@ -0,0 +1,71 @@ +package org.apache.tomcat.servlets.jsp; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; + +/** + * Filter for JSPs to support 'preCompile' support. + * This is a silly feature, can and should be left out in prod. + * It needs to be used in the default config to pass the tests. + * + * @author Costin Manolache + */ +public class PreCompileFilter extends HttpServlet { + // Copied from jasper.Constants to avoid compile dep + /** + * The query parameter that causes the JSP engine to just + * pregenerated the servlet but not invoke it. + */ + public static final String PRECOMPILE = + System.getProperty("org.apache.jasper.Constants.PRECOMPILE", "jsp_precompile"); + + /** + * If called from a servlet, compile the servlet and init it. + */ + public void init(ServletConfig arg0) throws ServletException { + super.init(arg0); + } + + boolean preCompile(HttpServletRequest request) throws ServletException { + String queryString = request.getQueryString(); + if (queryString == null) { + return (false); + } + int start = queryString.indexOf(PRECOMPILE); + if (start < 0) { + return (false); + } + queryString = + queryString.substring(start + PRECOMPILE.length()); + if (queryString.length() == 0) { + return (true); // ?jsp_precompile + } + if (queryString.startsWith("&")) { + return (true); // ?jsp_precompile&foo=bar... + } + if (!queryString.startsWith("=")) { + return (false); // part of some other name or value + } + int limit = queryString.length(); + int ampersand = queryString.indexOf("&"); + if (ampersand > 0) { + limit = ampersand; + } + String value = queryString.substring(1, limit); + if (value.equals("true")) { + return (true); // ?jsp_precompile=true + } else if (value.equals("false")) { + // Spec says if jsp_precompile=false, the request should not + // be delivered to the JSP page; the easiest way to implement + // this is to set the flag to true, and precompile the page anyway. + // This still conforms to the spec, since it says the + // precompilation request can be ignored. + return (true); // ?jsp_precompile=false + } else { + throw new ServletException("Cannot have request parameter " + + PRECOMPILE + " set to " + value); + } + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/SimpleTemplateClassMapper.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/SimpleTemplateClassMapper.java new file mode 100644 index 000000000..7c01312b2 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/SimpleTemplateClassMapper.java @@ -0,0 +1,214 @@ +/* + * 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.servlets.jsp; + +import java.util.Vector; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +import org.apache.tomcat.addons.UserTemplateClassMapper; + +/** + * Extracted from Jasper - maps a template file ( foo/my.jsp ) to a classname + * that is generated by the UserTemplateCompiler. + * + * TODO: transform this to an interface, paired with UserTemplateCompiler. + * + * @author Costin Manolache + */ +public class SimpleTemplateClassMapper implements UserTemplateClassMapper { + + /** + * Load the proxied jsp, if any. + * @param config + * @throws ServletException + */ + public Servlet loadProxy(String jspFile, + ServletContext ctx, + ServletConfig config) throws ServletException { + String mangledClass = getClassName( jspFile ); + + HttpServlet jsp = null; + Class jspC = null; + + // Already created + if( jspC == null ) { + try { + jspC=Class.forName( mangledClass ); + } catch( Throwable t ) { + // Not found - first try + } + } + + if (jspC == null) { + // Class not found - needs to be compiled + return compileAndInitPage(ctx, jspFile, config); + } + + try { + jsp=(HttpServlet)jspC.newInstance(); + } catch( Throwable t ) { + t.printStackTrace(); + } + jsp.init(config); + return jsp; + } + + public boolean needsReload(String jspFile, Servlet s) { + return false; + } + + protected Servlet compileAndInitPage(ServletContext ctx, + String jspUri, + ServletConfig cfg) + throws ServletException { + throw new ServletException("Pre-compiled page not found, please " + + "add a compiler addon to compile at runtime"); + } + + /** Convert an identifier to a class name, using jasper conventions + * + * @param jspUri a relative JSP file + * @return class name that would be generated by jasper + */ + public String getClassName( String jspUri ) { + int iSep = jspUri.lastIndexOf('/') + 1; + String className = makeJavaIdentifier(jspUri.substring(iSep)); + String basePackageName = JSP_PACKAGE_NAME; + + iSep--; + String derivedPackageName = (iSep > 0) ? + makeJavaPackage(jspUri.substring(1,iSep)) : ""; + + if (derivedPackageName.length() == 0) { + return basePackageName + "." + className; + } + return basePackageName + '.' + derivedPackageName + "." + className; + } + + // ------------- Copied from jasper --------------------------- + + private static final String JSP_PACKAGE_NAME = "org.apache.jsp"; + + private static final String makeJavaIdentifier(String identifier) { + StringBuffer modifiedIdentifier = + new StringBuffer(identifier.length()); + if (!Character.isJavaIdentifierStart(identifier.charAt(0))) { + modifiedIdentifier.append('_'); + } + for (int i = 0; i < identifier.length(); i++) { + char ch = identifier.charAt(i); + if (Character.isJavaIdentifierPart(ch) && ch != '_') { + modifiedIdentifier.append(ch); + } else if (ch == '.') { + modifiedIdentifier.append('_'); + } else { + modifiedIdentifier.append(mangleChar(ch)); + } + } + if (isJavaKeyword(modifiedIdentifier.toString())) { + modifiedIdentifier.append('_'); + } + return modifiedIdentifier.toString(); + } + + private static final String javaKeywords[] = { + "abstract", "assert", "boolean", "break", "byte", "case", + "catch", "char", "class", "const", "continue", + "default", "do", "double", "else", "enum", "extends", + "final", "finally", "float", "for", "goto", + "if", "implements", "import", "instanceof", "int", + "interface", "long", "native", "new", "package", + "private", "protected", "public", "return", "short", + "static", "strictfp", "super", "switch", "synchronized", + "this", "throws", "transient", "try", "void", + "volatile", "while" }; + + private static final String makeJavaPackage(String path) { + String classNameComponents[] = split(path,"/"); + StringBuffer legalClassNames = new StringBuffer(); + for (int i = 0; i < classNameComponents.length; i++) { + legalClassNames.append(makeJavaIdentifier(classNameComponents[i])); + if (i < classNameComponents.length - 1) { + legalClassNames.append('.'); + } + } + return legalClassNames.toString(); + } + + private static final String [] split(String path, String pat) { + Vector comps = new Vector(); + int pos = path.indexOf(pat); + int start = 0; + while( pos >= 0 ) { + if(pos > start ) { + String comp = path.substring(start,pos); + comps.add(comp); + } + start = pos + pat.length(); + pos = path.indexOf(pat,start); + } + if( start < path.length()) { + comps.add(path.substring(start)); + } + String [] result = new String[comps.size()]; + for(int i=0; i < comps.size(); i++) { + result[i] = (String)comps.elementAt(i); + } + return result; + } + + + /** + * Test whether the argument is a Java keyword + */ + private static boolean isJavaKeyword(String key) { + int i = 0; + int j = javaKeywords.length; + while (i < j) { + int k = (i+j)/2; + int result = javaKeywords[k].compareTo(key); + if (result == 0) { + return true; + } + if (result < 0) { + i = k+1; + } else { + j = k; + } + } + return false; + } + + /** + * Mangle the specified character to create a legal Java class name. + */ + private static final String mangleChar(char ch) { + char[] result = new char[5]; + result[0] = '_'; + result[1] = Character.forDigit((ch >> 12) & 0xf, 16); + result[2] = Character.forDigit((ch >> 8) & 0xf, 16); + result[3] = Character.forDigit((ch >> 4) & 0xf, 16); + result[4] = Character.forDigit(ch & 0xf, 16); + return new String(result); + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/SingleThreadedProxyServlet.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/SingleThreadedProxyServlet.java new file mode 100644 index 000000000..02cf9c359 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/SingleThreadedProxyServlet.java @@ -0,0 +1,61 @@ +/* + */ +package org.apache.tomcat.servlets.jsp; + +import java.util.Stack; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** For SingleThreadedServlet support. + * + * This is container independent. + * + * Will maintain a pool of servlets, etc + * + * @author Costin Manolache + */ +public class SingleThreadedProxyServlet extends HttpServlet { + + private Class classClass = null; + private transient boolean singleThreadModel = false; + /** + * Stack containing the STM instances. + */ + private transient Stack instancePool = null; + + /** + * Extra params: + * - servlet-class - the class of the single-threaded servlet + * - + * + */ + public void init() { + + } + + public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException { + synchronized (instancePool) { + if (instancePool.isEmpty()) { + try { + Servlet newServlet = null; // loadServlet(); + + // todo: should we init each of them ? + + newServlet.service(req, res); + + + } catch (Throwable e) { + throw new ServletException("allocate ", + e); + } + } + Servlet s = (Servlet) instancePool.pop(); + } + + + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/WildcardTemplateServlet.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/WildcardTemplateServlet.java new file mode 100644 index 000000000..609dbc203 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/jsp/WildcardTemplateServlet.java @@ -0,0 +1,108 @@ +/* + * 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.servlets.jsp; + +import java.io.IOException; +import java.util.HashMap; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.addons.UserTemplateClassMapper; +import org.apache.tomcat.integration.ObjectManager; + + +/** + * + * @author Costin Manolache + */ +public class WildcardTemplateServlet extends HttpServlet { + + // for the '*.jsp' case - need to keep track of the jsps + HashMap jsps=new HashMap(); + + UserTemplateClassMapper mapper; + + /** + * If called from a servlet, compile the servlet and init it. + */ + public void init(ServletConfig config) throws ServletException { + super.init(config); + ObjectManager om = + (ObjectManager) config.getServletContext().getAttribute(ObjectManager.ATTRIBUTE); + mapper = + (UserTemplateClassMapper) om.get( + UserTemplateClassMapper.class); + if (mapper == null) { + mapper = new SimpleTemplateClassMapper(); + } + } + + // TODO: use context extensions to register the servlet as if it would be + // loaded from web.xml + + protected void service(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException + { + // This is a *.jsp mapping. + String jspPath = null; + + /** + * Magic to get the jsp file from the mappings or container + */ + if (jspPath == null) { + jspPath = (String)req.getAttribute("org.apache.catalina.jsp_file"); + } + if (jspPath == null) { + // RequestDispatcher.include() + jspPath = (String)req.getAttribute("javax.servlet.include.servlet_path"); + if (jspPath != null) { + String pathInfo = (String)req.getAttribute("javax.servlet.include.path_info"); + if (pathInfo != null) { + jspPath += pathInfo; + } + } else { + jspPath = req.getServletPath(); + String pathInfo = req.getPathInfo(); + if (pathInfo != null) { + jspPath += pathInfo; + } + } + } + + // now we should have jspUri == the path to the jsp. + Servlet realJspServlet = jsps.get(jspPath); + + // TODO: support reload + + if (realJspServlet == null) { + realJspServlet = mapper.loadProxy(jspPath, + getServletContext(), getServletConfig()); + if (realJspServlet != null) { + jsps.put(jspPath, realJspServlet); + } else { + throw new ServletException(jspPath + " not found"); + } + } + + realJspServlet.service(req, res); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/AccessFilter.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/AccessFilter.java new file mode 100644 index 000000000..5af1e7f52 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/AccessFilter.java @@ -0,0 +1,146 @@ +/* + * Copyright 1999-2001,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.sec; + + +import java.io.IOException; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.addons.UserAuthentication; + + +/** + * Access control. + * + * In Catalina, the AuthenticatorBase.invoke() will apply the security + * filters based on LoginConfig. All constraints are applied in the + * valve. For sessions - the Principal will be cached. The TTL needs to be + * indicated by the authenticator. + * + * This works differently - it's a regular filter, could be modified and + * set in web.xml explicitely, or set by the container when reading web.xml + * + * + * Mappings: + * + * /[FORM]/j_security_check ( with j_password, j_username params ) + * -> authentication servlet + * Assert: no other j_security_check mapping + * + * + * For each security rule we define one AccessFilter, configured with the + * right init-params. + * + * 1. For each security_constraint, create a (base) filter named: _access_nnn, + * and add init-params for roles + * + * 1.1 For each web-resource-collection, take the method set, sort it. Create + * one filter for each set of methods, named _access_nnn_methodlist + * + * 2.For each pattern in each web-resource-collection, add a mapping to + * the appropriate filter, at the end of web.xml ( after normal filters + * and servlets ). + * + * + * @author Costin Manolache + */ +public class AccessFilter implements Filter { + + // web-resource-collection: name -> url+method rules + // Since filters don't match method, for each method subset we need a new + // filter instance + private String[] methods; + + // Wildcard on roles - anyone authenticated can access the resource + private boolean allRoles = false; + + // if false - no access control needed. + // if true - must check roles - either allRoles or specific list + private boolean authConstraint = false; + + // roles to check + private String authRoles[] = new String[0]; + + /** + * The user data constraint for this security constraint. Must be NONE, + * INTEGRAL, or CONFIDENTIAL. + */ + private String userConstraint = "NONE"; + + + public void destroy() { + } + + public static class AuthToken { + public String headerValue; // key + public long expiry; + public Principal principal; + } + + Map cachedTokens = new HashMap(); + + UserAuthentication auth; + String method; + + public void doFilter(ServletRequest request, + ServletResponse servletResponse, + FilterChain chain) + throws IOException, ServletException { + + HttpServletResponse res = (HttpServletResponse)servletResponse; + HttpServletRequest req = (HttpServletRequest)request; + + String method = req.getMethod(); + // exclude the context path + String uri = req.getServletPath() + req.getPathInfo(); + + // no authorization or principal not found. + // Call the auth servlet. + // TODO: use URL or RD.include. + + Principal p = auth.authenticate(req, res, method); + // set the principal on req! + if (p == null) { + // couldn't authenticate - response is set. + return; + } else { + // we are ok - cache the response, forward + + chain.doFilter(req, res); + } + + } + + + + public void init(FilterConfig filterConfig) throws ServletException { + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/BasicAuthentication.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/BasicAuthentication.java new file mode 100644 index 000000000..fa98e5a8d --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/BasicAuthentication.java @@ -0,0 +1,94 @@ +/* + * 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.servlets.sec; + + +import java.io.IOException; +import java.security.Principal; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.addons.UserAuthentication; + + +/** + * Implements Basic authentication. + */ +public class BasicAuthentication implements UserAuthentication { + + private static Log log = LogFactory.getLog(BasicAuthentication.class); + String realm; + + @Override + public Principal authenticate(HttpServletRequest request, + HttpServletResponse response, + String requestedMethod) throws IOException { + + if (realm == null) + realm = request.getServerName() + ":" + request.getServerPort(); + + // Validate any credentials already included with this request + String authorization = request.getHeader("authorization"); + if (authorization != null) { + Principal principal = null; // processDigestHeader(request, authorization); + if (principal != null) { + return principal; + } + } + + String domain = request.getContextPath(); + String authHeader = getAuthenticateHeader(domain, false); + + response.setHeader("WWW-Authenticate", authHeader); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return null; + } + + + /** Generate the auth header + */ + protected String getAuthenticateHeader(String domain, + boolean stale) { + + long currentTime = System.currentTimeMillis(); + return ""; + } + + // ------------------------------------------------------ Protected Methods + + + public Principal authenticate(final String username, String clientDigest, + String nOnce, String nc, String cnonce, + String qop, String realm, + String md5a2) { + + + String serverDigest = null; + + return null; + } + + + @Override + public boolean isUserInRole(HttpServletRequest req, Principal p, String role) { + return false; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/FormAuthentication.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/FormAuthentication.java new file mode 100644 index 000000000..d02908f15 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/FormAuthentication.java @@ -0,0 +1,95 @@ +/* + * 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.servlets.sec; + + +import java.io.IOException; +import java.security.Principal; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.addons.UserAuthentication; + + +/** + * Implements Form authentication. + */ +public class FormAuthentication implements UserAuthentication { + + private static Log log = LogFactory.getLog(FormAuthentication.class); + String realm; + String url; + + @Override + public Principal authenticate(HttpServletRequest request, + HttpServletResponse response, + String requestedMethod) throws IOException { + + if (realm == null) + realm = request.getServerName() + ":" + request.getServerPort(); + + // Validate any credentials already included with this request + String authorization = request.getHeader("authorization"); + if (authorization != null) { + Principal principal = null; // processDigestHeader(request, authorization); + if (principal != null) { + return principal; + } + } + + String domain = request.getContextPath(); + String authHeader = getAuthenticateHeader(domain, false); + + response.setHeader("WWW-Authenticate", authHeader); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return null; + } + + + /** Generate the auth header + */ + protected String getAuthenticateHeader(String domain, + boolean stale) { + + long currentTime = System.currentTimeMillis(); + return ""; + } + + // ------------------------------------------------------ Protected Methods + + + public Principal authenticate(final String username, String clientDigest, + String nOnce, String nc, String cnonce, + String qop, String realm, + String md5a2) { + + + String serverDigest = null; + + return null; + } + + + @Override + public boolean isUserInRole(HttpServletRequest req, Principal p, String role) { + return false; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/IPFilter.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/IPFilter.java new file mode 100644 index 000000000..fd032a292 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/IPFilter.java @@ -0,0 +1,118 @@ +/* + * Copyright 1999-2001,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.sec; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + + +/** + * Reimplementation of catalina IP valve. + * + */ +public class IPFilter implements Filter { + /** + * The set of allow regular expressions we will evaluate. + */ + protected Pattern allows[] = new Pattern[0]; + + + /** + * The set of deny regular expressions we will evaluate. + */ + protected Pattern denies[] = new Pattern[0]; + + // --------------------------------------------------------- Public Methods + + public void setAllows(String pattern) { + allows = getPatterns(pattern); + } + + public void setDenies(String pattern) { + denies = getPatterns(pattern); + } + + private Pattern[] getPatterns(String pattern) { + String[] patSplit = pattern.split(","); + ArrayList allowsAL = new ArrayList(); + for( int i=0; i 0) && (allows.length == 0)) { + chain.doFilter(request, response); + return; + } + + // Deny this request + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + + + public void init(FilterConfig filterConfig) throws ServletException { + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/SimpleUserAuthDB.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/SimpleUserAuthDB.java new file mode 100644 index 000000000..a216b8299 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/SimpleUserAuthDB.java @@ -0,0 +1,83 @@ +/* + * Copyright 1999-2001,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.sec; + + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Properties; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Load user/passwords from a file. + * + * @author Costin Manolache + */ +public class SimpleUserAuthDB implements UserDB { + + private static final String USER_PREFIX = "u."; + private static final String ROLE_PREFIX = "r."; + + HashMap users = new HashMap(); + HashMap roles = new HashMap(); + + boolean hasMessageDigest = false; + String realm = null; + + public void addUser(String name, String pass) { + users.put(name, pass); + } + + public void addRole(String user, String value) { + String[] userRoles = value.split(","); + roles.put(user, userRoles); + } + + public void setFilename(String fileName) { + + } + + public void init(Properties p) throws ServletException { + } + + public void init(ServletConfig servletConfig) throws ServletException { + Enumeration names = servletConfig.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String)names.nextElement(); + String value = servletConfig.getInitParameter(name); + if (name.startsWith(USER_PREFIX)) { + addUser(name.substring(USER_PREFIX.length()), value); + } + if (name.startsWith(ROLE_PREFIX)) { + addRole(name.substring(ROLE_PREFIX.length()), value); + } + } + } + + public void checkAuth(String method, String cookie) { + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/UserDB.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/UserDB.java new file mode 100644 index 000000000..19ae39db0 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/sec/UserDB.java @@ -0,0 +1,14 @@ +/* + */ +package org.apache.tomcat.servlets.sec; + +/** + * Interface to a password/role storage, used by the classes in + * this directory. + * + * @author Costin Manolache + */ +public interface UserDB { + + //public void auth(String user, String md5); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/HttpSessionImpl.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/HttpSessionImpl.java new file mode 100644 index 000000000..4c73f525b --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/HttpSessionImpl.java @@ -0,0 +1,784 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.session; + + +import java.io.Serializable; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; +import javax.servlet.http.HttpSessionContext; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.apache.tomcat.servlets.util.Enumerator; + +/** + * Standard implementation of the Session interface. + * + * This is a minimal, non-serializable HttpSession. You can use a different + * session manager, but you should keep in mind that persistent or distributed + * sessions are a bad thing. + * + * The session is best for caching data across requests, and tracking the + * user flow. For any data you don't want to lose or is worth preserving - + * use a transaction manager, or any form of storage that provides the + * set of ACID characteristics you need. + * + * Even the most sophisticated sessions managers can't guarantee data integrity + * in 100% of cases, and can't notify you of the cases where a replication + * failed. Using such a manager might fool users into making incorrect + * assumptions. Computers and networks do crash at random points, and all + * the theory on transactions exists for a good reason. + * + * Note: this is a user-space implementation, i.e. this can be used in any + * container by using the WebappSessionManager class. + * + * @author Costin Manolache - removed most of the code + * @author Craig R. McClanahan + * @author Sean Legassick + * @author Jon S. Stevens + */ +public class HttpSessionImpl implements HttpSession, Serializable { + + /** + * Type array, used as param to toArray() + */ + protected static final String EMPTY_ARRAY[] = new String[0]; + + /** + * The HTTP session context associated with this session. + */ + protected HttpSessionContext sessionContext = null; + + + /** + * The Manager with which this Session is associated. + */ + protected transient SimpleSessionManager manager = null; + + /** + * The session identifier of this Session. + */ + protected String id = null; + + /** + * The collection of user data attributes associated with this Session. + */ + protected Map attributes = new HashMap(); + + /** + * The time this session was created, in milliseconds since midnight, + * January 1, 1970 GMT. + */ + protected long creationTime = 0L; + + /** + * The last accessed time for this Session. + */ + protected long lastAccessedTime = creationTime; + + /** + * The current accessed time for this session. + */ + protected long thisAccessedTime = creationTime; + + /** + * The maximum time interval, in seconds, between client requests before + * the servlet container may invalidate this session. A negative time + * indicates that the session should never time out. + */ + protected int maxInactiveInterval = -1; + + /** + * The access count for this session - how many requests are using this + * session ( so we can prevent expiry ) + */ + protected transient int accessCount = 0; + + /** + * We are currently processing a session expiration, so bypass + * certain IllegalStateException tests. + */ + protected transient boolean expiring = false; + + + /** + * Flag indicating whether this session is new or not. + */ + protected boolean isNew = false; + + + /** + * Flag indicating whether this session is valid or not. + */ + protected boolean isValid = false; + + + /** Only the manager can create sessions, so it knows about them. + */ + HttpSessionImpl(SimpleSessionManager manager) { + this.manager = manager; + } + + // ---------- API methods --------- + + /** + * Return the session identifier for this session. + */ + public String getId() { + return this.id; + } + + /** + * Return the last time the client sent a request associated with this + * session, as the number of milliseconds since midnight, January 1, 1970 + * GMT. Actions that your application takes, such as getting or setting + * a value associated with the session, do not affect the access time. + */ + public long getLastAccessedTime() { + checkValid(); + return this.lastAccessedTime; + + } + + /** + * Return the maximum time interval, in seconds, between client requests + * before the servlet container will invalidate the session. A negative + * time indicates that the session should never time out. + */ + public int getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + + /** + * Set the maximum time interval, in seconds, between client requests + * before the servlet container will invalidate the session. A negative + * time indicates that the session should never time out. + * + * @param interval The new maximum interval + */ + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + if (isValid && interval == 0) { + expire(); + } + } + + /** + * Return the time when this session was created, in milliseconds since + * midnight, January 1, 1970 GMT. + * + * @exception IllegalStateException if this method is called on an + * invalidated session + */ + public long getCreationTime() { + checkValid(); + return this.creationTime; + + } + + public ServletContext getServletContext() { + if (manager == null) + return null; // Should never happen + ServletContext context = (ServletContext)manager.getContext(); + return context; + } + + + public HttpSessionContext getSessionContext() { + if (sessionContext == null) + sessionContext = new StandardSessionContext(); + return (sessionContext); + } + + /** + * Return the object bound with the specified name in this session, or + * null if no object is bound with that name. + * + * @param name Name of the attribute to be returned + * + * @exception IllegalStateException if this method is called on an + * invalidated session + */ + public Object getAttribute(String name) { + checkValid(); + return attributes.get(name); + } + + + /** + * Return an Enumeration of String objects + * containing the names of the objects bound to this session. + * + * @exception IllegalStateException if this method is called on an + * invalidated session + */ + public Enumeration getAttributeNames() { + checkValid(); + return new Enumerator(attributes.keySet(), true); + } + + + /** + * Return the object bound with the specified name in this session, or + * null if no object is bound with that name. + * + * @param name Name of the value to be returned + * + * @exception IllegalStateException if this method is called on an + * invalidated session + * + * @deprecated As of Version 2.2, this method is replaced by + * getAttribute() + */ + public Object getValue(String name) { + return (getAttribute(name)); + } + + + /** + * Return the set of names of objects bound to this session. If there + * are no such objects, a zero-length array is returned. + * + * @exception IllegalStateException if this method is called on an + * invalidated session + * + * @deprecated As of Version 2.2, this method is replaced by + * getAttributeNames() + */ + public String[] getValueNames() { + checkValid(); + return keys(); // same, but no check for validity + } + + + /** + * Invalidates this session and unbinds any objects bound to it. + * + * @exception IllegalStateException if this method is called on + * an invalidated session + */ + public void invalidate() { + checkValid(); + expire(); + } + + + /** + * Return true if the client does not yet know about the + * session, or if the client chooses not to join the session. For + * example, if the server used only cookie-based sessions, and the client + * has disabled the use of cookies, then a session would be new on each + * request. + * + * @exception IllegalStateException if this method is called on an + * invalidated session + */ + public boolean isNew() { + checkValid(); + return (this.isNew); + } + + public void putValue(String name, Object value) { + setAttribute(name, value); + } + + public void removeAttribute(String name) { + checkValid(); + removeAttributeInternal(name, true); + } + + public void removeValue(String name) { + removeAttribute(name); + } + + + /** + * Bind an object to this session, using the specified name. If an object + * of the same name is already bound to this session, the object is + * replaced. + *

+ * After this method executes, and if the object implements + * HttpSessionBindingListener, the container calls + * valueBound() on the object. + * + * @param name Name to which the object is bound, cannot be null + * @param value Object to be bound, cannot be null + * + * @exception IllegalArgumentException if an attempt is made to add a + * non-serializable object in an environment marked distributable. + * @exception IllegalStateException if this method is called on an + * invalidated session + */ + public void setAttribute(String name, Object value) { + // Name cannot be null + if (name == null) return; + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + checkValid(); + + if ((manager != null) && manager.getDistributable() && + !(value instanceof Serializable)) + throw new IllegalArgumentException("setAttribute() not serializable"); + + // Construct an event with the new value + HttpSessionBindingEvent event = null; + + // Call the valueBound() method if necessary + if (value instanceof HttpSessionBindingListener) { + // Don't call any notification if replacing with the same value + Object oldValue = attributes.get(name); + if (value != oldValue) { + event = new HttpSessionBindingEvent(getSession(), name, value); + try { + ((HttpSessionBindingListener) value).valueBound(event); + } catch (Throwable t){ + manager.log.error("Listener valueBound() error", t); + } + } + } + + // Replace or add this attribute + Object unbound = attributes.put(name, value); + + // Call the valueUnbound() method if necessary + if ((unbound != null) && (unbound != value) && + (unbound instanceof HttpSessionBindingListener)) { + try { + ((HttpSessionBindingListener) unbound).valueUnbound + (new HttpSessionBindingEvent(getSession(), name)); + } catch (Throwable t) { + manager.log.error("Listener valueUnbound()", t); + } + } + + // Notify interested application event listeners + ServletContext context = manager.getContext(); + List listeners = manager.getEventListeners(); + if (listeners.size() == 0) + return; + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof HttpSessionAttributeListener)) + continue; + HttpSessionAttributeListener listener = + (HttpSessionAttributeListener) listeners.get(i); + try { + if (unbound != null) { + if (event == null) { + event = new HttpSessionBindingEvent + (getSession(), name, unbound); + } + listener.attributeReplaced(event); + } else { + if (event == null) { + event = new HttpSessionBindingEvent + (getSession(), name, value); + } + listener.attributeAdded(event); + } + } catch (Throwable t) { + manager.log.error("Listener attibuteAdded/Replaced()", t); + } + } + + } + + // -------- Implementation - interactions with SessionManager ----- + + /** + * Set the creation time for this session. This method is called by the + * Manager when an existing Session instance is reused. + */ + public void setCreationTime(long time) { + this.creationTime = time; + this.lastAccessedTime = time; + this.thisAccessedTime = time; + } + + /** + * Set the session identifier for this session and notify listeners about + * new session + * + * @param id The new session identifier + */ + public void setId(String id) { + + if ((this.id != null) && (manager != null)) + manager.remove(this); + + this.id = id; + + if (manager != null) + manager.add(this); + tellNew(); + } + + + /** + * Inform the listeners about the new session. + */ + public void tellNew() { + // Notify interested application event listeners + ServletContext context = manager.getContext(); + List listeners = manager.getEventListeners(); + if (listeners.size() > 0) { + HttpSessionEvent event = + new HttpSessionEvent(getSession()); + for (int i = 0; i < listeners.size(); i++) { + Object listenerObj = listeners.get(i); + if (!(listenerObj instanceof HttpSessionListener)) + continue; + HttpSessionListener listener = + (HttpSessionListener) listenerObj; + try { + listener.sessionCreated(event); + } catch (Throwable t) { + manager.log.error("listener.sessionCreated()", t); + } + } + } + + } + + /** + * Return the Manager within which this Session is valid. + */ + public SimpleSessionManager getManager() { + return (this.manager); + } + + + + /** + * Set the isNew flag for this session. + * + * @param isNew The new value for the isNew flag + */ + public void setNew(boolean isNew) { + this.isNew = isNew; + } + + public HttpSession getSession() { + return this; + } + + private void checkValid() { + if ( !isValid() ) { + throw new IllegalStateException("checkValid"); + } + } + + /** + * Return the isValid flag for this session. + */ + public boolean isValid() { + if (this.expiring) { + return true; + } + if (!this.isValid ) { + return false; + } + if (accessCount > 0) { + return true; + } + if (maxInactiveInterval >= 0) { + long timeNow = System.currentTimeMillis(); + int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); + if (timeIdle >= maxInactiveInterval) { + expire(true); + } + } + return this.isValid; + } + + public void setValid(boolean isValid) { + this.isValid = isValid; + } + + /** + * Update the accessed time information for this session. This method + * should be called by the context when a request comes in for a particular + * session, even if the application does not reference it. + */ + public void access() { + this.lastAccessedTime = this.thisAccessedTime; + this.thisAccessedTime = System.currentTimeMillis(); + + evaluateIfValid(); + accessCount++; + } + + + /** + * End the access. + */ + public void endAccess() { + isNew = false; + accessCount--; + } + + /** + * Perform the internal processing required to invalidate this session, + * without triggering an exception if the session has already expired. + */ + public void expire() { + expire(true); + } + + + /** + * Perform the internal processing required to invalidate this session, + * without triggering an exception if the session has already expired. + * + * @param notify Should we notify listeners about the demise of + * this session? + */ + public void expire(boolean notify) { + + // Mark this session as "being expired" if needed + if (expiring) + return; + + synchronized (this) { + + if (manager == null) + return; + + expiring = true; + + // Notify interested application event listeners + // FIXME - Assumes we call listeners in reverse order + ServletContext context = manager.getContext(); + List listeners = manager.getEventListeners(); + if (notify && (listeners.size() > 0)) { + HttpSessionEvent event = + new HttpSessionEvent(getSession()); + for (int i = 0; i < listeners.size(); i++) { + Object listenerObj = listeners.get(i); + int j = (listeners.size() - 1) - i; + if (!(listenerObj instanceof HttpSessionListener)) + continue; + HttpSessionListener listener = + (HttpSessionListener) listenerObj; + try { + listener.sessionDestroyed(event); + } catch (Throwable t) { + manager.log.error("listener.sessionDestroyed", t); + } + } + } + accessCount = 0; + isValid = false; + + /* + * Compute how long this session has been alive, and update + * session manager's related properties accordingly + */ + long timeNow = System.currentTimeMillis(); + int timeAlive = (int) ((timeNow - creationTime)/1000); + manager.addExpiredSession(timeAlive); + + // Remove this session from our manager's active sessions + manager.remove(this); + + expiring = false; + + // Unbind any objects associated with this session + String keys[] = keys(); + for (int i = 0; i < keys.length; i++) + removeAttributeInternal(keys[i], notify); + } + } + + + /** + * Release all object references, and initialize instance variables, in + * preparation for reuse of this object. + */ + public void recycle() { + + // Reset the instance variables associated with this Session + attributes.clear(); + creationTime = 0L; + expiring = false; + id = null; + lastAccessedTime = 0L; + maxInactiveInterval = -1; + accessCount = 0; + isNew = false; + isValid = false; + manager = null; + + } + + /** + * Return a string representation of this object. + */ + public String toString() { + + StringBuffer sb = new StringBuffer(); + sb.append("StandardSession["); + sb.append(id); + sb.append("]"); + return (sb.toString()); + } + + protected void evaluateIfValid() { + /* + * If this session has expired or is in the process of expiring or + * will never expire, return + */ + if (!this.isValid || expiring || maxInactiveInterval < 0) + return; + + isValid(); + + } + + /** + * Return the names of all currently defined session attributes + * as an array of Strings. If there are no defined attributes, a + * zero-length array is returned. + */ + protected String[] keys() { + return ((String[]) attributes.keySet().toArray(EMPTY_ARRAY)); + } + + + /** + * Remove the object bound with the specified name from this session. If + * the session does not have an object bound with this name, this method + * does nothing. + *

+ * After this method executes, and if the object implements + * HttpSessionBindingListener, the container calls + * valueUnbound() on the object. + * + * @param name Name of the object to remove from this session. + * @param notify Should we notify interested listeners that this + * attribute is being removed? + */ + protected void removeAttributeInternal(String name, boolean notify) { + // Remove this attribute from our collection + Object value = attributes.remove(name); + + // Do we need to do valueUnbound() and attributeRemoved() notification? + if (!notify || (value == null)) { + return; + } + + // Call the valueUnbound() method if necessary + HttpSessionBindingEvent event = null; + if (value instanceof HttpSessionBindingListener) { + event = new HttpSessionBindingEvent(getSession(), name, value); + ((HttpSessionBindingListener) value).valueUnbound(event); + } + + // Notify interested application event listeners + ServletContext context = manager.getContext(); + List listeners = manager.getEventListeners(); + if (listeners.size() == 0) + return; + for (int i = 0; i < listeners.size(); i++) { + if (!(listeners.get(i) instanceof HttpSessionAttributeListener)) + continue; + HttpSessionAttributeListener listener = + (HttpSessionAttributeListener) listeners.get(i); + try { + if (event == null) { + event = new HttpSessionBindingEvent + (getSession(), name, value); + } + listener.attributeRemoved(event); + } catch (Throwable t) { + manager.log.error("listener.attributeRemoved", t); + } + } + + } + +} + + +// ------------------------------------------------------------ Protected Class + + +/** + * This class is a dummy implementation of the HttpSessionContext + * interface, to conform to the requirement that such an object be returned + * when HttpSession.getSessionContext() is called. + * + * @author Craig R. McClanahan + * + * @deprecated As of Java Servlet API 2.1 with no replacement. The + * interface will be removed in a future version of this API. + */ + +final class StandardSessionContext implements HttpSessionContext { + + + protected HashMap dummy = new HashMap(); + + /** + * Return the session identifiers of all sessions defined + * within this context. + * + * @deprecated As of Java Servlet API 2.1 with no replacement. + * This method must return an empty Enumeration + * and will be removed in a future version of the API. + */ + public Enumeration getIds() { + + return (new Enumerator(dummy)); + + } + + + /** + * Return the HttpSession associated with the + * specified session identifier. + * + * @param id Session identifier for which to look up a session + * + * @deprecated As of Java Servlet API 2.1 with no replacement. + * This method must return null and will be removed in a + * future version of the API. + */ + public HttpSession getSession(String id) { + + return (null); + + } + + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/RandomGenerator.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/RandomGenerator.java new file mode 100644 index 000000000..a4a6921b4 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/RandomGenerator.java @@ -0,0 +1,352 @@ +package org.apache.tomcat.servlets.session; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Generates random IDs, useable as cookies. + * + * Based on code from tomcat session manager - but general purpose. + * Can use /dev/urandom or similar file. + * + * + */ +public class RandomGenerator { + protected DataInputStream randomIS=null; + protected String devRandomSource="/dev/urandom"; + + protected static Log log = LogFactory.getLog(RandomGenerator.class); + + /** + * The message digest algorithm to be used when generating session + * identifiers. This must be an algorithm supported by the + * java.security.MessageDigest class on your platform. + */ + protected String algorithm = "MD5"; + + /** + * The session id length of Sessions created by this Manager. + */ + protected int sessionIdLength = 16; + + + /** + * Return the MessageDigest implementation to be used when + * creating session identifiers. + */ + protected MessageDigest digest = null; + + public String jvmRoute; + + /** + * A String initialization parameter used to increase the entropy of + * the initialization of our random number generator. + */ + protected String entropy = null; + + /** + * A random number generator to use when generating session identifiers. + */ + protected Random random = null; + + /** + * Return the message digest algorithm for this Manager. + */ + public String getAlgorithm() { + return (this.algorithm); + } + + public void init() { + // Initialize random number generation + getRandomBytes(new byte[16]); + } + + + /** + * Set the message digest algorithm for this Manager. + * + * @param algorithm The new message digest algorithm + */ + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + /** + * Return the MessageDigest object to be used for calculating + * session identifiers. If none has been created yet, initialize + * one the first time this method is called. + */ + public synchronized MessageDigest getDigest() { + + if (this.digest == null) { + long t1=System.currentTimeMillis(); + try { + this.digest = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + log.error("Algorithm not found", e); + try { + this.digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException f) { + log.error("No message digest available", f); + this.digest = null; + } + } + long t2=System.currentTimeMillis(); + if( log.isDebugEnabled() ) + log.debug("getDigest() " + (t2-t1)); + } + + return (this.digest); + + } + + /** + * Generate and return a new session identifier. + */ + public synchronized String generateSessionId() { + + byte random[] = new byte[16]; + String result = null; + + // Render the result as a String of hexadecimal digits + StringBuffer buffer = new StringBuffer(); + int resultLenBytes = 0; + + while (resultLenBytes < this.sessionIdLength) { + getRandomBytes(random); + random = getDigest().digest(random); + for (int j = 0; + j < random.length && resultLenBytes < this.sessionIdLength; + j++) { + byte b1 = (byte) ((random[j] & 0xf0) >> 4); + byte b2 = (byte) (random[j] & 0x0f); + if (b1 < 10) + buffer.append((char) ('0' + b1)); + else + buffer.append((char) ('A' + (b1 - 10))); + if (b2 < 10) + buffer.append((char) ('0' + b2)); + else + buffer.append((char) ('A' + (b2 - 10))); + resultLenBytes++; + } + } + if (jvmRoute != null) { + buffer.append('.').append(jvmRoute); + } + result = buffer.toString(); + return (result); + + } + + protected void getRandomBytes(byte bytes[]) { + // Generate a byte array containing a session identifier + if (devRandomSource != null && randomIS == null) { + setRandomFile(devRandomSource); + } + if (randomIS != null) { + try { + int len = randomIS.read(bytes); + if (len == bytes.length) { + return; + } + if(log.isDebugEnabled()) + log.debug("Got " + len + " " + bytes.length ); + } catch (Exception ex) { + // Ignore + } + devRandomSource = null; + + try { + randomIS.close(); + } catch (Exception e) { + log.warn("Failed to close randomIS."); + } + + randomIS = null; + } + getRandom().nextBytes(bytes); + } + + /** + * Return the random number generator instance we should use for + * generating session identifiers. If there is no such generator + * currently defined, construct and seed a new one. + */ + public Random getRandom() { + if (this.random == null) { + // Calculate the new random number generator seed + long seed = System.currentTimeMillis(); + long t1 = seed; + char entropy[] = getEntropy().toCharArray(); + for (int i = 0; i < entropy.length; i++) { + long update = ((byte) entropy[i]) << ((i % 8) * 8); + seed ^= update; + } + try { + // Construct and seed a new random number generator + Class clazz = Class.forName(randomClass); + this.random = (Random) clazz.newInstance(); + this.random.setSeed(seed); + } catch (Exception e) { + // Fall back to the simple case + log.error("Failed to create random " + randomClass, e); + this.random = new java.util.Random(); + this.random.setSeed(seed); + } + if(log.isDebugEnabled()) { + long t2=System.currentTimeMillis(); + if( (t2-t1) > 100 ) + log.debug("Init random: " + " " + (t2-t1)); + } + } + + return (this.random); + + } + + /** + * Return the entropy increaser value, or compute a semi-useful value + * if this String has not yet been set. + */ + public String getEntropy() { + + // Calculate a semi-useful value if this has not been set + if (this.entropy == null) { + // Use APR to get a crypto secure entropy value + byte[] result = new byte[32]; + boolean apr = false; + try { + String methodName = "random"; + Class paramTypes[] = new Class[2]; + paramTypes[0] = result.getClass(); + paramTypes[1] = int.class; + Object paramValues[] = new Object[2]; + paramValues[0] = result; + paramValues[1] = new Integer(32); + Method method = Class.forName("org.apache.tomcat.jni.OS") + .getMethod(methodName, paramTypes); + method.invoke(null, paramValues); + apr = true; + } catch (Throwable t) { + // Ignore + } + if (apr) { + setEntropy(new String(result)); + } else { + setEntropy(this.toString()); + } + } + + return (this.entropy); + + } + + + /** + * Set the entropy increaser value. + * + * @param entropy The new entropy increaser value + */ + public void setEntropy(String entropy) { + this.entropy = entropy; + } + + + /** + * Return the random number generator class name. + */ + public String getRandomClass() { + + return (this.randomClass); + + } + + + /** + * Set the random number generator class name. + * + * @param randomClass The new random number generator class name + */ + public void setRandomClass(String randomClass) { + this.randomClass = randomClass; + } + + /** + * The Java class name of the random number generator class to be used + * when generating session identifiers. + */ + protected String randomClass = "java.security.SecureRandom"; + /** + * Use /dev/random-type special device. This is new code, but may reduce + * the big delay in generating the random. + * + * You must specify a path to a random generator file. Use /dev/urandom + * for linux ( or similar ) systems. Use /dev/random for maximum security + * ( it may block if not enough "random" exist ). You can also use + * a pipe that generates random. + * + * The code will check if the file exists, and default to java Random + * if not found. There is a significant performance difference, very + * visible on the first call to getSession ( like in the first JSP ) + * - so use it if available. + */ + public void setRandomFile( String s ) { + // as a hack, you can use a static file - and genarate the same + // session ids ( good for strange debugging ) + try{ + devRandomSource=s; + File f=new File( devRandomSource ); + if( ! f.exists() ) return; + randomIS= new DataInputStream( new FileInputStream(f)); + randomIS.readLong(); + if( log.isDebugEnabled() ) + log.debug( "Opening " + devRandomSource ); + } catch( IOException ex ) { + try { + randomIS.close(); + } catch (Exception e) { + log.warn("Failed to close randomIS."); + } + + randomIS=null; + } + } + + public String getRandomFile() { + return devRandomSource; + } + + + /** + * Gets the session id length (in bytes) of Sessions created by + * this Manager. + * + * @return The session id length + */ + public int getSessionIdLength() { + + return (this.sessionIdLength); + + } + + + /** + * Sets the session id length (in bytes) for Sessions created by this + * Manager. + * + * @param idLength The session id length + */ + public void setSessionIdLength(int idLength) { + this.sessionIdLength = idLength; + } +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/SimpleSessionManager.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/SimpleSessionManager.java new file mode 100644 index 000000000..72400ea51 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/session/SimpleSessionManager.java @@ -0,0 +1,545 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.session; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.addons.UserSessionManager; + +// TODO: move 'expiring objects' to a separate utility class +// TODO: hook the background thread + +/** + * Minimal implementation of the Manager interface that supports + * no session persistence or distributable capabilities. This class may + * be subclassed to create more sophisticated Manager implementations. + * + * @author Costin Manolache + * @author Craig R. McClanahan + */ +public class SimpleSessionManager implements UserSessionManager { + protected static Log log = LogFactory.getLog(SimpleSessionManager.class); + + protected RandomGenerator randomG = new RandomGenerator(); + + protected ServletContext context; + + + /** + * The distributable flag for Sessions created by this Manager. If this + * flag is set to true, any user attributes added to a + * session controlled by this Manager must be Serializable. + * + * This is for compliance with the spec - tomcat-lite is not intended for + * session replication ( use a full version for that ) + */ + protected boolean distributable; + + /** + * The default maximum inactive interval for Sessions created by + * this Manager. + */ + protected int maxInactiveInterval = 60; + + /** + * The longest time (in seconds) that an expired session had been alive. + */ + protected int sessionMaxAliveTime; + + + /** + * Average time (in seconds) that expired sessions had been alive. + */ + protected int sessionAverageAliveTime; + + + /** + * Number of sessions that have expired. + */ + protected int expiredSessions = 0; + + static class SessionLRU extends LinkedHashMap { + protected boolean removeEldestEntry(Map.Entry eldest) { + HttpSessionImpl s = (HttpSessionImpl)eldest.getValue(); + int size = this.size(); + + // TODO: check if eldest is expired or if we're above the limit. + // if eldest is expired, turn a flag to check for more. + + // Note: this doesn't work well for sessions that set shorter + // expiry time, or longer expiry times. + return false; + } + + } + + /** + * The set of currently active Sessions for this Manager, keyed by + * session identifier. + */ + protected LinkedHashMap sessions = new SessionLRU(); + + // Number of sessions created by this manager + protected int sessionCounter=0; + + protected int maxActive=0; + + // number of duplicated session ids - anything >0 means we have problems + protected int duplicates=0; + + protected boolean initialized=false; + + /** + * Processing time during session expiration. + */ + protected long processingTime = 0; + + static List EMPTY_LIST = new ArrayList(); + + // One per machine - it has an internal pool, can schedule + // tasks for multiple webapps. + static Timer timer = new Timer(); + boolean active = false; + + TimerTask task = new TimerTask() { + public void run() { + processExpires(); + synchronized (sessions) { + // We don't want a timer thread running around if + // there is no activity + if (sessions.size() == 0) { + active = false; + this.cancel(); + } + } + } + }; + + public List getEventListeners() { + List l = + (List) context.getAttribute("context-listeners"); + if (l == null) return EMPTY_LIST; + return l; + } + + /** + * Total sessions created by this manager. + */ + public int getSessionCounter() { + return sessionCounter; + } + + + /** + * Number of duplicated session IDs generated by the random source. + * Anything bigger than 0 means problems. + * + * @return The count of duplicates + */ + public int getDuplicates() { + return duplicates; + } + + + /** + * Returns the number of active sessions + * + * @return number of sessions active + */ + public int getActiveSessions() { + return sessions.size(); + } + + + /** + * Max number of concurrent active sessions + * + * @return The highest number of concurrent active sessions + */ + public int getMaxActive() { + return maxActive; + } + + + public void setMaxActive(int maxActive) { + this.maxActive = maxActive; + } + + + /** + * Gets the longest time (in seconds) that an expired session had been + * alive. + * + * @return Longest time (in seconds) that an expired session had been + * alive. + */ + public int getSessionMaxAliveTime() { + return sessionMaxAliveTime; + } + + /** + * Gets the average time (in seconds) that expired sessions had been + * alive. + * + * @return Average time (in seconds) that expired sessions had been + * alive. + */ + public int getSessionAverageAliveTime() { + return sessionAverageAliveTime; + } + + /** + * Return the Container with which this Manager is associated. + */ + public ServletContext getContext() { + return (this.context); + } + + + /** + * Set the Container with which this Manager is associated. + * + * @param container The newly associated Container + */ + public void setContext(ServletContext container) { + this.context = container; + } + + /** + * Return the distributable flag for the sessions supported by + * this Manager. + */ + public boolean getDistributable() { + return (this.distributable); + } + + /** + * Set the distributable flag for the sessions supported by this + * Manager. If this flag is set, all user data objects added to + * sessions associated with this manager must implement Serializable. + * + * @param distributable The new distributable flag + */ + public void setDistributable(boolean distributable) { + this.distributable = distributable; + } + + /** + * Return the default maximum inactive interval (in seconds) + * for Sessions created by this Manager. + */ + public int getSessionTimeout() { + return (this.maxInactiveInterval); + } + + public void setSessionTimeout(int stout) { + maxInactiveInterval = stout; + } + + /** + * Gets the number of sessions that have expired. + * + * @return Number of sessions that have expired + */ + public int getExpiredSessions() { + return expiredSessions; + } + + /** + * Called when a session is expired, add the time to statistics + */ + public void addExpiredSession(int timeAlive) { + synchronized (this) { + this.expiredSessions++; // should be atomic + // not sure it's the best solution + sessionAverageAliveTime = + ((sessionAverageAliveTime * (expiredSessions-1)) + + timeAlive)/expiredSessions; + if (timeAlive > sessionMaxAliveTime) { + sessionMaxAliveTime = timeAlive; + } + } + } + + public long getProcessingTime() { + return processingTime; + } + + private void enableTimer() { + if (active) { + return; + } + active = true; + timer.scheduleAtFixedRate(task, getSessionTimeout(), getSessionTimeout()); + } + + + + /** + * Invalidate all sessions that have expired. + */ + public void processExpires() { + + long timeNow = System.currentTimeMillis(); + HttpSessionImpl sessions[] = findSessions(); + int expireHere = 0 ; + + if(log.isDebugEnabled()) + log.debug("Start expire sessions " + " at " + timeNow + " sessioncount " + sessions.length); + + for (int i = 0; i < sessions.length; i++) { + if (!sessions[i].isValid()) { + expiredSessions++; + expireHere++; + } + } + + long timeEnd = System.currentTimeMillis(); + if(log.isDebugEnabled()) + log.debug("End expire sessions " + " processingTime " + + (timeEnd - timeNow) + " expired sessions: " + expireHere); + + processingTime += ( timeEnd - timeNow ); + + } + + public SimpleSessionManager() { + randomG.init(); + } + + public void destroy() { + initialized=false; + } + + /** + * Add this Session to the set of active Sessions for this Manager. + * + * @param session Session to be added + */ + public void add(HttpSessionImpl session) { + synchronized (sessions) { + sessions.put(session.getId(), session); + if( sessions.size() > maxActive ) { + maxActive=sessions.size(); + } + + // Make sure the timer is set. + enableTimer(); + } + } + + + /** + * Construct and return a new session object, based on the default + * settings specified by this Manager's properties. The session + * id specified will be used as the session id. + * If a new session cannot be created for any reason, return + * null. + * + * @param sessionId The session id which should be used to create the + * new session; if null, a new session id will be + * generated + * @exception IllegalStateException if a new session cannot be + * instantiated for any reason + */ + public HttpSessionImpl createSession(String sessionId) { + + // Recycle or create a Session instance + HttpSessionImpl session = createEmptySession(); + + // Initialize the properties of the new session and return it + session.setNew(true); + session.setValid(true); + session.setCreationTime(System.currentTimeMillis()); + session.setMaxInactiveInterval(this.maxInactiveInterval); + if (sessionId == null ) { + while (sessionId == null) { + sessionId = randomG.generateSessionId(); + if (sessions.get(sessionId) != null) { + duplicates++; + sessionId = null; + } + } + } +/* } else { + // FIXME: Code to be used in case route replacement is needed + String jvmRoute = randomG.jvmRoute; + if (jvmRoute != null) { + String requestJvmRoute = null; + int index = sessionId.indexOf("."); + if (index > 0) { + requestJvmRoute = sessionId + .substring(index + 1, sessionId.length()); + } + if (requestJvmRoute != null && !requestJvmRoute.equals(jvmRoute)) { + sessionId = sessionId.substring(0, index) + "." + jvmRoute; + } + } +*/ + session.setId(sessionId); + sessionCounter++; + return (session); + } + + + /** + * Get a session from the recycled ones or create a new empty one. + * The PersistentManager manager does not need to create session data + * because it reads it from the Store. + */ + public HttpSessionImpl createEmptySession() { + return new HttpSessionImpl(this); + } + + + /** + * Return the active Session, associated with this Manager, with the + * specified session id (if any); otherwise return null. + * + * @param id The session id for the session to be returned + * + * @exception IllegalStateException if a new session cannot be + * instantiated for any reason + * @exception IOException if an input/output error occurs while + * processing this request + */ + public HttpSessionImpl findSession(String id) throws IOException { + + if (id == null) + return (null); + synchronized (sessions) { + HttpSessionImpl session = (HttpSessionImpl) sessions.get(id); + return (session); + } + + } + + + /** + * Return the set of active Sessions associated with this Manager. + * If this Manager has no active Sessions, a zero-length array is returned. + */ + public HttpSessionImpl[] findSessions() { + + HttpSessionImpl results[] = null; + synchronized (sessions) { + results = new HttpSessionImpl[sessions.size()]; + results = (HttpSessionImpl[]) sessions.values().toArray(results); + } + return (results); + + } + + + /** + * Remove this Session from the active Sessions for this Manager. + * + * @param session Session to be removed + */ + public void remove(HttpSessionImpl session) { + + synchronized (sessions) { + sessions.remove(session.getId()); + } + + } + + // ------------------------------------------------------ Protected Methods + + /** JMX and debugging + */ + public void expireSession( String sessionId ) { + HttpSessionImpl s=(HttpSessionImpl)sessions.get(sessionId); + if( s==null ) { + return; + } + s.expire(); + } + + + /** JMX method or debugging + */ + public String getLastAccessedTime( String sessionId ) { + HttpSessionImpl s=(HttpSessionImpl)sessions.get(sessionId); + if( s==null ) { + return ""; + } + return new Date(s.getLastAccessedTime()).toString(); + } + + ThreadLocal httpSession; + + public String getSessionCookieName() { + return "JSESSIONID"; + } + + + /** Parse the cookies. Since multiple session cookies could be set ( + * different paths for example ), we need to lookup and find a valid one + * for our context. + * + * If none is found - the last (bad) session id is returned. + * + * As side effect, an attribute is set on the req with the session ( we + * already looked it up while searching ). + */ + public HttpSession getRequestedSessionId(HttpServletRequest req) { + Cookie[] cookies = req.getCookies(); + String cn = getSessionCookieName(); + for (int i=0; iEnumeration around a Java2 + * collection classes object Iterator so that existing APIs + * returning Enumerations can easily run on top of the new collections. + * Constructors are provided to easliy create such wrappers. + * + * @author Craig R. McClanahan + */ +public final class Enumerator implements Enumeration { + + + // ----------------------------------------------------------- Constructors + + + /** + * Return an Enumeration over the values of the specified Collection. + * + * @param collection Collection whose values should be enumerated + */ + public Enumerator(Collection collection) { + + this(collection.iterator()); + + } + + + /** + * Return an Enumeration over the values of the specified Collection. + * + * @param collection Collection whose values should be enumerated + * @param clone true to clone iterator + */ + public Enumerator(Collection collection, boolean clone) { + + this(collection.iterator(), clone); + + } + + + /** + * Return an Enumeration over the values returned by the + * specified Iterator. + * + * @param iterator Iterator to be wrapped + */ + public Enumerator(Iterator iterator) { + + super(); + this.iterator = iterator; + + } + + + /** + * Return an Enumeration over the values returned by the + * specified Iterator. + * + * @param iterator Iterator to be wrapped + * @param clone true to clone iterator + */ + public Enumerator(Iterator iterator, boolean clone) { + + super(); + if (!clone) { + this.iterator = iterator; + } else { + List list = new ArrayList(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + this.iterator = list.iterator(); + } + + } + + + /** + * Return an Enumeration over the values of the specified Map. + * + * @param map Map whose values should be enumerated + */ + public Enumerator(Map map) { + + this(map.values().iterator()); + + } + + + /** + * Return an Enumeration over the values of the specified Map. + * + * @param map Map whose values should be enumerated + * @param clone true to clone iterator + */ + public Enumerator(Map map, boolean clone) { + + this(map.values().iterator(), clone); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The Iterator over which the Enumeration + * represented by this class actually operates. + */ + private Iterator iterator = null; + + + // --------------------------------------------------------- Public Methods + + + /** + * Tests if this enumeration contains more elements. + * + * @return true if and only if this enumeration object + * contains at least one more element to provide, false + * otherwise + */ + public boolean hasMoreElements() { + + return (iterator.hasNext()); + + } + + + /** + * Returns the next element of this enumeration if this enumeration + * has at least one more element to provide. + * + * @return the next element of this enumeration + * + * @exception NoSuchElementException if no more elements exist + */ + public Object nextElement() throws NoSuchElementException { + + return (iterator.next()); + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/LocaleParser.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/LocaleParser.java new file mode 100644 index 000000000..b3d167cc9 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/LocaleParser.java @@ -0,0 +1,393 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.util; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.TreeMap; + + +/** + * Utility class for string parsing that is higher performance than + * StringParser for simple delimited text cases. Parsing is performed + * by setting the string, and then using the findXxxx() and + * skipXxxx() families of methods to remember significant + * offsets. To retrieve the parsed substrings, call the extract() + * method with the appropriate saved offset values. + * + * @author Craig R. McClanahan + */ +public final class LocaleParser { + + public LocaleParser() { + this(null); + } + + public LocaleParser(String string) { + setString(string); + } + + public TreeMap parseLocale(String value) { + // Store the accumulated languages that have been requested in + // a local collection, sorted by the quality value (so we can + // add Locales in descending order). The values will be ArrayLists + // containing the corresponding Locales to be added + TreeMap locales = new TreeMap(); + + // Preprocess the value to remove all whitespace + int white = value.indexOf(' '); + if (white < 0) + white = value.indexOf('\t'); + if (white >= 0) { + StringBuffer sb = new StringBuffer(); + int len = value.length(); + for (int i = 0; i < len; i++) { + char ch = value.charAt(i); + if ((ch != ' ') && (ch != '\t')) + sb.append(ch); + } + value = sb.toString(); + } + + LocaleParser parser = this; + // Process each comma-delimited language specification + parser.setString(value); // ASSERT: parser is available to us + int length = parser.getLength(); + while (true) { + + // Extract the next comma-delimited entry + int start = parser.getIndex(); + if (start >= length) + break; + int end = parser.findChar(','); + String entry = parser.extract(start, end).trim(); + parser.advance(); // For the following entry + + // Extract the quality factor for this entry + double quality = 1.0; + int semi = entry.indexOf(";q="); + if (semi >= 0) { + try { + quality = Double.parseDouble(entry.substring(semi + 3)); + } catch (NumberFormatException e) { + quality = 0.0; + } + entry = entry.substring(0, semi); + } + + // Skip entries we are not going to keep track of + if (quality < 0.00005) + continue; // Zero (or effectively zero) quality factors + if ("*".equals(entry)) + continue; // FIXME - "*" entries are not handled + + // Extract the language and country for this entry + String language = null; + String country = null; + String variant = null; + int dash = entry.indexOf('-'); + if (dash < 0) { + language = entry; + country = ""; + variant = ""; + } else { + language = entry.substring(0, dash); + country = entry.substring(dash + 1); + int vDash = country.indexOf('-'); + if (vDash > 0) { + String cTemp = country.substring(0, vDash); + variant = country.substring(vDash + 1); + country = cTemp; + } else { + variant = ""; + } + } + + // Add a new Locale to the list of Locales for this quality level + Locale locale = new Locale(language, country, variant); + Double key = new Double(-quality); // Reverse the order + ArrayList values = (ArrayList) locales.get(key); + if (values == null) { + values = new ArrayList(); + locales.put(key, values); + } + values.add(locale); + + } + + return locales; + } + + /** + * The characters of the current string, as a character array. Stored + * when the string is first specified to speed up access to characters + * being compared during parsing. + */ + private char chars[] = null; + + + /** + * The zero-relative index of the current point at which we are + * positioned within the string being parsed. NOTE: + * the value of this index can be one larger than the index of the last + * character of the string (i.e. equal to the string length) if you + * parse off the end of the string. This value is useful for extracting + * substrings that include the end of the string. + */ + private int index = 0; + + + /** + * The length of the String we are currently parsing. Stored when the + * string is first specified to avoid repeated recalculations. + */ + private int length = 0; + + + /** + * The String we are currently parsing. + */ + private String string = null; + + + // ------------------------------------------------------------- Properties + + + /** + * Return the zero-relative index of our current parsing position + * within the string being parsed. + */ + public int getIndex() { + + return (this.index); + + } + + + /** + * Return the length of the string we are parsing. + */ + public int getLength() { + + return (this.length); + + } + + + /** + * Return the String we are currently parsing. + */ + public String getString() { + + return (this.string); + + } + + + /** + * Set the String we are currently parsing. The parser state is also reset + * to begin at the start of this string. + * + * @param string The string to be parsed. + */ + public void setString(String string) { + + this.string = string; + if (string != null) { + this.length = string.length(); + chars = this.string.toCharArray(); + } else { + this.length = 0; + chars = new char[0]; + } + reset(); + + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Advance the current parsing position by one, if we are not already + * past the end of the string. + */ + public void advance() { + + if (index < length) + index++; + + } + + + /** + * Extract and return a substring that starts at the specified position, + * and extends to the end of the string being parsed. If this is not + * possible, a zero-length string is returned. + * + * @param start Starting index, zero relative, inclusive + */ + public String extract(int start) { + + if ((start < 0) || (start >= length)) + return (""); + else + return (string.substring(start)); + + } + + + /** + * Extract and return a substring that starts at the specified position, + * and ends at the character before the specified position. If this is + * not possible, a zero-length string is returned. + * + * @param start Starting index, zero relative, inclusive + * @param end Ending index, zero relative, exclusive + */ + public String extract(int start, int end) { + + if ((start < 0) || (start >= end) || (end > length)) + return (""); + else + return (string.substring(start, end)); + + } + + + /** + * Return the index of the next occurrence of the specified character, + * or the index of the character after the last position of the string + * if no more occurrences of this character are found. The current + * parsing position is updated to the returned value. + * + * @param ch Character to be found + */ + public int findChar(char ch) { + + while ((index < length) && (ch != chars[index])) + index++; + return (index); + + } + + + /** + * Return the index of the next occurrence of a non-whitespace character, + * or the index of the character after the last position of the string + * if no more non-whitespace characters are found. The current + * parsing position is updated to the returned value. + */ + public int findText() { + + while ((index < length) && isWhite(chars[index])) + index++; + return (index); + + } + + + /** + * Return the index of the next occurrence of a whitespace character, + * or the index of the character after the last position of the string + * if no more whitespace characters are found. The current parsing + * position is updated to the returned value. + */ + public int findWhite() { + + while ((index < length) && !isWhite(chars[index])) + index++; + return (index); + + } + + + /** + * Reset the current state of the parser to the beginning of the + * current string being parsed. + */ + public void reset() { + + index = 0; + + } + + + /** + * Advance the current parsing position while it is pointing at the + * specified character, or until it moves past the end of the string. + * Return the final value. + * + * @param ch Character to be skipped + */ + public int skipChar(char ch) { + + while ((index < length) && (ch == chars[index])) + index++; + return (index); + + } + + + /** + * Advance the current parsing position while it is pointing at a + * non-whitespace character, or until it moves past the end of the string. + * Return the final value. + */ + public int skipText() { + + while ((index < length) && !isWhite(chars[index])) + index++; + return (index); + + } + + + /** + * Advance the current parsing position while it is pointing at a + * whitespace character, or until it moves past the end of the string. + * Return the final value. + */ + public int skipWhite() { + + while ((index < length) && isWhite(chars[index])) + index++; + return (index); + + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Is the specified character considered to be whitespace? + * + * @param ch Character to be checked + */ + protected boolean isWhite(char ch) { + + if ((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n')) + return (true); + else + return (false); + + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/Range.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/Range.java new file mode 100644 index 000000000..4cb02aec5 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/Range.java @@ -0,0 +1,146 @@ +/** + * + */ +package org.apache.tomcat.servlets.util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.StringTokenizer; + +/** + * Utils to process HTTP/1.1 ranges. Used by default servlet, could + * be used by any servlet that needs to deal with ranges. + * + * It is very good to support ranges if you have large content. In most + * cases supporting one range is enough - getting multiple ranges doesn't + * seem very common, and it's complex (multipart response). + * + * @author Costin Manolache + * @author Remy Maucherat + * @author - see DefaultServlet in Catalin for other contributors + */ +public class Range { + + public long start; + public long end; + public long length; + + /** + * Validate range. + */ + public boolean validate() { + if (end >= length) + end = length - 1; + return ( (start >= 0) && (end >= 0) && (start <= end) + && (length > 0) ); + } + + public void recycle() { + start = 0; + end = 0; + length = 0; + } + + /** Parse ranges. + * + * @return null if the range is invalid or can't be parsed + */ + public static ArrayList parseRanges(long fileLength, + String rangeHeader) throws IOException { + ArrayList result = new ArrayList(); + StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ","); + + // Parsing the range list + while (commaTokenizer.hasMoreTokens()) { + String rangeDefinition = commaTokenizer.nextToken().trim(); + + Range currentRange = new Range(); + currentRange.length = fileLength; + + int dashPos = rangeDefinition.indexOf('-'); + + if (dashPos == -1) { + return null; + } + + if (dashPos == 0) { + try { + long offset = Long.parseLong(rangeDefinition); + currentRange.start = fileLength + offset; + currentRange.end = fileLength - 1; + } catch (NumberFormatException e) { + return null; + } + } else { + + try { + currentRange.start = Long.parseLong + (rangeDefinition.substring(0, dashPos)); + if (dashPos < rangeDefinition.length() - 1) + currentRange.end = Long.parseLong + (rangeDefinition.substring + (dashPos + 1, rangeDefinition.length())); + else + currentRange.end = fileLength - 1; + } catch (NumberFormatException e) { + return null; + } + + } + if (!currentRange.validate()) { + return null; + } + result.add(currentRange); + } + return result; + } + + + /** + * Parse the Content-Range header. Used with PUT or in response. + * + * @return Range + */ + public static Range parseContentRange(String rangeHeader) + throws IOException { + if (rangeHeader == null) + return null; + + // bytes is the only range unit supported + if (!rangeHeader.startsWith("bytes")) { + return null; + } + + rangeHeader = rangeHeader.substring(6).trim(); + + int dashPos = rangeHeader.indexOf('-'); + int slashPos = rangeHeader.indexOf('/'); + + if (dashPos == -1) { + return null; + } + + if (slashPos == -1) { + return null; + } + + Range range = new Range(); + + try { + range.start = Long.parseLong(rangeHeader.substring(0, dashPos)); + range.end = + Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos)); + range.length = Long.parseLong + (rangeHeader.substring(slashPos + 1, rangeHeader.length())); + } catch (NumberFormatException e) { + return null; + } + + if (!range.validate()) { + return null; + } + + return range; + } + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/RequestUtil.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/RequestUtil.java new file mode 100644 index 000000000..eb9a54327 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/RequestUtil.java @@ -0,0 +1,509 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.servlets.util; + +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Map; +import java.util.TimeZone; + +import javax.servlet.http.Cookie; + + +/** + * General purpose request parsing and encoding utility methods. + * + * @author Craig R. McClanahan + * @author Tim Tye + * @version $Revision: 302905 $ $Date: 2004-05-26 18:41:54 +0200 (mer., 26 mai 2004) $ + */ + +public final class RequestUtil { + + + /** + * The DateFormat to use for generating readable dates in cookies. + */ + private static SimpleDateFormat format = + new SimpleDateFormat(" EEEE, dd-MMM-yy kk:mm:ss zz"); + + static { + format.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + + /** + * Encode a cookie as per RFC 2109. The resulting string can be used + * as the value for a Set-Cookie header. + * + * @param cookie The cookie to encode. + * @return A string following RFC 2109. + */ + public static String encodeCookie(Cookie cookie) { + + StringBuffer buf = new StringBuffer( cookie.getName() ); + buf.append("="); + buf.append(cookie.getValue()); + + if (cookie.getComment() != null) { + buf.append("; Comment=\""); + buf.append(cookie.getComment()); + buf.append("\""); + } + + if (cookie.getDomain() != null) { + buf.append("; Domain=\""); + buf.append(cookie.getDomain()); + buf.append("\""); + } + + long age = cookie.getMaxAge(); + if (cookie.getMaxAge() >= 0) { + buf.append("; Max-Age=\""); + buf.append(cookie.getMaxAge()); + buf.append("\""); + } + + if (cookie.getPath() != null) { + buf.append("; Path=\""); + buf.append(cookie.getPath()); + buf.append("\""); + } + + if (cookie.getSecure()) { + buf.append("; Secure"); + } + + if (cookie.getVersion() > 0) { + buf.append("; Version=\""); + buf.append(cookie.getVersion()); + buf.append("\""); + } + + return (buf.toString()); + } + + + /** + * 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 + */ + public static String filter(String message) { + + if (message == null) + return (null); + + 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()); + + } + + + /** + * Normalize a relative URI path that may have relative values ("/./", + * "/../", and so on ) it it. WARNING - This method is + * useful only for normalizing application-generated paths. It does not + * try to perform security checks for malicious input. + * + * @param path Relative path to be normalized + */ + public static String normalize(String path) { + + if (path == null) + return null; + + // Create a place for the normalized path + String normalized = path; + + if (normalized.equals("/.")) + return "/"; + + // Add a leading "/" if necessary + if (!normalized.startsWith("/")) + normalized = "/" + normalized; + + // Resolve occurrences of "//" in the normalized path + while (true) { + int index = normalized.indexOf("//"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + while (true) { + int index = normalized.indexOf("/./"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + while (true) { + int index = normalized.indexOf("/../"); + if (index < 0) + break; + if (index == 0) + return (null); // Trying to go outside our context + int index2 = normalized.lastIndexOf('/', index - 1); + normalized = normalized.substring(0, index2) + + normalized.substring(index + 3); + } + + // Return the normalized path that we have completed + return (normalized); + + } + + + /** + * Parse the character encoding from the specified content type header. + * If the content type is null, or there is no explicit character encoding, + * null is returned. + * + * @param contentType a content type header + */ + public static String parseCharacterEncoding(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()); + + } + + + /** + * Parse a cookie header into an array of cookies according to RFC 2109. + * + * @param header Value of an HTTP "Cookie" header + */ + public static Cookie[] parseCookieHeader(String header) { + + if ((header == null) || (header.length() < 1)) + return (new Cookie[0]); + + ArrayList cookies = new ArrayList(); + while (header.length() > 0) { + int semicolon = header.indexOf(';'); + if (semicolon < 0) + semicolon = header.length(); + if (semicolon == 0) + break; + String token = header.substring(0, semicolon); + if (semicolon < header.length()) + header = header.substring(semicolon + 1); + else + header = ""; + try { + int equals = token.indexOf('='); + if (equals > 0) { + String name = token.substring(0, equals).trim(); + String value = token.substring(equals+1).trim(); + cookies.add(new Cookie(name, value)); + } + } catch (Throwable e) { + ; + } + } + + return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()])); + + } + + + /** + * Append request parameters from the specified String to the specified + * Map. It is presumed that the specified Map is not accessed from any + * other thread, so no synchronization is performed. + *

+ * IMPLEMENTATION NOTE: URL decoding is performed + * individually on the parsed name and value elements, rather than on + * the entire query string ahead of time, to properly deal with the case + * where the name or value includes an encoded "=" or "&" character + * that would otherwise be interpreted as a delimiter. + * + * @param map Map that accumulates the resulting parameters + * @param data Input string containing request parameters + * + * @exception IllegalArgumentException if the data is malformed + */ + public static void parseParameters(Map map, String data, String encoding) + throws UnsupportedEncodingException { + + if ((data != null) && (data.length() > 0)) { + + // use the specified encoding to extract bytes out of the + // given string so that the encoding is not lost. If an + // encoding is not specified, let it use platform default + byte[] bytes = null; + try { + if (encoding == null) { + bytes = data.getBytes(); + } else { + bytes = data.getBytes(encoding); + } + } catch (UnsupportedEncodingException uee) { + } + + parseParameters(map, bytes, encoding); + } + + } + + + /** + * Decode and return the specified URL-encoded String. + * When the byte array is converted to a string, the system default + * character encoding is used... This may be different than some other + * servers. + * + * @param str The url-encoded string + * + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(String str) { + + return URLDecode(str, null); + + } + + + /** + * Decode and return the specified URL-encoded String. + * + * @param str The url-encoded string + * @param enc The encoding to use; if null, the default encoding is used + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(String str, String enc) { + + if (str == null) + return (null); + + // use the specified encoding to extract bytes out of the + // given string so that the encoding is not lost. If an + // encoding is not specified, let it use platform default + byte[] bytes = null; + try { + if (enc == null) { + bytes = str.getBytes(); + } else { + bytes = str.getBytes(enc); + } + } catch (UnsupportedEncodingException uee) {} + + return URLDecode(bytes, enc); + + } + + + /** + * Decode and return the specified URL-encoded byte array. + * + * @param bytes The url-encoded byte array + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(byte[] bytes) { + return URLDecode(bytes, null); + } + + + /** + * Decode and return the specified URL-encoded byte array. + * + * @param bytes The url-encoded byte array + * @param enc The encoding to use; if null, the default encoding is used + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(byte[] bytes, String enc) { + + if (bytes == null) + return (null); + + int len = bytes.length; + int ix = 0; + int ox = 0; + while (ix < len) { + byte b = bytes[ix++]; // Get byte to test + if (b == '+') { + b = (byte)' '; + } else if (b == '%') { + b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + + convertHexDigit(bytes[ix++])); + } + bytes[ox++] = b; + } + if (enc != null) { + try { + return new String(bytes, 0, ox, enc); + } catch (Exception e) { + e.printStackTrace(); + } + } + return new String(bytes, 0, ox); + + } + + + /** + * Convert a byte character value to hexidecimal digit value. + * + * @param b the character value byte + */ + private static byte convertHexDigit( byte b ) { + if ((b >= '0') && (b <= '9')) return (byte)(b - '0'); + if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10); + if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10); + return 0; + } + + + /** + * Put name and value pair in map. When name already exist, add value + * to array of values. + * + * @param map The map to populate + * @param name The parameter name + * @param value The parameter value + */ + private static void putMapEntry( Map map, String name, String value) { + String[] newValues = null; + String[] oldValues = (String[]) map.get(name); + if (oldValues == null) { + newValues = new String[1]; + newValues[0] = value; + } else { + newValues = new String[oldValues.length + 1]; + System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); + newValues[oldValues.length] = value; + } + map.put(name, newValues); + } + + + /** + * Append request parameters from the specified String to the specified + * Map. It is presumed that the specified Map is not accessed from any + * other thread, so no synchronization is performed. + *

+ * IMPLEMENTATION NOTE: URL decoding is performed + * individually on the parsed name and value elements, rather than on + * the entire query string ahead of time, to properly deal with the case + * where the name or value includes an encoded "=" or "&" character + * that would otherwise be interpreted as a delimiter. + * + * NOTE: byte array data is modified by this method. Caller beware. + * + * @param map Map that accumulates the resulting parameters + * @param data Input string containing request parameters + * @param encoding Encoding to use for converting hex + * + * @exception UnsupportedEncodingException if the data is malformed + */ + public static void parseParameters(Map map, byte[] data, String encoding) + throws UnsupportedEncodingException { + + if (data != null && data.length > 0) { + int pos = 0; + int ix = 0; + int ox = 0; + String key = null; + String value = null; + while (ix < data.length) { + byte c = data[ix++]; + switch ((char) c) { + case '&': + value = new String(data, 0, ox, encoding); + if (key != null) { + putMapEntry(map, key, value); + key = null; + } + ox = 0; + break; + case '=': + if (key == null) { + key = new String(data, 0, ox, encoding); + ox = 0; + } else { + data[ox++] = c; + } + break; + case '+': + data[ox++] = (byte)' '; + break; + case '%': + data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4) + + convertHexDigit(data[ix++])); + break; + default: + data[ox++] = c; + } + } + //The last value does not end in '&'. So save it now. + if (key != null) { + value = new String(data, 0, ox, encoding); + putMapEntry(map, key, value); + } + } + + } + + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/UrlUtils.java b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/UrlUtils.java new file mode 100644 index 000000000..3049de453 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/servlets/util/UrlUtils.java @@ -0,0 +1,67 @@ +package org.apache.tomcat.servlets.util; + +public class UrlUtils { + + /** Used by webdav. + * + * Return a context-relative path, beginning with a "/", that represents + * the canonical version of the specified path after ".." and "." elements + * are resolved out. If the specified path attempts to go outside the + * boundaries of the current context (i.e. too many ".." path elements + * are present), return null instead. + * + * @param path Path to be normalized + */ + public static String normalize(String path) { + + if (path == null) + return null; + + // Create a place for the normalized path + String normalized = path; + + if (normalized.equals("/.")) + return "/"; + + // Normalize the slashes and add leading slash if necessary + if (normalized.indexOf('\\') >= 0) + normalized = normalized.replace('\\', '/'); + + if (!normalized.startsWith("/")) + normalized = "/" + normalized; + + // Resolve occurrences of "//" in the normalized path + while (true) { + int index = normalized.indexOf("//"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + while (true) { + int index = normalized.indexOf("/./"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + while (true) { + int index = normalized.indexOf("/../"); + if (index < 0) + break; + if (index == 0) + return (null); // Trying to go outside our context + int index2 = normalized.lastIndexOf('/', index - 1); + normalized = normalized.substring(0, index2) + + normalized.substring(index + 3); + } + + // Return the normalized path that we have completed + return (normalized); + } + +}