com.bytelightning.opensource.pokerface.ScriptHelperImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.bytelightning.opensource.pokerface.ScriptHelperImpl.java

Source

package com.bytelightning.opensource.pokerface;

/*
 The MIT License (MIT)
    
 PokerFace: Asynchronous, streaming, HTTP/1.1, scriptable, reverse proxy.
    
 Copyright (c) 2015 Frank Stock
    
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
    
 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.
    
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 */
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.pool2.ObjectPool;
import org.apache.http.Header;
import org.apache.http.HttpInetConnection;
import org.apache.http.HttpRequest;
import org.apache.http.nio.NHttpServerConnection;
import org.apache.http.nio.reactor.ssl.SSLIOSession;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;

import sun.net.www.MimeEntry;

/**
 * Implementation of <code>ScriptHelper</code>
 */
@SuppressWarnings("restriction")
public class ScriptHelperImpl implements ScriptHelper, Closeable {
    /**
     * Primary constructor
     * 
     * @param request Request as it was received from the client
     * @param context The context of this http transaction
     * @param bufferPool A pool from which <code>ByteBuffer</code>s can be obtained.
     */
    public ScriptHelperImpl(HttpRequest request, HttpContext context, ObjectPool<ByteBuffer> bufferPool) {
        this.request = request;
        this.context = context;
        this.bufferPool = bufferPool;
    }

    private final ObjectPool<ByteBuffer> bufferPool;
    private final HttpRequest request;
    private final HttpContext context;

    /**
     * Close out any resources that are owned by this object.
     */
    @Override
    public void close() {
        if (borrowedBuffers != null) {
            for (ByteBuffer buffer : borrowedBuffers) {
                try {
                    bufferPool.returnObject(buffer);
                } catch (Exception e) {
                    ScriptResponseProducer.Logger.error("Unable to return ByteBuffer to pool", e);
                }
            }
            borrowedBuffers.clear();
            borrowedBuffers = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ByteBuffer createBuffer() {
        ByteBuffer buffer;
        try {
            buffer = bufferPool.borrowObject();
            if (borrowedBuffers == null)
                borrowedBuffers = new ArrayList<ByteBuffer>();
            borrowedBuffers.add(buffer);
        } catch (Exception e) {
            buffer = ByteBuffer.allocateDirect(1024 * 1024);
        }
        return buffer;
    }

    private List<ByteBuffer> borrowedBuffers;

    /**
     * {@inheritDoc}
     */
    @Override
    public String getScheme() {
        // Note that we are getting the connection context, not our own httpcontext
        if (getConnection().getContext().getAttribute("http.session.ssl") instanceof SSLIOSession)
            return "https";
        return "https";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String makeAbsoluteUrl(String location) {
        if (location == null)
            return location;
        StringBuffer sb = new StringBuffer();
        boolean leadingSlash = location.startsWith("/");

        if (location.startsWith("//")) {
            // Scheme relative; Add the scheme
            String scheme = getScheme();
            sb.append(scheme, 0, scheme.length());
            sb.append(':');
            sb.append(location, 0, location.length());
            return sb.toString();

        } else if (leadingSlash || !HasScheme(location)) {
            String scheme = getScheme();
            String name = getHOSTName();
            int port = getHOSTPort();

            try {
                sb.append(scheme, 0, scheme.length());
                sb.append("://", 0, 3);
                sb.append(name, 0, name.length());
                if ((scheme.equals("http") && port != 80) || (scheme.equals("https") && port != 443)) {
                    sb.append(':');
                    String portS = port + "";
                    sb.append(portS, 0, portS.length());
                }
                if (!leadingSlash) {
                    String relativePath = request.getRequestLine().getUri();
                    sb.append(PercentEncodeRfc3986(relativePath));
                    sb.append('/');
                }
                sb.append(location, 0, location.length());

                sb = new StringBuffer(NormalizeURL(sb.toString()));
            } catch (IOException e) {
                IllegalArgumentException iae = new IllegalArgumentException(location);
                iae.initCause(e);
                throw iae;
            }

            return sb.toString();

        } else
            return location;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String[] getAcceptableLocales() {
        ArrayList<String> retVal = new ArrayList<String>();
        Header[] hdrs = request.getHeaders("Accept-Language");
        if ((hdrs != null) && (hdrs.length > 0)) {
            // Store the accumulated languages that have been requested in a local collection, sorted by the quality value (so we can add Locales in descending order).
            // The values will be ArrayLists containing the corresponding Locales to be added
            TreeMap<Double, ArrayList<Locale>> locales = new TreeMap<Double, ArrayList<Locale>>();
            for (Header hdr : hdrs)
                parseLocalesHeader(hdr.getValue(), locales);
            // Process the quality values in highest->lowest order (due to negating the Double value when creating the key)
            for (ArrayList<Locale> list : locales.values())
                for (Locale locale : list)
                    retVal.add(locale.toLanguageTag());
            Collections.reverse(retVal);
        }
        retVal.add(Locale.getDefault().toLanguageTag());
        return retVal.toArray(new String[retVal.size()]);
    }

    /**
     * Courtesy of 'Peter' at http://stackoverflow.com/questions/6824157/parse-accept-language-header-in-java
     */
    private void parseLocalesHeader(String value, TreeMap<Double, ArrayList<Locale>> locales) {
        for (String str : value.split(",")) {
            String[] arr = str.trim().replace("-", "_").split(";");

            // Parse the q-value
            Double q = 1.0d;
            for (String s : arr) {
                s = s.trim();
                if (s.startsWith("q=")) {
                    try {
                        String ds = s.substring(2).trim();
                        if (ds.length() > 0 && ds.length() <= 5)
                            q = Math.rint(Double.parseDouble(ds) * 10000d) / 10000d;
                        else
                            q = 0d;
                    } catch (NumberFormatException e) {
                        q = 0.0;
                    }
                    break;
                }
            }
            if (q >= 0.00005) {
                // Parse the locale
                Locale locale;
                String[] l = arr[0].split("_");
                switch (l.length) {
                case 2:
                    locale = new Locale(l[0], l[1]);
                    break;
                case 3:
                    locale = new Locale(l[0], l[1], l[2]);
                    break;
                default:
                    locale = new Locale(l[0]);
                    break;
                }
                ArrayList<Locale> list = locales.get(q);
                if (list == null) {
                    list = new ArrayList<Locale>();
                    locales.put(q, list);
                }
                list.add(locale);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MimeEntry findMimeEntryByType(String mimeType) {
        return MimeTypeMap.get(mimeType);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MimeEntry findMimeEntryByExt(String ext) {
        return MimeExtensionsMap.get(ext);
    }

    protected static final HashMap<String, MimeEntry> MimeTypeMap;
    protected static final HashMap<String, MimeEntry> MimeExtensionsMap;
    private static final sun.net.www.MimeTable MimeHashTable;
    static {
        MimeHashTable = sun.net.www.MimeTable.getDefaultTable();
        AddMimeEntryImpl("text/css", ".css");
        AddMimeEntryImpl("application/javascript", ".js");
        AddMimeEntryImpl("application/json", ".json");
        // CHECKOUT http://www.iana.org/assignments/media-types/media-types.xhtml
        AddMimeEntryImpl("audio/vnd.wave", ".wav");
        AddMimeEntryImpl("audio/mpeg", ".mp3");
        AddMimeEntryImpl("audio/ogg", ".ogg");
        AddMimeEntryImpl("image/x-icon", ".ico");
        AddMimeEntryImpl("image/svg+xml", ".svg,.svgz");
        AddMimeEntryImpl("application/font-woff", ".woff");// http://www.iana.org/assignments/media-types/application/font-woff
        AddMimeEntryImpl("application/font-sfnt", ".otf,.ttf"); // http://www.iana.org/assignments/media-types/application/font-sfnt
        AddMimeEntryImpl("application/vnd.ms-fontobject", ".eot");
        MimeTypeMap = new HashMap<String, MimeEntry>(MimeHashTable.getSize() * 2, 0.25f);
        MimeExtensionsMap = new HashMap<String, MimeEntry>(MimeHashTable.getSize() * 2, 0.25f);
        RebuildMimeMaps();
    }

    /**
     * Add a mime entry to the table.
     * If the type already exists, the 'extensions' will be added, otherwise a new type will be added with the specified extensions.
     * @param type   Mime formatted type
     * @param extensions   Comma separated list of dot (.) prefixed extensions.
     * @throws RuntimeException If the extensions string is not properly formatted (see parameters).
     */
    protected static void AddMimeEntry(String type, String extensions) {
        AddMimeEntryImpl(type, extensions);
        RebuildMimeMaps();
    }

    /**
     * <code>Hashtable</code> is synchronized, and there is no need for us to suffer that slow down.
     * This method rebuilds two <code>HashMap</code>s that we use to lookup <code>MimeEntry</code>s with.
     */
    private static void RebuildMimeMaps() {
        synchronized (MimeHashTable) {
            MimeTypeMap.clear();
            MimeExtensionsMap.clear();
            Enumeration<MimeEntry> elems = MimeHashTable.elements();
            while (elems.hasMoreElements()) {
                MimeEntry elem = elems.nextElement();
                MimeTypeMap.put(elem.getType(), elem);
                String[] extensions = elem.getExtensions();
                if (extensions != null)
                    for (String ext : extensions)
                        MimeExtensionsMap.put(ext, elem);
            }
        }
    }

    /**
     * @see AddMimeEntry
     */
    private static void AddMimeEntryImpl(String type, String extensions) {
        LinkedHashSet<String> extSet = new LinkedHashSet<String>();
        MimeEntry entry = MimeHashTable.find(type);
        if (entry == null)
            entry = new sun.net.www.MimeEntry(type.intern());
        // Ensure the type is an interned string
        entry.setType(type.intern());

        String[] existing = entry.getExtensions();
        if (existing != null)
            for (String ext : existing)
                extSet.add(ext);
        String[] additional = extensions.split(",");
        for (int i = 0; i < additional.length; i++) {
            additional[i] = additional[i].trim().toLowerCase();
            if (additional[i].length() == 0)
                throw new RuntimeException("Invalid mime extensions for: " + type);
            if (additional[i].charAt(0) != '.')
                throw new RuntimeException("mime extensions must start with a '.' (" + type + ")");
            extSet.add(additional[i]);
        }
        StringBuffer sb = new StringBuffer();
        for (String ext : extSet) {
            if (sb.length() > 0)
                sb.append(',');
            sb.append(ext);
        }
        entry.setExtensions(sb.toString());
        // This little hack ensures that the MimeEntry itself has interned strings in it's list.  Yes it's a trade off between bad practice and speed.
        String[] processed = entry.getExtensions();
        for (int i = 0; i < processed.length; i++)
            processed[i] = processed[i].intern();
    }

    /**
     * Return the underlying connection for this request / response transaction
     */
    private NHttpServerConnection getConnection() {
        return (NHttpServerConnection) context.getAttribute(HttpCoreContext.HTTP_CONNECTION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public InetAddress getLocalAddress() {
        HttpInetConnection inetConn = (HttpInetConnection) getConnection();
        return inetConn.getLocalAddress();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getLocalPort() {
        HttpInetConnection inetConn = (HttpInetConnection) getConnection();
        return inetConn.getLocalPort();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public InetAddress getRemoteAddress() {
        HttpInetConnection inetConn = (HttpInetConnection) getConnection();
        return inetConn.getRemoteAddress();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getRemotePort() {
        HttpInetConnection inetConn = (HttpInetConnection) getConnection();
        return inetConn.getRemotePort();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getHOSTName() {
        String val = null;
        Header hdr = request.getFirstHeader("HOST");
        if (hdr != null)
            val = hdr.getValue().trim();
        if ((val != null) && (val.length() > 0)) {
            int pos = val.indexOf(':');
            if (pos > 0)
                return val.substring(0, pos);
        }
        return getLocalAddress().getHostName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getHOSTPort() {
        String val = null;
        Header hdr = request.getFirstHeader("HOST");
        if (hdr != null)
            val = hdr.getValue().trim();
        if ((val != null) && (val.length() > 0)) {
            int pos = val.indexOf(':');
            if (pos > 0) {
                int end = val.indexOf(';', pos);
                if (end <= pos)
                    end = val.length();
                try {
                    return Integer.parseInt(val.substring(pos + 1, end));
                } catch (Throwable t) {
                    // Ignore any possible error.
                }
            }
        }
        if ("https".equals(getScheme()))
            return 443;
        return 80;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCharacterEncoding() {
        Header hdr = request.getFirstHeader("Content-Type");
        if (hdr != null)
            return GetCharsetFromContentType(hdr.getValue());
        return "utf-8";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DateFormat getHTTPDateFormater() {
        return Utils.GetHTTPDateFormater();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String formatDate(long millisecondsSinceEpoch) {
        return Utils.GetHTTPDateFormater().format(new Date(millisecondsSinceEpoch));
    }

    /**
     * Determine if a URI string has a <code>scheme</code> component.
     */
    public static boolean HasScheme(String uri) {
        int len = uri.length();
        for (int i = 0; i < len; i++) {
            char c = uri.charAt(i);
            if (c == ':')
                return i > 0;
            else if (!(Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.'))
                return false;
        }
        return false;
    }

    /**
     * Percent-encode values according the RFC 3986. The built-in Java URLEncoder does not encode according to the RFC, so we make the extra replacements.
     * 
     * @param string Decoded string.
     * @return Encoded string per RFC 3986.
     */
    public static String PercentEncodeRfc3986(final String string) {
        try {
            return URLEncoder.encode(string, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            return string;
        }
    }

    /**
     * Normalization code courtesy of 'Mike Houston' http://stackoverflow.com/questions/2993649/how-to-normalize-a-url-in-java
     */
    public static String NormalizeURL(final String taintedURL) throws MalformedURLException {
        final URL url;
        try {
            url = new URI(taintedURL).normalize().toURL();
        } catch (URISyntaxException e) {
            throw new MalformedURLException(e.getMessage());
        }

        final String path = url.getPath().replace("/$", "");
        final SortedMap<String, String> params = CreateParameterMap(url.getQuery());
        final int port = url.getPort();
        final String queryString;

        if (params != null) {
            // Some params are only relevant for user tracking, so remove the most commons ones.
            for (Iterator<String> i = params.keySet().iterator(); i.hasNext();) {
                final String key = i.next();
                if (key.startsWith("utm_") || key.contains("session"))
                    i.remove();
            }
            queryString = "?" + Canonicalize(params);
        } else
            queryString = "";

        return url.getProtocol() + "://" + url.getHost() + (port != -1 && port != 80 ? ":" + port : "") + path
                + queryString;
    }

    /**
     * Takes a query string, separates the constituent name-value pairs, and stores them in a SortedMap ordered by lexicographical order.
     * 
     * @return Null if there is no query string.
     */
    private static SortedMap<String, String> CreateParameterMap(final String queryString) {
        if (queryString == null || queryString.isEmpty())
            return null;
        final String[] pairs = queryString.split("&");
        final Map<String, String> params = new HashMap<String, String>(pairs.length);
        for (final String pair : pairs) {
            if (pair.length() < 1)
                continue;
            String[] tokens = pair.split("=", 2);
            for (int j = 0; j < tokens.length; j++) {
                try {
                    tokens[j] = URLDecoder.decode(tokens[j], "UTF-8");
                } catch (UnsupportedEncodingException ex) {
                    ex.printStackTrace();
                }
            }
            switch (tokens.length) {
            case 0:
                break;
            case 1:
                if (pair.charAt(0) == '=')
                    params.put("", tokens[0]);
                else
                    params.put(tokens[0], "");
                break;
            case 2:
            default:
                params.put(tokens[0], tokens[1]);
                break;
            }
        }
        return new TreeMap<String, String>(params);
    }

    /**
     * Canonicalize the query string.
     * 
     * @param sortedParamMap Parameter name-value pairs in lexicographical order.
     * @return Canonical form of query string.
     */
    private static String Canonicalize(final SortedMap<String, String> sortedParamMap) {
        if (sortedParamMap == null || sortedParamMap.isEmpty())
            return "";
        final StringBuffer sb = new StringBuffer(350);
        final Iterator<Map.Entry<String, String>> iter = sortedParamMap.entrySet().iterator();
        while (iter.hasNext()) {
            final Map.Entry<String, String> pair = iter.next();
            sb.append(PercentEncodeRfc3986(pair.getKey()));
            sb.append('=');
            sb.append(PercentEncodeRfc3986(pair.getValue()));
            if (iter.hasNext())
                sb.append('&');
        }
        return sb.toString();
    }

    /**
     * Parse the character encoding from the specified content type header. 
     * If the content type is null, or there is no explicit character encoding, <code>null</code> is returned.
     * 
     * @param contentType a content type header
     */
    public static String GetCharsetFromContentType(String contentType) {
        if (contentType == null)
            return null;
        int start = contentType.indexOf("charset=");
        if (start < 0)
            return null;
        String encoding = contentType.substring(start + 8);
        int end = encoding.indexOf(';');
        if (end >= 0)
            encoding = encoding.substring(0, end);
        encoding = encoding.trim();
        if ((encoding.length() > 2) && (encoding.startsWith("\"")) && (encoding.endsWith("\"")))
            encoding = encoding.substring(1, encoding.length() - 1);
        return encoding.trim();
    }
}