All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fi.iki.elonen.SimpleWebServer Maven / Gradle / Ivy

Go to download

nanohttpd-webserver can serve any local directory as a webserver using nanohttpd.

The newest version!
package fi.iki.elonen;

/*
 * #%L
 * NanoHttpd-Webserver
 * %%
 * 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.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.StringTokenizer;

import fi.iki.elonen.NanoHTTPD.Response.IStatus;
import fi.iki.elonen.util.ServerRunner;

public class SimpleWebServer extends NanoHTTPD {

    /**
     * Default Index file names.
     */
    @SuppressWarnings("serial")
    public static final List INDEX_FILE_NAMES = new ArrayList() {

        {
            add("index.html");
            add("index.htm");
        }
    };

    /**
     * The distribution licence
     */
    private static final String LICENCE;
    static {
        mimeTypes();
        String text;
        try {
            InputStream stream = SimpleWebServer.class.getResourceAsStream("/LICENSE.txt");
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int count;
            while ((count = stream.read(buffer)) >= 0) {
                bytes.write(buffer, 0, count);
            }
            text = bytes.toString("UTF-8");
        } catch (Exception e) {
            text = "unknown";
        }
        LICENCE = text;
    }

    private static Map mimeTypeHandlers = new HashMap();

    /**
     * Starts as a standalone file server and waits for Enter.
     */
    public static void main(String[] args) {
        // Defaults
        int port = 8080;

        String host = null; // bind to all interfaces by default
        List rootDirs = new ArrayList();
        boolean quiet = false;
        String cors = null;
        Map options = new HashMap();

        // Parse command-line, with short and long versions of the options.
        for (int i = 0; i < args.length; ++i) {
            if ("-h".equalsIgnoreCase(args[i]) || "--host".equalsIgnoreCase(args[i])) {
                host = args[i + 1];
            } else if ("-p".equalsIgnoreCase(args[i]) || "--port".equalsIgnoreCase(args[i])) {
                port = Integer.parseInt(args[i + 1]);
            } else if ("-q".equalsIgnoreCase(args[i]) || "--quiet".equalsIgnoreCase(args[i])) {
                quiet = true;
            } else if ("-d".equalsIgnoreCase(args[i]) || "--dir".equalsIgnoreCase(args[i])) {
                rootDirs.add(new File(args[i + 1]).getAbsoluteFile());
            } else if (args[i].startsWith("--cors")) {
                cors = "*";
                int equalIdx = args[i].indexOf('=');
                if (equalIdx > 0) {
                    cors = args[i].substring(equalIdx + 1);
                }
            } else if ("--licence".equalsIgnoreCase(args[i])) {
                System.out.println(SimpleWebServer.LICENCE + "\n");
            } else if (args[i].startsWith("-X:")) {
                int dot = args[i].indexOf('=');
                if (dot > 0) {
                    String name = args[i].substring(0, dot);
                    String value = args[i].substring(dot + 1, args[i].length());
                    options.put(name, value);
                }
            }
        }

        if (rootDirs.isEmpty()) {
            rootDirs.add(new File(".").getAbsoluteFile());
        }
        options.put("host", host);
        options.put("port", "" + port);
        options.put("quiet", String.valueOf(quiet));
        StringBuilder sb = new StringBuilder();
        for (File dir : rootDirs) {
            if (sb.length() > 0) {
                sb.append(":");
            }
            try {
                sb.append(dir.getCanonicalPath());
            } catch (IOException ignored) {
            }
        }
        options.put("home", sb.toString());
        ServiceLoader serviceLoader = ServiceLoader.load(WebServerPluginInfo.class);
        for (WebServerPluginInfo info : serviceLoader) {
            String[] mimeTypes = info.getMimeTypes();
            for (String mime : mimeTypes) {
                String[] indexFiles = info.getIndexFilesForMimeType(mime);
                if (!quiet) {
                    System.out.print("# Found plugin for Mime type: \"" + mime + "\"");
                    if (indexFiles != null) {
                        System.out.print(" (serving index files: ");
                        for (String indexFile : indexFiles) {
                            System.out.print(indexFile + " ");
                        }
                    }
                    System.out.println(").");
                }
                registerPluginForMimeType(indexFiles, mime, info.getWebServerPlugin(mime), options);
            }
        }
        ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet, cors));
    }

    protected static void registerPluginForMimeType(String[] indexFiles, String mimeType, WebServerPlugin plugin, Map commandLineOptions) {
        if (mimeType == null || plugin == null) {
            return;
        }

        if (indexFiles != null) {
            for (String filename : indexFiles) {
                int dot = filename.lastIndexOf('.');
                if (dot >= 0) {
                    String extension = filename.substring(dot + 1).toLowerCase();
                    mimeTypes().put(extension, mimeType);
                }
            }
            SimpleWebServer.INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles));
        }
        SimpleWebServer.mimeTypeHandlers.put(mimeType, plugin);
        plugin.initialize(commandLineOptions);
    }

    private final boolean quiet;

    private final String cors;

    protected List rootDirs;

    public SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors) {
        this(host, port, Collections.singletonList(wwwroot), quiet, cors);
    }

    public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
        this(host, port, Collections.singletonList(wwwroot), quiet, null);
    }

    public SimpleWebServer(String host, int port, List wwwroots, boolean quiet) {
        this(host, port, wwwroots, quiet, null);
    }

    public SimpleWebServer(String host, int port, List wwwroots, boolean quiet, String cors) {
        super(host, port);
        this.quiet = quiet;
        this.cors = cors;
        this.rootDirs = new ArrayList(wwwroots);

        init();
    }

    private boolean canServeUri(String uri, File homeDir) {
        boolean canServeUri;
        File f = new File(homeDir, uri);
        canServeUri = f.exists();
        if (!canServeUri) {
            WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(getMimeTypeForFile(uri));
            if (plugin != null) {
                canServeUri = plugin.canServeUri(uri, homeDir);
            }
        }
        return canServeUri;
    }

    /**
     * URL-encodes everything between "/"-characters. Encodes spaces as '%20'
     * instead of '+'.
     */
    private String encodeUri(String uri) {
        String newUri = "";
        StringTokenizer st = new StringTokenizer(uri, "/ ", true);
        while (st.hasMoreTokens()) {
            String tok = st.nextToken();
            if ("/".equals(tok)) {
                newUri += "/";
            } else if (" ".equals(tok)) {
                newUri += "%20";
            } else {
                try {
                    newUri += URLEncoder.encode(tok, "UTF-8");
                } catch (UnsupportedEncodingException ignored) {
                }
            }
        }
        return newUri;
    }

    private String findIndexFileInDirectory(File directory) {
        for (String fileName : SimpleWebServer.INDEX_FILE_NAMES) {
            File indexFile = new File(directory, fileName);
            if (indexFile.isFile()) {
                return fileName;
            }
        }
        return null;
    }

    protected Response getForbiddenResponse(String s) {
        return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
    }

    protected Response getInternalErrorResponse(String s) {
        return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
    }

    protected Response getNotFoundResponse() {
        return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
    }

    /**
     * Used to initialize and customize the server.
     */
    public void init() {
    }

    protected String listDirectory(String uri, File f) {
        String heading = "Directory " + uri;
        StringBuilder msg =
                new StringBuilder("" + heading + "" + "

" + heading + "

"); String up = null; if (uri.length() > 1) { String u = uri.substring(0, uri.length() - 1); int slash = u.lastIndexOf('/'); if (slash >= 0 && slash < u.length()) { up = uri.substring(0, slash + 1); } } List files = Arrays.asList(f.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return new File(dir, name).isFile(); } })); Collections.sort(files); List directories = Arrays.asList(f.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return new File(dir, name).isDirectory(); } })); Collections.sort(directories); if (up != null || directories.size() + files.size() > 0) { msg.append("
    "); if (up != null || directories.size() > 0) { msg.append("
    "); if (up != null) { msg.append("
  • ..
  • "); } for (String directory : directories) { String dir = directory + "/"; msg.append("
  • ").append(dir).append("
  • "); } msg.append("
    "); } if (files.size() > 0) { msg.append("
    "); for (String file : files) { msg.append("
  • ").append(file).append(""); File curFile = new File(f, file); long len = curFile.length(); msg.append(" ("); if (len < 1024) { msg.append(len).append(" bytes"); } else if (len < 1024 * 1024) { msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB"); } else { msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB"); } msg.append(")
  • "); } msg.append("
    "); } msg.append("
"); } msg.append(""); return msg.toString(); } public static Response newFixedLengthResponse(IStatus status, String mimeType, String message) { Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message); response.addHeader("Accept-Ranges", "bytes"); return response; } private Response respond(Map headers, IHTTPSession session, String uri) { // First let's handle CORS OPTION query Response r; if (cors != null && Method.OPTIONS.equals(session.getMethod())) { r = new NanoHTTPD.Response(Response.Status.OK, MIME_PLAINTEXT, null, 0); } else { r = defaultRespond(headers, session, uri); } if (cors != null) { r = addCORSHeaders(headers, r, cors); } return r; } private Response defaultRespond(Map headers, IHTTPSession session, String uri) { // Remove URL arguments uri = uri.trim().replace(File.separatorChar, '/'); if (uri.indexOf('?') >= 0) { uri = uri.substring(0, uri.indexOf('?')); } // Prohibit getting out of current directory if (uri.contains("../")) { return getForbiddenResponse("Won't serve ../ for security reasons."); } boolean canServeUri = false; File homeDir = null; for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) { homeDir = this.rootDirs.get(i); canServeUri = canServeUri(uri, homeDir); } if (!canServeUri) { return getNotFoundResponse(); } // Browsers get confused without '/' after the directory, send a // redirect. File f = new File(homeDir, uri); if (f.isDirectory() && !uri.endsWith("/")) { uri += "/"; Response res = newFixedLengthResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "Redirected: " + uri + ""); res.addHeader("Location", uri); return res; } if (f.isDirectory()) { // First look for index files (index.html, index.htm, etc) and if // none found, list the directory if readable. String indexFile = findIndexFileInDirectory(f); if (indexFile == null) { if (f.canRead()) { // No index file, list the directory if it is readable return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f)); } else { return getForbiddenResponse("No directory listing."); } } else { return respond(headers, session, uri + indexFile); } } String mimeTypeForFile = getMimeTypeForFile(uri); WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(mimeTypeForFile); Response response = null; if (plugin != null && plugin.canServeUri(uri, homeDir)) { response = plugin.serveFile(uri, headers, session, f, mimeTypeForFile); if (response != null && response instanceof InternalRewrite) { InternalRewrite rewrite = (InternalRewrite) response; return respond(rewrite.getHeaders(), session, rewrite.getUri()); } } else { response = serveFile(uri, headers, f, mimeTypeForFile); } return response != null ? response : getNotFoundResponse(); } @Override public Response serve(IHTTPSession session) { Map header = session.getHeaders(); Map parms = session.getParms(); String uri = session.getUri(); if (!this.quiet) { System.out.println(session.getMethod() + " '" + uri + "' "); Iterator e = header.keySet().iterator(); while (e.hasNext()) { String value = e.next(); System.out.println(" HDR: '" + value + "' = '" + header.get(value) + "'"); } e = parms.keySet().iterator(); while (e.hasNext()) { String value = e.next(); System.out.println(" PRM: '" + value + "' = '" + parms.get(value) + "'"); } } for (File homeDir : this.rootDirs) { // Make sure we won't die of an exception later if (!homeDir.isDirectory()) { return getInternalErrorResponse("given path is not a directory (" + homeDir + ")."); } } return respond(Collections.unmodifiableMap(header), session, uri); } /** * Serves file from homeDir and its' subdirectories (only). Uses only URI, * ignores all headers and HTTP parameters. */ Response serveFile(String uri, Map header, File file, String mime) { Response res; try { // Calculate etag String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode()); // Support (simple) skipping: long startFrom = 0; long endAt = -1; String range = header.get("range"); if (range != null) { if (range.startsWith("bytes=")) { range = range.substring("bytes=".length()); int minus = range.indexOf('-'); try { if (minus > 0) { startFrom = Long.parseLong(range.substring(0, minus)); endAt = Long.parseLong(range.substring(minus + 1)); } } catch (NumberFormatException ignored) { } } } // get if-range header. If present, it must match etag or else we // should ignore the range request String ifRange = header.get("if-range"); boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange)); String ifNoneMatch = header.get("if-none-match"); boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag)); // Change return code and add Content-Range header when skipping is // requested long fileLen = file.length(); if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) { // range request that matches current etag // and the startFrom of the range is satisfiable if (headerIfNoneMatchPresentAndMatching) { // range request that matches current etag // and the startFrom of the range is satisfiable // would return range from file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { if (endAt < 0) { endAt = fileLen - 1; } long newLen = endAt - startFrom + 1; if (newLen < 0) { newLen = 0; } FileInputStream fis = new FileInputStream(file); fis.skip(startFrom); res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen); res.addHeader("Accept-Ranges", "bytes"); res.addHeader("Content-Length", "" + newLen); res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); res.addHeader("ETag", etag); } } else { if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) { // return the size of the file // 4xx responses are not trumped by if-none-match res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, ""); res.addHeader("Content-Range", "bytes */" + fileLen); res.addHeader("ETag", etag); } else if (range == null && headerIfNoneMatchPresentAndMatching) { // full-file-fetch request // would return entire file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) { // range request that doesn't match current etag // would return entire (different) file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { // supply the file res = newFixedFileResponse(file, mime); res.addHeader("Content-Length", "" + fileLen); res.addHeader("ETag", etag); } } } catch (IOException ioe) { res = getForbiddenResponse("Reading file failed."); } return res; } private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException { Response res; res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length()); res.addHeader("Accept-Ranges", "bytes"); return res; } protected Response addCORSHeaders(Map queryHeaders, Response resp, String cors) { resp.addHeader("Access-Control-Allow-Origin", cors); resp.addHeader("Access-Control-Allow-Headers", calculateAllowHeaders(queryHeaders)); resp.addHeader("Access-Control-Allow-Credentials", "true"); resp.addHeader("Access-Control-Allow-Methods", ALLOWED_METHODS); resp.addHeader("Access-Control-Max-Age", "" + MAX_AGE); return resp; } private String calculateAllowHeaders(Map queryHeaders) { // here we should use the given asked headers // but NanoHttpd uses a Map whereas it is possible for requester to send // several time the same header // let's just use default values for this version return System.getProperty(ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, DEFAULT_ALLOWED_HEADERS); } private final static String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, HEAD"; private final static int MAX_AGE = 42 * 60 * 60; // explicitly relax visibility to package for tests purposes final static String DEFAULT_ALLOWED_HEADERS = "origin,accept,content-type"; public final static String ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME = "AccessControlAllowHeader"; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy