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

org.dstadler.commons.http.NanoHTTPD Maven / Gradle / Ivy

There is a newer version: 1.3.4
Show newest version
package org.dstadler.commons.http;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.FastDateFormat;

/**
 * A simple, tiny, nicely embeddable HTTP 1.0 server in Java
 *
 * 

NanoHTTPD version 1.14, * Copyright © 2001,2005-2010 Jarno Elonen ([email protected], http://iki.fi/elonen/) * *

Features + limitations:

    * *
  • Only one Java file
  • *
  • Java 1.1 compatible
  • *
  • Released as open source, Modified BSD licence
  • *
  • No fixed config files, logging, authorization etc. (Implement yourself if you need them.)
  • *
  • Supports parameter parsing of GET and POST methods
  • *
  • Supports both dynamic content and file serving
  • *
  • Never caches anything
  • *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • *
  • Default code serves files and shows all HTTP parameters and headers
  • *
  • File server supports directory listing, index.html and index.htm
  • *
  • File server does the 301 redirection trick for directories without '/'
  • *
  • File server supports simple skipping for files (continue download)
  • *
  • File server uses current directory as a web root
  • *
  • File server serves also very long files without memory overhead
  • *
  • Contains a built-in list of most common mime types
  • *
  • All header names are converted lowercase so they don't vary between browsers/clients
  • * *
* *

Ways to use:

    * *
  • Run as a standalone app, serves files from current directory and shows requests
  • *
  • Subclass serve() and embed to your own program
  • *
  • Call serveFile() from serve() with your own base directory
  • * *
* * See the end of the source file for distribution license * (Modified BSD licence) */ public class NanoHTTPD { private final static Logger logger = Logger.getLogger(NanoHTTPD.class.getName()); /** * GMT date formatter, have a local instance to avoid multi-threading issues */ private static FastDateFormat gmtFrmt = FastDateFormat.getInstance( "E, d MMM yyyy HH:mm:ss 'GMT'", TimeZone.getTimeZone("GMT"), Locale.US); private static String encoding = null; public static void setEncoding(String encoding) { NanoHTTPD.encoding = encoding; } // ================================================== // API parts // ================================================== /** * Override this to customize the server.

* * (By default, this delegates to serveFile() and allows directory listing.) * * @parm uri Percent-decoded URI without parameters, for example "/index.cgi" * @parm method "GET", "POST" etc. * @parm parms Parsed, percent decoded parameters from URI and, in case of POST, data. * @parm header Header entries, percent decoded * @return HTTP response, see class Response for details */ public Response serve( String uri, String method, Properties header, Properties parms ) { System.out.println( method + " '" + uri + "' " ); Enumeration e = header.propertyNames(); while ( e.hasMoreElements()) { String value = (String)e.nextElement(); System.out.println( " HDR: '" + value + "' = '" + header.getProperty( value ) + "'" ); } e = parms.propertyNames(); while ( e.hasMoreElements()) { String value = (String)e.nextElement(); System.out.println( " PRM: '" + value + "' = '" + parms.getProperty( value ) + "'" ); } return serveFile( uri, header, new File("."), true ); } /** * HTTP response. * Return one of these from serve(). */ public static class Response { /** * Default constructor: response = HTTP_OK, data = mime = 'null' */ public Response() { this.status = HTTP_OK; } /** * Basic constructor. */ public Response( String status, String mimeType, InputStream data ) { this.status = status; this.mimeType = mimeType; this.data = data; } /** * Convenience method that makes an InputStream out of * given text. */ public Response( String status, String mimeType, String txt ) { this.status = status; this.mimeType = mimeType; try { this.data = new ByteArrayInputStream( encoding != null ? txt.getBytes(encoding) : txt.getBytes()); // NOSONAR } catch (UnsupportedEncodingException e) { logger.log(Level.WARNING, "Unsupported encoding: " + encoding, e); } } /** * Adds given line to the header. */ public void addHeader( String name, String value ) { header.put( name, value ); } /** * HTTP status code after processing, e.g. "200 OK", HTTP_OK */ public String status; /** * MIME type of content, e.g. "text/html" */ public String mimeType; /** * Data of the response, may be null. */ public InputStream data; /** * Headers for the HTTP response. Use addHeader() * to add lines. */ public Properties header = new Properties(); } /** * Some HTTP response status codes */ public static final String HTTP_OK = "200 OK", HTTP_REDIRECT = "301 Moved Permanently", HTTP_FORBIDDEN = "403 Forbidden", HTTP_NOTFOUND = "404 Not Found", HTTP_BADREQUEST = "400 Bad Request", HTTP_INTERNALERROR = "500 Internal Server Error", HTTP_NOTIMPLEMENTED = "501 Not Implemented"; /** * Common mime types for dynamic content */ public static final String MIME_PLAINTEXT = "text/plain", MIME_HTML = "text/html", MIME_DEFAULT_BINARY = "application/octet-stream"; // ================================================== // Socket & server code // ================================================== /** * Starts a HTTP server to given port and binds on all host-names.

* Throws an IOException if the socket is already in use * */ public NanoHTTPD( int port) throws IOException { this(port, null); } /** * Starts a HTTP server to given port and only binds on the given name if specifed non-null.

* Throws an IOException if the socket is already in use */ public NanoHTTPD( int port, InetAddress bindHost ) throws IOException { myServerSocket = new ServerSocket( port, 50, bindHost ); myThread = new Thread("NanoHTTPD Micro Webserver Thread") { @Override public void run() { try { while( true ) { HTTPSession httpSession = new HTTPSession( myServerSocket.accept()); httpSession.start(); } } catch ( IOException ioe ) // NOPMD - imported code {} } }; myThread.setDaemon( true ); myThread.start(); } /** * Stops the server. */ public void stop() { try { myServerSocket.close(); myThread.join(); } catch ( IOException ioe ) { logger.log(Level.WARNING, "Had unexpected exception during stop", ioe); } catch ( InterruptedException e ) { logger.log(Level.WARNING, "Had unexpected exception during stop", e); } } /** * Starts as a standalone file server and waits for Enter. * @throws IOException */ public static void main( String[] args ) throws IOException { System.out.println( "NanoHTTPD 1.14 (C) 2001,2005-2010 Jarno Elonen\n" + "(Command line options: [port] [--licence])\n" ); // Show licence if requested int lopt = -1; for ( int i=0; i 0 && lopt != 0 ) { port = Integer.parseInt( args[0] ); } if ( args.length > 1 && args[1].toLowerCase().endsWith( "licence" )) { System.out.println( LICENCE + "\n" ); } NanoHTTPD nh = null; try { nh = new NanoHTTPD( port ); } catch( IOException ioe ) { System.err.println( "Couldn't start server:\n" + ioe ); System.exit( -1 ); return; } System.out.println( "Now serving files in port " + port + " from \"" + new File("").getAbsolutePath() + "\"" ); System.out.println( "Hit Enter to stop.\n" ); try { System.in.read(); } finally { nh.stop(); } } /** * Handles one session, i.e. parses the HTTP request * and returns the response. */ private class HTTPSession implements Runnable { public HTTPSession( Socket s ) { mySocket = s; } public void start() { Thread t = new Thread( this, "NanoHTTPD Session Thread" ); t.setDaemon( true ); t.start(); } @Override public void run() { try { InputStream is = mySocket.getInputStream(); if ( is == null) { return; } BufferedReader in = new BufferedReader( new InputStreamReader( is )); // NOSONAR - test class works only locally anyway // Read the request line String inLine = in.readLine(); try { if (inLine == null) { return; } StringTokenizer st = new StringTokenizer( inLine ); if ( !st.hasMoreTokens()) { sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" ); } String method = st.nextToken(); if ( !st.hasMoreTokens()) { sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" ); } String uri = st.nextToken(); // Decode parameters from the URI Properties parms = new Properties(); int qmi = uri.indexOf( '?' ); if ( qmi >= 0 ) { decodeParms( uri.substring( qmi+1 ), parms ); uri = decodePercent( uri.substring( 0, qmi )); } else { uri = decodePercent(uri); } // If there's another token, it's protocol version, // followed by HTTP headers. Ignore version but parse headers. // NOTE: this now forces header names uppercase since they are // case insensitive and vary by client. Properties header = new Properties(); if ( st.hasMoreTokens()) { readProperties(in, header); } // If the method is POST, there may be parameters // in data section, too, read it: if ( method.equalsIgnoreCase( "POST" )) { handlePOST(in, parms, header); } // Ok, now do the serve() Response r = serve( uri, method, header, parms ); if ( r == null ) { // NOSONAR - server() can be overwritten and thus could return null! sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." ); } else { sendResponse( r.status, r.mimeType, r.header, r.data ); } } catch ( InterruptedException ie ) { // Thrown by sendError, ignore and exit the thread. } catch (Throwable e) { // NOSONAR - test class works only locally anyway logger.log(Level.WARNING, "Had Exception in HTTPSession handling thread", e); String msg = "Exception in HTTPSession handling thread, error: " + e.getMessage() + ""; try { sendError( HTTP_INTERNALERROR, msg); } catch ( Throwable t ) {} // NOPMD - imported code } finally { in.close(); } } catch ( IOException ioe ) { try { sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); } catch ( Throwable t ) {} // NOPMD - imported code } } private void readProperties(BufferedReader in, Properties header) throws IOException { String line = in.readLine(); while ( line != null && line.trim().length() > 0 ) { int p = line.indexOf( ':' ); header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim()); line = in.readLine(); } } private void handlePOST(BufferedReader in, Properties parms, Properties header) throws IOException, InterruptedException { long size = 0x7FFFFFFFFFFFFFFFl; String contentLength = header.getProperty("content-length"); if (contentLength != null) { try { size = Integer.parseInt(contentLength); } catch (NumberFormatException ex) {} // NOPMD - imported code } StringBuilder postLine = new StringBuilder(); char buf[] = new char[512]; int read = in.read(buf); while ( read >= 0 && size > 0 && !postLine.toString().endsWith("\r\n") ) { size -= read; postLine.append(String.valueOf(buf, 0, read)); if ( size > 0 ) { read = in.read(buf); } } decodeParms( postLine.toString().trim(), parms ); } /** * Decodes the percent encoding scheme.
* For example: "an+example%20string" -> "an example string" */ private String decodePercent( String str ) throws InterruptedException { try { StringBuffer sb = new StringBuffer(); for( int i=0; i e = header.keys(); while ( e.hasMoreElements()) { String key = (String)e.nextElement(); String value = header.getProperty( key ); pw.print( key + ": " + value + "\r\n"); } } pw.print("\r\n"); pw.flush(); if ( data != null ) { IOUtils.copy(data, out); } out.flush(); } } if ( data != null ) { data.close(); } } catch( IOException ioe ) { // Couldn't write? No can do. try { mySocket.close(); } catch( Throwable t ) {} // NOPMD - imported code } } private Socket mySocket; } /** * URL-encodes everything between '/'-characters. * Encodes spaces as '%20' instead of '+'. */ private String encodeUri( String uri ) { StringBuilder newUri = new StringBuilder(); StringTokenizer st = new StringTokenizer( uri, "/ ", true ); while ( st.hasMoreTokens()) { String tok = st.nextToken(); if ( tok.equals( "/" )) { newUri.append('/'); } else if ( tok.equals( " " )) { newUri.append("%20"); } else { try { newUri.append(URLEncoder.encode( tok, "UTF-8" )); } catch ( UnsupportedEncodingException uee ) {} // NOPMD - imported code } } return newUri.toString(); } private final ServerSocket myServerSocket; private Thread myThread; // ================================================== // File server code // ================================================== /** * Serves file from homeDir and its' subdirectories (only). * Uses only URI, ignores all headers and HTTP parameters. */ public Response serveFile( String uriIn, Properties header, File homeDir, boolean allowDirectoryListing ) { // Make sure we won't die of an exception later if ( !homeDir.isDirectory()) { return new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT, "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." ); } // Remove URL arguments String uri = uriIn.trim().replace( File.separatorChar, '/' ); if ( uri.indexOf( '?' ) >= 0 ) { uri = uri.substring(0, uri.indexOf( '?' )); } // Prohibit getting out of current directory if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 ) { return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons." ); } File f = new File( homeDir, uri ); if ( !f.exists()) { return new Response( HTTP_NOTFOUND, MIME_PLAINTEXT, "Error 404, file not found." ); } // List the directory, if necessary if ( f.isDirectory()) { // Browsers get confused without '/' after the // directory, send a redirect. if ( !uri.endsWith( "/" )) { uri += '/'; Response r = new Response( HTTP_REDIRECT, MIME_HTML, "Redirected: " + uri + ""); r.addHeader( "Location", uri ); return r; } // First try index.html and index.htm if ( new File( f, "index.html" ).exists()) { f = new File( homeDir, uri + "/index.html" ); } else if ( new File( f, "index.htm" ).exists()) { f = new File( homeDir, uri + "/index.htm" ); } else if ( allowDirectoryListing ) { String msg = createDirListing(uri, f); return new Response( HTTP_OK, MIME_HTML, msg ); } else { return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: No directory listing." ); } } try { // Get MIME type from file name extension, if possible String mime = getMIMEType(f); // Support (simple) skipping: long startFrom = getRange(header); try (InputStream fis = new FileInputStream( f )) { if(fis.skip( startFrom ) != startFrom) { logger.info("Skipped less bytes than expected: " + startFrom); } Response r = new Response( HTTP_OK, mime, fis ); r.addHeader( "Content-length", "" + (f.length() - startFrom)); r.addHeader( "Content-range", "" + startFrom + "-" + (f.length()-1) + '/' + f.length()); return r; } } catch( IOException ioe ) { return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." ); } } private long getRange(Properties header) { String range = header.getProperty( "range" ); if ( range != null && range.startsWith( "bytes=" )) { range = range.substring( "bytes=".length()); int minus = range.indexOf( '-' ); if ( minus >= 0 ) { range = range.substring( 0, minus ); } try { return Long.parseLong( range ); } catch ( NumberFormatException nfe ) { logger.log(Level.WARNING, "Could not parse range specified in the headers: " + range, nfe); } } return 0; } private String getMIMEType(File f) throws IOException { String mime = null; int dot = f.getCanonicalPath().lastIndexOf( '.' ); if ( dot >= 0 ) { mime = theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase()); } if ( mime == null ) { mime = MIME_DEFAULT_BINARY; } return mime; } private String createDirListing(String uri, File f) { String[] files = f.list(); String msg = "

Directory " + uri + "


"; if ( uri.length() > 1 ) { String u = uri.substring( 0, uri.length()-1 ); int slash = u.lastIndexOf( '/' ); if ( slash >= 0 && slash < u.length()) { msg += "..
"; } } for ( int i=0; i" + files[i] + ""; // Show file size if ( curFile.isFile()) { long len = curFile.length(); msg += "  ("; if ( len < 1024 ) { msg += curFile.length() + " bytes"; } else if ( len < 1024 * 1024 ) { msg += curFile.length()/1024 + "." + (curFile.length()%1024/10%100) + " KB"; } else { msg += curFile.length()/(1024*1024) + "." + curFile.length()%(1024*1024)/10%100 + " MB"; } msg += ")"; } msg += "
"; if ( dir ) { msg += ""; } } return msg; } /** * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE */ private static Map theMimeTypes = new HashMap<>(); static { StringTokenizer st = new StringTokenizer( "htm text/html "+ "html text/html "+ "txt text/plain "+ "asc text/plain "+ "gif image/gif "+ "jpg image/jpeg "+ "jpeg image/jpeg "+ "png image/png "+ "mp3 audio/mpeg "+ "m3u audio/mpeg-url " + "pdf application/pdf "+ "doc application/msword "+ "ogg application/x-ogg "+ "zip application/octet-stream "+ "exe application/octet-stream "+ "class application/octet-stream " ); while ( st.hasMoreTokens()) { theMimeTypes.put( st.nextToken(), st.nextToken()); } } /** * The distribution licence */ private static final String LICENCE = "Copyright (C) 2001,2005-2010 by Jarno Elonen \n"+ "\n"+ "Redistribution and use in source and binary forms, with or without\n"+ "modification, are permitted provided that the following conditions\n"+ "are met:\n"+ "\n"+ "Redistributions of source code must retain the above copyright notice,\n"+ "this list of conditions and the following disclaimer. Redistributions in\n"+ "binary form must reproduce the above copyright notice, this list of\n"+ "conditions and the following disclaimer in the documentation and/or other\n"+ "materials provided with the distribution. The name of the author may not\n"+ "be used to endorse or promote products derived from this software without\n"+ "specific prior written permission. \n"+ " \n"+ "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+ "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+ "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+ "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+ "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+ "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+ "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+ "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+ "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."; }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy