/* 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.js; import org.esxx.*; import org.esxx.util.StringUtil; import java.io.UnsupportedEncodingException; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.regex.Pattern; import javax.mail.internet.ContentType; import javax.xml.soap.MessageFactory; import javax.xml.soap.MimeHeaders; import javax.xml.soap.SOAPConstants; import javax.xml.soap.SOAPException; import org.mozilla.javascript.*; public class JSRequest extends ScriptableObject { private static final long serialVersionUID = -777379647478473562L; public JSRequest() { super(); } public JSRequest(Request request, Context cx, Scriptable scope) { this(); ESXX esxx = ESXX.getInstance(); this.request = request; requestURI = (JSURI) cx.newObject(scope, "URI", new Object[] { request.getRequestURI() }); scriptURI = (JSURI) cx.newObject(scope, "URI", new Object[] { request.getScriptURI() }); env = cx.newObject(scope); headers = cx.newObject(scope); cookies = cx.newObject(scope); accept = cx.newObject(scope); query = cx.newObject(scope); args = null; params = cx.newObject(scope); acceptValueOf = new FunctionObject("valueOf", acceptValueOfMethod, accept); for (String name : request.getProperties().stringPropertyNames()) { String value = request.getProperties().getProperty(name).trim(); // Add environtment variable to esxx.env ScriptableObject.putProperty(env, name, value); // If this is an HTTP header, get the original name back String hdr = esxx.cgiToHTTP(name); if (hdr != null) { // Add real HTTP header to this.headers addHeader(hdr, value); // Decode cookies handleCookieHeader(hdr, value); // Decode Accept* HTTP headers handleAcceptHeader(hdr, value, cx, accept); // Decode Content-* HTTP headers handleContentHeader(hdr, value); // Handle SOAPAction if (hdr.equals("SOAPAction")) { soapAction = value; } } if (name.equals("QUERY_STRING")) { try { StringUtil.decodeFormVariables(value, query); } catch (UnsupportedEncodingException ex) { throw new ESXXException("Unable to parse request entity: " + ex.getMessage(), ex); } } } logger = JSESXX.newObject(cx, scope, "Logger", new Object[] { request, request.getScriptName() }); } public void setArgs(Scriptable uri_params) { args = uri_params; } public Request getRequest() { return request; } static public Object jsConstructor(Context cx, java.lang.Object[] args, Function ctorObj, boolean inNewExpr) { return new JSRequest((Request) args[0], cx, ctorObj); } @Override public String getClassName() { return "Request"; } public String jsGet_requestMethod() { return request.getRequestMethod(); } public JSURI jsGet_requestURI() { return requestURI; } public JSURI jsGet_scriptURI() { return scriptURI; } public String jsGet_scriptName() { return request.getScriptName(); } public String jsGet_pathInfo() { return request.getPathInfo(); } public Scriptable jsGet_env() { return env; } public Scriptable jsGet_headers() { return headers; } public Scriptable jsGet_cookies() { return cookies; } public Scriptable jsGet_accept() { return accept; } public Scriptable jsGet_query() { return query; } public Scriptable jsGet_args() { return args; } public Scriptable jsGet_params() { return params; } public void jsSet_params(Scriptable params) { this.params = params; } public Scriptable jsGet_log() { return logger; } public String jsGet_contentType() { if (contentType == null) { return null; } return contentType.getBaseType(); } public long jsGet_contentLength() { return contentLength; } public synchronized Object jsGet_message() { if (message == null) { // Now parse the POST/PUT/etc. message message = parseMessage(); } return message; } public String jsGet_soapAction() { return soapAction; } private Request request; private JSURI requestURI; private JSURI scriptURI; private Scriptable env; private Scriptable headers; private Scriptable cookies; private Scriptable accept; private Scriptable query; private Scriptable args; private Scriptable params; private Scriptable logger; private Object message; private String soapAction; private ContentType contentType; private long contentLength = -1; private Scriptable acceptValueOf; static private java.lang.reflect.Method acceptValueOfMethod; @SuppressWarnings("unused") private static Object acceptValueOf(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return thisObj.get("value", thisObj); } static { try { acceptValueOfMethod = JSRequest.class.getDeclaredMethod("acceptValueOf", Context.class, Scriptable.class, Object[].class, Function.class); } catch (NoSuchMethodException ex) { throw new ESXXException("Failed to find JSRequest.acceptValueOf(): ", ex); } } private void addHeader(String name, String value) { ScriptableObject.putProperty(headers, name, value); } static private Pattern cookieSeparator = Pattern.compile("\\s*;\\s*"); static private Pattern valueSeparator = Pattern.compile("\\s*=\\s*"); private void handleCookieHeader(String hdr, String value) { if (hdr.equals("Cookie")) { for (String cookie : cookieSeparator.split(value)) { String[] parts = valueSeparator.split(cookie, 2); String cn = parts[0]; String cv = parts.length < 2 || parts[1] == null ? "" : parts[1]; ScriptableObject.putProperty(cookies, cn, cv); } } } private void handleAcceptHeader(String hdr, String value, Context cx, Scriptable accept) { String subname; if (hdr.equals("Accept")) { subname = "media"; } else if (hdr.startsWith("Accept-")) { subname = hdr.substring(7).toLowerCase(); } else { // Do nothing return; } Map<Double, List<Scriptable>> objects = new TreeMap<Double, List<Scriptable>>(); String[] values = value.split(","); for (String v : values) { double q = 1.0; double w = 0.0; String[] parts = v.split(";"); Scriptable object = cx.newObject(accept); object.put("valueOf", object, acceptValueOf); object.put("value", object, parts[0].trim()); // Add all attributes for (int i = 1; i < parts.length; ++i) { String[] attr = parts[i].split("=", 2); if (attr.length == 2) { // Parse Q factor if (attr[0].trim().equals("q")) { q = Double.parseDouble(attr[1].trim()); } else { object.put(attr[0].trim(), object, attr[1].trim()); } } } object.put("q", object, "" + q); // Calculate implicit weight if (parts[0].trim().equals("*/*")) { w = 0.0000; } else if (parts[0].trim().endsWith("/*")) { w = 0.0001; } else { w = 0.0002; } // Attributes give extra points w += parts.length * 0.00001; // Add to tree multi-map, inverse order double key = -(q + w); List<Scriptable> l = objects.get(key); if (l == null) { l = new ArrayList<Scriptable>(); objects.put(key, l); } l.add(object); } Scriptable object = cx.newArray(accept, objects.size()); accept.put(subname, accept, object); int i = 0; for (List<Scriptable> l : objects.values()) { for (Scriptable s : l) { object.put(i++, object, s); } } } public void handleContentHeader(String name, String value) { if (name.startsWith("Content-") && !value.isEmpty()) { if (name.equals("Content-Type")) { try { contentType = new ContentType(value); } catch (javax.mail.internet.ParseException ex) { throw new ESXXException(400, "Invalid Content-Type header: " + ex.getMessage(), ex); } } else if (name.equals("Content-Length")) { contentLength = Long.parseLong(value); } else { throw new ESXXException(501, "Unsupported Content header: " + name); } } } private Object parseMessage() { // Consume SOAP message, if any // TODO: Add a SOAP handler in Parser.java if (soapAction != null) { try { MimeHeaders mime_headers = new MimeHeaders(); for (Object k : headers.getIds()) { if (k instanceof String) { String name = (String) k; String value = Context.toString(ScriptableObject.getProperty(headers, name)); mime_headers.addHeader(name, value); } } return MessageFactory.newInstance(SOAPConstants.DYNAMIC_SOAP_PROTOCOL) .createMessage(mime_headers, request.getInputStream()); } catch (IOException ex) { throw new ESXXException("Unable to read SOAP message stream: " + ex.getMessage()); } catch (SOAPException ex) { throw new ESXXException(400 /* Bad Request */, "Invalid SOAP message: " + ex.getMessage()); } finally { try { request.getInputStream().close(); } catch (Exception ignored) {} } } else if (contentType != null && contentLength > 0) { try { ESXX esxx = ESXX.getInstance(); return esxx.parseStream(contentType, request.getInputStream(), URI.create("urn:x-esxx:incoming-request-entity"), null, new java.io.PrintWriter(request.getErrorWriter()), Context.getCurrentContext(), this); } catch (Exception ex) { throw new ESXXException(400 /* Bad Request */, "Unable to parse request entity: " + ex.getMessage(), ex); } finally { try { request.getInputStream().close(); } catch (Exception ignored) {} } } else { // Return a dummy object return Context.getCurrentContext().newObject(this); } } }