/* ESXX - The friendly ECMAscript/XML Application Server Copyright (C) 2007-2015 Martin Blom <martin@blom.org> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.esxx.request; import java.io.*; import java.net.*; import java.util.Properties; import org.esxx.*; import org.esxx.util.JS; import org.esxx.util.StringUtil; import org.esxx.util.XML; import org.mozilla.javascript.*; import javax.xml.transform.dom.*; import net.sf.saxon.s9api.*; import org.w3c.dom.Document; import org.w3c.dom.Element; import static net.sf.saxon.s9api.Serializer.Property.*; public abstract class WebRequest extends Request implements ESXX.ResponseHandler { protected WebRequest(InputStream in, OutputStream err) { super(in, err); } protected void initRequest(String request_method, URI request_uri, URI path_translated, Properties cgi_env, URI fs_root_uri, boolean update_env) { URI script_uri = null; String path_info = null; URI script_filename = null; URI working_directory = null; Response quick_response; try { quick_response = getEmbeddedResource(request_uri.getQuery()); if (quick_response == null) { request_uri = request_uri.normalize(); path_translated = path_translated.normalize(); fs_root_uri = fs_root_uri.normalize(); File script_file = new File(path_translated); if (!script_file.isAbsolute()) { throw new IllegalArgumentException(path_translated + " is not an absolute path"); } if (!path_translated.toString().startsWith(fs_root_uri.toString())) { getReqLogger().warning("Document " + path_translated + " from request URI " + request_uri + " is outside root path " + fs_root_uri); throw new FileNotFoundException("Document is outside root path"); } boolean iterated = false; while (!script_file.exists()) { script_file = script_file.getParentFile(); iterated = true; } if (script_file.isDirectory()) { if (iterated) { throw new FileNotFoundException("Resource '" + path_translated + "' not found."); } quick_response = getFileListingResponse(request_uri.getPath(), script_file); } else if (!ESXX.fileTypeMap.getContentType(script_file) .equals("application/vnd.esxx.webapp+xml")) { if (iterated) { throw new FileNotFoundException("Only .esxx files can have a trailing path."); } quick_response = new Response(200, ESXX.fileTypeMap.getContentType(script_file), new FileInputStream(script_file), null); } else { working_directory = script_file.getParentFile().toURI(); script_filename = script_file.toURI(); path_info = script_filename.relativize(path_translated).getRawPath(); path_info = StringUtil.decodeURI(path_info, false); String req_path = StringUtil.decodeURI(request_uri.getRawPath(), false); String script_name = null; if (req_path.endsWith(path_info)) { int length = req_path.length() - path_info.length(); // Make sure script_name does not end with a slash while (length > 0 && req_path.charAt(length - 1) == '/') { --length; } script_name = req_path.substring(0, length); script_name = StringUtil.encodeURI(script_name, true); // Create the URI version of script_name, and terminate it // with a slash to make it easy to resolve subresources. script_uri = request_uri.resolve(script_name + "/").normalize(); } if (!path_info.startsWith("/")) { // path_info should always begin with a slash. path_info = "/" + path_info; } // Complete CGI environment (using native OS file paths) if (update_env) { cgi_env.setProperty("PATH_TRANSLATED", new File(path_translated).toString()); cgi_env.setProperty("PATH_INFO", path_info); cgi_env.setProperty("SCRIPT_FILENAME", script_file.toString()); cgi_env.setProperty("SCRIPT_NAME", script_name); } } } } catch (Exception ex) { quick_response = createErrorResponse(ex); } super.initRequest(request_method, request_uri, script_uri, path_info, script_filename, working_directory, cgi_env, quick_response); } public Integer handleError(Throwable ex) { try { return handleResponse(createErrorResponse(ex)); } catch (Exception ex2) { // Hmm ex2.printStackTrace(); return 20; } } public Integer reportInternalError(int code, String title, String subtitle, String message, Throwable ex) { try { return handleResponse(createErrorResponse(code, title, subtitle, message, ex)); } catch (Exception ex2) { // Hmm ex2.printStackTrace(); return 20; } } protected static URI getPathTranslated(URI fs_root_uri, String raw_request_path, String context_path) { if (!raw_request_path.startsWith(context_path)) { throw new IllegalArgumentException(raw_request_path + " must begin with " + context_path); } int offset = context_path.length(); while (offset < raw_request_path.length() && raw_request_path.charAt(offset) == '/') { ++offset; // Trim leading slashes } return fs_root_uri.resolve(raw_request_path.substring(offset)); } protected Properties createCGIEnvironment(String request_method, String protocol, URI full_request_uri, String local_host, int local_port, String remote_host, int remote_port, URI root_uri) { Properties p = new Properties(); String query = full_request_uri.getRawQuery(); if (query == null) { query = ""; } if (local_host == null) { // Probably a Google App Engine problem local_host = "0.0.0.0"; } p.setProperty("GATEWAY_INTERFACE", "CGI/1.1"); p.setProperty("SERVER_SOFTWARE", "ESXX/1.0"); p.setProperty("DOCUMENT_ROOT", new File(root_uri).toString()); p.setProperty("REQUEST_METHOD", request_method); p.setProperty("SERVER_NAME", full_request_uri.getHost()); p.setProperty("REQUEST_URI", full_request_uri.getRawPath()); p.setProperty("QUERY_STRING", query); p.setProperty("SERVER_PROTOCOL", protocol); p.setProperty("REMOTE_ADDR", remote_host); p.setProperty("REMOTE_PORT", "" + remote_port); p.setProperty("SERVER_ADDR", local_host); p.setProperty("SERVER_PORT", "" + local_port); return p; } /** A pattern that matches '!esxx-rsrc=' followed by a string of valid characters and dot. (Slash is not valid.) */ private static java.util.regex.Pattern esxxResource = java.util.regex.Pattern.compile("^!esxx-rsrc=[a-zA-Z0-9.]+$"); private Response getEmbeddedResource(String qs) throws IOException { if (qs != null && esxxResource.matcher(qs).matches()) { String embedded = qs.substring(11); InputStream rsrc = ESXX.getInstance().openCachedURI(URI.create("esxx-rsrc:" + embedded)); if (rsrc == null) { throw new ESXXException(404, "Embedded resource '" + embedded + "' not found"); } else { java.util.TreeMap<String, String> hdr = new java.util.TreeMap<String, String>(); hdr.put("Cache-Control", "max-age=3600"); return new Response(200, ESXX.fileTypeMap.getContentType(embedded), rsrc, hdr); } } else { return null; } } private Response createErrorResponse(Throwable ex) { int code = ex instanceof ESXXException ? ((ESXXException) ex).getStatus() : 500; String title = "ESXX Server Error"; String subtitle = "Unhandled exception: " + ex.getClass().getSimpleName(); String message = ex.getMessage(); if (ex instanceof FileNotFoundException) { code = 404; subtitle = "Not Found"; } return createErrorResponse(code, title, subtitle, message, ex); } private Response createErrorResponse(int code, String title, String subtitle, String message, Throwable ex) { ESXX esxx = ESXX.getInstance(); Document doc = esxx.createDocument("error"); Element root = doc.getDocumentElement(); String stacktrace = null; if (ex != null) { if (ex instanceof RhinoException) { RhinoException re = (RhinoException) ex; stacktrace = re.getScriptStackTrace(new JS.JSFilenameFilter()); message = message + " [" + re.sourceName() + ", line " + re.lineNumber() + ", column " + re.columnNumber() + ": " + re.lineSource() + "]"; } else if (ex instanceof ESXXException || ex instanceof FileNotFoundException || ex instanceof javax.xml.transform.TransformerException || ex instanceof net.sf.saxon.s9api.SaxonApiException) { // Don't print stack trace } else { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); pw.flush(); stacktrace = sw.toString(); } } // Log all unhandled error responses getReqLogger().log(java.util.logging.Level.WARNING, message, ex); XML.addChild(root, "title", title); XML.addChild(root, "subtitle", subtitle); XML.addChild(root, "message", message); if (stacktrace != null) { XML.addChild(root, "stacktrace", stacktrace); } ByteArrayOutputStream os = new ByteArrayOutputStream(); String ct = renderHTML(doc, os); return new Response(code, ct, os, null); } private Response getFileListingResponse(String req_uri, File dir) throws Exception { Document doc = org.esxx.js.protocol.FILEHandler.createDirectoryListing(dir); doc.getDocumentElement().setAttributeNS(null, "requestURI", req_uri); ByteArrayOutputStream os = new ByteArrayOutputStream(); String ct = renderHTML(doc, os); return new Response(200, ct, os, null); } private String renderHTML(Document doc, OutputStream dst) { try { ESXX esxx = ESXX.getInstance(); Stylesheet xslt = esxx.getCachedStylesheet(new URI("esxx-rsrc:esxx.xslt"), null); XsltExecutable xe = xslt.getExecutable(); XsltTransformer tr = xe.load(); Serializer s = new Serializer(); s.setOutputStream(dst); // Remove this code when upgrading to Saxon 9.1 (?) Properties op = xe.getUnderlyingCompiledStylesheet().getOutputProperties(); s.setOutputProperty(BYTE_ORDER_MARK, op.getProperty("byte-order-mark")); s.setOutputProperty(CDATA_SECTION_ELEMENTS, op.getProperty("cdata-section-elements")); s.setOutputProperty(DOCTYPE_PUBLIC, op.getProperty("doctype-public")); s.setOutputProperty(DOCTYPE_SYSTEM, op.getProperty("doctype-system")); s.setOutputProperty(ENCODING, op.getProperty("encoding")); s.setOutputProperty(ESCAPE_URI_ATTRIBUTES, op.getProperty("escape-uri-attributes")); s.setOutputProperty(INCLUDE_CONTENT_TYPE, op.getProperty("include-content-type")); s.setOutputProperty(INDENT, op.getProperty("indent")); s.setOutputProperty(MEDIA_TYPE, op.getProperty("media-type", "text/html")); s.setOutputProperty(METHOD, op.getProperty("method")); // s.setOutputProperty(NORMALIZATION_FORM, op.getProperty("normalization-form")); s.setOutputProperty(OMIT_XML_DECLARATION, op.getProperty("omit-xml-declaration")); s.setOutputProperty(STANDALONE, op.getProperty("standalone")); s.setOutputProperty(UNDECLARE_PREFIXES, op.getProperty("undeclare-prefixes")); s.setOutputProperty(USE_CHARACTER_MAPS, op.getProperty("use-character-maps")); s.setOutputProperty(VERSION, op.getProperty("version")); tr.setSource(new DOMSource(doc)); tr.setDestination(s); tr.transform(); return s.getOutputProperty(MEDIA_TYPE); } catch (Exception ex) { // This should never happen PrintWriter pw = new PrintWriter(dst); ex.printStackTrace(pw); pw.close(); return "text/plain"; } } }