org.nectarframework.base.service.nanohttp.NanoHttpService.java Source code

Java tutorial

Introduction

Here is the source code for org.nectarframework.base.service.nanohttp.NanoHttpService.java

Source

package org.nectarframework.base.service.nanohttp;

/*
 * #%L
 * NanoHttpd-Core
 * %%
 * Copyright (C) 2012 - 2015 nanohttpd
 * %%
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the nanohttpd nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.nectarframework.base.action.Action;
import org.nectarframework.base.exception.ConfigurationException;
import org.nectarframework.base.exception.ServiceUnavailableException;
import org.nectarframework.base.form.Form;
import org.nectarframework.base.service.Log;
import org.nectarframework.base.service.Service;
import org.nectarframework.base.service.ServiceParameters;
import org.nectarframework.base.service.ServiceRegister;
import org.nectarframework.base.service.file.FileInfo;
import org.nectarframework.base.service.file.FileService;
import org.nectarframework.base.service.file.ReadFileAccessDeniedException;
import org.nectarframework.base.service.file.ReadFileNotAFileException;
import org.nectarframework.base.service.file.ReadFileNotFoundException;
import org.nectarframework.base.service.http.RawAction;
import org.nectarframework.base.service.pathfinder.ActionResolution;
import org.nectarframework.base.service.pathfinder.ActionResolution.OutputType;
import org.nectarframework.base.service.template.TemplateService;
import org.nectarframework.base.service.template.thymeleaf.ThymeleafService;
import org.nectarframework.base.service.pathfinder.AliasResolution;
import org.nectarframework.base.service.pathfinder.PathFinderService;
import org.nectarframework.base.service.pathfinder.ProxyResolution;
import org.nectarframework.base.service.pathfinder.RedirectResolution;
import org.nectarframework.base.service.pathfinder.StaticResolution;
import org.nectarframework.base.service.pathfinder.UriResolution;
import org.nectarframework.base.service.thread.ThreadService;
import org.nectarframework.base.service.xml.Element;
import org.nectarframework.base.service.xml.XmlService;
import org.nectarframework.base.tools.FastByteArrayOutputStream;
import org.nectarframework.base.tools.IoTools;
import org.nectarframework.base.tools.StringTools;

/**
 * A simple, tiny, nicely embeddable HTTP server in Java
 * <p/>
 * <p/>
 * NanoHTTPD
 * <p>
 * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen,
 * 2010 by Konstantinos Togias
 * </p>
 * <p/>
 * <p/>
 * <b>Features + limitations: </b>
 * <ul>
 * <p/>
 * <li>Only one Java file</li>
 * <li>Java 5 compatible</li>
 * <li>Released as open source, Modified BSD licence</li>
 * <li>No fixed config files, logging, authorization etc. (Implement yourself if
 * you need them.)</li>
 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT
 * support in 1.25)</li>
 * <li>Supports both dynamic content and file serving</li>
 * <li>Supports file upload (since version 1.2, 2010)</li>
 * <li>Supports partial content (streaming)</li>
 * <li>Supports ETags</li>
 * <li>Never caches anything</li>
 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
 * <li>Default code serves files and shows all HTTP parameters and headers</li>
 * <li>File server supports directory listing, index.html and index.htm</li>
 * <li>File server supports partial content (streaming)</li>
 * <li>File server supports ETags</li>
 * <li>File server does the 301 redirection trick for directories without '/'
 * </li>
 * <li>File server supports simple skipping for files (continue download)</li>
 * <li>File server serves also very long files without memory overhead</li>
 * <li>Contains a built-in list of most common MIME types</li>
 * <li>All header names are converted to lower case so they don't vary between
 * browsers/clients</li>
 * <p/>
 * </ul>
 * <p/>
 * <p/>
 * <b>How to use: </b>
 * <ul>
 * <p/>
 * <li>Subclass and implement serve() and embed to your own program</li>
 * <p/>
 * </ul>
 * <p/>
 * See the separate "LICENSE.md" file for the distribution license (Modified BSD
 * licence)
 */
public class NanoHttpService extends Service {

    /**
     * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
     * This is required as the Keep-Alive HTTP connections would otherwise block
     * the socket reading thread forever (or as long the browser is open).
     */
    private int socketReadTimeout = 15000;

    private String listeningHost = "127.0.0.1";

    private int listeningSSLPort = 443;

    private int sslSocketBacklog = 20;

    private int listeningPort = 80;

    private int socketBacklog = 20;

    private PathFinderService pathFinderService;

    private Thread myThread;

    private String keyStoreFilePath;

    private ThreadService threadService;

    private ServerSocket sslServerSocket;

    private ServerSocket simpleServerSocket;

    private OutputType defaultOutput = OutputType.template;

    private XmlService xmlService;

    private FileService fileService;

    private TemplateService templateService;

    private String staticFileLocalDirectory;

    private ThymeleafService thymeleafService;

    private long staticFileCacheExpiry;

    /**
     * Common MIME type for dynamic content: plain text
     */
    public static final String MIME_PLAINTEXT = "text/plain";

    /**
     * Common MIME type for dynamic content: html
     */
    public static final String MIME_HTML = "text/html";

    /**
     * Pseudo-Parameter to use to store the actual query string in the
     * parameters map for later re-processing.
     */
    static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";

    @Override
    public void checkParameters(ServiceParameters sp) throws ConfigurationException {
        socketReadTimeout = sp.getInt("socketReadTimeout", 0, Integer.MAX_VALUE, 15000);
        listeningHost = sp.getString("listeningHost", null);
        listeningPort = sp.getInt("listeningPort", 1, Short.MAX_VALUE, 80);
        listeningSSLPort = sp.getInt("listeningSSLPort", 1, Short.MAX_VALUE, 443);
        keyStoreFilePath = sp.getString("keyStoreFilePath", "config/keystore.jks");

        staticFileLocalDirectory = sp.getString("staticFileLocalDirectory", "public_root");

        staticFileCacheExpiry = sp.getInt("staticFileCacheExpiry", -1, Integer.MAX_VALUE, 24 * 60 * 60 * 1000); // 24
        // hours
    }

    @Override
    public boolean establishDependencies() throws ServiceUnavailableException {
        threadService = (ThreadService) dependency(ThreadService.class);
        pathFinderService = (PathFinderService) dependency(PathFinderService.class);
        xmlService = (XmlService) this.dependency(XmlService.class);
        fileService = (FileService) this.dependency(FileService.class);
        templateService = (TemplateService) this.dependency(TemplateService.class);
        return true;
    }

    @Override
    protected boolean init() {
        thymeleafService = (ThymeleafService) ServiceRegister.getService(ThymeleafService.class);
        return true;
    }

    @Override
    protected boolean run() {
        Log.trace(pathFinderService.dumpConfig());
        try {

            try {
                this.sslServerSocket = makeSSLServerSocket(keyStoreFilePath, ("suckre").toCharArray());
            } catch (IOException e1) {
                Log.warn("NanoHTTP couldn't start SSL because of missing keyfile.", e1);
                this.sslServerSocket = null;
            }
            this.simpleServerSocket = new ServerSocket();

            this.simpleServerSocket.setReuseAddress(true);

            ServerRunnable serverRunnable = new ServerRunnable(socketReadTimeout, simpleServerSocket,
                    this.listeningHost, this.listeningPort, threadService, this, fileService);
            this.myThread = new Thread(serverRunnable);
            this.myThread.setDaemon(true);
            this.myThread.setName("NanoHttpService Main Listener");
            this.myThread.start();
            while (!serverRunnable.isBinded() && serverRunnable.getBindException() == null) {
                try {
                    Thread.sleep(10L);
                } catch (Throwable t) {
                    // on android this may not be allowed, that's why we
                    // catch throwable the wait should be very short because we
                    // are
                    // just waiting for the bind of the socket
                }
            }
            if (serverRunnable.getBindException() != null) {
                throw serverRunnable.getBindException();
            }
        } catch (IOException e) {
            Log.fatal("NanoHttpService failed to start:", e);
            return false;
        }

        return true;
    }

    @Override
    protected boolean shutdown() {
        try {
            IoTools.safeClose(this.simpleServerSocket);
            IoTools.safeClose(this.sslServerSocket);
            if (this.myThread != null) {
                this.myThread.join();
            }
        } catch (Exception e) {
            Log.warn("Could not stop all connections", e);
            return false;
        }
        return true;
    }

    /**
     * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
     * certificate and passphrase
     */
    public ServerSocket makeSSLServerSocket(String keyAndTrustStoreClasspathPath, char[] passphrase)
            throws IOException {
        try {
            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            InputStream keystoreStream = new FileInputStream(new File(keyAndTrustStoreClasspathPath));

            keystore.load(keystoreStream, passphrase);
            KeyManagerFactory keyManagerFactory = KeyManagerFactory
                    .getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keystore, passphrase);

            SSLServerSocketFactory res = null;
            try {
                TrustManagerFactory trustManagerFactory = TrustManagerFactory
                        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
                trustManagerFactory.init(keystore);
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
                res = ctx.getServerSocketFactory();

            } catch (Exception e) {
                throw new IOException(e.getMessage());
            }

            SSLServerSocket ss = null;
            ss = (SSLServerSocket) res.createServerSocket();
            ss.setEnabledProtocols(ss.getSupportedProtocols());
            ss.setUseClientMode(false);
            ss.setWantClientAuth(false);
            ss.setNeedClientAuth(false);

            return ss;

        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * create a instance of the client handler, subclasses can return a subclass
     * of the ClientHandler.
     * 
     * @param finalAccept
     *            the socket the cleint is connected to
     * @param inputStream
     *            the input stream
     * @return the client handler
     */
    protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
        return new ClientHandler(inputStream, finalAccept, this, fileService);
    }

    /**
     * Create a response with unknown length (using HTTP 1.1 chunking).
     */
    public static Response newChunkedResponse(Status status, String mimeType, InputStream data) {
        return new Response(status, mimeType, data, -1);
    }

    /**
     * Create a response with known length.
     */
    public static Response newFixedLengthResponse(Status status, String mimeType, InputStream data,
            long totalBytes) {
        return new Response(status, mimeType, data, totalBytes);
    }

    /**
     * Create a text response with known length.
     */
    public static Response newFixedLengthResponse(Status status, String mimeType, String txt) {
        ContentType contentType = new ContentType(mimeType);
        if (txt == null) {
            return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
        } else {
            byte[] bytes;
            try {
                CharsetEncoder newEncoder = Charset.forName(contentType.getEncoding()).newEncoder();
                if (!newEncoder.canEncode(txt)) {
                    contentType = contentType.tryUTF8();
                }
                bytes = txt.getBytes(contentType.getEncoding());
            } catch (UnsupportedEncodingException e) {
                Log.warn("encoding problem, responding nothing", e);
                bytes = new byte[0];
            }
            return newFixedLengthResponse(status, contentType.getContentTypeHeader(),
                    new ByteArrayInputStream(bytes), bytes.length);
        }
    }

    /**
     * Create a text response with known length.
     */
    public static Response newFixedLengthResponse(String msg) {
        return newFixedLengthResponse(Status.OK, NanoHttpService.MIME_HTML, msg);
    }

    /**
     * Override this to customize the server.
     * <p/>
     * <p/>
     * (By default, this returns a 404 "Not Found" plain text error response.)
     * 
     * @param session
     *            The HTTP session
     * @return HTTP response, see class Response for details
     */
    public Response serve(HTTPSession session) {
        Map<String, String> files = new HashMap<String, String>();
        Method method = session.getMethod();
        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
            try {
                session.parseBody(files);
            } catch (IOException ioe) {
                return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                        "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
            } catch (ResponseException re) {
                return newFixedLengthResponse(re.getStatus(), NanoHttpService.MIME_PLAINTEXT, re.getMessage());
            }
        }

        Map<String, List<String>> parms = session.getParameters();
        return serve(session.getUri(), method, session.getHeaders(), parms, session.getQueryParameterString(),
                files);
    }

    /**
     * Override this to customize the server.
     * <p/>
     * <p/>
     * (By default, this returns a 404 "Not Found" plain text error response.)
     * 
     * @param uri
     *            Percent-decoded URI without parameters, for example
     *            "/index.cgi"
     * @param method
     *            "GET", "POST" etc.
     * @param parms
     *            Parsed, percent decoded parameters from URI and, in case of
     *            POST, data.
     * @param headers
     *            Header entries, percent decoded
     * @return HTTP response, see class Response for details
     */
    public Response serve(String uri, Method method, Map<String, String> headers, Map<String, List<String>> parms,
            String queryParameterString, Map<String, String> files) {

        Log.trace("[NanoHttpService:serve] uri=" + uri + " method=" + method.toString() + " headers="
                + StringTools.mapToString(headers) + " parms=" + StringTools.mapToString(parms) + " query="
                + queryParameterString + " files=" + StringTools.mapToString(files));

        String hostHeader = headers.get("host");
        if (hostHeader == null) {
            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23
            // Host is an obligatory header
            return newFixedLengthResponse(Status.BAD_REQUEST, NanoHttpService.MIME_PLAINTEXT, "400 Bad Request");
        }

        if (!hostHeader.contains(":")) {
            hostHeader += ":80";
        }

        UriResolution uriResolution = pathFinderService.resolveUri(hostHeader, uri);

        if (uriResolution == null) {
            Log.warn("[NanoHttpService]serve: " + hostHeader + " " + uri + " NOT FOUND.");
            return newFixedLengthResponse(Status.NOT_FOUND, NanoHttpService.MIME_PLAINTEXT, "Not Found");
        }

        try {
            switch (uriResolution.getType()) {
            case Action:
                return serveAction((ActionResolution) uriResolution, uri, method, headers, parms,
                        queryParameterString, files);
            case Alias:
                return serveAlias((AliasResolution) uriResolution, uri, method, headers, parms,
                        queryParameterString, files);
            case Proxy:
                return serveProxy((ProxyResolution) uriResolution, uri, method, headers, parms,
                        queryParameterString, files);
            case Redirect:
                return serveRedirect((RedirectResolution) uriResolution, uri, method, headers, parms,
                        queryParameterString, files);
            case Static:
                return serveStatic((StaticResolution) uriResolution, uri, method, headers, parms,
                        queryParameterString, files);
            }
        } catch (Throwable e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                    "SERVER INTERNAL ERROR: Exception: " + e.getMessage());

        }

        return newFixedLengthResponse(Status.NOT_FOUND, NanoHttpService.MIME_PLAINTEXT, "Not Found");
    }

    private Response serveAction(ActionResolution actionResolution, String uri, Method method,
            Map<String, String> headers, Map<String, List<String>> parms, String queryParameterString,
            Map<String, String> files) {

        Action action = null;
        try {
            action = actionResolution.getActionClass().newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            Log.fatal(e);
            return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                    "SERVER INTERNAL ERROR: Exception: " + e.getMessage());
        }

        Form form = new Form(actionResolution.getForm(), parms);

        if (!form.isValid()) {
            Log.fatal("FormValidationException: ");// TODO add details
            return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                    "SERVER INTERNAL ERROR: FormValidationException");
        }

        action._init(form);
        // execute the action

        OutputType output = this.defaultOutput;
        if (actionResolution.getDefaultOutput() != null) {
            output = actionResolution.getDefaultOutput();
        }

        // Raw output

        Element elm = null;

        // the action's implementation runs now.
        elm = action.execute();

        FastByteArrayOutputStream outputByteArrayOutputStream = null;

        Locale locale = Locale.getDefault();
        if (form.getSession() != null) {
            locale = form.getSession().getLocale();
        }

        String mimeType = null;
        byte[] byteBuff = null;
        // processing the resulting element:
        switch (output) {
        case raw:
            mimeType = elm.get("httpContentType");
            byteBuff = ((RawAction) action).getRawByteArray();
            return newFixedLengthResponse(Status.OK, mimeType, new ByteArrayInputStream(byteBuff), byteBuff.length);
        case json:
            mimeType = "application/json";
            outputByteArrayOutputStream = new FastByteArrayOutputStream();
            try {
                XmlService.toNDOJson(elm, outputByteArrayOutputStream);
            } catch (IOException e) {
                return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                        "SERVER INTERNAL ERROR: IOException" + e.getMessage());
            }
            byteBuff = outputByteArrayOutputStream.toByteArray();
            return newFixedLengthResponse(Status.OK, mimeType, new ByteArrayInputStream(byteBuff), byteBuff.length);
        case template:
            mimeType = "text/html";
            outputByteArrayOutputStream = new FastByteArrayOutputStream();
            try {
                templateService.outputTemplate(outputByteArrayOutputStream, actionResolution.getTemplateName(),
                        locale, elm, null);
            } catch (IOException e) {
                return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                        "SERVER INTERNAL ERROR: IOException" + e.getMessage());
            }
            byteBuff = outputByteArrayOutputStream.toByteArray();
            return newFixedLengthResponse(Status.OK, mimeType, new ByteArrayInputStream(byteBuff), byteBuff.length);
        case thymeleaf:
            if (thymeleafService == null) {
                return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                        "SERVER INTERNAL ERROR: Thymeleaf Service not available.");
            } else {
                mimeType = "text/html";
                outputByteArrayOutputStream = new FastByteArrayOutputStream();
                thymeleafService.output(locale, "", actionResolution.getTemplateName(), elm,
                        outputByteArrayOutputStream);
                byteBuff = outputByteArrayOutputStream.toByteArray();
                return newFixedLengthResponse(Status.OK, mimeType, new ByteArrayInputStream(byteBuff),
                        byteBuff.length);
            }
        case xml:
            mimeType = "text/xml";
            outputByteArrayOutputStream = new FastByteArrayOutputStream();
            try {
                outputByteArrayOutputStream.write(
                        ("<?xml version=\"1.0\" encoding=\"" + Charset.defaultCharset().toString() + "\"?>\n")
                                .getBytes());
                XmlService.toXml(elm, outputByteArrayOutputStream);
            } catch (IOException e) {
                return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                        "SERVER INTERNAL ERROR: IOException" + e.getMessage());
            }
            byteBuff = outputByteArrayOutputStream.toByteArray();
            return newFixedLengthResponse(Status.OK, mimeType, new ByteArrayInputStream(byteBuff), byteBuff.length);
        case xsl:
            mimeType = "text/xsl";
            return newFixedLengthResponse(Status.SERVICE_UNAVAILABLE, NanoHttpService.MIME_PLAINTEXT,
                    "Not Implemented: ");
        default:
            break;
        }
        return newFixedLengthResponse(Status.SERVICE_UNAVAILABLE, NanoHttpService.MIME_PLAINTEXT,
                "Not Implemented: ");
    }

    private Response serveAlias(AliasResolution aliasResolution, String uri, Method method,
            Map<String, String> headers, Map<String, List<String>> parms, String queryParameterString,
            Map<String, String> files) {

        for (String key : aliasResolution.getVariables().keySet()) {
            if (!parms.containsKey(key)) {
                parms.put(key, new LinkedList<String>());
            }
            parms.get(key).addAll(aliasResolution.getVariables().get(key));
        }

        if (aliasResolution.isRelative()) {
            uri = uri.substring(0, uri.indexOf('/')) + aliasResolution.getToPath();
        } else {
            uri = aliasResolution.getToPath();
        }
        Log.trace(
                "[NanoHttpService]serveAlias: " + aliasResolution.getPath() + " to " + aliasResolution.getToPath());
        return serve(uri, method, headers, parms, queryParameterString, files);
    }

    private String getStaticLocalDirectory() {
        return staticFileLocalDirectory;
    }

    private Response serveStatic(StaticResolution uriResolution, String uri, Method method,
            Map<String, String> headers, Map<String, List<String>> parms, String queryParameterString,
            Map<String, String> files) {

        // TODO: hand off to FileService
        // remember to filter ../
        try {

            InputStream is = null;
            FileInfo fileInfo = fileService.getFileInfo("/" + getStaticLocalDirectory() + uri);
            is = fileService.getFileAsInputStream("/" + getStaticLocalDirectory() + uri,
                    this.staticFileCacheExpiry);

            String contentType = IoTools.getMimeTypeForFile(uri);
            if (contentType == null) {
                contentType = "application/octet-stream";
            }

            // Log.accessLog(this.request.getPath().getPath(), null, null, null,
            // stopwatch.sinceStart(),
            // request.getClientAddress().toString(), null);

            Log.trace(" Static Request processed");

            return newFixedLengthResponse(Status.OK, contentType, is, fileInfo.getLength());
        } catch (ReadFileNotFoundException e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.NOT_FOUND, NanoHttpService.MIME_PLAINTEXT, "File Not Found.");
        } catch (ReadFileAccessDeniedException e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.FORBIDDEN, NanoHttpService.MIME_PLAINTEXT, "Access Denied.");
        } catch (ReadFileNotAFileException e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.NOT_FOUND, NanoHttpService.MIME_PLAINTEXT, "Not a File.");
        } catch (IOException e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                    "IOException." + e.getMessage());
        }

    }

    private Response serveProxy(ProxyResolution proxyResolution, String uri, Method method,
            Map<String, String> headers, Map<String, List<String>> parms, String queryParameterString,
            Map<String, String> files) {

        String remoteTarget = uri.substring(proxyResolution.getPath().length() + 1);
        if (!remoteTarget.startsWith("/")) {
            remoteTarget = "/" + remoteTarget;
        }

        CloseableHttpClient httpclient = HttpClients.createDefault();
        URIBuilder remoteUri = new URIBuilder();
        remoteUri.setScheme("http");
        remoteUri.setHost(proxyResolution.getHost());
        remoteUri.setPort(proxyResolution.getPort());
        remoteUri.setPath(proxyResolution.getRequestPath() + remoteTarget);
        remoteUri.setCharset(Charset.defaultCharset());
        for (String k : parms.keySet()) {
            remoteUri.addParameter(k, parms.get(k).get(0));
        }

        HttpGet httpget;
        try {
            httpget = new HttpGet(remoteUri.build());
        } catch (URISyntaxException e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                    "SERVER INTERNAL ERROR: URISyntaxException" + e.getMessage());
        }

        CloseableHttpResponse response = null;
        Response resp = null;
        try {
            response = httpclient.execute(httpget);

            HttpEntity entity = response.getEntity();
            if (entity != null) {
                InputStream is = entity.getContent();
                long isl = entity.getContentLength();
                resp = new Response(Status.lookup(response.getStatusLine().getStatusCode()), null, is, isl);
            } else {
                resp = new Response(Status.lookup(response.getStatusLine().getStatusCode()), null, null, 0);
            }

            Header[] remoteHeaders = response.getAllHeaders();
            for (Header h : remoteHeaders) {
                resp.addHeader(h.getName(), h.getValue());
            }

            resp.setProxyResponse(response);

        } catch (IOException e) {
            Log.warn(e);
            return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHttpService.MIME_PLAINTEXT,
                    "SERVER INTERNAL ERROR: IOException" + e.getMessage());
        }

        return resp;
    }

    private Response serveRedirect(RedirectResolution redirectResolution, String uri, Method method,
            Map<String, String> headers, Map<String, List<String>> parms, String queryParameterString,
            Map<String, String> files) {

        Response response = new Response(Status.REDIRECT, null, null, 0);
        response.addHeader("Location", redirectResolution.getToUrl());
        return response;
    }

}