diff options
Diffstat (limited to 'src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java')
-rw-r--r-- | src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java | 1122 |
1 files changed, 1122 insertions, 0 deletions
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java new file mode 100644 index 0000000..53434be --- /dev/null +++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java @@ -0,0 +1,1122 @@ +package com.galois.fiveui; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLEncoder; +import java.util.Date; +import java.util.Enumeration; +import java.util.Vector; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TimeZone; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; + +/** + * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java + * + * <p> NanoHTTPD version 1.25, + * Copyright © 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) + * and Copyright © 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr) + * + * <p><b>Features + limitations: </b><ul> + * + * <li> Only one Java file </li> + * <li> Java 1.1 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 lowercase so they don't vary between browsers/clients </li> + * + * </ul> + * + * <p><b>Ways to use: </b><ul> + * + * <li> Run as a standalone app, serves files and shows requests</li> + * <li> Subclass serve() and embed to your own program </li> + * <li> Call serveFile() from serve() with your own base directory </li> + * + * </ul> + * + * See the end of the source file for distribution license + * (Modified BSD licence) + */ +public class NanoHTTPD +{ + // ================================================== + // API parts + // ================================================== + + /** + * Override this to customize the server.<p> + * + * (By default, this delegates to serveFile() and allows directory listing.) + * + * @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 header Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + public Response serve( String uri, String method, Properties header, Properties parms, Properties files ) + { + myOut.println( method + " '" + uri + "' " ); + + Enumeration<?> e = header.propertyNames(); + while ( e.hasMoreElements()) + { + String value = (String)e.nextElement(); + myOut.println( " HDR: '" + value + "' = '" + + header.getProperty( value ) + "'" ); + } + e = parms.propertyNames(); + while ( e.hasMoreElements()) + { + String value = (String)e.nextElement(); + myOut.println( " PRM: '" + value + "' = '" + + parms.getProperty( value ) + "'" ); + } + e = files.propertyNames(); + while ( e.hasMoreElements()) + { + String value = (String)e.nextElement(); + myOut.println( " UPLOADED: '" + value + "' = '" + + files.getProperty( value ) + "'" ); + } + + return serveFile( uri, header, myRootDir, true ); + } + + /** + * HTTP response. + * Return one of these from serve(). + */ + public 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( txt.getBytes("UTF-8")); + } + catch ( java.io.UnsupportedEncodingException uee ) + { + uee.printStackTrace(); + } + } + + /** + * 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_PARTIALCONTENT = "206 Partial Content", + HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable", + HTTP_REDIRECT = "301 Moved Permanently", + HTTP_NOTMODIFIED = "304 Not Modified", + 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", + MIME_XML = "text/xml"; + + // ================================================== + // Socket & server code + // ================================================== + + /** + * Starts a HTTP server to given port.<p> + * Throws an IOException if the socket is already in use + */ + public NanoHTTPD( int port, File wwwroot ) throws IOException + { + myTcpPort = port; + this.myRootDir = wwwroot; + myServerSocket = new ServerSocket( myTcpPort ); + myThread = new Thread( new Runnable() + { + public void run() + { + try + { + while( true ) + new HTTPSession( myServerSocket.accept()); + } + catch ( IOException ioe ) + {} + } + }); + myThread.setDaemon( true ); + myThread.start(); + } + + /** + * Stops the server. + */ + public void stop() + { + try + { + myServerSocket.close(); + myThread.join(); + } + catch ( IOException ioe ) {} + catch ( InterruptedException e ) {} + } + + + /** + * Starts as a standalone file server and waits for Enter. + */ + public static void main( String[] args ) + { + myOut.println( "NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" + + "(Command line options: [-p port] [-d root-dir] [--licence])\n" ); + + // Defaults + int port = 80; + File wwwroot = new File(".").getAbsoluteFile(); + + // Show licence if requested + for ( int i=0; i<args.length; ++i ) + if(args[i].equalsIgnoreCase("-p")) + port = Integer.parseInt( args[i+1] ); + else if(args[i].equalsIgnoreCase("-d")) + wwwroot = new File( args[i+1] ).getAbsoluteFile(); + else if ( args[i].toLowerCase().endsWith( "licence" )) + { + myOut.println( LICENCE + "\n" ); + break; + } + + try + { + new NanoHTTPD( port, wwwroot ); + } + catch( IOException ioe ) + { + myErr.println( "Couldn't start server:\n" + ioe ); + System.exit( -1 ); + } + + myOut.println( "Now serving files in port " + port + " from \"" + wwwroot + "\"" ); + myOut.println( "Hit Enter to stop.\n" ); + + try { System.in.read(); } catch( Throwable t ) {} + } + + /** + * Handles one session, i.e. parses the HTTP request + * and returns the response. + */ + private class HTTPSession implements Runnable + { + public HTTPSession( Socket s ) + { + mySocket = s; + Thread t = new Thread( this ); + t.setDaemon( true ); + t.start(); + } + + public void run() + { + try + { + InputStream is = mySocket.getInputStream(); + if ( is == null) return; + + // Read the first 8192 bytes. + // The full header should fit in here. + // Apache's default header limit is 8KB. + int bufsize = 8192; + byte[] buf = new byte[bufsize]; + int rlen = is.read(buf, 0, bufsize); + if (rlen <= 0) return; + + // Create a BufferedReader for parsing the header. + ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen); + BufferedReader hin = new BufferedReader( new InputStreamReader( hbis )); + Properties pre = new Properties(); + Properties parms = new Properties(); + Properties header = new Properties(); + Properties files = new Properties(); + + // Decode the header into parms and header java properties + decodeHeader(hin, pre, parms, header); + String method = pre.getProperty("method"); + String uri = pre.getProperty("uri"); + + long size = 0x7FFFFFFFFFFFFFFFl; + String contentLength = header.getProperty("content-length"); + if (contentLength != null) + { + try { size = Integer.parseInt(contentLength); } + catch (NumberFormatException ex) {} + } + + // We are looking for the byte separating header from body. + // It must be the last byte of the first two sequential new lines. + int splitbyte = 0; + boolean sbfound = false; + while (splitbyte < rlen) + { + if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') { + sbfound = true; + break; + } + splitbyte++; + } + splitbyte++; + + // Write the part of body already read to ByteArrayOutputStream f + ByteArrayOutputStream f = new ByteArrayOutputStream(); + if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte); + + // While Firefox sends on the first read all the data fitting + // our buffer, Chrome and Opera sends only the headers even if + // there is data for the body. So we do some magic here to find + // out whether we have already consumed part of body, if we + // have reached the end of the data to be sent or we should + // expect the first byte of the body at the next read. + if (splitbyte < rlen) + size -= rlen - splitbyte +1; + else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl) + size = 0; + + // Now read all the body and write it to f + buf = new byte[512]; + while ( rlen >= 0 && size > 0 ) + { + rlen = is.read(buf, 0, 512); + size -= rlen; + if (rlen > 0) + f.write(buf, 0, rlen); + } + + // Get the raw body as a byte [] + byte [] fbuf = f.toByteArray(); + + // Create a BufferedReader for easily reading it as string. + ByteArrayInputStream bin = new ByteArrayInputStream(fbuf); + BufferedReader in = new BufferedReader( new InputStreamReader(bin)); + + // If the method is POST, there may be parameters + // in data section, too, read it: + if ( method.equalsIgnoreCase( "POST" )) + { + String contentType = ""; + String contentTypeHeader = header.getProperty("content-type"); + StringTokenizer st = new StringTokenizer( contentTypeHeader , "; " ); + if ( st.hasMoreTokens()) { + contentType = st.nextToken(); + } + + if (contentType.equalsIgnoreCase("multipart/form-data")) + { + // Handle multipart/form-data + if ( !st.hasMoreTokens()) + sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" ); + String boundaryExp = st.nextToken(); + st = new StringTokenizer( boundaryExp , "=" ); + if (st.countTokens() != 2) + sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" ); + st.nextToken(); + String boundary = st.nextToken(); + + decodeMultipartData(boundary, fbuf, in, parms, files); + } + else + { + // Handle application/x-www-form-urlencoded + String postLine = ""; + char pbuf[] = new char[512]; + int read = in.read(pbuf); + while ( read >= 0 && !postLine.endsWith("\r\n") ) + { + postLine += String.valueOf(pbuf, 0, read); + read = in.read(pbuf); + } + postLine = postLine.trim(); + decodeParms( postLine, parms ); + } + } + + if ( method.equalsIgnoreCase( "PUT" )) + files.put("content", saveTmpFile( fbuf, 0, f.size())); + + // Ok, now do the serve() + Response r = serve( uri, method, header, parms, files ); + if ( r == null ) + sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." ); + else + sendResponse( r.status, r.mimeType, r.header, r.data ); + + in.close(); + is.close(); + } + catch ( IOException ioe ) + { + try + { + sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } + catch ( Throwable t ) {} + } + catch ( InterruptedException ie ) + { + // Thrown by sendError, ignore and exit the thread. + } + } + + /** + * Decodes the sent headers and loads the data into + * java Properties' key - value pairs + **/ + private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header) + throws InterruptedException + { + try { + // Read the request line + String inLine = in.readLine(); + 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(); + pre.put("method", method); + + if ( !st.hasMoreTokens()) + sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" ); + + String uri = st.nextToken(); + + // Decode parameters from the URI + 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 lowercase since they are + // case insensitive and vary by client. + if ( st.hasMoreTokens()) + { + String line = in.readLine(); + while ( line != null && line.trim().length() > 0 ) + { + int p = line.indexOf( ':' ); + if ( p >= 0 ) + header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim()); + line = in.readLine(); + } + } + + pre.put("uri", uri); + } + catch ( IOException ioe ) + { + sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } + } + + /** + * Decodes the Multipart Body data and put it + * into java Properties' key - value pairs. + **/ + private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files) + throws InterruptedException + { + try + { + int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes()); + int boundarycount = 1; + String mpline = in.readLine(); + while ( mpline != null ) + { + if (mpline.indexOf(boundary) == -1) + sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" ); + boundarycount++; + Properties item = new Properties(); + mpline = in.readLine(); + while (mpline != null && mpline.trim().length() > 0) + { + int p = mpline.indexOf( ':' ); + if (p != -1) + item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim()); + mpline = in.readLine(); + } + if (mpline != null) + { + String contentDisposition = item.getProperty("content-disposition"); + if (contentDisposition == null) + { + sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" ); + } + StringTokenizer st = new StringTokenizer( contentDisposition , "; " ); + Properties disposition = new Properties(); + while ( st.hasMoreTokens()) + { + String token = st.nextToken(); + int p = token.indexOf( '=' ); + if (p!=-1) + disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim()); + } + String pname = disposition.getProperty("name"); + pname = pname.substring(1,pname.length()-1); + + String value = ""; + if (item.getProperty("content-type") == null) { + while (mpline != null && mpline.indexOf(boundary) == -1) + { + mpline = in.readLine(); + if ( mpline != null) + { + int d = mpline.indexOf(boundary); + if (d == -1) + value+=mpline; + else + value+=mpline.substring(0,d-2); + } + } + } + else + { + if (boundarycount> bpositions.length) + sendError( HTTP_INTERNALERROR, "Error processing request" ); + int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]); + String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4); + files.put(pname, path); + value = disposition.getProperty("filename"); + value = value.substring(1,value.length()-1); + do { + mpline = in.readLine(); + } while (mpline != null && mpline.indexOf(boundary) == -1); + } + parms.put(pname, value); + } + } + } + catch ( IOException ioe ) + { + sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } + } + + /** + * Find the byte positions where multipart boundaries start. + **/ + public int[] getBoundaryPositions(byte[] b, byte[] boundary) + { + int matchcount = 0; + int matchbyte = -1; + Vector<Integer> matchbytes = new Vector<Integer>(); + for (int i=0; i<b.length; i++) + { + if (b[i] == boundary[matchcount]) + { + if (matchcount == 0) + matchbyte = i; + matchcount++; + if (matchcount==boundary.length) + { + matchbytes.addElement(new Integer(matchbyte)); + matchcount = 0; + matchbyte = -1; + } + } + else + { + i -= matchcount; + matchcount = 0; + matchbyte = -1; + } + } + int[] ret = new int[matchbytes.size()]; + for (int i=0; i < ret.length; i++) + { + ret[i] = ((Integer)matchbytes.elementAt(i)).intValue(); + } + return ret; + } + + /** + * Retrieves the content of a sent file and saves it + * to a temporary file. + * The full path to the saved file is returned. + **/ + private String saveTmpFile(byte[] b, int offset, int len) + { + String path = ""; + if (len > 0) + { + String tmpdir = System.getProperty("java.io.tmpdir"); + try { + File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir)); + OutputStream fstream = new FileOutputStream(temp); + fstream.write(b, offset, len); + fstream.close(); + path = temp.getAbsolutePath(); + } catch (Exception e) { // Catch exception if any + myErr.println("Error: " + e.getMessage()); + } + } + return path; + } + + + /** + * It returns the offset separating multipart file headers + * from the file's data. + **/ + private int stripMultipartHeaders(byte[] b, int offset) + { + int i = 0; + for (i=offset; i<b.length; i++) + { + if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n') + break; + } + return i+1; + } + + /** + * Decodes the percent encoding scheme. <br/> + * 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<str.length(); i++ ) + { + char c = str.charAt( i ); + switch ( c ) + { + case '+': + sb.append( ' ' ); + break; + case '%': + sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 )); + i += 2; + break; + default: + sb.append( c ); + break; + } + } + return sb.toString(); + } + catch( Exception e ) + { + sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." ); + return null; + } + } + + /** + * Decodes parameters in percent-encoded URI-format + * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and + * adds them to given Properties. NOTE: this doesn't support multiple + * identical keys due to the simplicity of Properties -- if you need multiples, + * you might want to replace the Properties with a Hashtable of Vectors or such. + */ + private void decodeParms( String parms, Properties p ) + throws InterruptedException + { + if ( parms == null ) + return; + + StringTokenizer st = new StringTokenizer( parms, "&" ); + while ( st.hasMoreTokens()) + { + String e = st.nextToken(); + int sep = e.indexOf( '=' ); + if ( sep >= 0 ) + p.put( decodePercent( e.substring( 0, sep )).trim(), + decodePercent( e.substring( sep+1 ))); + } + } + + /** + * Returns an error message as a HTTP response and + * throws InterruptedException to stop further request processing. + */ + private void sendError( String status, String msg ) throws InterruptedException + { + sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes())); + throw new InterruptedException(); + } + + /** + * Sends given response to the socket. + */ + private void sendResponse( String status, String mime, Properties header, InputStream data ) + { + try + { + if ( status == null ) + throw new Error( "sendResponse(): Status can't be null." ); + + OutputStream out = mySocket.getOutputStream(); + PrintWriter pw = new PrintWriter( out ); + pw.print("HTTP/1.0 " + status + " \r\n"); + + if ( mime != null ) + pw.print("Content-Type: " + mime + "\r\n"); + + if ( header == null || header.getProperty( "Date" ) == null ) + pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n"); + + if ( header != null ) + { + Enumeration<Object> 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 ) + { + int pending = data.available(); // This is to support partial sends, see serveFile() + byte[] buff = new byte[theBufferSize]; + while (pending>0) + { + int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending )); + if (read <= 0) break; + out.write( buff, 0, read ); + pending -= read; + } + } + out.flush(); + out.close(); + if ( data != null ) + data.close(); + } + catch( IOException ioe ) + { + // Couldn't write? No can do. + try { mySocket.close(); } catch( Throwable t ) {} + } + } + + private Socket mySocket; + } + + /** + * URL-encodes everything between "/"-characters. + * Encodes spaces as '%20' instead of '+'. + */ + @SuppressWarnings("deprecation") + private String encodeUri( String uri ) + { + String newUri = ""; + StringTokenizer st = new StringTokenizer( uri, "/ ", true ); + while ( st.hasMoreTokens()) + { + String tok = st.nextToken(); + if ( tok.equals( "/" )) + newUri += "/"; + else if ( tok.equals( " " )) + newUri += "%20"; + else + { + newUri += URLEncoder.encode( tok ); + // For Java 1.4 you'll want to use this instead: + // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {} + } + } + return newUri; + } + + private int myTcpPort; + private final ServerSocket myServerSocket; + private Thread myThread; + private File myRootDir; + + // ================================================== + // File server code + // ================================================== + + /** + * Serves file from homeDir and its' subdirectories (only). + * Uses only URI, ignores all headers and HTTP parameters. + */ + public Response serveFile( String uri, Properties header, File homeDir, + boolean allowDirectoryListing ) + { + Response res = null; + + // Make sure we won't die of an exception later + if ( !homeDir.isDirectory()) + res = new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT, + "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." ); + + if ( res == null ) + { + // 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.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 ) + res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, + "FORBIDDEN: Won't serve ../ for security reasons." ); + } + + File f = new File( homeDir, uri ); + if ( res == null && !f.exists()) + res = new Response( HTTP_NOTFOUND, MIME_PLAINTEXT, + "Error 404, file not found." ); + + // List the directory, if necessary + if ( res == null && f.isDirectory()) + { + // Browsers get confused without '/' after the + // directory, send a redirect. + if ( !uri.endsWith( "/" )) + { + uri += "/"; + res = new Response( HTTP_REDIRECT, MIME_HTML, + "<html><body>Redirected: <a href=\"" + uri + "\">" + + uri + "</a></body></html>"); + res.addHeader( "Location", uri ); + } + + if ( res == null ) + { + // 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" ); + // No index file, list the directory if it is readable + else if ( allowDirectoryListing && f.canRead() ) + { + String[] files = f.list(); + String msg = "<html><body><h1>Directory " + uri + "</h1><br/>"; + + if ( uri.length() > 1 ) + { + String u = uri.substring( 0, uri.length()-1 ); + int slash = u.lastIndexOf( '/' ); + if ( slash >= 0 && slash < u.length()) + msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>"; + } + + if (files!=null) + { + for ( int i=0; i<files.length; ++i ) + { + File curFile = new File( f, files[i] ); + boolean dir = curFile.isDirectory(); + if ( dir ) + { + msg += "<b>"; + files[i] += "/"; + } + + msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" + + files[i] + "</a>"; + + // Show file size + if ( curFile.isFile()) + { + long len = curFile.length(); + msg += " <font size=2>("; + if ( len < 1024 ) + msg += len + " bytes"; + else if ( len < 1024 * 1024 ) + msg += len/1024 + "." + (len%1024/10%100) + " KB"; + else + msg += len/(1024*1024) + "." + len%(1024*1024)/10%100 + " MB"; + + msg += ")</font>"; + } + msg += "<br/>"; + if ( dir ) msg += "</b>"; + } + } + msg += "</body></html>"; + res = new Response( HTTP_OK, MIME_HTML, msg ); + } + else + { + res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, + "FORBIDDEN: No directory listing." ); + } + } + } + + try + { + if ( res == null ) + { + // Get MIME type from file name extension, if possible + String mime = null; + int dot = f.getCanonicalPath().lastIndexOf( '.' ); + if ( dot >= 0 ) + mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase()); + if ( mime == null ) + mime = MIME_DEFAULT_BINARY; + + // Calculate etag + String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode()); + + // Support (simple) skipping: + long startFrom = 0; + long endAt = -1; + String range = header.getProperty( "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 nfe ) {} + } + } + + // Change return code and add Content-Range header when skipping is requested + long fileLen = f.length(); + if (range != null && startFrom >= 0) + { + if ( startFrom >= fileLen) + { + res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" ); + res.addHeader( "Content-Range", "bytes 0-0/" + fileLen); + res.addHeader( "ETag", etag); + } + else + { + if ( endAt < 0 ) + endAt = fileLen-1; + long newLen = endAt - startFrom + 1; + if ( newLen < 0 ) newLen = 0; + + final long dataLen = newLen; + FileInputStream fis = new FileInputStream( f ) { + public int available() throws IOException { return (int)dataLen; } + }; + fis.skip( startFrom ); + + res = new Response( HTTP_PARTIALCONTENT, mime, fis ); + res.addHeader( "Content-Length", "" + dataLen); + res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); + res.addHeader( "ETag", etag); + } + } + else + { + if (etag.equals(header.getProperty("if-none-match"))) + res = new Response( HTTP_NOTMODIFIED, mime, ""); + else + { + res = new Response( HTTP_OK, mime, new FileInputStream( f )); + res.addHeader( "Content-Length", "" + fileLen); + res.addHeader( "ETag", etag); + } + } + } + } + catch( IOException ioe ) + { + res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." ); + } + + res.addHeader( "Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes + return res; + } + + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + private static Hashtable<String, String> theMimeTypes = new Hashtable<String, String>(); + static + { + StringTokenizer st = new StringTokenizer( + "css text/css "+ + "htm text/html "+ + "html text/html "+ + "xml text/xml "+ + "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 " + + "mp4 video/mp4 " + + "ogv video/ogg " + + "flv video/x-flv " + + "mov video/quicktime " + + "swf application/x-shockwave-flash " + + "js application/javascript "+ + "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()); + } + + private static int theBufferSize = 16 * 1024; + + // Change these if you want to log to somewhere else than stdout + protected static PrintStream myOut = System.out; + protected static PrintStream myErr = System.err; + + /** + * GMT date formatter + */ + private static java.text.SimpleDateFormat gmtFrmt; + static + { + gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + /** + * The distribution licence + */ + private static final String LICENCE = + "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+ + "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\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."; +} + |