Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- 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 matchbytes = new Vector();
- 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 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 '+'.
- */
- 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 theMimeTypes = new Hashtable();
- 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.";
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement