diff options
author | Max Romanov <max.romanov@gmail.com> | 2019-02-28 18:02:42 +0300 |
---|---|---|
committer | Max Romanov <max.romanov@gmail.com> | 2019-02-28 18:02:42 +0300 |
commit | 5bfdebb9e4161a689113d73775498949a09d7fb5 (patch) | |
tree | fcb69169e3da983db0eb1f48d2dccc2ac2ff867b /src/java/nginx | |
parent | ec7319d32c9c41597a99a9422ff324c97a92bb21 (diff) | |
download | unit-5bfdebb9e4161a689113d73775498949a09d7fb5.tar.gz unit-5bfdebb9e4161a689113d73775498949a09d7fb5.tar.bz2 |
Introducing Java Servlet Container beta.
Diffstat (limited to '')
19 files changed, 6721 insertions, 0 deletions
diff --git a/src/java/nginx/unit/Context.java b/src/java/nginx/unit/Context.java new file mode 100644 index 00000000..643a336b --- /dev/null +++ b/src/java/nginx/unit/Context.java @@ -0,0 +1,3502 @@ +package nginx.unit; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +import java.lang.ClassLoader; +import java.lang.ClassNotFoundException; +import java.lang.IllegalArgumentException; +import java.lang.IllegalStateException; +import java.lang.reflect.Constructor; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; + +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.zip.ZipEntry; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.FilterRegistration.Dynamic; +import javax.servlet.FilterRegistration; +import javax.servlet.MultipartConfigElement; +import javax.servlet.Registration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContainerInitializer; +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.ServletOutputStream; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletResponse; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.ServletSecurityElement; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.annotation.HandlesTypes; +import javax.servlet.annotation.WebInitParam; +import javax.servlet.annotation.WebServlet; +import javax.servlet.annotation.WebFilter; +import javax.servlet.annotation.WebListener; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.descriptor.JspPropertyGroupDescriptor; +import javax.servlet.descriptor.TaglibDescriptor; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.jetty.http.MimeTypes; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import org.apache.jasper.servlet.JspServlet; +import org.apache.jasper.servlet.JasperInitializer; + + +public class Context implements ServletContext, InitParams +{ + public final static int SERVLET_MAJOR_VERSION = 3; + public final static int SERVLET_MINOR_VERSION = 1; + + private String context_path_ = ""; + private String server_info_ = "unit"; + private String app_version_ = ""; + private MimeTypes mime_types_; + private boolean metadata_complete_ = false; + private boolean welcome_files_list_found_ = false; + private boolean ctx_initialized_ = false; + + private ClassLoader loader_; + private File webapp_; + private File extracted_dir_; + private File temp_dir_; + + private final Map<String, String> init_params_ = new HashMap<>(); + private final Map<String, Object> attributes_ = new HashMap<>(); + + private final Map<String, URLPattern> parsed_patterns_ = new HashMap<>(); + + private final List<FilterReg> filters_ = new ArrayList<>(); + private final Map<String, FilterReg> name2filter_ = new HashMap<>(); + private final List<FilterMap> filter_maps_ = new ArrayList<>(); + + private final List<ServletReg> servlets_ = new ArrayList<>(); + private final Map<String, ServletReg> name2servlet_ = new HashMap<>(); + private final Map<String, ServletReg> pattern2servlet_ = new HashMap<>(); + private final Map<String, ServletReg> exact2servlet_ = new HashMap<>(); + private final List<PrefixPattern> prefix_patterns_ = new ArrayList<>(); + private final Map<String, ServletReg> suffix2servlet_ = new HashMap<>(); + private ServletReg default_servlet_; + private ServletReg system_default_servlet_; + + private final List<String> welcome_files_ = new ArrayList<>(); + + private final Map<String, String> exception2location_ = new HashMap<>(); + private final Map<Integer, String> error2location_ = new HashMap<>(); + + public static final Class<?>[] LISTENER_TYPES = new Class[] { + ServletContextListener.class, + ServletContextAttributeListener.class, + ServletRequestListener.class, + ServletRequestAttributeListener.class, + HttpSessionAttributeListener.class, + HttpSessionIdListener.class, + HttpSessionListener.class + }; + + private final List<String> pending_listener_classnames_ = new ArrayList<>(); + private final Set<String> listener_classnames_ = new HashSet<>(); + + private final List<ServletContextListener> ctx_listeners_ = new ArrayList<>(); + private final List<ServletContextListener> destroy_listeners_ = new ArrayList<>(); + private final List<ServletContextAttributeListener> ctx_attr_listeners_ = new ArrayList<>(); + private final List<ServletRequestListener> req_init_listeners_ = new ArrayList<>(); + private final List<ServletRequestListener> req_destroy_listeners_ = new ArrayList<>(); + private final List<ServletRequestAttributeListener> req_attr_listeners_ = new ArrayList<>(); + + private ServletRequestAttributeListener req_attr_proxy_ = null; + + private final List<HttpSessionAttributeListener> sess_attr_listeners_ = new ArrayList<>(); + private final List<HttpSessionIdListener> sess_id_listeners_ = new ArrayList<>(); + private final List<HttpSessionListener> sess_listeners_ = new ArrayList<>(); + + private HttpSessionAttributeListener sess_attr_proxy_ = null; + + private final SessionCookieConfig session_cookie_config_ = new UnitSessionCookieConfig(); + private final Set<SessionTrackingMode> default_session_tracking_modes_ = new HashSet<>(); + private Set<SessionTrackingMode> session_tracking_modes_ = default_session_tracking_modes_; + private int session_timeout_ = 60; + + private final Map<String, Session> sessions_ = new HashMap<>(); + + private static final String WEB_INF = "WEB-INF/"; + private static final String WEB_INF_CLASSES = WEB_INF + "classes/"; + private static final String WEB_INF_LIB = WEB_INF + "lib/"; + + private class PrefixPattern implements Comparable<PrefixPattern> + { + public final String pattern; + public final ServletReg servlet; + + public PrefixPattern(String p, ServletReg s) + { + pattern = p; + servlet = s; + } + + public boolean match(String url) + { + return url.startsWith(pattern) && ( + url.length() == pattern.length() + || url.charAt(pattern.length()) == '/'); + } + + @Override + public int compareTo(PrefixPattern p) + { + return p.pattern.length() - pattern.length(); + } + } + + private class StaticServlet extends HttpServlet + { + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + doGet(request, response); + } + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + String path = null; + + if (request.getDispatcherType() == DispatcherType.INCLUDE) { + path = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + } + + if (path == null) { + path = request.getServletPath(); + } + + /* + 10.6 Web Application Archive File + ... + This directory [META-INF] must not be directly served as + content by the container in response to a Web client's request, + though its contents are visible to servlet code via the + getResource and getResourceAsStream calls on the + ServletContext. Also, any requests to access the resources in + META-INF directory must be returned with a SC_NOT_FOUND(404) + response. + */ + if (request.getDispatcherType() == DispatcherType.REQUEST + && (path.equals("/WEB-INF") || path.startsWith("/WEB-INF/") + || path.equals("/META-INF") || path.startsWith("/META-INF/"))) + { + response.sendError(response.SC_NOT_FOUND); + return; + } + + if (path.startsWith("/")) { + path = path.substring(1); + } + + File f = new File(webapp_, path); + if (!f.exists()) { + if (request.getDispatcherType() == DispatcherType.INCLUDE) { + /* + 9.3 The Include Method + ... + If the default servlet is the target of a + RequestDispatch.include() and the requested resource + does not exist, then the default servlet MUST throw + FileNotFoundException. + */ + + throw new FileNotFoundException(); + } + + response.sendError(response.SC_NOT_FOUND); + return; + } + + long ims = request.getDateHeader("If-Modified-Since"); + long lm = f.lastModified(); + + if (lm < ims) { + response.sendError(response.SC_NOT_MODIFIED); + return; + } + + response.setDateHeader("Last-Modified", f.lastModified()); + + if (f.isDirectory()) { + String url = request.getRequestURL().toString(); + if (!url.endsWith("/")) { + response.setHeader("Location", url + "/"); + response.sendError(response.SC_FOUND); + return; + } + + String[] ls = f.list(); + + PrintWriter writer = response.getWriter(); + + for (String n : ls) { + writer.println("<a href=\"" + n + "\">" + n + "</a><br>"); + } + + writer.close(); + + } else { + response.setContentLengthLong(f.length()); + + InputStream is = new FileInputStream(f); + byte[] buffer = new byte[response.getBufferSize()]; + ServletOutputStream os = response.getOutputStream(); + while (true) { + int read = is.read(buffer); + if (read == -1) { + break; + } + os.write(buffer, 0, read); + } + + os.close(); + } + } + } + + public static Context start(String webapp, URL[] classpaths) + throws Exception + { + Context ctx = new Context(); + + ctx.loadApp(webapp, classpaths); + ctx.initialized(); + + return ctx; + } + + public Context() + { + default_session_tracking_modes_.add(SessionTrackingMode.COOKIE); + + context_path_ = System.getProperty("nginx.unit.context.path", "").trim(); + + if (context_path_.endsWith("/")) { + context_path_ = context_path_.substring(0, context_path_.length() - 1); + } + + if (!context_path_.isEmpty() && !context_path_.startsWith("/")) { + context_path_ = "/" + context_path_; + } + + if (context_path_.isEmpty()) { + session_cookie_config_.setPath("/"); + } else { + session_cookie_config_.setPath(context_path_); + } + } + + public void loadApp(String webapp, URL[] classpaths) + throws Exception + { + File root = new File(webapp); + if (!root.exists()) { + throw new FileNotFoundException( + "Unable to determine code source archive from " + root); + } + + ArrayList<URL> url_list = new ArrayList<>(); + + for (URL u : classpaths) { + url_list.add(u); + } + + if (!root.isDirectory()) { + root = extractWar(root); + extracted_dir_ = root; + } + + webapp_ = root; + + Path tmpDir = Files.createTempDirectory("webapp"); + temp_dir_ = tmpDir.toFile(); + setAttribute(ServletContext.TEMPDIR, temp_dir_); + + File web_inf_classes = new File(root, WEB_INF_CLASSES); + if (web_inf_classes.exists() && web_inf_classes.isDirectory()) { + url_list.add(new URL("file:" + root.getAbsolutePath() + "/" + WEB_INF_CLASSES)); + } + + File lib = new File(root, WEB_INF_LIB); + File[] libs = lib.listFiles(); + + if (libs != null) { + for (File l : libs) { + url_list.add(new URL("file:" + l.getAbsolutePath())); + } + } + + URL[] urls = new URL[url_list.size()]; + + for (int i = 0; i < url_list.size(); i++) { + urls[i] = url_list.get(i); + trace("archives: " + urls[i]); + } + + String custom_listener = System.getProperty("nginx.unit.context.listener", "").trim(); + if (!custom_listener.isEmpty()) { + pending_listener_classnames_.add(custom_listener); + } + + processWebXml(root); + + loader_ = new AppClassLoader(urls, + Context.class.getClassLoader().getParent()); + + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader_); + + try { + for (String listener_classname : pending_listener_classnames_) { + addListener(listener_classname); + } + + ScanResult scan_res = null; + + if (!metadata_complete_) { + ClassGraph classgraph = new ClassGraph() + //.verbose() + .overrideClassLoaders(loader_) + .ignoreParentClassLoaders() + .enableClassInfo() + .enableAnnotationInfo() + //.enableSystemPackages() + .whitelistModules("javax.*") + //.enableAllInfo() + ; + + String verbose = System.getProperty("nginx.unit.context.classgraph.verbose", "").trim(); + + if (verbose.equals("true")) { + classgraph.verbose(); + } + + scan_res = classgraph.scan(); + + loadInitializers(scan_res); + } + + if (!metadata_complete_) { + scanClasses(scan_res); + } + + /* + 8.1.6 Other annotations / conventions + ... + By default all applications will have index.htm(l) and index.jsp + in the list of welcome-file-list. The descriptor may to be used + to override these default settings. + */ + if (!welcome_files_list_found_) { + welcome_files_.add("index.htm"); + welcome_files_.add("index.html"); + welcome_files_.add("index.jsp"); + } + + ServletReg jsp_servlet = name2servlet_.get("jsp"); + if (jsp_servlet == null) { + jsp_servlet = new ServletReg("jsp", JspServlet.class); + jsp_servlet.system_jsp_servlet_ = true; + servlets_.add(jsp_servlet); + name2servlet_.put("jsp", jsp_servlet); + } + + if (jsp_servlet.getClassName() == null) { + jsp_servlet.setClass(JspServlet.class); + jsp_servlet.system_jsp_servlet_ = true; + } + + if (jsp_servlet.patterns_.isEmpty()) { + parseURLPattern("*.jsp", jsp_servlet); + parseURLPattern("*.jspx", jsp_servlet); + } + + ServletReg def_servlet = name2servlet_.get("default"); + if (def_servlet == null) { + def_servlet = new ServletReg("default", new StaticServlet()); + def_servlet.servlet_ = new StaticServlet(); + servlets_.add(def_servlet); + name2servlet_.put("default", def_servlet); + } + + if (def_servlet.getClassName() == null) { + def_servlet.setClass(StaticServlet.class); + def_servlet.servlet_ = new StaticServlet(); + } + + system_default_servlet_ = def_servlet; + + for (PrefixPattern p : prefix_patterns_) { + /* + Optimization: add prefix patterns to exact2servlet_ map. + This should not affect matching result because full path + is the longest matched prefix. + */ + if (!exact2servlet_.containsKey(p.pattern)) { + trace("adding prefix pattern " + p.pattern + " to exact patterns map"); + exact2servlet_.put(p.pattern, p.servlet); + } + } + + Collections.sort(prefix_patterns_); + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + + private static class AppClassLoader extends URLClassLoader + { + static { + ClassLoader.registerAsParallelCapable(); + } + + private final static String[] system_prefix = + { + "java/", // Java SE classes (per servlet spec v2.5 / SRV.9.7.2) + "javax/", // Java SE classes (per servlet spec v2.5 / SRV.9.7.2) + "org/w3c/", // needed by javax.xml + "org/xml/", // needed by javax.xml + }; + + private ClassLoader system_loader; + + public AppClassLoader(URL[] urls, ClassLoader parent) + { + super(urls, parent); + + ClassLoader j = String.class.getClassLoader(); + if (j == null) { + j = getSystemClassLoader(); + while (j.getParent() != null) { + j = j.getParent(); + } + } + system_loader = j; + } + + private boolean isSystemPath(String path) + { + int i = Arrays.binarySearch(system_prefix, path); + + if (i >= 0) { + return true; + } + + i = -i - 1; + + if (i > 0) { + return path.startsWith(system_prefix[i - 1]); + } + + return false; + } + + @Override + public URL getResource(String name) + { + URL res; + + String s = "getResource: " + name; + trace(0, s, s.length()); + + /* + This is a required for compatibility with Tomcat which + stores all resources prefixed with '/' and application code + may try to get resource with leading '/' (like Jira). Jetty + also has such workaround in WebAppClassLoader.getResource(). + */ + if (name.startsWith("/")) { + name = name.substring(1); + } + + if (isSystemPath(name)) { + return super.getResource(name); + } + + res = system_loader.getResource(name); + if (res != null) { + return res; + } + + res = findResource(name); + if (res != null) { + return res; + } + + return super.getResource(name); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + synchronized (this) { + Class<?> res = findLoadedClass(name); + if (res != null) { + return res; + } + + try { + res = system_loader.loadClass(name); + + if (resolve) { + resolveClass(res); + } + + return res; + } catch (ClassNotFoundException e) { + } + + String path = name.replace('.', '/').concat(".class"); + + if (isSystemPath(path)) { + return super.loadClass(name, resolve); + } + + URL url = findResource(path); + + if (url != null) { + res = super.findClass(name); + + if (resolve) { + resolveClass(res); + } + + return res; + } + + return super.loadClass(name, resolve); + } + + } + } + + private File extractWar(File war) throws IOException + { + Path tmpDir = Files.createTempDirectory("webapp"); + + JarFile jf = new JarFile(war); + + for (Enumeration<JarEntry> en = jf.entries(); en.hasMoreElements();) { + JarEntry e = en.nextElement(); + long mod_time = e.getTime(); + Path ep = tmpDir.resolve(e.getName()); + Path p; + if (e.isDirectory()) { + p = ep; + } else { + p = ep.getParent(); + } + + if (!p.toFile().isDirectory()) { + Files.createDirectories(p); + } + + if (!e.isDirectory()) { + Files.copy(jf.getInputStream(e), ep, + StandardCopyOption.REPLACE_EXISTING); + } + + if (mod_time > 0) { + ep.toFile().setLastModified(mod_time); + } + } + + return tmpDir.toFile(); + } + + private class CtxFilterChain implements FilterChain + { + private int filter_index_ = 0; + private final ServletReg servlet_; + private final List<FilterReg> filters_; + + CtxFilterChain(ServletReg servlet, String path, DispatcherType dtype) + { + servlet_ = servlet; + + List<FilterReg> filters = new ArrayList<>(); + + for (FilterMap m : filter_maps_) { + if (filters.indexOf(m.filter_) != -1) { + continue; + } + + if (!m.dtypes_.contains(dtype)) { + continue; + } + + if (m.pattern_.match(path)) { + filters.add(m.filter_); + + trace("add filter (matched): " + m.filter_.getName()); + } + } + + for (FilterMap m : servlet.filters_) { + if (filters.indexOf(m.filter_) != -1) { + continue; + } + + if (!m.dtypes_.contains(dtype)) { + continue; + } + + filters.add(m.filter_); + + trace("add filter (servlet): " + m.filter_.getName()); + } + + filters_ = filters; + } + + @Override + public void doFilter (ServletRequest request, ServletResponse response) + throws IOException, ServletException + { + if (filter_index_ < filters_.size()) { + filters_.get(filter_index_++).filter_.doFilter(request, response, this); + + return; + } + + servlet_.service(request, response); + } + } + + private ServletReg findServlet(String path, DynamicPathRequest req) + { + /* + 12.1 Use of URL Paths + ... + 1. The container will try to find an exact match of the path of the + request to the path of the servlet. A successful match selects + the servlet. + */ + ServletReg servlet = exact2servlet_.get(path); + if (servlet != null) { + trace("findServlet: '" + path + "' exact matched pattern"); + req.setServletPath(path, null); + return servlet; + } + + /* + 2. The container will recursively try to match the longest + path-prefix. This is done by stepping down the path tree a + directory at a time, using the '/' character as a path separator. + The longest match determines the servlet selected. + */ + for (PrefixPattern p : prefix_patterns_) { + if (p.match(path)) { + trace("findServlet: '" + path + "' matched prefix pattern '" + p.pattern + "'"); + if (p.pattern.length() == path.length()) { + log("findServlet: WARNING: it is expected '" + path + "' exactly matches " + p.pattern); + req.setServletPath(path, p.pattern, null); + } else { + req.setServletPath(path, p.pattern, path.substring(p.pattern.length())); + } + return p.servlet; + } + } + + /* + 3. If the last segment in the URL path contains an extension + (e.g. .jsp), the servlet container will try to match a servlet + that handles requests for the extension. An extension is defined + as the part of the last segment after the last '.' character. + */ + int suffix_start = path.lastIndexOf('.'); + if (suffix_start != -1) { + String suffix = path.substring(suffix_start); + servlet = suffix2servlet_.get(suffix); + if (servlet != null) { + trace("findServlet: '" + path + "' matched suffix pattern"); + req.setServletPath(path, null); + return servlet; + } + } + + /* + 4. If neither of the previous three rules result in a servlet match, + the container will attempt to serve content appropriate for the + resource requested. If a "default" servlet is defined for the + application, it will be used. ... + */ + if (default_servlet_ != null) { + trace("findServlet: '" + path + "' matched default servlet"); + req.setServletPath(path, null); + return default_servlet_; + } + + trace("findServlet: '" + path + "' no servlet found"); + + /* + 10.10 Welcome Files + ... + If a Web container receives a valid partial request, the Web + container must examine the welcome file list defined in the + deployment descriptor. + ... + */ + if (path.endsWith("/")) { + + /* + The Web server must append each welcome file in the order + specified in the deployment descriptor to the partial request + and check whether a static resource in the WAR is mapped to + that request URI. + */ + for (String wf : welcome_files_) { + String wpath = path + wf; + + File f = new File(webapp_, wpath.substring(1)); + if (!f.exists()) { + continue; + } + + trace("findServlet: '" + path + "' found static welcome " + + "file '" + wf + "'"); + + /* + Even if static file found, we should try to find matching + servlet for JSP serving etc. + */ + servlet = findWelcomeServlet(wpath, true, req); + if (servlet != null) { + return servlet; + } + + req.setServletPath(wpath, null); + + return system_default_servlet_; + } + + /* + If no match is found, the Web server MUST again append each + welcome file in the order specified in the deployment + descriptor to the partial request and check if a servlet is + mapped to that request URI. The Web container must send the + request to the first resource in the WAR that matches. + */ + for (String wf : welcome_files_) { + String wpath = path + wf; + + servlet = findWelcomeServlet(wpath, false, req); + if (servlet != null) { + return servlet; + } + } + } + + trace("findServlet: '" + path + "' fallback to system default servlet"); + req.setServletPath(path, null); + + return system_default_servlet_; + } + + private ServletReg findWelcomeServlet(String path, boolean exists, + DynamicPathRequest req) + { + ServletReg servlet = exact2servlet_.get(path); + if (servlet != null) { + trace("findWelcomeServlet: '" + path + "' exact matched pattern"); + req.setServletPath(path, null); + + return servlet; + } + + int suffix_start = path.lastIndexOf('.'); + if (suffix_start == -1) { + return null; + } + + String suffix = path.substring(suffix_start); + servlet = suffix2servlet_.get(suffix); + if (servlet == null) { + return null; + } + + trace("findWelcomeServlet: '" + path + "' matched suffix pattern"); + + /* + If we want to show the directory content when + index.jsp is absent, then we have to check file + presence here. Otherwise user will get 404. + */ + + if (servlet.system_jsp_servlet_ && !exists) { + trace("findWelcomeServlet: '" + path + "' not exists"); + return null; + } + + req.setServletPath(path, null); + + return servlet; + } + + public void service(Request req, Response resp) + throws ServletException, IOException + { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader_); + + ServletRequestEvent sre = null; + + try { + if (!req_init_listeners_.isEmpty()) { + sre = new ServletRequestEvent(this, req); + + for (ServletRequestListener l : req_init_listeners_) { + l.requestInitialized(sre); + } + } + + URI uri = new URI(req.getRequestURI()); + String path = uri.getPath(); + + if (!path.startsWith(context_path_) + || (path.length() > context_path_.length() + && path.charAt(context_path_.length()) != '/')) + { + trace("service: '" + path + "' not started with '" + context_path_ + "'"); + + resp.sendError(resp.SC_NOT_FOUND); + return; + } + + if (path.equals(context_path_)) { + String url = req.getRequestURL().toString(); + if (!url.endsWith("/")) { + resp.setHeader("Location", url + "/"); + resp.sendError(resp.SC_FOUND); + return; + } + } + + path = path.substring(context_path_.length()); + + ServletReg servlet = findServlet(path, req); + + FilterChain fc = new CtxFilterChain(servlet, req.getFilterPath(), DispatcherType.REQUEST); + + fc.doFilter(req, resp); + + Object code = req.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + if (code != null && code instanceof Integer) { + handleStatusCode((Integer) code, req, resp); + } + } catch (Throwable e) { + trace("service: caught " + e); + + try { + if (!resp.isCommitted() && !exception2location_.isEmpty()) { + handleException(e, req, resp); + } + + if (!resp.isCommitted()) { + resp.reset(); + resp.setStatus(resp.SC_INTERNAL_SERVER_ERROR); + resp.setContentType("text/plain"); + + PrintWriter w = resp.getWriter(); + w.println("Unhandled exception: " + e); + e.printStackTrace(w); + + w.close(); + } + } finally { + throw new ServletException(e); + } + } finally { + resp.flushBuffer(); + + try { + if (!req_destroy_listeners_.isEmpty()) { + for (ServletRequestListener l : req_destroy_listeners_) { + l.requestDestroyed(sre); + } + } + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + } + + private void handleException(Throwable e, Request req, Response resp) + throws ServletException, IOException + { + String location; + + Class<?> cls = e.getClass(); + while (cls != null && !cls.equals(Throwable.class)) { + location = exception2location_.get(cls.getName()); + + if (location != null) { + trace("Exception " + e + " matched. Error page location: " + location); + + req.setAttribute_(RequestDispatcher.ERROR_EXCEPTION, e); + req.setAttribute_(RequestDispatcher.ERROR_EXCEPTION_TYPE, e.getClass()); + req.setAttribute_(RequestDispatcher.ERROR_REQUEST_URI, req.getRequestURI()); + req.setAttribute_(RequestDispatcher.ERROR_STATUS_CODE, resp.SC_INTERNAL_SERVER_ERROR); + + handleError(location, req, resp); + + return; + } + + cls = cls.getSuperclass(); + } + + if (ServletException.class.isAssignableFrom(e.getClass())) { + ServletException se = (ServletException) e; + + handleException(se.getRootCause(), req, resp); + } + } + + private void handleStatusCode(int code, Request req, Response resp) + throws ServletException, IOException + { + String location; + + location = error2location_.get(code); + + if (location != null) { + trace("Status " + code + " matched. Error page location: " + location); + + req.setAttribute_(RequestDispatcher.ERROR_REQUEST_URI, req.getRequestURI()); + + handleError(location, req, resp); + } + } + + public void handleError(String location, Request req, Response resp) + throws ServletException, IOException + { + try { + log("handleError: " + location); + + String filter_path = req.getFilterPath(); + String servlet_path = req.getServletPath(); + String path_info = req.getPathInfo(); + String req_uri = req.getRequestURI(); + DispatcherType dtype = req.getDispatcherType(); + + URI uri; + + if (location.startsWith("/")) { + uri = new URI(context_path_ + location); + } else { + uri = new URI(req_uri).resolve(location); + } + + req.setRequestURI(uri.getRawPath()); + req.setDispatcherType(DispatcherType.ERROR); + + String path = uri.getPath().substring(context_path_.length()); + + ServletReg servlet = findServlet(path, req); + + FilterChain fc = new CtxFilterChain(servlet, req.getFilterPath(), DispatcherType.ERROR); + + fc.doFilter(req, resp); + + req.setServletPath(filter_path, servlet_path, path_info); + req.setRequestURI(req_uri); + req.setDispatcherType(dtype); + } catch (URISyntaxException e) { + throw new ServletException(e); + } + } + + private void processWebXml(File root) throws Exception + { + if (root.isDirectory()) { + File web_xml = new File(root, "WEB-INF/web.xml"); + if (web_xml.exists()) { + trace("start: web.xml file found"); + + InputStream is = new FileInputStream(web_xml); + + processWebXml(is); + + is.close(); + } + } else { + JarFile jf = new JarFile(root); + ZipEntry ze = jf.getEntry("WEB-INF/web.xml"); + + if (ze == null) { + trace("start: web.xml entry NOT found"); + } else { + trace("start: web.xml entry found"); + + processWebXml(jf.getInputStream(ze)); + } + + jf.close(); + } + } + + private void processWebXml(InputStream is) + throws ParserConfigurationException, SAXException, IOException + { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + + Document doc = builder.parse(is); + + Element doc_elem = doc.getDocumentElement(); + String doc_elem_name = doc_elem.getNodeName(); + if (!doc_elem_name.equals("web-app")) { + throw new RuntimeException("Invalid web.xml: 'web-app' element expected, not '" + doc_elem_name + "'"); + } + + metadata_complete_ = doc_elem.getAttribute("metadata-complete").equals("true"); + app_version_ = doc_elem.getAttribute("version"); + + NodeList welcome_file_lists = doc_elem.getElementsByTagName("welcome-file-list"); + + if (welcome_file_lists.getLength() > 0) { + welcome_files_list_found_ = true; + } + + for (int i = 0; i < welcome_file_lists.getLength(); i++) { + Element list_el = (Element) welcome_file_lists.item(i); + NodeList files = list_el.getElementsByTagName("welcome-file"); + for (int j = 0; j < files.getLength(); j++) { + Node node = files.item(j); + String wf = node.getTextContent().trim(); + + /* + 10.10 Welcome Files + ... + The welcome file list is an ordered list of partial URLs + with no trailing or leading /. + */ + + if (wf.startsWith("/") || wf.endsWith("/")) { + log("invalid welcome file: " + wf); + continue; + } + + welcome_files_.add(wf); + } + } + + NodeList context_params = doc_elem.getElementsByTagName("context-param"); + for (int i = 0; i < context_params.getLength(); i++) { + processXmlInitParam(this, (Element) context_params.item(i)); + } + + NodeList filters = doc_elem.getElementsByTagName("filter"); + + for (int i = 0; i < filters.getLength(); i++) { + Element filter_el = (Element) filters.item(i); + NodeList names = filter_el.getElementsByTagName("filter-name"); + if (names == null || names.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'filter-name' tag not found"); + } + + String filter_name = names.item(0).getTextContent().trim(); + trace("filter-name=" + filter_name); + + FilterReg reg = new FilterReg(filter_name); + + NodeList child_nodes = filter_el.getChildNodes(); + for(int j = 0; j < child_nodes.getLength(); j++) { + Node child_node = child_nodes.item(j); + String tag_name = child_node.getNodeName(); + + if (tag_name.equals("filter-class")) { + reg.setClassName(child_node.getTextContent().trim()); + continue; + } + + if (tag_name.equals("async-supported")) { + reg.setAsyncSupported(child_node.getTextContent().trim() + .equals("true")); + continue; + } + + if (tag_name.equals("init-param")) { + processXmlInitParam(reg, (Element) child_node); + continue; + } + } + + filters_.add(reg); + name2filter_.put(filter_name, reg); + } + + NodeList filter_mappings = doc_elem.getElementsByTagName("filter-mapping"); + + for(int i = 0; i < filter_mappings.getLength(); i++) { + Element mapping_el = (Element) filter_mappings.item(i); + NodeList names = mapping_el.getElementsByTagName("filter-name"); + if (names == null || names.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'filter-name' tag not found"); + } + + String filter_name = names.item(0).getTextContent().trim(); + trace("filter-name=" + filter_name); + + FilterReg reg = name2filter_.get(filter_name); + if (reg == null) { + throw new RuntimeException("Invalid web.xml: filter '" + filter_name + "' not found"); + } + + EnumSet<DispatcherType> dtypes = EnumSet.noneOf(DispatcherType.class); + NodeList dispatchers = mapping_el.getElementsByTagName("dispatcher"); + for (int j = 0; j < dispatchers.getLength(); j++) { + Node child_node = dispatchers.item(j); + dtypes.add(DispatcherType.valueOf(child_node.getTextContent().trim())); + } + + if (dtypes.isEmpty()) { + dtypes.add(DispatcherType.REQUEST); + } + + boolean match_after = false; + + NodeList child_nodes = mapping_el.getChildNodes(); + for (int j = 0; j < child_nodes.getLength(); j++) { + Node child_node = child_nodes.item(j); + String tag_name = child_node.getNodeName(); + + if (tag_name.equals("url-pattern")) { + reg.addMappingForUrlPatterns(dtypes, match_after, child_node.getTextContent().trim()); + continue; + } + + if (tag_name.equals("servlet-name")) { + reg.addMappingForServletNames(dtypes, match_after, child_node.getTextContent().trim()); + continue; + } + } + } + + NodeList servlets = doc_elem.getElementsByTagName("servlet"); + + for (int i = 0; i < servlets.getLength(); i++) { + Element servlet_el = (Element) servlets.item(i); + NodeList names = servlet_el.getElementsByTagName("servlet-name"); + if (names == null || names.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'servlet-name' tag not found"); + } + + String servlet_name = names.item(0).getTextContent().trim(); + trace("servlet-name=" + servlet_name); + + ServletReg reg = new ServletReg(servlet_name); + + NodeList child_nodes = servlet_el.getChildNodes(); + for(int j = 0; j < child_nodes.getLength(); j++) { + Node child_node = child_nodes.item(j); + String tag_name = child_node.getNodeName(); + + if (tag_name.equals("servlet-class")) { + reg.setClassName(child_node.getTextContent().trim()); + continue; + } + + if (tag_name.equals("async-supported")) { + reg.setAsyncSupported(child_node.getTextContent().trim() + .equals("true")); + continue; + } + + if (tag_name.equals("init-param")) { + processXmlInitParam(reg, (Element) child_node); + continue; + } + + if (tag_name.equals("load-on-startup")) { + reg.setLoadOnStartup(Integer.parseInt(child_node.getTextContent().trim())); + continue; + } + } + + servlets_.add(reg); + name2servlet_.put(servlet_name, reg); + } + + NodeList servlet_mappings = doc_elem.getElementsByTagName("servlet-mapping"); + + for(int i = 0; i < servlet_mappings.getLength(); i++) { + Element mapping_el = (Element) servlet_mappings.item(i); + NodeList names = mapping_el.getElementsByTagName("servlet-name"); + if (names == null || names.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'servlet-name' tag not found"); + } + + String servlet_name = names.item(0).getTextContent().trim(); + trace("servlet-name=" + servlet_name); + + ServletReg reg = name2servlet_.get(servlet_name); + if (reg == null) { + throw new RuntimeException("Invalid web.xml: servlet '" + servlet_name + "' not found"); + } + + NodeList child_nodes = mapping_el.getElementsByTagName("url-pattern"); + String patterns[] = new String[child_nodes.getLength()]; + for(int j = 0; j < child_nodes.getLength(); j++) { + Node child_node = child_nodes.item(j); + patterns[j] = child_node.getTextContent().trim(); + } + + reg.addMapping(patterns); + } + + NodeList listeners = doc_elem.getElementsByTagName("listener"); + + for (int i = 0; i < listeners.getLength(); i++) { + Element listener_el = (Element) listeners.item(i); + NodeList classes = listener_el.getElementsByTagName("listener-class"); + if (classes == null || classes.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'listener-class' tag not found"); + } + + String class_name = classes.item(0).getTextContent().trim(); + trace("listener-class=" + class_name); + + pending_listener_classnames_.add(class_name); + } + + NodeList error_pages = doc_elem.getElementsByTagName("error-page"); + + for (int i = 0; i < error_pages.getLength(); i++) { + Element error_page_el = (Element) error_pages.item(i); + NodeList locations = error_page_el.getElementsByTagName("location"); + if (locations == null || locations.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'location' tag not found"); + } + + String location = locations.item(0).getTextContent().trim(); + + NodeList child_nodes = error_page_el.getChildNodes(); + for(int j = 0; j < child_nodes.getLength(); j++) { + Node child_node = child_nodes.item(j); + String tag_name = child_node.getNodeName(); + + if (tag_name.equals("exception-type")) { + String ex = child_node.getTextContent().trim(); + + exception2location_.put(ex, location); + trace("error-page: exception " + ex + " -> " + location); + continue; + } + + if (tag_name.equals("error-code")) { + Integer code = Integer.parseInt(child_node.getTextContent().trim()); + + error2location_.put(code, location); + trace("error-page: code " + code + " -> " + location); + continue; + } + } + } + + NodeList session_config = doc_elem.getElementsByTagName("session-config"); + + for (int i = 0; i < session_config.getLength(); i++) { + Element session_config_el = (Element) session_config.item(i); + NodeList session_timeout = session_config_el.getElementsByTagName("session-timeout"); + if (session_timeout != null) { + String timeout = session_timeout.item(0).getTextContent().trim(); + + trace("session_timeout: " + timeout); + session_timeout_ = Integer.parseInt(timeout); + break; + } + } + + NodeList jsp_configs = doc_elem.getElementsByTagName("jsp-config"); + + for (int i = 0; i < jsp_configs.getLength(); i++) { + Element jsp_config_el = (Element) jsp_configs.item(i); + + NodeList jsp_nodes = jsp_config_el.getChildNodes(); + + for(int j = 0; j < jsp_nodes.getLength(); j++) { + Node jsp_node = jsp_nodes.item(j); + String tag_name = jsp_node.getNodeName(); + + if (tag_name.equals("taglib")) { + NodeList tl_nodes = ((Element) jsp_node).getChildNodes(); + Taglib tl = new Taglib(tl_nodes); + + trace("add taglib"); + + taglibs_.add(tl); + continue; + } + + if (tag_name.equals("jsp-property-group")) { + NodeList jpg_nodes = ((Element) jsp_node).getChildNodes(); + JspPropertyGroup conf = new JspPropertyGroup(jpg_nodes); + + trace("add prop group"); + + prop_groups_.add(conf); + continue; + } + } + } + } + + private static int compareVersion(String ver1, String ver2) + { + String[] varr1 = ver1.split("\\."); + String[] varr2 = ver2.split("\\."); + + int max_len = varr1.length > varr2.length ? varr1.length : varr2.length; + for (int i = 0; i < max_len; i++) { + int l = i < varr1.length ? Integer.parseInt(varr1[i]) : 0; + int r = i < varr2.length ? Integer.parseInt(varr2[i]) : 0; + + int res = l - r; + + if (res != 0) { + return res; + } + } + + return 0; + } + + private void processXmlInitParam(InitParams params, Element elem) + throws RuntimeException + { + NodeList n = elem.getElementsByTagName("param-name"); + if (n == null || n.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'param-name' tag not found"); + } + + NodeList v = elem.getElementsByTagName("param-value"); + if (v == null || v.getLength() != 1) { + throw new RuntimeException("Invalid web.xml: 'param-value' tag not found"); + } + params.setInitParameter(n.item(0).getTextContent().trim(), + v.item(0).getTextContent().trim()); + } + + private void loadInitializers(ScanResult scan_res) + { + trace("load initializer(s)"); + + ServiceLoader<ServletContainerInitializer> initializers = + ServiceLoader.load(ServletContainerInitializer.class, loader_); + + for (ServletContainerInitializer sci : initializers) { + + trace("loadInitializers: initializer: " + sci.getClass().getName()); + + HandlesTypes ann = sci.getClass().getAnnotation(HandlesTypes.class); + if (ann == null) { + trace("loadInitializers: no HandlesTypes annotation"); + continue; + } + + Class<?>[] classes = ann.value(); + if (classes == null) { + trace("loadInitializers: no handles classes"); + continue; + } + + Set<Class<?>> handles_classes = new HashSet<>(); + + for (Class<?> c : classes) { + trace("loadInitializers: find handles: " + c.getName()); + + ClassInfoList handles = c.isInterface() + ? scan_res.getClassesImplementing(c.getName()) + : scan_res.getSubclasses(c.getName()); + + for (ClassInfo ci : handles) { + if (ci.isInterface() + || ci.isAnnotation() + || ci.isAbstract()) + { + continue; + } + + trace("loadInitializers: handles class: " + ci.getName()); + handles_classes.add(ci.loadClass()); + } + } + + if (handles_classes.isEmpty()) { + trace("loadInitializers: no handles implementations"); + continue; + } + + try { + sci.onStartup(handles_classes, this); + metadata_complete_ = true; + } catch(Exception e) { + System.err.println("loadInitializers: exception caught: " + e.toString()); + } + } + } + + private void scanClasses(ScanResult scan_res) + throws ReflectiveOperationException + { + ClassInfoList filters = scan_res.getClassesWithAnnotation(WebFilter.class.getName()); + + for (ClassInfo ci : filters) { + if (ci.isInterface() + || ci.isAnnotation() + || ci.isAbstract() + || !ci.implementsInterface(Filter.class.getName())) + { + trace("scanClasses: ignoring Filter impl: " + ci.getName()); + continue; + } + + trace("scanClasses: found Filter class: " + ci.getName()); + + Class<?> cls = ci.loadClass(); + if (!Filter.class.isAssignableFrom(cls)) { + trace("scanClasses: " + ci.getName() + " cannot be assigned to Filter"); + continue; + } + + WebFilter ann = cls.getAnnotation(WebFilter.class); + + if (ann == null) { + trace("scanClasses: no WebFilter annotation for " + ci.getName()); + continue; + } + + String filter_name = ann.filterName(); + + if (filter_name.isEmpty()) { + filter_name = ci.getName(); + } + + FilterReg reg = name2filter_.get(filter_name); + + if (reg == null) { + reg = new FilterReg(filter_name, cls); + filters_.add(reg); + name2filter_.put(filter_name, reg); + } else { + reg.setClass(cls); + } + + EnumSet<DispatcherType> dtypes = EnumSet.noneOf(DispatcherType.class); + DispatcherType[] dispatchers = ann.dispatcherTypes(); + for (DispatcherType d : dispatchers) { + dtypes.add(d); + } + + if (dtypes.isEmpty()) { + dtypes.add(DispatcherType.REQUEST); + } + + boolean match_after = false; + + reg.addMappingForUrlPatterns(dtypes, match_after, ann.value()); + reg.addMappingForUrlPatterns(dtypes, match_after, ann.urlPatterns()); + reg.addMappingForServletNames(dtypes, match_after, ann.servletNames()); + + for (WebInitParam p : ann.initParams()) { + reg.setInitParameter(p.name(), p.value()); + } + + reg.setAsyncSupported(ann.asyncSupported()); + } + + ClassInfoList servlets = scan_res.getClassesWithAnnotation(WebServlet.class.getName()); + + for (ClassInfo ci : servlets) { + if (ci.isInterface() + || ci.isAnnotation() + || ci.isAbstract() + || !ci.extendsSuperclass(HttpServlet.class.getName())) + { + trace("scanClasses: ignoring HttpServlet subclass: " + ci.getName()); + continue; + } + + trace("scanClasses: found HttpServlet class: " + ci.getName()); + + Class<?> cls = ci.loadClass(); + if (!HttpServlet.class.isAssignableFrom(cls)) { + trace("scanClasses: " + ci.getName() + " cannot be assigned to HttpFilter"); + continue; + } + + WebServlet ann = cls.getAnnotation(WebServlet.class); + + if (ann == null) { + trace("scanClasses: no WebServlet annotation"); + continue; + } + + String servlet_name = ann.name(); + + if (servlet_name.isEmpty()) { + servlet_name = ci.getName(); + } + + ServletReg reg = name2servlet_.get(servlet_name); + + if (reg == null) { + reg = new ServletReg(servlet_name, cls); + servlets_.add(reg); + name2servlet_.put(servlet_name, reg); + } else { + reg.setClass(cls); + } + + reg.addMapping(ann.value()); + reg.addMapping(ann.urlPatterns()); + + for (WebInitParam p : ann.initParams()) { + reg.setInitParameter(p.name(), p.value()); + } + + reg.setAsyncSupported(ann.asyncSupported()); + } + + + ClassInfoList lstnrs = scan_res.getClassesWithAnnotation(WebListener.class.getName()); + + for (ClassInfo ci : lstnrs) { + if (ci.isInterface() + || ci.isAnnotation() + || ci.isAbstract()) + { + trace("scanClasses: listener impl: " + ci.getName()); + continue; + } + + trace("scanClasses: listener class: " + ci.getName()); + + if (listener_classnames_.contains(ci.getName())) { + trace("scanClasses: " + ci.getName() + " already added as listener"); + continue; + } + + Class<?> cls = ci.loadClass(); + Class<?> lclass = null; + for (Class<?> c : LISTENER_TYPES) { + if (c.isAssignableFrom(cls)) { + lclass = c; + break; + } + } + + if (lclass == null) { + log("scanClasses: " + ci.getName() + " implements none of known listener interfaces"); + continue; + } + + WebListener ann = cls.getAnnotation(WebListener.class); + + if (ann == null) { + log("scanClasses: no WebListener annotation"); + continue; + } + + Constructor<?> ctor = cls.getConstructor(); + EventListener listener = (EventListener) ctor.newInstance(); + + addListener(listener); + + listener_classnames_.add(ci.getName()); + } + } + + public void stop() throws IOException + { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader_); + + try { + for (ServletReg s : servlets_) { + s.destroy(); + } + + for (FilterReg f : filters_) { + f.destroy(); + } + + if (!destroy_listeners_.isEmpty()) { + ServletContextEvent event = new ServletContextEvent(this); + for (ServletContextListener listener : destroy_listeners_) { + listener.contextDestroyed(event); + } + } + + if (extracted_dir_ != null) { + removeDir(extracted_dir_); + } + + if (temp_dir_ != null) { + removeDir(temp_dir_); + } + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + + private void removeDir(File dir) throws IOException + { + Files.walkFileTree(dir.toPath(), + new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult postVisitDirectory( + Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile( + Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + } + + private class CtxInitParams implements InitParams + { + private final Map<String, String> init_params_ = + new HashMap<String, String>(); + + public boolean setInitParameter(String name, String value) + { + trace("CtxInitParams.setInitParameter " + name + " = " + value); + + return init_params_.putIfAbsent(name, value) == null; + } + + public String getInitParameter(String name) + { + trace("CtxInitParams.getInitParameter for " + name); + + return init_params_.get(name); + } + + public Set<String> setInitParameters(Map<String, String> initParameters) + { + // illegalStateIfContextStarted(); + Set<String> clash = null; + for (Map.Entry<String, String> entry : initParameters.entrySet()) + { + if (entry.getKey() == null) { + throw new IllegalArgumentException("init parameter name required"); + } + + if (entry.getValue() == null) { + throw new IllegalArgumentException("non-null value required for init parameter " + entry.getKey()); + } + + if (init_params_.get(entry.getKey()) != null) + { + if (clash == null) + clash = new HashSet<String>(); + clash.add(entry.getKey()); + } + + trace("CtxInitParams.setInitParameters " + entry.getKey() + " = " + entry.getValue()); + } + + if (clash != null) { + return clash; + } + + init_params_.putAll(initParameters); + return Collections.emptySet(); + } + + public Map<String, String> getInitParameters() + { + trace("CtxInitParams.getInitParameters"); + return init_params_; + } + + public Enumeration<String> getInitParameterNames() + { + return Collections.enumeration(init_params_.keySet()); + } + } + + private class NamedReg extends CtxInitParams + implements Registration + { + private final String name_; + private String class_name_; + + public NamedReg(String name) + { + name_ = name; + } + + public NamedReg(String name, String class_name) + { + name_ = name; + class_name_ = class_name; + } + + @Override + public String getName() + { + return name_; + } + + @Override + public String getClassName() + { + return class_name_; + } + + public void setClassName(String class_name) + { + class_name_ = class_name; + } + } + + private class ServletReg extends NamedReg + implements ServletRegistration.Dynamic, ServletConfig + { + private Class<?> servlet_class_; + private Servlet servlet_; + private String role_; + private boolean async_supported_ = false; + private final List<String> patterns_ = new ArrayList<>(); + private int load_on_startup_ = -1; + private boolean initialized_ = false; + private final List<FilterMap> filters_ = new ArrayList<>(); + private boolean system_jsp_servlet_ = false; + + public ServletReg(String name, Class<?> servlet_class) + { + super(name, servlet_class.getName()); + servlet_class_ = servlet_class; + } + + public ServletReg(String name, Servlet servlet) + { + super(name, servlet.getClass().getName()); + servlet_ = servlet; + } + + public ServletReg(String name, String servlet_class_name) + { + super(name, servlet_class_name); + } + + public ServletReg(String name) + { + super(name); + } + + private void init() throws ServletException + { + if (initialized_) { + return; + } + + trace("ServletReg.init(): " + getName()); + + if (system_jsp_servlet_) { + JasperInitializer ji = new JasperInitializer(); + + ji.onStartup(Collections.emptySet(), Context.this); + } + + if (servlet_ == null) { + try { + if (servlet_class_ == null) { + servlet_class_ = loader_.loadClass(getClassName()); + } + + Constructor<?> ctor = servlet_class_.getConstructor(); + servlet_ = (Servlet) ctor.newInstance(); + } catch(Exception e) { + log("ServletReg.init() failed " + e); + throw new ServletException(e); + } + } + + servlet_.init((ServletConfig) this); + + initialized_ = true; + } + + public void startup() throws ServletException + { + if (load_on_startup_ < 0) { + return; + } + + init(); + } + + public void destroy() + { + if (initialized_) { + servlet_.destroy(); + } + } + + public void setClassName(String class_name) throws IllegalStateException + { + if (servlet_ != null + || servlet_class_ != null + || getClassName() != null) + { + throw new IllegalStateException("Class already initialized"); + } + + super.setClassName(class_name); + } + + public void setClass(Class<?> servlet_class) + throws IllegalStateException + { + if (servlet_ != null + || servlet_class_ != null + || getClassName() != null) + { + throw new IllegalStateException("Class already initialized"); + } + + super.setClassName(servlet_class.getName()); + servlet_class_ = servlet_class; + } + + public void service(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + init(); + + servlet_.service(request, response); + } + + public void addFilter(FilterMap fmap) + { + filters_.add(fmap); + } + + @Override + public Set<String> addMapping(String... urlPatterns) + { + checkContextState(); + + Set<String> clash = null; + for (String pattern : urlPatterns) { + trace("ServletReg.addMapping: " + pattern); + + if (pattern2servlet_.containsKey(pattern)) { + if (clash == null) { + clash = new HashSet<String>(); + } + clash.add(pattern); + } + } + + /* if there were any clashes amongst the urls, return them */ + if (clash != null) { + return clash; + } + + for (String pattern : urlPatterns) { + patterns_.add(pattern); + pattern2servlet_.put(pattern, this); + parseURLPattern(pattern, this); + } + + return Collections.emptySet(); + } + + @Override + public Collection<String> getMappings() + { + trace("ServletReg.getMappings"); + return patterns_; + } + + @Override + public String getRunAsRole() + { + return role_; + } + + @Override + public void setLoadOnStartup(int loadOnStartup) + { + checkContextState(); + + trace("ServletReg.setLoadOnStartup: " + loadOnStartup); + load_on_startup_ = loadOnStartup; + } + + @Override + public Set<String> setServletSecurity(ServletSecurityElement constraint) + { + log("ServletReg.setServletSecurity"); + return Collections.emptySet(); + } + + @Override + public void setMultipartConfig( + MultipartConfigElement multipartConfig) + { + log("ServletReg.setMultipartConfig"); + } + + @Override + public void setRunAsRole(String roleName) + { + log("ServletReg.setRunAsRole: " + roleName); + role_ = roleName; + } + + @Override + public void setAsyncSupported(boolean isAsyncSupported) + { + log("ServletReg.setAsyncSupported: " + isAsyncSupported); + async_supported_ = isAsyncSupported; + } + + @Override + public String getServletName() + { + return getName(); + } + + @Override + public ServletContext getServletContext() + { + return (ServletContext) Context.this; + } + } + + public void checkContextState() throws IllegalStateException + { + if (ctx_initialized_) { + throw new IllegalStateException("Context already initialized"); + } + } + + public void parseURLPattern(String p, ServletReg servlet) + throws IllegalArgumentException + { + URLPattern pattern = parseURLPattern(p); + + switch (pattern.type_) { + case PREFIX: + prefix_patterns_.add(new PrefixPattern(pattern.pattern_, servlet)); + return; + + case SUFFIX: + suffix2servlet_.put(pattern.pattern_, servlet); + return; + + case EXACT: + exact2servlet_.put(pattern.pattern_, servlet); + return; + + case DEFAULT: + default_servlet_ = servlet; + return; + } + + /* TODO process other cases, throw IllegalArgumentException */ + } + + public URLPattern parseURLPattern(String p) + throws IllegalArgumentException + { + URLPattern pattern = parsed_patterns_.get(p); + if (pattern == null) { + pattern = new URLPattern(p); + parsed_patterns_.put(p, pattern); + } + + return pattern; + } + + private static enum URLPatternType { + PREFIX, + SUFFIX, + DEFAULT, + EXACT, + }; + + private class URLPattern + { + private final String pattern_; + private final URLPatternType type_; + + public URLPattern(String p) + throws IllegalArgumentException + { + /* + 12.2 Specification of Mappings + ... + A string beginning with a '/' character and ending with a '/*' + suffix is used for path mapping. + */ + if (p.startsWith("/") && p.endsWith("/*")) { + trace("URLPattern: '" + p + "' is a prefix pattern"); + pattern_ = p.substring(0, p.length() - 2); + type_ = URLPatternType.PREFIX; + return; + } + + /* + A string beginning with a '*.' prefix is used as an extension + mapping. + */ + if (p.startsWith("*.")) { + trace("URLPattern: '" + p + "' is a suffix pattern"); + pattern_ = p.substring(1, p.length()); + type_ = URLPatternType.SUFFIX; + return; + } + + /* + The empty string ("") is a special URL pattern that exactly maps to + the application's context root, i.e., requests of the form + http://host:port/<context- root>/. In this case the path info is '/' + and the servlet path and context path is empty string (""). + */ + if (p.isEmpty()) { + trace("URLPattern: '" + p + "' is a root"); + pattern_ = "/"; + type_ = URLPatternType.EXACT; + return; + } + + /* + A string containing only the '/' character indicates the "default" + servlet of the application. In this case the servlet path is the + request URI minus the context path and the path info is null. + */ + if (p.equals("/")) { + trace("URLPattern: '" + p + "' is a default"); + pattern_ = p; + type_ = URLPatternType.DEFAULT; + return; + } + + /* + All other strings are used for exact matches only. + */ + trace("URLPattern: '" + p + "' is an exact pattern"); + pattern_ = p; + type_ = URLPatternType.EXACT; + + /* TODO process other cases, throw IllegalArgumentException */ + } + + public boolean match(String url) + { + switch (type_) { + case PREFIX: + return url.startsWith(pattern_) && ( + url.length() == pattern_.length() + || url.charAt(pattern_.length()) == '/'); + + case SUFFIX: + return url.endsWith(pattern_); + + case EXACT: + return url.equals(pattern_); + + case DEFAULT: + return true; + } + + return false; + } + } + + private class FilterReg extends NamedReg + implements FilterRegistration.Dynamic, FilterConfig + { + private Class<?> filter_class_; + private Filter filter_; + private boolean async_supported_ = false; + private boolean initialized_ = false; + + public FilterReg(String name, Class<?> filter_class) + { + super(name, filter_class.getName()); + filter_class_ = filter_class; + } + + public FilterReg(String name, Filter filter) + { + super(name, filter.getClass().getName()); + filter_ = filter; + } + + public FilterReg(String name, String filter_class_name) + { + super(name, filter_class_name); + } + + public FilterReg(String name) + { + super(name); + } + + public void setClassName(String class_name) throws IllegalStateException + { + if (filter_ != null + || filter_class_ != null + || getClassName() != null) + { + throw new IllegalStateException("Class already initialized"); + } + + super.setClassName(class_name); + } + + public void setClass(Class<?> filter_class) throws IllegalStateException + { + if (filter_ != null + || filter_class_ != null + || getClassName() != null) + { + throw new IllegalStateException("Class already initialized"); + } + + super.setClassName(filter_class.getName()); + filter_class_ = filter_class; + } + + public void init() throws ServletException + { + if (filter_ == null) { + try { + if (filter_class_ == null) { + filter_class_ = loader_.loadClass(getClassName()); + } + + Constructor<?> ctor = filter_class_.getConstructor(); + filter_ = (Filter) ctor.newInstance(); + } catch(Exception e) { + log("FilterReg.init() failed " + e); + throw new ServletException(e); + } + } + + filter_.init((FilterConfig) this); + + initialized_ = true; + } + + public void destroy() + { + if (initialized_) { + filter_.destroy(); + } + } + + @Override + public void addMappingForServletNames( + EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, + String... servletNames) + { + checkContextState(); + + for (String n : servletNames) { + trace("FilterReg.addMappingForServletNames: ... " + n); + + ServletReg sreg = name2servlet_.get(n); + if (sreg == null) { + sreg = new ServletReg(n); + servlets_.add(sreg); + name2servlet_.put(n, sreg); + } + + FilterMap map = new FilterMap(this, sreg, dispatcherTypes, + isMatchAfter); + + sreg.addFilter(map); + } + } + + @Override + public Collection<String> getServletNameMappings() + { + checkContextState(); + + log("FilterReg.getServletNameMappings"); + return Collections.emptySet(); + } + + @Override + public void addMappingForUrlPatterns( + EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, + String... urlPatterns) + { + checkContextState(); + + for (String u : urlPatterns) { + trace("FilterReg.addMappingForUrlPatterns: ... " + u); + + URLPattern p = parseURLPattern(u); + FilterMap map = new FilterMap(this, p, dispatcherTypes, + isMatchAfter); + + filter_maps_.add(map); + } + } + + @Override + public Collection<String> getUrlPatternMappings() + { + log("FilterReg.getUrlPatternMappings"); + return Collections.emptySet(); + } + + @Override + public void setAsyncSupported(boolean isAsyncSupported) + { + log("FilterReg.setAsyncSupported: " + isAsyncSupported); + async_supported_ = isAsyncSupported; + } + + @Override + public String getFilterName() + { + return getName(); + } + + @Override + public ServletContext getServletContext() + { + return (ServletContext) Context.this; + } + } + + private class FilterMap + { + private final FilterReg filter_; + private final ServletReg servlet_; + private final URLPattern pattern_; + private final EnumSet<DispatcherType> dtypes_; + private final boolean match_after_; + + public FilterMap(FilterReg filter, ServletReg servlet, + EnumSet<DispatcherType> dtypes, boolean match_after) + { + filter_ = filter; + servlet_ = servlet; + pattern_ = null; + dtypes_ = dtypes; + match_after_ = match_after; + } + + public FilterMap(FilterReg filter, URLPattern pattern, + EnumSet<DispatcherType> dtypes, boolean match_after) + { + filter_ = filter; + servlet_ = null; + pattern_ = pattern; + dtypes_ = dtypes; + match_after_ = match_after; + } + } + + private void initialized() + { + if (!sess_attr_listeners_.isEmpty()) { + sess_attr_proxy_ = new SessionAttrProxy(sess_attr_listeners_); + } + + if (!req_attr_listeners_.isEmpty()) { + req_attr_proxy_ = new RequestAttrProxy(req_attr_listeners_); + } + + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader_); + + try { + // Call context listeners + destroy_listeners_.clear(); + if (!ctx_listeners_.isEmpty()) { + ServletContextEvent event = new ServletContextEvent(this); + for (ServletContextListener listener : ctx_listeners_) + { + try { + listener.contextInitialized(event); + } catch(AbstractMethodError e) { + log("initialized: AbstractMethodError exception caught: " + e); + } + destroy_listeners_.add(0, listener); + } + } + + for (ServletReg sr : servlets_) { + try { + sr.startup(); + } catch(ServletException e) { + log("initialized: exception caught: " + e); + } + } + + for (FilterReg fr : filters_) { + try { + fr.init(); + } catch(ServletException e) { + log("initialized: exception caught: " + e); + } + } + + ctx_initialized_ = true; + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + + @Override + public ServletContext getContext(String uripath) + { + trace("getContext for " + uripath); + return this; + } + + @Override + public int getMajorVersion() + { + trace("getMajorVersion"); + return SERVLET_MAJOR_VERSION; + } + + @Override + public String getMimeType(String file) + { + log("getMimeType for " + file); + if (mime_types_ == null) { + mime_types_ = new MimeTypes(); + } + return mime_types_.getMimeByExtension(file); + } + + @Override + public int getMinorVersion() + { + trace("getMinorVersion"); + return SERVLET_MINOR_VERSION; + } + + private class URIRequestDispatcher implements RequestDispatcher + { + private final URI uri_; + + public URIRequestDispatcher(URI uri) + { + uri_ = uri; + } + + public URIRequestDispatcher(String uri) + throws URISyntaxException + { + uri_ = new URI(uri); + } + + @Override + public void forward(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + /* + 9.4 The Forward Method + ... + If the response has been committed, an IllegalStateException + must be thrown. + */ + if (response.isCommitted()) { + throw new IllegalStateException("Response already committed"); + } + + ForwardRequestWrapper req = new ForwardRequestWrapper(request); + + try { + trace("URIRequestDispatcher.forward"); + + String path = uri_.getPath().substring(context_path_.length()); + + ServletReg servlet = findServlet(path, req); + + req.setRequestURI(uri_.getRawPath()); + req.setQueryString(uri_.getRawQuery()); + req.setDispatcherType(DispatcherType.FORWARD); + + /* + 9.4 The Forward Method + ... + If output data exists in the response buffer that has not + been committed, the content must be cleared before the + target servlet's service method is called. + */ + response.resetBuffer(); + + FilterChain fc = new CtxFilterChain(servlet, req.getFilterPath(), DispatcherType.FORWARD); + + fc.doFilter(request, response); + + /* + 9.4 The Forward Method + ... + Before the forward method of the RequestDispatcher interface + returns without exception, the response content must be sent + and committed, and closed by the servlet container, unless + the request was put into the asynchronous mode. If an error + occurs in the target of the RequestDispatcher.forward() the + exception may be propagated back through all the calling + filters and servlets and eventually back to the container + */ + if (!request.isAsyncStarted()) { + response.flushBuffer(); + } + + /* + 9.5 Error Handling + + If the servlet that is the target of a request dispatcher + throws a runtime exception or a checked exception of type + ServletException or IOException, it should be propagated + to the calling servlet. All other exceptions should be + wrapped as ServletExceptions and the root cause of the + exception set to the original exception, as it should + not be propagated. + */ + } catch (ServletException e) { + throw e; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } finally { + req.close(); + + trace("URIRequestDispatcher.forward done"); + } + } + + @Override + public void include(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + IncludeRequestWrapper req = new IncludeRequestWrapper(request); + + try { + trace("URIRequestDispatcher.include"); + + String path = uri_.getPath().substring(context_path_.length()); + + ServletReg servlet = findServlet(path, req); + + req.setRequestURI(uri_.getRawPath()); + req.setQueryString(uri_.getRawQuery()); + req.setDispatcherType(DispatcherType.INCLUDE); + + FilterChain fc = new CtxFilterChain(servlet, req.getFilterPath(), DispatcherType.INCLUDE); + + fc.doFilter(request, new IncludeResponseWrapper(response)); + + } catch (ServletException e) { + throw e; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } finally { + req.close(); + + trace("URIRequestDispatcher.include done"); + } + } + } + + private class ServletDispatcher implements RequestDispatcher + { + private final ServletReg servlet_; + + public ServletDispatcher(ServletReg servlet) + { + servlet_ = servlet; + } + + @Override + public void forward(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + /* + 9.4 The Forward Method + ... + If the response has been committed, an IllegalStateException + must be thrown. + */ + if (response.isCommitted()) { + throw new IllegalStateException("Response already committed"); + } + + trace("ServletDispatcher.forward"); + + DispatcherType dtype = request.getDispatcherType(); + + Request req; + if (request instanceof Request) { + req = (Request) request; + } else { + req = (Request) request.getAttribute(Request.BARE); + } + + try { + req.setDispatcherType(DispatcherType.FORWARD); + + /* + 9.4 The Forward Method + ... + If output data exists in the response buffer that has not + been committed, the content must be cleared before the + target servlet's service method is called. + */ + response.resetBuffer(); + + servlet_.service(request, response); + + /* + 9.4 The Forward Method + ... + Before the forward method of the RequestDispatcher interface + returns without exception, the response content must be sent + and committed, and closed by the servlet container, unless + the request was put into the asynchronous mode. If an error + occurs in the target of the RequestDispatcher.forward() the + exception may be propagated back through all the calling + filters and servlets and eventually back to the container + */ + if (!request.isAsyncStarted()) { + response.flushBuffer(); + } + + /* + 9.5 Error Handling + + If the servlet that is the target of a request dispatcher + throws a runtime exception or a checked exception of type + ServletException or IOException, it should be propagated + to the calling servlet. All other exceptions should be + wrapped as ServletExceptions and the root cause of the + exception set to the original exception, as it should + not be propagated. + */ + } catch (ServletException e) { + throw e; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } finally { + req.setDispatcherType(dtype); + + trace("ServletDispatcher.forward done"); + } + } + + @Override + public void include(ServletRequest request, ServletResponse response) + throws ServletException, IOException + { + trace("ServletDispatcher.include"); + + DispatcherType dtype = request.getDispatcherType(); + + Request req; + if (request instanceof Request) { + req = (Request) request; + } else { + req = (Request) request.getAttribute(Request.BARE); + } + + try { + req.setDispatcherType(DispatcherType.INCLUDE); + + servlet_.service(request, new IncludeResponseWrapper(response)); + + } catch (ServletException e) { + throw e; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } finally { + req.setDispatcherType(dtype); + + trace("ServletDispatcher.include done"); + } + } + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) + { + trace("getNamedDispatcher for " + name); + + ServletReg servlet = name2servlet_.get(name); + if (servlet != null) { + return new ServletDispatcher(servlet); + } + + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(String uriInContext) + { + trace("getRequestDispatcher for " + uriInContext); + try { + return new URIRequestDispatcher(context_path_ + uriInContext); + } catch (URISyntaxException e) { + log("getRequestDispatcher: failed to create dispatcher: " + e); + } + + return null; + } + + public RequestDispatcher getRequestDispatcher(URI uri) + { + trace("getRequestDispatcher for " + uri.getRawPath()); + return new URIRequestDispatcher(uri); + } + + @Override + public String getRealPath(String path) + { + trace("getRealPath for " + path); + + File f = new File(webapp_, path.substring(1)); + + return f.getAbsolutePath(); + } + + @Override + public URL getResource(String path) throws MalformedURLException + { + trace("getResource for " + path); + + File f = new File(webapp_, path.substring(1)); + + if (f.exists()) { + return new URL("file:" + f.getAbsolutePath()); + } + + return null; + } + + @Override + public InputStream getResourceAsStream(String path) + { + trace("getResourceAsStream for " + path); + + try { + File f = new File(webapp_, path.substring(1)); + + return new FileInputStream(f); + } catch (FileNotFoundException e) { + log("getResourceAsStream: failed " + e); + + return null; + } + } + + @Override + public Set<String> getResourcePaths(String path) + { + trace("getResourcePaths for " + path); + + File dir = new File(webapp_, path.substring(1)); + File[] list = dir.listFiles(); + + if (list == null) { + return null; + } + + Set<String> res = new HashSet<>(); + Path root = webapp_.toPath(); + + for (File f : list) { + String r = "/" + root.relativize(f.toPath()); + if (f.isDirectory()) { + r += "/"; + } + + trace("getResourcePaths: " + r); + + res.add(r); + } + + return res; + } + + @Override + public String getServerInfo() + { + trace("getServerInfo: " + server_info_); + return server_info_; + } + + @Override + @Deprecated + public Servlet getServlet(String name) throws ServletException + { + log("getServlet for " + name); + return null; + } + + @SuppressWarnings("unchecked") + @Override + @Deprecated + public Enumeration<String> getServletNames() + { + log("getServletNames"); + return Collections.enumeration(Collections.EMPTY_LIST); + } + + @SuppressWarnings("unchecked") + @Override + @Deprecated + public Enumeration<Servlet> getServlets() + { + log("getServlets"); + return Collections.enumeration(Collections.EMPTY_LIST); + } + + @Override + @Deprecated + public void log(Exception exception, String msg) + { + log(msg, exception); + } + + @Override + public void log(String msg) + { + msg = "Context." + msg; + log(0, msg, msg.length()); + } + + @Override + public void log(String message, Throwable throwable) + { + log(message); + } + + private static native void log(long ctx_ptr, String msg, int msg_len); + + + public static void trace(String msg) + { + msg = "Context." + msg; + trace(0, msg, msg.length()); + } + + private static native void trace(long ctx_ptr, String msg, int msg_len); + + @Override + public String getInitParameter(String name) + { + trace("getInitParameter for " + name); + return init_params_.get(name); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getInitParameterNames() + { + trace("getInitParameterNames"); + return Collections.enumeration(Collections.EMPTY_LIST); + } + + @Override + public String getServletContextName() + { + log("getServletContextName"); + return "No Context"; + } + + @Override + public String getContextPath() + { + trace("getContextPath"); + return context_path_; + } + + @Override + public boolean setInitParameter(String name, String value) + { + trace("setInitParameter " + name + " = " + value); + return init_params_.putIfAbsent(name, value) == null; + } + + @Override + public Object getAttribute(String name) + { + trace("getAttribute " + name); + + return attributes_.get(name); + } + + @Override + public Enumeration<String> getAttributeNames() + { + trace("getAttributeNames"); + + Set<String> names = attributes_.keySet(); + return Collections.enumeration(names); + } + + @Override + public void setAttribute(String name, Object object) + { + trace("setAttribute " + name); + + Object prev = attributes_.put(name, object); + + if (ctx_attr_listeners_.isEmpty()) { + return; + } + + ServletContextAttributeEvent scae = new ServletContextAttributeEvent( + this, name, prev == null ? object : prev); + + for (ServletContextAttributeListener l : ctx_attr_listeners_) { + if (prev == null) { + l.attributeAdded(scae); + } else { + l.attributeReplaced(scae); + } + } + } + + @Override + public void removeAttribute(String name) + { + trace("removeAttribute " + name); + + Object value = attributes_.remove(name); + + if (ctx_attr_listeners_.isEmpty()) { + return; + } + + ServletContextAttributeEvent scae = new ServletContextAttributeEvent( + this, name, value); + + for (ServletContextAttributeListener l : ctx_attr_listeners_) { + l.attributeRemoved(scae); + } + } + + @Override + public FilterRegistration.Dynamic addFilter(String name, + Class<? extends Filter> filterClass) + { + log("addFilter<C> " + name + ", " + filterClass.getName()); + + checkContextState(); + + FilterReg reg = new FilterReg(name, filterClass); + filters_.add(reg); + name2filter_.put(name, reg); + return reg; + } + + @Override + public FilterRegistration.Dynamic addFilter(String name, Filter filter) + { + log("addFilter<F> " + name); + + checkContextState(); + + FilterReg reg = new FilterReg(name, filter); + filters_.add(reg); + name2filter_.put(name, reg); + return reg; + } + + @Override + public FilterRegistration.Dynamic addFilter(String name, String className) + { + log("addFilter<N> " + name + ", " + className); + + checkContextState(); + + FilterReg reg = new FilterReg(name, className); + filters_.add(reg); + name2filter_.put(name, reg); + return reg; + } + + @Override + public ServletRegistration.Dynamic addServlet(String name, + Class<? extends Servlet> servletClass) + { + log("addServlet<C> " + name + ", " + servletClass.getName()); + + checkContextState(); + + ServletReg reg = null; + try { + reg = new ServletReg(name, servletClass); + servlets_.add(reg); + name2servlet_.put(name, reg); + } catch(Exception e) { + System.err.println("addServlet: exception caught: " + e.toString()); + } + + return reg; + } + + @Override + public ServletRegistration.Dynamic addServlet(String name, Servlet servlet) + { + log("addServlet<S> " + name); + + checkContextState(); + + ServletReg reg = null; + try { + reg = new ServletReg(name, servlet); + servlets_.add(reg); + name2servlet_.put(name, reg); + } catch(Exception e) { + System.err.println("addServlet: exception caught: " + e.toString()); + } + + return reg; + } + + @Override + public ServletRegistration.Dynamic addServlet(String name, String className) + { + log("addServlet<N> " + name + ", " + className); + + checkContextState(); + + ServletReg reg = null; + try { + reg = new ServletReg(name, className); + servlets_.add(reg); + name2servlet_.put(name, reg); + } catch(Exception e) { + System.err.println("addServlet: exception caught: " + e.toString()); + } + + return reg; + } + + @Override + public ServletRegistration.Dynamic addJspFile(String jspName, String jspFile) + { + log("addJspFile: " + jspName + " " + jspFile); + + return null; + } + + @Override + public <T extends Filter> T createFilter(Class<T> c) throws ServletException + { + log("createFilter<C> " + c.getName()); + + checkContextState(); + + try { + Constructor<T> ctor = c.getConstructor(); + T filter = ctor.newInstance(); + return filter; + } catch (Exception e) { + log("createFilter() failed " + e); + + throw new ServletException(e); + } + } + + @Override + public <T extends Servlet> T createServlet(Class<T> c) throws ServletException + { + log("createServlet<C> " + c.getName()); + + checkContextState(); + + try { + Constructor<T> ctor = c.getConstructor(); + T servlet = ctor.newInstance(); + return servlet; + } catch (Exception e) { + log("createServlet() failed " + e); + + throw new ServletException(e); + } + } + + @Override + public Set<SessionTrackingMode> getDefaultSessionTrackingModes() + { + log("getDefaultSessionTrackingModes"); + + return default_session_tracking_modes_; + } + + @Override + public Set<SessionTrackingMode> getEffectiveSessionTrackingModes() + { + log("getEffectiveSessionTrackingModes"); + + return session_tracking_modes_; + } + + public boolean isSessionIdValid(String id) + { + synchronized (sessions_) { + return sessions_.containsKey(id); + } + } + + public Session getSession(String id) + { + synchronized (sessions_) { + Session s = sessions_.get(id); + + if (s != null) { + s.accessed(); + + if (s.checkTimeOut()) { + s.invalidate(); + return null; + } + } + + return s; + } + } + + public Session createSession() + { + Session session = new Session(this, generateSessionId(), + sess_attr_proxy_, session_timeout_ * 60); + + if (!sess_listeners_.isEmpty()) + { + HttpSessionEvent event = new HttpSessionEvent(session); + + for (HttpSessionListener l : sess_listeners_) + { + l.sessionCreated(event); + } + } + + synchronized (sessions_) { + sessions_.put(session.getId(), session); + + return session; + } + } + + public void invalidateSession(Session session) + { + synchronized (sessions_) { + sessions_.remove(session.getId()); + } + + if (!sess_listeners_.isEmpty()) + { + HttpSessionEvent event = new HttpSessionEvent(session); + + for (int i = sess_listeners_.size() - 1; i >= 0; i--) + { + sess_listeners_.get(i).sessionDestroyed(event); + } + } + } + + public void changeSessionId(Session session) + { + String old_id; + + synchronized (sessions_) { + old_id = session.getId(); + sessions_.remove(old_id); + + session.setId(generateSessionId()); + + sessions_.put(session.getId(), session); + } + + if (!sess_id_listeners_.isEmpty()) + { + HttpSessionEvent event = new HttpSessionEvent(session); + for (HttpSessionIdListener l : sess_id_listeners_) + { + l.sessionIdChanged(event, old_id); + } + } + } + + private String generateSessionId() + { + return UUID.randomUUID().toString(); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) + { + log("getFilterRegistration " + filterName); + return name2filter_.get(filterName); + } + + @Override + public Map<String, ? extends FilterRegistration> getFilterRegistrations() + { + log("getFilterRegistrations"); + return name2filter_; + } + + @Override + public ServletRegistration getServletRegistration(String servletName) + { + log("getServletRegistration " + servletName); + return name2servlet_.get(servletName); + } + + @Override + public Map<String, ? extends ServletRegistration> getServletRegistrations() + { + log("getServletRegistrations"); + return name2servlet_; + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + log("getSessionCookieConfig"); + + return session_cookie_config_; + } + + @Override + public void setSessionTrackingModes(Set<SessionTrackingMode> modes) + { + log("setSessionTrackingModes"); + + session_tracking_modes_ = modes; + } + + @Override + public void addListener(String className) + { + trace("addListener<N> " + className); + + checkContextState(); + + if (listener_classnames_.contains(className)) { + log("addListener<N> " + className + " already added as listener"); + return; + } + + try { + Class<?> cls = loader_.loadClass(className); + + Constructor<?> ctor = cls.getConstructor(); + EventListener listener = (EventListener) ctor.newInstance(); + + addListener(listener); + + listener_classnames_.add(className); + } catch (Exception e) { + log("addListener<N>: exception caught: " + e.toString()); + } + } + + @Override + public <T extends EventListener> void addListener(T t) + { + trace("addListener<T> " + t.getClass().getName()); + + checkContextState(); + + for (int i = 0; i < LISTENER_TYPES.length; i++) { + Class<?> c = LISTENER_TYPES[i]; + if (c.isAssignableFrom(t.getClass())) { + trace("addListener<T>: assignable to " + c.getName()); + } + } + + if (t instanceof ServletContextListener) { + ctx_listeners_.add((ServletContextListener) t); + } + + if (t instanceof ServletContextAttributeListener) { + ctx_attr_listeners_.add((ServletContextAttributeListener) t); + } + + if (t instanceof ServletRequestListener) { + req_init_listeners_.add((ServletRequestListener) t); + req_destroy_listeners_.add(0, (ServletRequestListener) t); + } + + if (t instanceof ServletRequestAttributeListener) { + req_attr_listeners_.add((ServletRequestAttributeListener) t); + } + + if (t instanceof HttpSessionAttributeListener) { + sess_attr_listeners_.add((HttpSessionAttributeListener) t); + } + + if (t instanceof HttpSessionIdListener) { + sess_id_listeners_.add((HttpSessionIdListener) t); + } + + if (t instanceof HttpSessionListener) { + sess_listeners_.add((HttpSessionListener) t); + } + } + + @Override + public void addListener(Class<? extends EventListener> listenerClass) + { + String className = listenerClass.getName(); + trace("addListener<C> " + className); + + checkContextState(); + + if (listener_classnames_.contains(className)) { + log("addListener<C> " + className + " already added as listener"); + return; + } + + try { + Constructor<?> ctor = listenerClass.getConstructor(); + EventListener listener = (EventListener) ctor.newInstance(); + + addListener(listener); + + listener_classnames_.add(className); + } catch (Exception e) { + log("addListener<C>: exception caught: " + e.toString()); + } + } + + @Override + public <T extends EventListener> T createListener(Class<T> clazz) + throws ServletException + { + trace("createListener<C> " + clazz.getName()); + + checkContextState(); + + try + { + return clazz.getDeclaredConstructor().newInstance(); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + @Override + public ClassLoader getClassLoader() + { + trace("getClassLoader"); + return loader_; + } + + @Override + public int getEffectiveMajorVersion() + { + log("getEffectiveMajorVersion"); + return SERVLET_MAJOR_VERSION; + } + + @Override + public int getEffectiveMinorVersion() + { + log("getEffectiveMinorVersion"); + return SERVLET_MINOR_VERSION; + } + + private final List<TaglibDescriptor> taglibs_ = new ArrayList<>(); + private final List<JspPropertyGroupDescriptor> prop_groups_ = new ArrayList<>(); + + private class JspConfig implements JspConfigDescriptor + { + @Override + public Collection<TaglibDescriptor> getTaglibs() + { + trace("getTaglibs"); + return taglibs_; + } + + @Override + public Collection<JspPropertyGroupDescriptor> getJspPropertyGroups() + { + trace("getJspPropertyGroups"); + return prop_groups_; + } + } + + private final JspConfig jsp_config_ = new JspConfig(); + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + trace("getJspConfigDescriptor"); + + return jsp_config_; + } + + @Override + public void declareRoles(String... roleNames) + { + log("declareRoles"); + //LOG.warn(__unimplmented); + } + + @Override + public String getVirtualServerName() + { + log("getVirtualServerName"); + return null; + } + + @Override + public int getSessionTimeout() + { + trace("getSessionTimeout"); + + return session_timeout_; + } + + @Override + public void setSessionTimeout(int sessionTimeout) + { + trace("setSessionTimeout: " + sessionTimeout); + + session_timeout_ = sessionTimeout; + } + + @Override + public String getRequestCharacterEncoding() + { + log("getRequestCharacterEncoding"); + + return null; + } + + @Override + public void setRequestCharacterEncoding(String encoding) + { + log("setRequestCharacterEncoding: " + encoding); + } + + @Override + public String getResponseCharacterEncoding() + { + log("getResponseCharacterEncoding"); + + return null; + } + + @Override + public void setResponseCharacterEncoding(String encoding) + { + log("setResponseCharacterEncoding: " + encoding); + } + + public ServletRequestAttributeListener getRequestAttributeListener() + { + return req_attr_proxy_; + } +} diff --git a/src/java/nginx/unit/DynamicDispatcherRequest.java b/src/java/nginx/unit/DynamicDispatcherRequest.java new file mode 100644 index 00000000..af0747eb --- /dev/null +++ b/src/java/nginx/unit/DynamicDispatcherRequest.java @@ -0,0 +1,8 @@ +package nginx.unit; + +import javax.servlet.DispatcherType; + +public interface DynamicDispatcherRequest +{ + public void setDispatcherType(DispatcherType type); +} diff --git a/src/java/nginx/unit/DynamicPathRequest.java b/src/java/nginx/unit/DynamicPathRequest.java new file mode 100644 index 00000000..efc1bcd1 --- /dev/null +++ b/src/java/nginx/unit/DynamicPathRequest.java @@ -0,0 +1,15 @@ +package nginx.unit; + +public interface DynamicPathRequest + extends DynamicDispatcherRequest +{ + public void setServletPath(String servlet_path, String path_info); + + public void setServletPath(String filter_path, String servlet_path, String path_info); + + public void setRequestURI(String uri); + + public void setQueryString(String query); + + public String getFilterPath(); +} diff --git a/src/java/nginx/unit/ForwardRequestWrapper.java b/src/java/nginx/unit/ForwardRequestWrapper.java new file mode 100644 index 00000000..f88b6aef --- /dev/null +++ b/src/java/nginx/unit/ForwardRequestWrapper.java @@ -0,0 +1,150 @@ +package nginx.unit; + +import java.util.List; +import java.util.Map; + +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; + +public class ForwardRequestWrapper implements DynamicPathRequest +{ + private final Request request_; + + private final boolean keep_attrs; + + private final String orig_filter_path; + private final String orig_servlet_path; + private final String orig_path_info; + private final String orig_uri; + private final String orig_context_path; + private final String orig_query; + + private final DispatcherType orig_dtype; + + private MultiMap<String> orig_parameters; + + public ForwardRequestWrapper(ServletRequest request) + { + if (request instanceof Request) { + request_ = (Request) request; + } else { + request_ = (Request) request.getAttribute(Request.BARE); + } + + keep_attrs = request_.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) != null; + + orig_dtype = request_.getDispatcherType(); + + orig_filter_path = request_.getFilterPath(); + orig_servlet_path = request_.getServletPath(); + orig_path_info = request_.getPathInfo(); + orig_uri = request_.getRequestURI(); + orig_context_path = request_.getContextPath(); + orig_query = request_.getQueryString(); + } + + @Override + public void setDispatcherType(DispatcherType type) + { + request_.setDispatcherType(type); + + /* + 9.4.2 Forwarded Request Parameters + ... + Note that these attributes must always reflect the information in + the original request even under the situation that multiple + forwards and subsequent includes are called. + */ + + if (keep_attrs) { + return; + } + + /* + 9.4.2 Forwarded Request Parameters + ... + The values of these attributes must be equal to the return values + of the HttpServletRequest methods getRequestURI, getContextPath, + getServletPath, getPathInfo, getQueryString respectively, invoked + on the request object passed to the first servlet object in the + call chain that received the request from the client. + */ + + request_.setAttribute_(RequestDispatcher.FORWARD_SERVLET_PATH, orig_servlet_path); + request_.setAttribute_(RequestDispatcher.FORWARD_PATH_INFO, orig_path_info); + request_.setAttribute_(RequestDispatcher.FORWARD_REQUEST_URI, orig_uri); + request_.setAttribute_(RequestDispatcher.FORWARD_CONTEXT_PATH, orig_context_path); + request_.setAttribute_(RequestDispatcher.FORWARD_QUERY_STRING, orig_query); + } + + @Override + public void setServletPath(String servlet_path, String path_info) + { + request_.setServletPath(servlet_path, path_info); + } + + @Override + public void setServletPath(String filter_path, String servlet_path, String path_info) + { + request_.setServletPath(filter_path, servlet_path, path_info); + } + + @Override + public void setRequestURI(String uri) + { + request_.setRequestURI(uri); + } + + @Override + public void setQueryString(String query) + { + if (query != null) { + orig_parameters = request_.getParameters(); + + MultiMap<String> parameters = new MultiMap<>(); + UrlEncoded.decodeUtf8To(query, parameters); + + for (Map.Entry<String, List<String>> e: orig_parameters.entrySet()) { + parameters.addValues(e.getKey(), e.getValue()); + } + + request_.setParameters(parameters); + + request_.setQueryString(query); + } + } + + @Override + public String getFilterPath() + { + return request_.getFilterPath(); + } + + public void close() + { + request_.setDispatcherType(orig_dtype); + + request_.setRequestURI(orig_uri); + request_.setServletPath(orig_filter_path, orig_servlet_path, orig_path_info); + request_.setQueryString(orig_query); + + if (orig_parameters != null) { + request_.setParameters(orig_parameters); + } + + if (keep_attrs) { + return; + } + + request_.setAttribute_(RequestDispatcher.FORWARD_SERVLET_PATH, null); + request_.setAttribute_(RequestDispatcher.FORWARD_PATH_INFO, null); + request_.setAttribute_(RequestDispatcher.FORWARD_REQUEST_URI, null); + request_.setAttribute_(RequestDispatcher.FORWARD_CONTEXT_PATH, null); + request_.setAttribute_(RequestDispatcher.FORWARD_QUERY_STRING, null); + } +} diff --git a/src/java/nginx/unit/HeaderNamesEnumeration.java b/src/java/nginx/unit/HeaderNamesEnumeration.java new file mode 100644 index 00000000..d81b8778 --- /dev/null +++ b/src/java/nginx/unit/HeaderNamesEnumeration.java @@ -0,0 +1,42 @@ +package nginx.unit; + +import java.lang.String; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class HeaderNamesEnumeration implements Enumeration<String> { + + private long headers_ptr; + private long size; + private long pos = 0; + + public HeaderNamesEnumeration(long _headers_ptr, long _size) { + headers_ptr = _headers_ptr; + size = _size; + } + + @Override + public boolean hasMoreElements() + { + if (pos >= size) { + return false; + } + + pos = nextElementPos(headers_ptr, size, pos); + return pos < size; + } + + static private native long nextElementPos(long headers_ptr, long size, long pos); + + @Override + public String nextElement() + { + if (pos >= size) { + throw new NoSuchElementException(); + } + + return nextElement(headers_ptr, size, pos++); + } + + static private native String nextElement(long headers_ptr, long size, long pos); +} diff --git a/src/java/nginx/unit/HeadersEnumeration.java b/src/java/nginx/unit/HeadersEnumeration.java new file mode 100644 index 00000000..31b5ae24 --- /dev/null +++ b/src/java/nginx/unit/HeadersEnumeration.java @@ -0,0 +1,40 @@ +package nginx.unit; + +import java.lang.String; +import java.util.Enumeration; + +public class HeadersEnumeration implements Enumeration<String> { + + private long headers_ptr; + private long size; + private long initial_pos; + private long pos; + + public HeadersEnumeration(long _headers_ptr, long _size, long _initial_pos) { + headers_ptr = _headers_ptr; + size = _size; + initial_pos = _initial_pos; + pos = _initial_pos; + } + + @Override + public boolean hasMoreElements() + { + if (pos >= size) { + return false; + } + + pos = nextElementPos(headers_ptr, size, initial_pos, pos); + return pos < size; + } + + static private native long nextElementPos(long headers_ptr, long size, long initial_pos, long pos); + + @Override + public String nextElement() + { + return nextElement(headers_ptr, size, initial_pos, pos++); + } + + static private native String nextElement(long headers_ptr, long size, long initial_pos, long pos); +} diff --git a/src/java/nginx/unit/IncludeRequestWrapper.java b/src/java/nginx/unit/IncludeRequestWrapper.java new file mode 100644 index 00000000..67a51b24 --- /dev/null +++ b/src/java/nginx/unit/IncludeRequestWrapper.java @@ -0,0 +1,88 @@ +package nginx.unit; + +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletRequest; + +public class IncludeRequestWrapper implements DynamicPathRequest +{ + private final Request request_; + + private final Object orig_servlet_path_attr; + private final Object orig_path_info_attr; + private final Object orig_uri_attr; + private final Object orig_context_path_attr; + private final Object orig_query_string_attr; + + private final DispatcherType orig_dtype; + + private String filter_path_; + + public IncludeRequestWrapper(ServletRequest request) + { + if (request instanceof Request) { + request_ = (Request) request; + } else { + request_ = (Request) request.getAttribute(Request.BARE); + } + + orig_servlet_path_attr = request_.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + orig_path_info_attr = request_.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + orig_uri_attr = request_.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); + orig_context_path_attr = request_.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH); + orig_query_string_attr = request_.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING); + + orig_dtype = request_.getDispatcherType(); + + request_.setAttribute_(RequestDispatcher.INCLUDE_CONTEXT_PATH, request_.getContextPath()); + } + + @Override + public void setDispatcherType(DispatcherType type) + { + request_.setDispatcherType(type); + } + + @Override + public void setServletPath(String servlet_path, String path_info) + { + setServletPath(servlet_path, servlet_path, path_info); + } + + @Override + public void setServletPath(String filter_path, String servlet_path, String path_info) + { + request_.setAttribute_(RequestDispatcher.INCLUDE_SERVLET_PATH, servlet_path); + request_.setAttribute_(RequestDispatcher.INCLUDE_PATH_INFO, path_info); + filter_path_ = filter_path; + } + + @Override + public void setRequestURI(String uri) + { + request_.setAttribute_(RequestDispatcher.INCLUDE_REQUEST_URI, uri); + } + + @Override + public void setQueryString(String query) + { + request_.setAttribute_(RequestDispatcher.INCLUDE_QUERY_STRING, query); + } + + @Override + public String getFilterPath() + { + return filter_path_; + } + + public void close() + { + request_.setDispatcherType(orig_dtype); + + request_.setAttribute_(RequestDispatcher.INCLUDE_SERVLET_PATH, orig_servlet_path_attr); + request_.setAttribute_(RequestDispatcher.INCLUDE_PATH_INFO, orig_path_info_attr); + request_.setAttribute_(RequestDispatcher.INCLUDE_REQUEST_URI, orig_uri_attr); + request_.setAttribute_(RequestDispatcher.INCLUDE_CONTEXT_PATH, orig_context_path_attr); + request_.setAttribute_(RequestDispatcher.INCLUDE_QUERY_STRING, orig_query_string_attr); + } +} diff --git a/src/java/nginx/unit/IncludeResponseWrapper.java b/src/java/nginx/unit/IncludeResponseWrapper.java new file mode 100644 index 00000000..4537114a --- /dev/null +++ b/src/java/nginx/unit/IncludeResponseWrapper.java @@ -0,0 +1,117 @@ +package nginx.unit; + +import java.io.IOException; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +public class IncludeResponseWrapper extends HttpServletResponseWrapper { + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + public IncludeResponseWrapper(ServletResponse response) + { + super((HttpServletResponse) response); + } + + @Override + public void addCookie(Cookie cookie) + { + trace("addCookie: " + cookie.getName() + "=" + cookie.getValue()); + } + + @Override + public void addDateHeader(String name, long date) + { + trace("addDateHeader: " + name + ": " + date); + } + + @Override + public void addHeader(String name, String value) + { + trace("addHeader: " + name + ": " + value); + } + + @Override + public void addIntHeader(String name, int value) + { + trace("addIntHeader: " + name + ": " + value); + } + + @Override + public void sendRedirect(String location) throws IOException + { + trace("sendRedirect: " + location); + } + + @Override + public void setDateHeader(String name, long date) + { + trace("setDateHeader: " + name + ": " + date); + } + + @Override + public void setHeader(String name, String value) + { + trace("setHeader: " + name + ": " + value); + } + + @Override + public void setIntHeader(String name, int value) + { + trace("setIntHeader: " + name + ": " + value); + } + + @Override + public void setStatus(int sc) + { + trace("setStatus: " + sc); + } + + @Override + @Deprecated + public void setStatus(int sc, String sm) + { + trace("setStatus: " + sc + "; " + sm); + } + + @Override + public void reset() + { + trace("reset"); + } + + @Override + public void setCharacterEncoding(String charset) + { + trace("setCharacterEncoding " + charset); + } + + @Override + public void setContentLength(int len) + { + trace("setContentLength: " + len); + } + + @Override + public void setContentLengthLong(long len) + { + trace("setContentLengthLong: " + len); + } + + @Override + public void setContentType(String type) + { + trace("setContentType: " + type); + } + + private void trace(String msg) + { + msg = "IncludeResponse." + msg; + Response.trace(0, msg.getBytes(UTF_8)); + } +} diff --git a/src/java/nginx/unit/InitParams.java b/src/java/nginx/unit/InitParams.java new file mode 100644 index 00000000..2f5dcbf9 --- /dev/null +++ b/src/java/nginx/unit/InitParams.java @@ -0,0 +1,7 @@ +package nginx.unit; + +public interface InitParams { + public boolean setInitParameter(String name, String value); + + public String getInitParameter(String name); +} diff --git a/src/java/nginx/unit/InputStream.java b/src/java/nginx/unit/InputStream.java new file mode 100644 index 00000000..6fe72ace --- /dev/null +++ b/src/java/nginx/unit/InputStream.java @@ -0,0 +1,90 @@ +package nginx.unit; + +import java.io.IOException; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; + +public class InputStream extends ServletInputStream { + + private long req_info_ptr; + + public InputStream(long ptr) + { + req_info_ptr = ptr; + } + + @Override + public int readLine(byte[] b, int off, int len) throws IOException { + + if (len <= 0) { + return 0; + } + return readLine(req_info_ptr, b, off, len); + } + + private static native int readLine(long req_info_ptr, byte[] b, int off, int len); + + + @Override + public boolean isFinished() + { + return isFinished(req_info_ptr); + } + + private static native boolean isFinished(long req_info_ptr); + + + @Override + public boolean isReady() + { + return true; + } + + + @Override + public void setReadListener(ReadListener listener) + { + } + + + @Override + public int read() throws IOException + { + return read(req_info_ptr); + } + + private static native int read(long req_info_ptr); + + + @Override + public int read(byte b[], int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + return read(req_info_ptr, b, off, len); + } + + private static native int read(long req_info_ptr, byte b[], int off, int len); + + + @Override + public long skip(long n) throws IOException { + return skip(req_info_ptr, n); + } + + private static native long skip(long req_info_ptr, long n); + + + @Override + public int available() throws IOException { + return available(req_info_ptr); + } + + private static native int available(long req_info_ptr); +} diff --git a/src/java/nginx/unit/JspPropertyGroup.java b/src/java/nginx/unit/JspPropertyGroup.java new file mode 100644 index 00000000..2df27718 --- /dev/null +++ b/src/java/nginx/unit/JspPropertyGroup.java @@ -0,0 +1,169 @@ +package nginx.unit; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.servlet.descriptor.JspPropertyGroupDescriptor; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class JspPropertyGroup implements JspPropertyGroupDescriptor +{ + private final List<String> url_patterns_ = new ArrayList<>(); + private String el_ignored_ = null; + private String page_encoding_ = null; + private String scripting_invalid_ = null; + private String is_xml_ = null; + private final List<String> include_preludes_ = new ArrayList<>(); + private final List<String> include_codas_ = new ArrayList<>(); + + private String deffered_syntax_allowed_as_literal_ = null; + private String trim_directive_whitespaces_ = null; + private String default_content_type_ = null; + private String buffer_ = null; + private String error_on_undeclared_namespace_ = null; + + public JspPropertyGroup(NodeList nodes) + { + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + String tag_name = node.getNodeName(); + + if (tag_name.equals("url-pattern")) { + url_patterns_.add(node.getTextContent().trim()); + continue; + } + + if (tag_name.equals("el-ignored")) { + el_ignored_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("page-encoding")) { + page_encoding_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("scripting-invalid")) { + scripting_invalid_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("is-xml")) { + is_xml_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("include-prelude")) { + include_preludes_.add(node.getTextContent().trim()); + continue; + } + + if (tag_name.equals("include-coda")) { + include_codas_.add(node.getTextContent().trim()); + continue; + } + + if (tag_name.equals("deferred-syntax-allowed-as-literal")) { + deffered_syntax_allowed_as_literal_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("trim-directive-whitespaces")) { + trim_directive_whitespaces_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("default-content-type")) { + default_content_type_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("buffer")) { + buffer_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("error-on-undeclared-namespace")) { + error_on_undeclared_namespace_ = node.getTextContent().trim(); + continue; + } + } + + } + + @Override + public Collection<String> getUrlPatterns() + { + return new ArrayList<>(url_patterns_); + } + + @Override + public String getElIgnored() + { + return el_ignored_; + } + + @Override + public String getPageEncoding() + { + return page_encoding_; + } + + @Override + public String getScriptingInvalid() + { + return scripting_invalid_; + } + + @Override + public String getIsXml() + { + return is_xml_; + } + + @Override + public Collection<String> getIncludePreludes() + { + return new ArrayList<>(include_preludes_); + } + + @Override + public Collection<String> getIncludeCodas() + { + return new ArrayList<>(include_codas_); + } + + @Override + public String getDeferredSyntaxAllowedAsLiteral() + { + return deffered_syntax_allowed_as_literal_; + } + + @Override + public String getTrimDirectiveWhitespaces() + { + return trim_directive_whitespaces_; + } + + @Override + public String getDefaultContentType() + { + return default_content_type_; + } + + @Override + public String getBuffer() + { + return buffer_; + } + + @Override + public String getErrorOnUndeclaredNamespace() + { + return error_on_undeclared_namespace_; + } +} + diff --git a/src/java/nginx/unit/OutputStream.java b/src/java/nginx/unit/OutputStream.java new file mode 100644 index 00000000..68af3423 --- /dev/null +++ b/src/java/nginx/unit/OutputStream.java @@ -0,0 +1,68 @@ +package nginx.unit; + +import java.io.IOException; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; + +public class OutputStream extends ServletOutputStream { + + private long req_info_ptr; + + public OutputStream(long ptr) { + req_info_ptr = ptr; + } + + @Override + public void write(int b) throws IOException + { + write(req_info_ptr, b); + } + + private static native void write(long req_info_ptr, int b); + + + @Override + public void write(byte b[], int off, int len) throws IOException + { + if (b == null) { + throw new NullPointerException(); + } else if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + write(req_info_ptr, b, off, len); + } + + private static native void write(long req_info_ptr, byte b[], int off, int len); + + @Override + public void flush() + { + flush(req_info_ptr); + } + + private static native void flush(long req_info_ptr); + + @Override + public void close() + { + close(req_info_ptr); + } + + private static native void close(long req_info_ptr); + + @Override + public boolean isReady() + { + return true; + } + + @Override + public void setWriteListener(WriteListener listener) + { + } +} diff --git a/src/java/nginx/unit/Request.java b/src/java/nginx/unit/Request.java new file mode 100644 index 00000000..dc73c656 --- /dev/null +++ b/src/java/nginx/unit/Request.java @@ -0,0 +1,1123 @@ +package nginx.unit; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import java.lang.IllegalArgumentException; +import java.lang.IllegalStateException; +import java.lang.Object; +import java.lang.String; +import java.lang.StringBuffer; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import java.security.Principal; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletResponse; +import javax.servlet.SessionTrackingMode; +import javax.servlet.SessionCookieConfig; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; + +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.StringUtil; + +import org.eclipse.jetty.server.CookieCutter; +import org.eclipse.jetty.http.MimeTypes; + +public class Request implements HttpServletRequest, DynamicPathRequest +{ + private final Context context; + private final long req_info_ptr; + private final long req_ptr; + + protected String authType = null; + + protected boolean cookiesParsed = false; + + protected CookieCutter cookies = null; + + private final Map<String, Object> attributes = new HashMap<>(); + + private MultiMap<String> parameters = null; + + private final String context_path; + private String filter_path = null; + private String servlet_path = null; + private String path_info = null; + private String request_uri = null; + private String query_string = null; + private boolean query_string_valid = false; + + private DispatcherType dispatcher_type = DispatcherType.REQUEST; + + private String characterEncoding = null; + + /** + * The only date format permitted when generating HTTP headers. + */ + public static final String RFC1123_DATE = + "EEE, dd MMM yyyy HH:mm:ss zzz"; + + private static final SimpleDateFormat formats[] = { + new SimpleDateFormat(RFC1123_DATE, 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) + }; + + private InputStream inputStream = null; + private BufferedReader reader = null; + + private boolean request_session_id_parsed = false; + private String request_session_id = null; + private boolean request_session_id_from_cookie = false; + private boolean request_session_id_from_url = false; + private Session session = null; + + private final ServletRequestAttributeListener attr_listener; + + public static final String BARE = "nginx.unit.request.bare"; + + public Request(Context ctx, long req_info, long req) { + context = ctx; + req_info_ptr = req_info; + req_ptr = req; + + attr_listener = context.getRequestAttributeListener(); + context_path = context.getContextPath(); + } + + @Override + public boolean authenticate(HttpServletResponse response) + throws IOException, ServletException + { + log("authenticate"); + + if (response.isCommitted()) { + throw new IllegalStateException(); + } + + return false; + } + + @Override + public String getAuthType() + { + log("getAuthType"); + + return authType; + } + + @Override + public String getContextPath() + { + trace("getContextPath: " + context_path); + + return context_path; + } + + @Override + public Cookie[] getCookies() + { + trace("getCookies"); + + if (!cookiesParsed) { + parseCookies(); + } + + //Javadoc for Request.getCookies() stipulates null for no cookies + if (cookies == null || cookies.getCookies().length == 0) { + return null; + } + + return cookies.getCookies(); + } + + protected void parseCookies() + { + cookiesParsed = true; + + cookies = new CookieCutter(); + + Enumeration<String> cookie_headers = getHeaders("Cookie"); + + while (cookie_headers.hasMoreElements()) { + cookies.addCookieField(cookie_headers.nextElement()); + } + } + + @Override + public long getDateHeader(String name) + { + trace("getDateHeader: " + name); + + String value = getHeader(name); + if (value == null) { + return -1L; + } + + long res = parseDate(value); + if (res == -1L) { + throw new IllegalArgumentException(value); + } + + return res; + } + + protected long parseDate(String value) + { + Date date = null; + for (int i = 0; (date == null) && (i < formats.length); i++) { + try { + date = formats[i].parse(value); + } catch (ParseException e) { + // Ignore + } + } + if (date == null) { + return -1L; + } + return date.getTime(); + } + + @Override + public String getHeader(String name) + { + String res = getHeader(req_ptr, name, name.length()); + + trace("getHeader: " + name + " = '" + res + "'"); + + return res; + } + + private static native String getHeader(long req_ptr, String name, int name_len); + + + @Override + public Enumeration<String> getHeaderNames() + { + trace("getHeaderNames"); + + return getHeaderNames(req_ptr); + } + + private static native Enumeration<String> getHeaderNames(long req_ptr); + + + @Override + public Enumeration<String> getHeaders(String name) + { + trace("getHeaders: " + name); + + return getHeaders(req_ptr, name, name.length()); + } + + private static native Enumeration<String> getHeaders(long req_ptr, String name, int name_len); + + + @Override + public int getIntHeader(String name) + { + trace("getIntHeader: " + name); + + return getIntHeader(req_ptr, name, name.length()); + } + + private static native int getIntHeader(long req_ptr, String name, int name_len); + + + @Override + public String getMethod() + { + trace("getMethod"); + + return getMethod(req_ptr); + } + + private static native String getMethod(long req_ptr); + + + @Override + public Part getPart(String name) throws IOException, ServletException + { + log("getPart: " + name); + + return null; + } + + @Override + public Collection<Part> getParts() throws IOException, ServletException + { + log("getParts"); + + return Collections.emptyList(); + } + + @Override + public String getPathInfo() + { + trace("getPathInfo: " + path_info); + + return path_info; + } + + @Override + public String getPathTranslated() + { + trace("getPathTranslated"); + + if (path_info == null) { + return null; + } + + return context.getRealPath(path_info); + } + + @Override + public String getQueryString() + { + if (!query_string_valid) { + query_string = getQueryString(req_ptr); + query_string_valid = true; + } + + trace("getQueryString: " + query_string); + + return query_string; + } + + private static native String getQueryString(long req_ptr); + + @Override + public void setQueryString(String query) + { + trace("setQueryString: " + query); + + query_string = query; + query_string_valid = true; + } + + @Override + public String getRemoteUser() + { + log("getRemoteUser"); + + /* TODO */ + return null; + } + + @Override + public String getRequestedSessionId() + { + trace("getRequestedSessionId"); + + if (!request_session_id_parsed) { + parseRequestSessionId(); + } + + return request_session_id; + } + + private void parseRequestSessionId() + { + request_session_id_parsed = true; + + Cookie[] cookies = getCookies(); + if (cookies == null) { + return; + } + + if (context.getEffectiveSessionTrackingModes().contains( + SessionTrackingMode.COOKIE)) + { + final String name = context.getSessionCookieConfig().getName(); + + for (Cookie c : cookies) { + if (c.getName().equals(name)) { + request_session_id = c.getValue(); + request_session_id_from_cookie = true; + + return; + } + } + } + } + + @Override + public String getRequestURI() + { + if (request_uri == null) { + request_uri = getRequestURI(req_ptr); + } + + trace("getRequestURI: " + request_uri); + + return request_uri; + } + + private static native String getRequestURI(long req_ptr); + + + @Override + public void setRequestURI(String uri) + { + trace("setRequestURI: " + uri); + + request_uri = uri; + } + + + @Override + public StringBuffer getRequestURL() + { + String host = getHeader("Host"); + String uri = getRequestURI(); + StringBuffer res = new StringBuffer("http://" + host + uri); + + trace("getRequestURL: " + res); + + return res; + } + + @Override + public String getServletPath() + { + trace("getServletPath: " + servlet_path); + + return servlet_path; + } + + @Override + public void setServletPath(String servlet_path, String path_info) + { + trace("setServletPath: " + servlet_path); + + this.filter_path = servlet_path; + this.servlet_path = servlet_path; + this.path_info = path_info; + } + + @Override + public void setServletPath(String filter_path, String servlet_path, String path_info) + { + trace("setServletPath: " + filter_path + ", " + servlet_path); + + this.filter_path = filter_path; + this.servlet_path = servlet_path; + this.path_info = path_info; + } + + @Override + public String getFilterPath() + { + return filter_path; + } + + @Override + public HttpSession getSession() + { + return getSession(true); + } + + @Override + public HttpSession getSession(boolean create) + { + if (session != null) { + if (context.isSessionIdValid(session.getId())) { + trace("getSession(" + create + "): " + session.getId()); + + return session; + } + + session = null; + } + + if (!request_session_id_parsed) { + parseRequestSessionId(); + + session = context.getSession(request_session_id); + } + + if (session != null || !create) { + trace("getSession(" + create + "): " + (session != null ? session.getId() : "null")); + + return session; + } + + session = context.createSession(); + + if (context.getEffectiveSessionTrackingModes().contains( + SessionTrackingMode.COOKIE)) + { + setSessionIdCookie(); + } + + trace("getSession(" + create + "): " + session.getId()); + + return session; + } + + private void setSessionIdCookie() + { + SessionCookieConfig config = context.getSessionCookieConfig(); + + Cookie c = new Cookie(config.getName(), session.getId()); + + c.setComment(config.getComment()); + if (!StringUtil.isBlank(config.getDomain())) { + c.setDomain(config.getDomain()); + } + + c.setHttpOnly(config.isHttpOnly()); + if (!StringUtil.isBlank(config.getPath())) { + c.setPath(config.getPath()); + } + + c.setMaxAge(config.getMaxAge()); + + getResponse(req_info_ptr).addSessionIdCookie(c); + } + + @Override + public Principal getUserPrincipal() + { + log("getUserPrincipal"); + + return null; + } + + @Override + public boolean isRequestedSessionIdFromCookie() + { + trace("isRequestedSessionIdFromCookie"); + + if (!request_session_id_parsed) { + parseRequestSessionId(); + } + + return request_session_id_from_cookie; + } + + @Override + @Deprecated + public boolean isRequestedSessionIdFromUrl() + { + trace("isRequestedSessionIdFromUrl"); + + if (!request_session_id_parsed) { + parseRequestSessionId(); + } + + return request_session_id_from_url; + } + + @Override + public boolean isRequestedSessionIdFromURL() + { + trace("isRequestedSessionIdFromURL"); + + if (!request_session_id_parsed) { + parseRequestSessionId(); + } + + return request_session_id_from_url; + } + + @Override + public boolean isRequestedSessionIdValid() + { + trace("isRequestedSessionIdValid"); + + if (!request_session_id_parsed) { + parseRequestSessionId(); + } + + return context.isSessionIdValid(request_session_id); + } + + @Override + public boolean isUserInRole(String role) + { + log("isUserInRole: " + role); + + return false; + } + + @Override + public void login(String username, String password) throws ServletException + { + log("login: " + username + "," + password); + } + + @Override + public void logout() throws ServletException + { + log("logout"); + } + + + @Override + public AsyncContext getAsyncContext() + { + log("getAsyncContext"); + + return null; + } + + @Override + public Object getAttribute(String name) + { + if (BARE.equals(name)) { + return this; + } + + Object o = attributes.get(name); + + trace("getAttribute: " + name + " = " + o); + + return o; + } + + @Override + public Enumeration<String> getAttributeNames() + { + trace("getAttributeNames"); + + Set<String> names = attributes.keySet(); + return Collections.enumeration(names); + } + + @Override + public String getCharacterEncoding() + { + trace("getCharacterEncoding"); + + if (characterEncoding != null) { + return characterEncoding; + } + + getContentType(); + + return characterEncoding; + } + + @Override + public int getContentLength() + { + trace("getContentLength"); + + return (int) getContentLength(req_ptr); + } + + private static native long getContentLength(long req_ptr); + + @Override + public long getContentLengthLong() + { + trace("getContentLengthLong"); + + return getContentLength(req_ptr); + } + + @Override + public String getContentType() + { + trace("getContentType"); + + String content_type = getContentType(req_ptr); + + if (characterEncoding == null && content_type != null) { + MimeTypes.Type mime = MimeTypes.CACHE.get(content_type); + String charset = (mime == null || mime.getCharset() == null) ? MimeTypes.getCharsetFromContentType(content_type) : mime.getCharset().toString(); + if (charset != null) { + characterEncoding = charset; + } + } + + return content_type; + } + + private static native String getContentType(long req_ptr); + + + @Override + public DispatcherType getDispatcherType() + { + trace("getDispatcherType: " + dispatcher_type); + + return dispatcher_type; + } + + @Override + public void setDispatcherType(DispatcherType type) + { + trace("setDispatcherType: " + type); + + dispatcher_type = type; + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + trace("getInputStream"); + + if (reader != null) { + throw new IllegalStateException("getInputStream: getReader() already used"); + } + + if (inputStream == null) { + inputStream = new InputStream(req_info_ptr); + } + + return inputStream; + } + + @Override + public String getLocalAddr() + { + trace("getLocalAddr"); + + return getLocalAddr(req_ptr); + } + + private static native String getLocalAddr(long req_ptr); + + + @Override + public Locale getLocale() + { + log("getLocale"); + + return Locale.getDefault(); + } + + @Override + public Enumeration<Locale> getLocales() + { + log("getLocales"); + + return Collections.emptyEnumeration(); + } + + @Override + public String getLocalName() + { + trace("getLocalName"); + + return getLocalName(req_ptr); + } + + private static native String getLocalName(long req_ptr); + + + @Override + public int getLocalPort() + { + trace("getLocalPort"); + + return getLocalPort(req_ptr); + } + + private static native int getLocalPort(long req_ptr); + + + public MultiMap<String> getParameters() + { + if (parameters != null) { + return parameters; + } + + parameters = new MultiMap<>(); + + String query = getQueryString(); + + if (query != null) { + UrlEncoded.decodeUtf8To(query, parameters); + } + + if (getContentLength() > 0 && + getMethod().equals("POST") && + getContentType().startsWith("application/x-www-form-urlencoded")) + { + try { + UrlEncoded.decodeUtf8To(new InputStream(req_info_ptr), + parameters, getContentLength(), -1); + } catch (IOException e) { + log("Unhandled IOException: " + e); + } + } + + return parameters; + } + + public void setParameters(MultiMap<String> p) + { + parameters = p; + } + + @Override + public String getParameter(String name) + { + trace("getParameter: " + name); + + return getParameters().getValue(name, 0); + } + + @Override + public Map<String,String[]> getParameterMap() + { + trace("getParameterMap"); + + return Collections.unmodifiableMap(getParameters().toStringArrayMap()); + } + + @Override + public Enumeration<String> getParameterNames() + { + trace("getParameterNames"); + + return Collections.enumeration(getParameters().keySet()); + } + + @Override + public String[] getParameterValues(String name) + { + trace("getParameterValues: " + name); + + List<String> vals = getParameters().getValues(name); + if (vals == null) + return null; + return vals.toArray(new String[vals.size()]); + } + + @Override + public String getProtocol() + { + trace("getProtocol"); + + return getProtocol(req_ptr); + } + + private static native String getProtocol(long req_ptr); + + @Override + public BufferedReader getReader() throws IOException + { + trace("getReader"); + + if (inputStream != null) { + throw new IllegalStateException("getReader: getInputStream() already used"); + } + + if (reader == null) { + reader = new BufferedReader(new InputStreamReader(new InputStream(req_info_ptr))); + } + + return reader; + } + + @Override + @Deprecated + public String getRealPath(String path) + { + trace("getRealPath: " + path); + + return context.getRealPath(path); + } + + @Override + public String getRemoteAddr() + { + String res = getRemoteAddr(req_ptr); + + trace("getRemoteAddr: " + res); + + return res; + } + + private static native String getRemoteAddr(long req_ptr); + + + @Override + public String getRemoteHost() + { + String res = getRemoteHost(req_ptr); + + trace("getRemoteHost: " + res); + + return res; + } + + private static native String getRemoteHost(long req_ptr); + + + @Override + public int getRemotePort() + { + int res = getRemotePort(req_ptr); + + trace("getRemotePort: " + res); + + return res; + } + + private static native int getRemotePort(long req_ptr); + + + @Override + public RequestDispatcher getRequestDispatcher(String path) + { + trace("getRequestDispatcher: " + path); + + if (path.startsWith("/")) { + return context.getRequestDispatcher(path); + } + + try { + URI uri = new URI(getRequestURI()); + uri = uri.resolve(path); + + return context.getRequestDispatcher(uri); + } catch (URISyntaxException e) { + log("getRequestDispatcher: failed to create dispatcher: " + e); + } + + return null; + } + + + @Override + public String getScheme() + { + log("getScheme"); + + return getScheme(req_ptr); + } + + private static native String getScheme(long req_ptr); + + + @Override + public String getServerName() + { + String res = getServerName(req_ptr); + + trace("getServerName: " + res); + + return res; + } + + private static native String getServerName(long req_ptr); + + + @Override + public int getServerPort() + { + int res = getServerPort(req_ptr); + + trace("getServerPort: " + res); + + return res; + } + + private static native int getServerPort(long req_ptr); + + @Override + public ServletContext getServletContext() + { + trace("getServletContext"); + + return context; + } + + @Override + public boolean isAsyncStarted() + { + log("isAsyncStarted"); + + return false; + } + + @Override + public boolean isAsyncSupported() + { + log("isAsyncSupported"); + + return false; + } + + @Override + public boolean isSecure() + { + log("isSecure"); + + return false; + } + + @Override + public void removeAttribute(String name) + { + trace("removeAttribute: " + name); + + Object prev = attributes.remove(name); + + if (attr_listener == null || prev == null) { + return; + } + + attr_listener.attributeRemoved( + new ServletRequestAttributeEvent(context, this, name, prev)); + } + + @Override + public void setAttribute(String name, Object o) + { + trace("setAttribute: " + name + ", " + o); + + Object prev; + + if (o != null) { + prev = attributes.put(name, o); + } else { + prev = attributes.remove(name); + } + + if (attr_listener == null) { + return; + } + + if (prev == null) { + if (o == null) { + return; + } + + attr_listener.attributeAdded(new ServletRequestAttributeEvent( + context, this, name, o)); + } else { + if (o != null) { + attr_listener.attributeReplaced( + new ServletRequestAttributeEvent(context, this, name, prev)); + } else { + attr_listener.attributeRemoved( + new ServletRequestAttributeEvent(context, this, name, prev)); + } + } + } + + public void setAttribute_(String name, Object o) + { + trace("setAttribute_: " + name + ", " + o); + + if (o != null) { + attributes.put(name, o); + } else { + attributes.remove(name); + } + } + + @Override + public void setCharacterEncoding(String env) throws UnsupportedEncodingException + { + trace("setCharacterEncoding: " + env); + + characterEncoding = env; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException + { + log("startAsync"); + + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException + { + log("startAsync(Req, resp)"); + + return null; + } + + @Override + public <T extends HttpUpgradeHandler> T upgrade( + Class<T> httpUpgradeHandlerClass) throws java.io.IOException, ServletException + { + log("upgrade: " + httpUpgradeHandlerClass.getName()); + + return null; + } + + @Override + public String changeSessionId() + { + trace("changeSessionId"); + + getSession(false); + + if (session == null) { + return null; + } + + context.changeSessionId(session); + + if (context.getEffectiveSessionTrackingModes().contains( + SessionTrackingMode.COOKIE)) + { + setSessionIdCookie(); + } + + return session.getId(); + } + + private void log(String msg) + { + msg = "Request." + msg; + log(req_info_ptr, msg, msg.length()); + } + + public static native void log(long req_info_ptr, String msg, int msg_len); + + + private void trace(String msg) + { + msg = "Request." + msg; + trace(req_info_ptr, msg, msg.length()); + } + + public static native void trace(long req_info_ptr, String msg, int msg_len); + + private static native Response getResponse(long req_info_ptr); +} + diff --git a/src/java/nginx/unit/RequestAttrProxy.java b/src/java/nginx/unit/RequestAttrProxy.java new file mode 100644 index 00000000..daedd01a --- /dev/null +++ b/src/java/nginx/unit/RequestAttrProxy.java @@ -0,0 +1,40 @@ +package nginx.unit; + +import java.util.List; + +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; + +public class RequestAttrProxy implements ServletRequestAttributeListener +{ + private final List<ServletRequestAttributeListener> listeners_; + + public RequestAttrProxy(List<ServletRequestAttributeListener> listeners) + { + listeners_ = listeners; + } + + @Override + public void attributeAdded(ServletRequestAttributeEvent srae) + { + for (ServletRequestAttributeListener l : listeners_) { + l.attributeAdded(srae); + } + } + + @Override + public void attributeReplaced(ServletRequestAttributeEvent srae) + { + for (ServletRequestAttributeListener l : listeners_) { + l.attributeReplaced(srae); + } + } + + @Override + public void attributeRemoved(ServletRequestAttributeEvent srae) + { + for (ServletRequestAttributeListener l : listeners_) { + l.attributeRemoved(srae); + } + } +} diff --git a/src/java/nginx/unit/Response.java b/src/java/nginx/unit/Response.java new file mode 100644 index 00000000..099d7f15 --- /dev/null +++ b/src/java/nginx/unit/Response.java @@ -0,0 +1,817 @@ +package nginx.unit; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import java.lang.IllegalArgumentException; +import java.lang.String; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import java.text.SimpleDateFormat; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.Locale; +import java.util.TimeZone; +import java.util.Vector; + +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.util.StringUtil; + +public class Response implements HttpServletResponse { + + private long req_info_ptr; + + private static final String defaultCharacterEncoding = "iso-8859-1"; + private String characterEncoding = defaultCharacterEncoding; + private String contentType = null; + private String contentTypeHeader = null; + + private static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + private static final String CONTENT_TYPE = "Content-Type"; + private static final byte[] SET_COOKIE_BYTES = "Set-Cookie".getBytes(ISO_8859_1); + private static final byte[] EXPIRES_BYTES = "Expires".getBytes(ISO_8859_1); + + /** + * The only date format permitted when generating HTTP headers. + */ + public static final String RFC1123_DATE = + "EEE, dd MMM yyyy HH:mm:ss zzz"; + + private static final SimpleDateFormat format = + new SimpleDateFormat(RFC1123_DATE, Locale.US); + + private static final String ZERO_DATE_STRING = dateToString(0); + private static final byte[] ZERO_DATE_BYTES = ZERO_DATE_STRING.getBytes(ISO_8859_1); + + /** + * If this string is found within the comment of a cookie added with {@link #addCookie(Cookie)}, then the cookie + * will be set as HTTP ONLY. + */ + public final static String HTTP_ONLY_COMMENT = "__HTTP_ONLY__"; + + private OutputStream outputStream = null; + + private PrintWriter writer = null; + + + public Response(long ptr) { + req_info_ptr = ptr; + } + + /** + * Format a set cookie value by RFC6265 + * + * @param name the name + * @param value the value + * @param domain the domain + * @param path the path + * @param maxAge the maximum age + * @param isSecure true if secure cookie + * @param isHttpOnly true if for http only + */ + public void addSetRFC6265Cookie( + final String name, + final String value, + final String domain, + final String path, + final long maxAge, + final boolean isSecure, + final boolean isHttpOnly) + { + // Check arguments + if (name == null || name.length() == 0) { + throw new IllegalArgumentException("Bad cookie name"); + } + + // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting + // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules + //Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name"); + // Ensure that Per RFC6265, Cookie.value follows syntax rules + //Syntax.requireValidRFC6265CookieValue(value); + + // Format value and params + StringBuilder buf = new StringBuilder(); + buf.append(name).append('=').append(value == null ? "" : value); + + // Append path + if (path != null && path.length() > 0) { + buf.append(";Path=").append(path); + } + + // Append domain + if (domain != null && domain.length() > 0) { + buf.append(";Domain=").append(domain); + } + + // Handle max-age and/or expires + if (maxAge >= 0) { + // Always use expires + // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies + buf.append(";Expires="); + if (maxAge == 0) + buf.append(ZERO_DATE_STRING); + else + buf.append(dateToString(System.currentTimeMillis() + 1000L * maxAge)); + + buf.append(";Max-Age="); + buf.append(maxAge); + } + + // add the other fields + if (isSecure) + buf.append(";Secure"); + if (isHttpOnly) + buf.append(";HttpOnly"); + + // add the set cookie + addHeader(req_info_ptr, SET_COOKIE_BYTES, + buf.toString().getBytes(ISO_8859_1)); + + // Expire responses with set-cookie headers so they do not get cached. + setHeader(req_info_ptr, EXPIRES_BYTES, ZERO_DATE_BYTES); + } + + @Override + public void addCookie(Cookie cookie) + { + trace("addCookie: " + cookie.getName() + "=" + cookie.getValue()); + + if (StringUtil.isBlank(cookie.getName())) { + throw new IllegalArgumentException("Cookie.name cannot be blank/null"); + } + + if (isCommitted()) { + return; + } + + addCookie_(cookie); + } + + private void addCookie_(Cookie cookie) + { + String comment = cookie.getComment(); + boolean httpOnly = false; + + if (comment != null && comment.contains(HTTP_ONLY_COMMENT)) { + httpOnly = true; + } + + addSetRFC6265Cookie(cookie.getName(), + cookie.getValue(), + cookie.getDomain(), + cookie.getPath(), + cookie.getMaxAge(), + cookie.getSecure(), + httpOnly || cookie.isHttpOnly()); + } + + public void addSessionIdCookie(Cookie cookie) + { + trace("addSessionIdCookie: " + cookie.getName() + "=" + cookie.getValue()); + + if (isCommitted()) { + /* + 9.3 The Include Method + + ... any call to HttpServletRequest.getSession() or + HttpServletRequest.getSession(boolean) that would require + adding a Cookie response header must throw an + IllegalStateException if the response has been committed. + */ + throw new IllegalStateException("Response already sent"); + } + + addCookie_(cookie); + } + + @Override + public void addDateHeader(String name, long date) + { + trace("addDateHeader: " + name + ": " + date); + + if (isCommitted()) { + return; + } + + String value = dateToString(date); + + addHeader(req_info_ptr, name.getBytes(ISO_8859_1), + value.getBytes(ISO_8859_1)); + } + + private static String dateToString(long date) + { + Date dateValue = new Date(date); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + return format.format(dateValue); + } + + + @Override + public void addHeader(String name, String value) + { + trace("addHeader: " + name + ": " + value); + + if (value == null) { + return; + } + + if (isCommitted()) { + return; + } + + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + setContentType(value); + return; + } + + addHeader(req_info_ptr, name.getBytes(ISO_8859_1), + value.getBytes(ISO_8859_1)); + } + + private static native void addHeader(long req_info_ptr, byte[] name, byte[] value); + + + @Override + public void addIntHeader(String name, int value) + { + trace("addIntHeader: " + name + ": " + value); + + if (isCommitted()) { + return; + } + + addIntHeader(req_info_ptr, name.getBytes(ISO_8859_1), value); + } + + private static native void addIntHeader(long req_info_ptr, byte[] name, int value); + + + @Override + public boolean containsHeader(String name) + { + trace("containsHeader: " + name); + + return containsHeader(req_info_ptr, name.getBytes(ISO_8859_1)); + } + + private static native boolean containsHeader(long req_info_ptr, byte[] name); + + + @Override + @Deprecated + public String encodeRedirectUrl(String url) + { + return encodeRedirectURL(url); + } + + @Override + public String encodeRedirectURL(String url) + { + log("encodeRedirectURL: " + url); + + return url; + } + + @Override + @Deprecated + public String encodeUrl(String url) + { + return encodeURL(url); + } + + @Override + public String encodeURL(String url) + { + log("encodeURL: " + url); + + return url; + } + + @Override + public String getHeader(String name) + { + trace("getHeader: " + name); + + return getHeader(req_info_ptr, name.getBytes(ISO_8859_1)); + } + + private static native String getHeader(long req_info_ptr, byte[] name); + + + @Override + public Collection<String> getHeaderNames() + { + trace("getHeaderNames"); + + Enumeration<String> e = getHeaderNames(req_info_ptr); + if (e == null) { + return Collections.emptyList(); + } + + return Collections.list(e); + } + + private static native Enumeration<String> getHeaderNames(long req_info_ptr); + + + @Override + public Collection<String> getHeaders(String name) + { + trace("getHeaders: " + name); + + Enumeration<String> e = getHeaders(req_info_ptr, name.getBytes(ISO_8859_1)); + if (e == null) { + return Collections.emptyList(); + } + + return Collections.list(e); + } + + private static native Enumeration<String> getHeaders(long req_info_ptr, byte[] name); + + + @Override + public int getStatus() + { + trace("getStatus"); + + return getStatus(req_info_ptr); + } + + private static native int getStatus(long req_info_ptr); + + + @Override + public void sendError(int sc) throws IOException + { + sendError(sc, null); + } + + @Override + public void sendError(int sc, String msg) throws IOException + { + trace("sendError: " + sc + ", " + msg); + + if (isCommitted()) { + throw new IllegalStateException("Response already sent"); + } + + setStatus(sc); + + Request request = getRequest(req_info_ptr); + + // If we are allowed to have a body, then produce the error page. + if (sc != SC_NO_CONTENT && sc != SC_NOT_MODIFIED && + sc != SC_PARTIAL_CONTENT && sc >= SC_OK) + { + request.setAttribute_(RequestDispatcher.ERROR_STATUS_CODE, sc); + request.setAttribute_(RequestDispatcher.ERROR_MESSAGE, msg); + request.setAttribute_(RequestDispatcher.ERROR_REQUEST_URI, + request.getRequestURI()); +/* + request.setAttribute_(RequestDispatcher.ERROR_SERVLET_NAME, + request.getServletName()); +*/ + } + +/* + Avoid commit and give chance for error handlers. + + if (!request.isAsyncStarted()) { + commit(); + } +*/ + } + + private static native Request getRequest(long req_info_ptr); + + private void commit() + { + if (writer != null) { + writer.close(); + + } else if (outputStream != null) { + outputStream.close(); + + } else { + commit(req_info_ptr); + } + } + + private static native void commit(long req_info_ptr); + + + @Override + public void sendRedirect(String location) throws IOException + { + trace("sendRedirect: " + location); + + if (isCommitted()) { + return; + } + + try { + URI uri = new URI(location); + + if (!uri.isAbsolute()) { + URI req_uri = new URI(getRequest(req_info_ptr).getRequestURL().toString()); + uri = req_uri.resolve(uri); + + location = uri.toString(); + } + } catch (URISyntaxException e) { + log("sendRedirect: failed to send redirect: " + e); + return; + } + + sendRedirect(req_info_ptr, location.getBytes(ISO_8859_1)); + } + + private static native void sendRedirect(long req_info_ptr, byte[] location); + + + @Override + public void setDateHeader(String name, long date) + { + trace("setDateHeader: " + name + ": " + date); + + if (isCommitted()) { + return; + } + + String value = dateToString(date); + + setHeader(req_info_ptr, name.getBytes(ISO_8859_1), + value.getBytes(ISO_8859_1)); + } + + + @Override + public void setHeader(String name, String value) + { + trace("setHeader: " + name + ": " + value); + + if (isCommitted()) { + return; + } + + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + setContentType(value); + return; + } + + /* + * When value is null container behaviour is undefined. + * - Tomcat ignores setHeader call; + * - Jetty & Resin acts as removeHeader; + */ + if (value == null) { + removeHeader(req_info_ptr, name.getBytes(ISO_8859_1)); + return; + } + + setHeader(req_info_ptr, name.getBytes(ISO_8859_1), + value.getBytes(ISO_8859_1)); + } + + private static native void setHeader(long req_info_ptr, byte[] name, byte[] value); + + private static native void removeHeader(long req_info_ptr, byte[] name); + + @Override + public void setIntHeader(String name, int value) + { + trace("setIntHeader: " + name + ": " + value); + + if (isCommitted()) { + return; + } + + setIntHeader(req_info_ptr, name.getBytes(ISO_8859_1), value); + } + + private static native void setIntHeader(long req_info_ptr, byte[] name, int value); + + + @Override + public void setStatus(int sc) + { + trace("setStatus: " + sc); + + if (isCommitted()) { + return; + } + + setStatus(req_info_ptr, sc); + } + + private static native void setStatus(long req_info_ptr, int sc); + + + @Override + @Deprecated + public void setStatus(int sc, String sm) + { + trace("setStatus: " + sc + "; " + sm); + + if (isCommitted()) { + return; + } + + setStatus(req_info_ptr, sc); + } + + + @Override + public void flushBuffer() throws IOException + { + trace("flushBuffer"); + + if (writer != null) { + writer.flush(); + } + + if (outputStream != null) { + outputStream.flush(); + } + } + + @Override + public int getBufferSize() + { + trace("getBufferSize"); + + return getBufferSize(req_info_ptr); + } + + public static native int getBufferSize(long req_info_ptr); + + + @Override + public String getCharacterEncoding() + { + trace("getCharacterEncoding"); + + return characterEncoding; + } + + @Override + public String getContentType() + { + /* In JIRA decorator get content type called after commit. */ + + String res = contentTypeHeader; + + trace("getContentType: " + res); + + return res; + } + + private static native String getContentType(long req_info_ptr); + + @Override + public Locale getLocale() + { + log("getLocale"); + + return null; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException + { + trace("getOutputStream"); + + if (writer != null) { + throw new IllegalStateException("Writer already created"); + } + + if (outputStream == null) { + outputStream = new OutputStream(req_info_ptr); + } + + return outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException + { + trace("getWriter ( characterEncoding = '" + characterEncoding + "' )"); + + if (outputStream != null) { + throw new IllegalStateException("OutputStream already created"); + } + + if (writer == null) { + ServletOutputStream stream = new OutputStream(req_info_ptr); + + writer = new PrintWriter( + new OutputStreamWriter(stream, Charset.forName(characterEncoding)), + false); + } + + return writer; + } + + @Override + public boolean isCommitted() + { + trace("isCommitted"); + + return isCommitted(req_info_ptr); + } + + public static native boolean isCommitted(long req_info_ptr); + + @Override + public void reset() + { + trace("reset"); + + if (isCommitted()) { + return; + } + + reset(req_info_ptr); + + writer = null; + outputStream = null; + } + + public static native void reset(long req_info_ptr); + + @Override + public void resetBuffer() + { + trace("resetBuffer"); + + resetBuffer(req_info_ptr); + + writer = null; + outputStream = null; + } + + public static native void resetBuffer(long req_info_ptr); + + @Override + public void setBufferSize(int size) + { + trace("setBufferSize: " + size); + + setBufferSize(req_info_ptr, size); + } + + public static native void setBufferSize(long req_info_ptr, int size); + + @Override + public void setCharacterEncoding(String charset) + { + trace("setCharacterEncoding " + charset); + + if (isCommitted()) { + return; + } + + if (charset == null) { + if (writer != null + && !characterEncoding.equalsIgnoreCase(defaultCharacterEncoding)) + { + /* TODO throw */ + return; + } + + characterEncoding = defaultCharacterEncoding; + } else { + if (writer != null + && !characterEncoding.equalsIgnoreCase(charset)) + { + /* TODO throw */ + return; + } + + characterEncoding = charset; + } + + if (contentType != null) { + String type = contentType + ";charset=" + characterEncoding; + + contentTypeHeader = type; + + setContentType(req_info_ptr, type.getBytes(ISO_8859_1)); + } + } + + + @Override + public void setContentLength(int len) + { + trace("setContentLength: " + len); + + if (isCommitted()) { + return; + } + + setContentLength(req_info_ptr, len); + } + + @Override + public void setContentLengthLong(long len) + { + trace("setContentLengthLong: " + len); + + if (isCommitted()) { + return; + } + + setContentLength(req_info_ptr, len); + } + + private static native void setContentLength(long req_info_ptr, long len); + + + @Override + public void setContentType(String type) + { + trace("setContentType: " + type); + + if (isCommitted()) { + return; + } + + if (type == null) { + removeContentType(req_info_ptr); + contentType = null; + contentTypeHeader = null; + return; + } + + String charset = MimeTypes.getCharsetFromContentType(type); + String ctype = MimeTypes.getContentTypeWithoutCharset(type); + + if (writer != null + && charset != null + && !characterEncoding.equalsIgnoreCase(charset)) + { + /* To late to change character encoding */ + charset = characterEncoding; + type = ctype + ";charset=" + characterEncoding; + } + + if (charset == null) { + type = type + ";charset=" + characterEncoding; + } else { + characterEncoding = charset; + } + + contentType = ctype; + contentTypeHeader = type; + + setContentType(req_info_ptr, type.getBytes(ISO_8859_1)); + } + + private static native void setContentType(long req_info_ptr, byte[] type); + + private static native void removeContentType(long req_info_ptr); + + + @Override + public void setLocale(Locale loc) + { + log("setLocale: " + loc); + } + + private void log(String msg) + { + msg = "Response." + msg; + log(req_info_ptr, msg.getBytes(UTF_8)); + } + + public static native void log(long req_info_ptr, byte[] msg); + + + private void trace(String msg) + { + msg = "Response." + msg; + trace(req_info_ptr, msg.getBytes(UTF_8)); + } + + public static native void trace(long req_info_ptr, byte[] msg); +} diff --git a/src/java/nginx/unit/Session.java b/src/java/nginx/unit/Session.java new file mode 100644 index 00000000..6be74709 --- /dev/null +++ b/src/java/nginx/unit/Session.java @@ -0,0 +1,251 @@ +package nginx.unit; + +import java.io.Serializable; + +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 java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Andrey Kazankov + */ +public class Session implements HttpSession, Serializable +{ + private final Map<String, Object> attributes = new HashMap<>(); + private final long creation_time = new Date().getTime(); + private long last_access_time = creation_time; + private long access_time = creation_time; + private int max_inactive_interval; + private String id; + private final Context context; + private boolean is_new = true; + private final HttpSessionAttributeListener attr_listener; + + public Session(Context context, String id, + HttpSessionAttributeListener al, int max_inactive_interval) + { + this.id = id; + this.context = context; + attr_listener = al; + this.max_inactive_interval = max_inactive_interval; + } + + public void setId(String id) + { + this.id = id; + } + + @Override + public long getCreationTime() + { + return creation_time; + } + + @Override + public String getId() + { + return id; + } + + @Override + public long getLastAccessedTime() + { + return last_access_time; + } + + @Override + public ServletContext getServletContext() + { + return context; + } + + @Override + public void setMaxInactiveInterval(int i) + { + max_inactive_interval = i; + } + + @Override + public int getMaxInactiveInterval() + { + return max_inactive_interval; + } + + @Deprecated + @Override + public javax.servlet.http.HttpSessionContext getSessionContext() + { + return null; + } + + @Override + public Object getAttribute(String s) + { + synchronized (attributes) { + return attributes.get(s); + } + } + + @Deprecated + @Override + public Object getValue(String s) + { + return getAttribute(s); + } + + @Override + public Enumeration<String> getAttributeNames() + { + synchronized (attributes) { + return Collections.enumeration(attributes.keySet()); + } + } + + @Deprecated + @Override + public String[] getValueNames() + { + synchronized (attributes) { + return attributes.keySet().toArray(new String[attributes.keySet().size()]); + } + } + + @Override + public void setAttribute(String s, Object o) + { + Object old; + + if (o != null && o instanceof HttpSessionBindingListener) { + HttpSessionBindingListener l = (HttpSessionBindingListener) o; + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s); + + l.valueBound(e); + } + + synchronized (attributes) { + if (o != null) { + old = attributes.put(s, o); + } else { + old = attributes.remove(s); + } + } + + if (old != null && old instanceof HttpSessionBindingListener) { + HttpSessionBindingListener l = (HttpSessionBindingListener) old; + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s); + + l.valueUnbound(e); + } + + if (attr_listener == null) { + return; + } + + if (o == null) { + if (old != null) { + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s, old); + attr_listener.attributeRemoved(e); + } + + return; + } + + if (old != null) { + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s, old); + attr_listener.attributeReplaced(e); + } else { + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s, o); + attr_listener.attributeAdded(e); + } + } + + @Deprecated + @Override + public void putValue(String s, Object o) + { + setAttribute(s,o); + } + + @Override + public void removeAttribute(String s) + { + Object o; + + synchronized (attributes) { + o = attributes.remove(s); + } + + if (o != null && o instanceof HttpSessionBindingListener) { + HttpSessionBindingListener l = (HttpSessionBindingListener) o; + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s); + + l.valueUnbound(e); + } + + if (attr_listener == null || o == null) { + return; + } + + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, s, o); + attr_listener.attributeRemoved(e); + } + + @Deprecated + @Override + public void removeValue(String s) + { + removeAttribute(s); + } + + @Override + public void invalidate() + { + context.invalidateSession(this); + + unboundAttributes(); + } + + private void unboundAttributes() + { + for (Map.Entry<String, Object> a : attributes.entrySet()) { + Object o = a.getValue(); + if (o != null && o instanceof HttpSessionBindingListener) { + HttpSessionBindingListener l = (HttpSessionBindingListener) o; + HttpSessionBindingEvent e = new HttpSessionBindingEvent(this, a.getKey()); + + l.valueUnbound(e); + } + } + + attributes.clear(); + } + + @Override + public boolean isNew() + { + return is_new; + } + + public void accessed() { + synchronized (this) { + is_new = false; + + last_access_time = access_time; + access_time = new Date().getTime(); + } + } + + public boolean checkTimeOut() + { + return (max_inactive_interval > 0) && + (access_time - last_access_time > max_inactive_interval * 1000); + } +}
\ No newline at end of file diff --git a/src/java/nginx/unit/SessionAttrProxy.java b/src/java/nginx/unit/SessionAttrProxy.java new file mode 100644 index 00000000..bddeede9 --- /dev/null +++ b/src/java/nginx/unit/SessionAttrProxy.java @@ -0,0 +1,40 @@ +package nginx.unit; + +import java.util.List; + +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; + +public class SessionAttrProxy implements HttpSessionAttributeListener +{ + private final List<HttpSessionAttributeListener> listeners_; + + public SessionAttrProxy(List<HttpSessionAttributeListener> listeners) + { + listeners_ = listeners; + } + + @Override + public void attributeAdded(HttpSessionBindingEvent event) + { + for (HttpSessionAttributeListener l : listeners_) { + l.attributeAdded(event); + } + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent event) + { + for (HttpSessionAttributeListener l : listeners_) { + l.attributeRemoved(event); + } + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent event) + { + for (HttpSessionAttributeListener l : listeners_) { + l.attributeReplaced(event); + } + } +} diff --git a/src/java/nginx/unit/Taglib.java b/src/java/nginx/unit/Taglib.java new file mode 100644 index 00000000..f72033ba --- /dev/null +++ b/src/java/nginx/unit/Taglib.java @@ -0,0 +1,44 @@ +package nginx.unit; + +import javax.servlet.descriptor.TaglibDescriptor; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class Taglib implements TaglibDescriptor +{ + private String uri_ = null; + private String location_ = null; + + public Taglib(NodeList nodes) + { + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + String tag_name = node.getNodeName(); + + if (tag_name.equals("taglib-uri")) { + uri_ = node.getTextContent().trim(); + continue; + } + + if (tag_name.equals("taglib-location")) { + location_ = node.getTextContent().trim(); + continue; + } + } + + } + + @Override + public String getTaglibURI() + { + return uri_; + } + + @Override + public String getTaglibLocation() + { + return location_; + } +} + diff --git a/src/java/nginx/unit/UnitSessionCookieConfig.java b/src/java/nginx/unit/UnitSessionCookieConfig.java new file mode 100644 index 00000000..e1b2ae04 --- /dev/null +++ b/src/java/nginx/unit/UnitSessionCookieConfig.java @@ -0,0 +1,110 @@ +package nginx.unit; + +import javax.servlet.SessionCookieConfig; + +/* + + <session-config> + <session-timeout>60</session-timeout> + <cookie-config></cookie-config> + <tracking-mode></tracking-mode> + </session-config> + + + */ +public class UnitSessionCookieConfig implements SessionCookieConfig { + + private static final String default_name = "JSESSIONID"; + + private String name = default_name; + private String domain; + private String path; + private String comment; + private boolean httpOnly = true; + private boolean secure = false; + private int maxAge = -1; + + @Override + public void setName(String name) + { + this.name = name; + } + + @Override + public String getName() + { + return name; + } + + @Override + public void setDomain(String domain) + { + this.domain = domain; + } + + @Override + public String getDomain() + { + return domain; + } + + @Override + public void setPath(String path) + { + this.path = path; + } + + @Override + public String getPath() + { + return path; + } + + @Override + public void setComment(String comment) + { + this.comment = comment; + } + + @Override + public String getComment() + { + return comment; + } + + @Override + public void setHttpOnly(boolean httpOnly) + { + this.httpOnly = httpOnly; + } + + @Override + public boolean isHttpOnly() + { + return httpOnly; + } + + @Override + public void setSecure(boolean secure) + { + this.secure = secure; + } + + @Override + public boolean isSecure() + { + return secure; + } + + @Override + public void setMaxAge(int maxAge) + { + this.maxAge = maxAge; + } + + @Override + public int getMaxAge() + { + return maxAge; + } +} |