Advertisement
Guest User

nanoHTTPD

a guest
Jun 27th, 2012
464
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 33.55 KB | None | 0 0
  1. import java.io.BufferedReader;
  2. import java.io.ByteArrayInputStream;
  3. import java.io.File;
  4. import java.io.FileInputStream;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. import java.io.InputStreamReader;
  8. import java.io.OutputStream;
  9. import java.io.PrintStream;
  10. import java.io.PrintWriter;
  11. import java.net.ServerSocket;
  12. import java.net.Socket;
  13. import java.net.URLEncoder;
  14. import java.util.Date;
  15. import java.util.Enumeration;
  16. import java.util.Vector;
  17. import java.util.Hashtable;
  18. import java.util.Locale;
  19. import java.util.Properties;
  20. import java.util.StringTokenizer;
  21. import java.util.TimeZone;
  22.  
  23. import java.io.ByteArrayOutputStream;
  24. import java.io.FileOutputStream;
  25.  
  26. /**
  27.  * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java
  28.  *
  29.  * <p> NanoHTTPD version 1.25,
  30.  * Copyright &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
  31.  * and Copyright &copy; 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr)
  32.  *
  33.  * <p><b>Features + limitations: </b><ul>
  34.  *
  35.  *    <li> Only one Java file </li>
  36.  *    <li> Java 1.1 compatible </li>
  37.  *    <li> Released as open source, Modified BSD licence </li>
  38.  *    <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
  39.  *    <li> Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25) </li>
  40.  *    <li> Supports both dynamic content and file serving </li>
  41.  *    <li> Supports file upload (since version 1.2, 2010) </li>
  42.  *    <li> Supports partial content (streaming)</li>
  43.  *    <li> Supports ETags</li>
  44.  *    <li> Never caches anything </li>
  45.  *    <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
  46.  *    <li> Default code serves files and shows all HTTP parameters and headers</li>
  47.  *    <li> File server supports directory listing, index.html and index.htm</li>
  48.  *    <li> File server supports partial content (streaming)</li>
  49.  *    <li> File server supports ETags</li>
  50.  *    <li> File server does the 301 redirection trick for directories without '/'</li>
  51.  *    <li> File server supports simple skipping for files (continue download) </li>
  52.  *    <li> File server serves also very long files without memory overhead </li>
  53.  *    <li> Contains a built-in list of most common mime types </li>
  54.  *    <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
  55.  *
  56.  * </ul>
  57.  *
  58.  * <p><b>Ways to use: </b><ul>
  59.  *
  60.  *    <li> Run as a standalone app, serves files and shows requests</li>
  61.  *    <li> Subclass serve() and embed to your own program </li>
  62.  *    <li> Call serveFile() from serve() with your own base directory </li>
  63.  *
  64.  * </ul>
  65.  *
  66.  * See the end of the source file for distribution license
  67.  * (Modified BSD licence)
  68.  */
  69. public class NanoHTTPD
  70. {
  71.     // ==================================================
  72.     // API parts
  73.     // ==================================================
  74.  
  75.     /**
  76.      * Override this to customize the server.<p>
  77.      *
  78.      * (By default, this delegates to serveFile() and allows directory listing.)
  79.      *
  80.      * @param uri   Percent-decoded URI without parameters, for example "/index.cgi"
  81.      * @param method    "GET", "POST" etc.
  82.      * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
  83.      * @param header    Header entries, percent decoded
  84.      * @return HTTP response, see class Response for details
  85.      */
  86.     public Response serve( String uri, String method, Properties header, Properties parms, Properties files )
  87.     {
  88.         myOut.println( method + " '" + uri + "' " );
  89.  
  90.         Enumeration e = header.propertyNames();
  91.         while ( e.hasMoreElements())
  92.         {
  93.             String value = (String)e.nextElement();
  94.             myOut.println( "  HDR: '" + value + "' = '" +
  95.                                 header.getProperty( value ) + "'" );
  96.         }
  97.         e = parms.propertyNames();
  98.         while ( e.hasMoreElements())
  99.         {
  100.             String value = (String)e.nextElement();
  101.             myOut.println( "  PRM: '" + value + "' = '" +
  102.                                 parms.getProperty( value ) + "'" );
  103.         }
  104.         e = files.propertyNames();
  105.         while ( e.hasMoreElements())
  106.         {
  107.             String value = (String)e.nextElement();
  108.             myOut.println( "  UPLOADED: '" + value + "' = '" +
  109.                                 files.getProperty( value ) + "'" );
  110.         }
  111.  
  112.         return serveFile( uri, header, myRootDir, true );
  113.     }
  114.  
  115.     /**
  116.      * HTTP response.
  117.      * Return one of these from serve().
  118.      */
  119.     public class Response
  120.     {
  121.         /**
  122.          * Default constructor: response = HTTP_OK, data = mime = 'null'
  123.          */
  124.         public Response()
  125.         {
  126.             this.status = HTTP_OK;
  127.         }
  128.  
  129.         /**
  130.          * Basic constructor.
  131.          */
  132.         public Response( String status, String mimeType, InputStream data )
  133.         {
  134.             this.status = status;
  135.             this.mimeType = mimeType;
  136.             this.data = data;
  137.         }
  138.  
  139.         /**
  140.          * Convenience method that makes an InputStream out of
  141.          * given text.
  142.          */
  143.         public Response( String status, String mimeType, String txt )
  144.         {
  145.             this.status = status;
  146.             this.mimeType = mimeType;
  147.             try
  148.             {
  149.                 this.data = new ByteArrayInputStream( txt.getBytes("UTF-8"));
  150.             }
  151.             catch ( java.io.UnsupportedEncodingException uee )
  152.             {
  153.                 uee.printStackTrace();
  154.             }
  155.         }
  156.  
  157.         /**
  158.          * Adds given line to the header.
  159.          */
  160.         public void addHeader( String name, String value )
  161.         {
  162.             header.put( name, value );
  163.         }
  164.  
  165.         /**
  166.          * HTTP status code after processing, e.g. "200 OK", HTTP_OK
  167.          */
  168.         public String status;
  169.  
  170.         /**
  171.          * MIME type of content, e.g. "text/html"
  172.          */
  173.         public String mimeType;
  174.  
  175.         /**
  176.          * Data of the response, may be null.
  177.          */
  178.         public InputStream data;
  179.  
  180.         /**
  181.          * Headers for the HTTP response. Use addHeader()
  182.          * to add lines.
  183.          */
  184.         public Properties header = new Properties();
  185.     }
  186.  
  187.     /**
  188.      * Some HTTP response status codes
  189.      */
  190.     public static final String
  191.         HTTP_OK = "200 OK",
  192.         HTTP_PARTIALCONTENT = "206 Partial Content",
  193.         HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable",
  194.         HTTP_REDIRECT = "301 Moved Permanently",
  195.         HTTP_NOTMODIFIED = "304 Not Modified",
  196.         HTTP_FORBIDDEN = "403 Forbidden",
  197.         HTTP_NOTFOUND = "404 Not Found",
  198.         HTTP_BADREQUEST = "400 Bad Request",
  199.         HTTP_INTERNALERROR = "500 Internal Server Error",
  200.         HTTP_NOTIMPLEMENTED = "501 Not Implemented";
  201.  
  202.     /**
  203.      * Common mime types for dynamic content
  204.      */
  205.     public static final String
  206.         MIME_PLAINTEXT = "text/plain",
  207.         MIME_HTML = "text/html",
  208.         MIME_DEFAULT_BINARY = "application/octet-stream",
  209.         MIME_XML = "text/xml";
  210.  
  211.     // ==================================================
  212.     // Socket & server code
  213.     // ==================================================
  214.  
  215.     /**
  216.      * Starts a HTTP server to given port.<p>
  217.      * Throws an IOException if the socket is already in use
  218.      */
  219.     public NanoHTTPD( int port, File wwwroot ) throws IOException
  220.     {
  221.         myTcpPort = port;
  222.         this.myRootDir = wwwroot;
  223.         myServerSocket = new ServerSocket( myTcpPort );
  224.         myThread = new Thread( new Runnable()
  225.             {
  226.                 public void run()
  227.                 {
  228.                     try
  229.                     {
  230.                         while( true )
  231.                             new HTTPSession( myServerSocket.accept());
  232.                     }
  233.                     catch ( IOException ioe )
  234.                     {}
  235.                 }
  236.             });
  237.         myThread.setDaemon( true );
  238.         myThread.start();
  239.     }
  240.  
  241.     /**
  242.      * Stops the server.
  243.      */
  244.     public void stop()
  245.     {
  246.         try
  247.         {
  248.             myServerSocket.close();
  249.             myThread.join();
  250.         }
  251.         catch ( IOException ioe ) {}
  252.         catch ( InterruptedException e ) {}
  253.     }
  254.  
  255.  
  256.     /**
  257.      * Starts as a standalone file server and waits for Enter.
  258.      */
  259.     public static void main( String[] args )
  260.     {
  261.         myOut.println( "NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" +
  262.                 "(Command line options: [-p port] [-d root-dir] [--licence])\n" );
  263.  
  264.         // Defaults
  265.         int port = 80;
  266.         File wwwroot = new File(".").getAbsoluteFile();
  267.  
  268.         // Show licence if requested
  269.         for ( int i=0; i<args.length; ++i )
  270.         if(args[i].equalsIgnoreCase("-p"))
  271.             port = Integer.parseInt( args[i+1] );
  272.         else if(args[i].equalsIgnoreCase("-d"))
  273.             wwwroot = new File( args[i+1] ).getAbsoluteFile();
  274.         else if ( args[i].toLowerCase().endsWith( "licence" ))
  275.         {
  276.             myOut.println( LICENCE + "\n" );
  277.             break;
  278.         }
  279.  
  280.         try
  281.         {
  282.             new NanoHTTPD( port, wwwroot );
  283.         }
  284.         catch( IOException ioe )
  285.         {
  286.             myErr.println( "Couldn't start server:\n" + ioe );
  287.             System.exit( -1 );
  288.         }
  289.  
  290.         myOut.println( "Now serving files in port " + port + " from \"" + wwwroot + "\"" );
  291.         myOut.println( "Hit Enter to stop.\n" );
  292.  
  293.         try { System.in.read(); } catch( Throwable t ) {}
  294.     }
  295.  
  296.     /**
  297.      * Handles one session, i.e. parses the HTTP request
  298.      * and returns the response.
  299.      */
  300.     private class HTTPSession implements Runnable
  301.     {
  302.         public HTTPSession( Socket s )
  303.         {
  304.             mySocket = s;
  305.             Thread t = new Thread( this );
  306.             t.setDaemon( true );
  307.             t.start();
  308.         }
  309.  
  310.         public void run()
  311.         {
  312.             try
  313.             {
  314.                 InputStream is = mySocket.getInputStream();
  315.                 if ( is == null) return;
  316.  
  317.                 // Read the first 8192 bytes.
  318.                 // The full header should fit in here.
  319.                 // Apache's default header limit is 8KB.
  320.                 int bufsize = 8192;
  321.                 byte[] buf = new byte[bufsize];
  322.                 int rlen = is.read(buf, 0, bufsize);
  323.                 if (rlen <= 0) return;
  324.  
  325.                 // Create a BufferedReader for parsing the header.
  326.                 ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
  327.                 BufferedReader hin = new BufferedReader( new InputStreamReader( hbis ));
  328.                 Properties pre = new Properties();
  329.                 Properties parms = new Properties();
  330.                 Properties header = new Properties();
  331.                 Properties files = new Properties();
  332.  
  333.                 // Decode the header into parms and header java properties
  334.                 decodeHeader(hin, pre, parms, header);
  335.                 String method = pre.getProperty("method");
  336.                 String uri = pre.getProperty("uri");
  337.  
  338.                 long size = 0x7FFFFFFFFFFFFFFFl;
  339.                 String contentLength = header.getProperty("content-length");
  340.                 if (contentLength != null)
  341.                 {
  342.                     try { size = Integer.parseInt(contentLength); }
  343.                     catch (NumberFormatException ex) {}
  344.                 }
  345.  
  346.                 // We are looking for the byte separating header from body.
  347.                 // It must be the last byte of the first two sequential new lines.
  348.                 int splitbyte = 0;
  349.                 boolean sbfound = false;
  350.                 while (splitbyte < rlen)
  351.                 {
  352.                     if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') {
  353.                         sbfound = true;
  354.                         break;
  355.                     }
  356.                     splitbyte++;
  357.                 }
  358.                 splitbyte++;
  359.  
  360.                 // Write the part of body already read to ByteArrayOutputStream f
  361.                 ByteArrayOutputStream f = new ByteArrayOutputStream();
  362.                 if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte);
  363.  
  364.                 // While Firefox sends on the first read all the data fitting
  365.                 // our buffer, Chrome and Opera sends only the headers even if
  366.                 // there is data for the body. So we do some magic here to find
  367.                 // out whether we have already consumed part of body, if we
  368.                 // have reached the end of the data to be sent or we should
  369.                 // expect the first byte of the body at the next read.
  370.                 if (splitbyte < rlen)
  371.                     size -= rlen - splitbyte +1;
  372.                 else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
  373.                     size = 0;
  374.  
  375.                 // Now read all the body and write it to f
  376.                 buf = new byte[512];
  377.                 while ( rlen >= 0 && size > 0 )
  378.                 {
  379.                     rlen = is.read(buf, 0, 512);
  380.                     size -= rlen;
  381.                     if (rlen > 0)
  382.                         f.write(buf, 0, rlen);
  383.                 }
  384.  
  385.                 // Get the raw body as a byte []
  386.                 byte [] fbuf = f.toByteArray();
  387.  
  388.                 // Create a BufferedReader for easily reading it as string.
  389.                 ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
  390.                 BufferedReader in = new BufferedReader( new InputStreamReader(bin));
  391.  
  392.                 // If the method is POST, there may be parameters
  393.                 // in data section, too, read it:
  394.                 if ( method.equalsIgnoreCase( "POST" ))
  395.                 {
  396.                     String contentType = "";
  397.                     String contentTypeHeader = header.getProperty("content-type");
  398.                     StringTokenizer st = new StringTokenizer( contentTypeHeader , "; " );
  399.                     if ( st.hasMoreTokens()) {
  400.                         contentType = st.nextToken();
  401.                     }
  402.  
  403.                     if (contentType.equalsIgnoreCase("multipart/form-data"))
  404.                     {
  405.                         // Handle multipart/form-data
  406.                         if ( !st.hasMoreTokens())
  407.                             sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" );
  408.                         String boundaryExp = st.nextToken();
  409.                         st = new StringTokenizer( boundaryExp , "=" );
  410.                         if (st.countTokens() != 2)
  411.                             sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" );
  412.                         st.nextToken();
  413.                         String boundary = st.nextToken();
  414.  
  415.                         decodeMultipartData(boundary, fbuf, in, parms, files);
  416.                     }
  417.                     else
  418.                     {
  419.                         // Handle application/x-www-form-urlencoded
  420.                         String postLine = "";
  421.                         char pbuf[] = new char[512];
  422.                         int read = in.read(pbuf);
  423.                         while ( read >= 0 && !postLine.endsWith("\r\n") )
  424.                         {
  425.                             postLine += String.valueOf(pbuf, 0, read);
  426.                             read = in.read(pbuf);
  427.                         }
  428.                         postLine = postLine.trim();
  429.                         decodeParms( postLine, parms );
  430.                     }
  431.                 }
  432.  
  433.                 if ( method.equalsIgnoreCase( "PUT" ))
  434.                     files.put("content", saveTmpFile( fbuf, 0, f.size()));
  435.  
  436.                 // Ok, now do the serve()
  437.                 Response r = serve( uri, method, header, parms, files );
  438.                 if ( r == null )
  439.                     sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
  440.                 else
  441.                     sendResponse( r.status, r.mimeType, r.header, r.data );
  442.  
  443.                 in.close();
  444.                 is.close();
  445.             }
  446.             catch ( IOException ioe )
  447.             {
  448.                 try
  449.                 {
  450.                     sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
  451.                 }
  452.                 catch ( Throwable t ) {}
  453.             }
  454.             catch ( InterruptedException ie )
  455.             {
  456.                 // Thrown by sendError, ignore and exit the thread.
  457.             }
  458.         }
  459.  
  460.         /**
  461.          * Decodes the sent headers and loads the data into
  462.          * java Properties' key - value pairs
  463.         **/
  464.         private  void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header)
  465.             throws InterruptedException
  466.         {
  467.             try {
  468.                 // Read the request line
  469.                 String inLine = in.readLine();
  470.                 if (inLine == null) return;
  471.                 StringTokenizer st = new StringTokenizer( inLine );
  472.                 if ( !st.hasMoreTokens())
  473.                     sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
  474.  
  475.                 String method = st.nextToken();
  476.                 pre.put("method", method);
  477.  
  478.                 if ( !st.hasMoreTokens())
  479.                     sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
  480.  
  481.                 String uri = st.nextToken();
  482.  
  483.                 // Decode parameters from the URI
  484.                 int qmi = uri.indexOf( '?' );
  485.                 if ( qmi >= 0 )
  486.                 {
  487.                     decodeParms( uri.substring( qmi+1 ), parms );
  488.                     uri = decodePercent( uri.substring( 0, qmi ));
  489.                 }
  490.                 else uri = decodePercent(uri);
  491.  
  492.                 // If there's another token, it's protocol version,
  493.                 // followed by HTTP headers. Ignore version but parse headers.
  494.                 // NOTE: this now forces header names lowercase since they are
  495.                 // case insensitive and vary by client.
  496.                 if ( st.hasMoreTokens())
  497.                 {
  498.                     String line = in.readLine();
  499.                     while ( line != null && line.trim().length() > 0 )
  500.                     {
  501.                         int p = line.indexOf( ':' );
  502.                         if ( p >= 0 )
  503.                             header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
  504.                         line = in.readLine();
  505.                     }
  506.                 }
  507.  
  508.                 pre.put("uri", uri);
  509.             }
  510.             catch ( IOException ioe )
  511.             {
  512.                 sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
  513.             }
  514.         }
  515.  
  516.         /**
  517.          * Decodes the Multipart Body data and put it
  518.          * into java Properties' key - value pairs.
  519.         **/
  520.         private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files)
  521.             throws InterruptedException
  522.         {
  523.             try
  524.             {
  525.                 int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes());
  526.                 int boundarycount = 1;
  527.                 String mpline = in.readLine();
  528.                 while ( mpline != null )
  529.                 {
  530.                     if (mpline.indexOf(boundary) == -1)
  531.                         sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" );
  532.                     boundarycount++;
  533.                     Properties item = new Properties();
  534.                     mpline = in.readLine();
  535.                     while (mpline != null && mpline.trim().length() > 0)
  536.                     {
  537.                         int p = mpline.indexOf( ':' );
  538.                         if (p != -1)
  539.                             item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim());
  540.                         mpline = in.readLine();
  541.                     }
  542.                     if (mpline != null)
  543.                     {
  544.                         String contentDisposition = item.getProperty("content-disposition");
  545.                         if (contentDisposition == null)
  546.                         {
  547.                             sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" );
  548.                         }
  549.                         StringTokenizer st = new StringTokenizer( contentDisposition , "; " );
  550.                         Properties disposition = new Properties();
  551.                         while ( st.hasMoreTokens())
  552.                         {
  553.                             String token = st.nextToken();
  554.                             int p = token.indexOf( '=' );
  555.                             if (p!=-1)
  556.                                 disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim());
  557.                         }
  558.                         String pname = disposition.getProperty("name");
  559.                         pname = pname.substring(1,pname.length()-1);
  560.  
  561.                         String value = "";
  562.                         if (item.getProperty("content-type") == null) {
  563.                             while (mpline != null && mpline.indexOf(boundary) == -1)
  564.                             {
  565.                                 mpline = in.readLine();
  566.                                 if ( mpline != null)
  567.                                 {
  568.                                     int d = mpline.indexOf(boundary);
  569.                                     if (d == -1)
  570.                                         value+=mpline;
  571.                                     else
  572.                                         value+=mpline.substring(0,d-2);
  573.                                 }
  574.                             }
  575.                         }
  576.                         else
  577.                         {
  578.                             if (boundarycount> bpositions.length)
  579.                                 sendError( HTTP_INTERNALERROR, "Error processing request" );
  580.                             int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]);
  581.                             String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4);
  582.                             files.put(pname, path);
  583.                             value = disposition.getProperty("filename");
  584.                             value = value.substring(1,value.length()-1);
  585.                             do {
  586.                                 mpline = in.readLine();
  587.                             } while (mpline != null && mpline.indexOf(boundary) == -1);
  588.                         }
  589.                         parms.put(pname, value);
  590.                     }
  591.                 }
  592.             }
  593.             catch ( IOException ioe )
  594.             {
  595.                 sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
  596.             }
  597.         }
  598.  
  599.         /**
  600.          * Find the byte positions where multipart boundaries start.
  601.         **/
  602.         public int[] getBoundaryPositions(byte[] b, byte[] boundary)
  603.         {
  604.             int matchcount = 0;
  605.             int matchbyte = -1;
  606.             Vector matchbytes = new Vector();
  607.             for (int i=0; i<b.length; i++)
  608.             {
  609.                 if (b[i] == boundary[matchcount])
  610.                 {
  611.                     if (matchcount == 0)
  612.                         matchbyte = i;
  613.                     matchcount++;
  614.                     if (matchcount==boundary.length)
  615.                     {
  616.                         matchbytes.addElement(new Integer(matchbyte));
  617.                         matchcount = 0;
  618.                         matchbyte = -1;
  619.                     }
  620.                 }
  621.                 else
  622.                 {
  623.                     i -= matchcount;
  624.                     matchcount = 0;
  625.                     matchbyte = -1;
  626.                 }
  627.             }
  628.             int[] ret = new int[matchbytes.size()];
  629.             for (int i=0; i < ret.length; i++)
  630.             {
  631.                 ret[i] = ((Integer)matchbytes.elementAt(i)).intValue();
  632.             }
  633.             return ret;
  634.         }
  635.  
  636.         /**
  637.          * Retrieves the content of a sent file and saves it
  638.          * to a temporary file.
  639.          * The full path to the saved file is returned.
  640.         **/
  641.         private String saveTmpFile(byte[] b, int offset, int len)
  642.         {
  643.             String path = "";
  644.             if (len > 0)
  645.             {
  646.                 String tmpdir = System.getProperty("java.io.tmpdir");
  647.                 try {
  648.                     File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
  649.                     OutputStream fstream = new FileOutputStream(temp);
  650.                     fstream.write(b, offset, len);
  651.                     fstream.close();
  652.                     path = temp.getAbsolutePath();
  653.                 } catch (Exception e) { // Catch exception if any
  654.                     myErr.println("Error: " + e.getMessage());
  655.                 }
  656.             }
  657.             return path;
  658.         }
  659.  
  660.  
  661.         /**
  662.          * It returns the offset separating multipart file headers
  663.          * from the file's data.
  664.         **/
  665.         private int stripMultipartHeaders(byte[] b, int offset)
  666.         {
  667.             int i = 0;
  668.             for (i=offset; i<b.length; i++)
  669.             {
  670.                 if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n')
  671.                     break;
  672.             }
  673.             return i+1;
  674.         }
  675.  
  676.         /**
  677.          * Decodes the percent encoding scheme. <br/>
  678.          * For example: "an+example%20string" -> "an example string"
  679.          */
  680.         private String decodePercent( String str ) throws InterruptedException
  681.         {
  682.             try
  683.             {
  684.                 StringBuffer sb = new StringBuffer();
  685.                 for( int i=0; i<str.length(); i++ )
  686.                 {
  687.                     char c = str.charAt( i );
  688.                     switch ( c )
  689.                     {
  690.                         case '+':
  691.                             sb.append( ' ' );
  692.                             break;
  693.                         case '%':
  694.                             sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
  695.                             i += 2;
  696.                             break;
  697.                         default:
  698.                             sb.append( c );
  699.                             break;
  700.                     }
  701.                 }
  702.                 return sb.toString();
  703.             }
  704.             catch( Exception e )
  705.             {
  706.                 sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
  707.                 return null;
  708.             }
  709.         }
  710.  
  711.         /**
  712.          * Decodes parameters in percent-encoded URI-format
  713.          * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
  714.          * adds them to given Properties. NOTE: this doesn't support multiple
  715.          * identical keys due to the simplicity of Properties -- if you need multiples,
  716.          * you might want to replace the Properties with a Hashtable of Vectors or such.
  717.          */
  718.         private void decodeParms( String parms, Properties p )
  719.             throws InterruptedException
  720.         {
  721.             if ( parms == null )
  722.                 return;
  723.  
  724.             StringTokenizer st = new StringTokenizer( parms, "&" );
  725.             while ( st.hasMoreTokens())
  726.             {
  727.                 String e = st.nextToken();
  728.                 int sep = e.indexOf( '=' );
  729.                 if ( sep >= 0 )
  730.                     p.put( decodePercent( e.substring( 0, sep )).trim(),
  731.                            decodePercent( e.substring( sep+1 )));
  732.             }
  733.         }
  734.  
  735.         /**
  736.          * Returns an error message as a HTTP response and
  737.          * throws InterruptedException to stop further request processing.
  738.          */
  739.         private void sendError( String status, String msg ) throws InterruptedException
  740.         {
  741.             sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
  742.             throw new InterruptedException();
  743.         }
  744.  
  745.         /**
  746.          * Sends given response to the socket.
  747.          */
  748.         private void sendResponse( String status, String mime, Properties header, InputStream data )
  749.         {
  750.             try
  751.             {
  752.                 if ( status == null )
  753.                     throw new Error( "sendResponse(): Status can't be null." );
  754.  
  755.                 OutputStream out = mySocket.getOutputStream();
  756.                 PrintWriter pw = new PrintWriter( out );
  757.                 pw.print("HTTP/1.0 " + status + " \r\n");
  758.  
  759.                 if ( mime != null )
  760.                     pw.print("Content-Type: " + mime + "\r\n");
  761.  
  762.                 if ( header == null || header.getProperty( "Date" ) == null )
  763.                     pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
  764.  
  765.                 if ( header != null )
  766.                 {
  767.                     Enumeration e = header.keys();
  768.                     while ( e.hasMoreElements())
  769.                     {
  770.                         String key = (String)e.nextElement();
  771.                         String value = header.getProperty( key );
  772.                         pw.print( key + ": " + value + "\r\n");
  773.                     }
  774.                 }
  775.  
  776.                 pw.print("\r\n");
  777.                 pw.flush();
  778.  
  779.                 if ( data != null )
  780.                 {
  781.                     int pending = data.available(); // This is to support partial sends, see serveFile()
  782.                     byte[] buff = new byte[theBufferSize];
  783.                     while (pending>0)
  784.                     {
  785.                         int read = data.read( buff, 0, ( (pending>theBufferSize) ?  theBufferSize : pending ));
  786.                         if (read <= 0)  break;
  787.                         out.write( buff, 0, read );
  788.                         pending -= read;
  789.                     }
  790.                 }
  791.                 out.flush();
  792.                 out.close();
  793.                 if ( data != null )
  794.                     data.close();
  795.             }
  796.             catch( IOException ioe )
  797.             {
  798.                 // Couldn't write? No can do.
  799.                 try { mySocket.close(); } catch( Throwable t ) {}
  800.             }
  801.         }
  802.  
  803.         private Socket mySocket;
  804.     }
  805.  
  806.     /**
  807.      * URL-encodes everything between "/"-characters.
  808.      * Encodes spaces as '%20' instead of '+'.
  809.      */
  810.     private String encodeUri( String uri )
  811.     {
  812.         String newUri = "";
  813.         StringTokenizer st = new StringTokenizer( uri, "/ ", true );
  814.         while ( st.hasMoreTokens())
  815.         {
  816.             String tok = st.nextToken();
  817.             if ( tok.equals( "/" ))
  818.                 newUri += "/";
  819.             else if ( tok.equals( " " ))
  820.                 newUri += "%20";
  821.             else
  822.             {
  823.                 newUri += URLEncoder.encode( tok );
  824.                 // For Java 1.4 you'll want to use this instead:
  825.                 // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {}
  826.             }
  827.         }
  828.         return newUri;
  829.     }
  830.  
  831.     private int myTcpPort;
  832.     private final ServerSocket myServerSocket;
  833.     private Thread myThread;
  834.     private File myRootDir;
  835.  
  836.     // ==================================================
  837.     // File server code
  838.     // ==================================================
  839.  
  840.     /**
  841.      * Serves file from homeDir and its' subdirectories (only).
  842.      * Uses only URI, ignores all headers and HTTP parameters.
  843.      */
  844.     public Response serveFile( String uri, Properties header, File homeDir,
  845.                                boolean allowDirectoryListing )
  846.     {
  847.         Response res = null;
  848.  
  849.         // Make sure we won't die of an exception later
  850.         if ( !homeDir.isDirectory())
  851.             res = new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
  852.                 "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
  853.  
  854.         if ( res == null )
  855.         {
  856.             // Remove URL arguments
  857.             uri = uri.trim().replace( File.separatorChar, '/' );
  858.             if ( uri.indexOf( '?' ) >= 0 )
  859.                 uri = uri.substring(0, uri.indexOf( '?' ));
  860.  
  861.             // Prohibit getting out of current directory
  862.             if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
  863.                 res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
  864.                     "FORBIDDEN: Won't serve ../ for security reasons." );
  865.         }
  866.  
  867.         File f = new File( homeDir, uri );
  868.         if ( res == null && !f.exists())
  869.             res = new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
  870.                 "Error 404, file not found." );
  871.  
  872.         // List the directory, if necessary
  873.         if ( res == null && f.isDirectory())
  874.         {
  875.             // Browsers get confused without '/' after the
  876.             // directory, send a redirect.
  877.             if ( !uri.endsWith( "/" ))
  878.             {
  879.                 uri += "/";
  880.                 res = new Response( HTTP_REDIRECT, MIME_HTML,
  881.                     "<html><body>Redirected: <a href=\"" + uri + "\">" +
  882.                     uri + "</a></body></html>");
  883.                 res.addHeader( "Location", uri );
  884.             }
  885.  
  886.             if ( res == null )
  887.             {
  888.                 // First try index.html and index.htm
  889.                 if ( new File( f, "index.html" ).exists())
  890.                     f = new File( homeDir, uri + "/index.html" );
  891.                 else if ( new File( f, "index.htm" ).exists())
  892.                     f = new File( homeDir, uri + "/index.htm" );
  893.                 // No index file, list the directory if it is readable
  894.                 else if ( allowDirectoryListing && f.canRead() )
  895.                 {
  896.                     String[] files = f.list();
  897.                     String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
  898.  
  899.                     if ( uri.length() > 1 )
  900.                     {
  901.                         String u = uri.substring( 0, uri.length()-1 );
  902.                         int slash = u.lastIndexOf( '/' );
  903.                         if ( slash >= 0 && slash  < u.length())
  904.                             msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
  905.                     }
  906.  
  907.                     if (files!=null)
  908.                     {
  909.                         for ( int i=0; i<files.length; ++i )
  910.                         {
  911.                             File curFile = new File( f, files[i] );
  912.                             boolean dir = curFile.isDirectory();
  913.                             if ( dir )
  914.                             {
  915.                                 msg += "<b>";
  916.                                 files[i] += "/";
  917.                             }
  918.  
  919.                             msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
  920.                                   files[i] + "</a>";
  921.  
  922.                             // Show file size
  923.                             if ( curFile.isFile())
  924.                             {
  925.                                 long len = curFile.length();
  926.                                 msg += " &nbsp;<font size=2>(";
  927.                                 if ( len < 1024 )
  928.                                     msg += len + " bytes";
  929.                                 else if ( len < 1024 * 1024 )
  930.                                     msg += len/1024 + "." + (len%1024/10%100) + " KB";
  931.                                 else
  932.                                     msg += len/(1024*1024) + "." + len%(1024*1024)/10%100 + " MB";
  933.  
  934.                                 msg += ")</font>";
  935.                             }
  936.                             msg += "<br/>";
  937.                             if ( dir ) msg += "</b>";
  938.                         }
  939.                     }
  940.                     msg += "</body></html>";
  941.                     res = new Response( HTTP_OK, MIME_HTML, msg );
  942.                 }
  943.                 else
  944.                 {
  945.                     res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
  946.                         "FORBIDDEN: No directory listing." );
  947.                 }
  948.             }
  949.         }
  950.  
  951.         try
  952.         {
  953.             if ( res == null )
  954.             {
  955.                 // Get MIME type from file name extension, if possible
  956.                 String mime = null;
  957.                 int dot = f.getCanonicalPath().lastIndexOf( '.' );
  958.                 if ( dot >= 0 )
  959.                     mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
  960.                 if ( mime == null )
  961.                     mime = MIME_DEFAULT_BINARY;
  962.  
  963.                 // Calculate etag
  964.                 String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
  965.  
  966.                 // Support (simple) skipping:
  967.                 long startFrom = 0;
  968.                 long endAt = -1;
  969.                 String range = header.getProperty( "range" );
  970.                 if ( range != null )
  971.                 {
  972.                     if ( range.startsWith( "bytes=" ))
  973.                     {
  974.                         range = range.substring( "bytes=".length());
  975.                         int minus = range.indexOf( '-' );
  976.                         try {
  977.                             if ( minus > 0 )
  978.                             {
  979.                                 startFrom = Long.parseLong( range.substring( 0, minus ));
  980.                                 endAt = Long.parseLong( range.substring( minus+1 ));
  981.                             }
  982.                         }
  983.                         catch ( NumberFormatException nfe ) {}
  984.                     }
  985.                 }
  986.  
  987.                 // Change return code and add Content-Range header when skipping is requested
  988.                 long fileLen = f.length();
  989.                 if (range != null && startFrom >= 0)
  990.                 {
  991.                     if ( startFrom >= fileLen)
  992.                     {
  993.                         res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" );
  994.                         res.addHeader( "Content-Range", "bytes 0-0/" + fileLen);
  995.                         res.addHeader( "ETag", etag);
  996.                     }
  997.                     else
  998.                     {
  999.                         if ( endAt < 0 )
  1000.                             endAt = fileLen-1;
  1001.                         long newLen = endAt - startFrom + 1;
  1002.                         if ( newLen < 0 ) newLen = 0;
  1003.  
  1004.                         final long dataLen = newLen;
  1005.                         FileInputStream fis = new FileInputStream( f ) {
  1006.                             public int available() throws IOException { return (int)dataLen; }
  1007.                         };
  1008.                         fis.skip( startFrom );
  1009.  
  1010.                         res = new Response( HTTP_PARTIALCONTENT, mime, fis );
  1011.                         res.addHeader( "Content-Length", "" + dataLen);
  1012.                         res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
  1013.                         res.addHeader( "ETag", etag);
  1014.                     }
  1015.                 }
  1016.                 else
  1017.                 {
  1018.                     if (etag.equals(header.getProperty("if-none-match")))
  1019.                         res = new Response( HTTP_NOTMODIFIED, mime, "");
  1020.                     else
  1021.                     {
  1022.                         res = new Response( HTTP_OK, mime, new FileInputStream( f ));
  1023.                         res.addHeader( "Content-Length", "" + fileLen);
  1024.                         res.addHeader( "ETag", etag);
  1025.                     }
  1026.                 }
  1027.             }
  1028.         }
  1029.         catch( IOException ioe )
  1030.         {
  1031.             res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
  1032.         }
  1033.  
  1034.         res.addHeader( "Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
  1035.         return res;
  1036.     }
  1037.  
  1038.     /**
  1039.      * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
  1040.      */
  1041.     private static Hashtable theMimeTypes = new Hashtable();
  1042.     static
  1043.     {
  1044.         StringTokenizer st = new StringTokenizer(
  1045.             "css        text/css "+
  1046.             "htm        text/html "+
  1047.             "html       text/html "+
  1048.             "xml        text/xml "+
  1049.             "txt        text/plain "+
  1050.             "asc        text/plain "+
  1051.             "gif        image/gif "+
  1052.             "jpg        image/jpeg "+
  1053.             "jpeg       image/jpeg "+
  1054.             "png        image/png "+
  1055.             "mp3        audio/mpeg "+
  1056.             "m3u        audio/mpeg-url " +
  1057.             "mp4        video/mp4 " +
  1058.             "ogv        video/ogg " +
  1059.             "flv        video/x-flv " +
  1060.             "mov        video/quicktime " +
  1061.             "swf        application/x-shockwave-flash " +
  1062.             "js         application/javascript "+
  1063.             "pdf        application/pdf "+
  1064.             "doc        application/msword "+
  1065.             "ogg        application/x-ogg "+
  1066.             "zip        application/octet-stream "+
  1067.             "exe        application/octet-stream "+
  1068.             "class      application/octet-stream " );
  1069.         while ( st.hasMoreTokens())
  1070.             theMimeTypes.put( st.nextToken(), st.nextToken());
  1071.     }
  1072.  
  1073.     private static int theBufferSize = 16 * 1024;
  1074.  
  1075.     // Change these if you want to log to somewhere else than stdout
  1076.     protected static PrintStream myOut = System.out;
  1077.     protected static PrintStream myErr = System.err;
  1078.  
  1079.     /**
  1080.      * GMT date formatter
  1081.      */
  1082.     private static java.text.SimpleDateFormat gmtFrmt;
  1083.     static
  1084.     {
  1085.         gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
  1086.         gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
  1087.     }
  1088.  
  1089.     /**
  1090.      * The distribution licence
  1091.      */
  1092.     private static final String LICENCE =
  1093.         "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+
  1094.         "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"+
  1095.         "\n"+
  1096.         "Redistribution and use in source and binary forms, with or without\n"+
  1097.         "modification, are permitted provided that the following conditions\n"+
  1098.         "are met:\n"+
  1099.         "\n"+
  1100.         "Redistributions of source code must retain the above copyright notice,\n"+
  1101.         "this list of conditions and the following disclaimer. Redistributions in\n"+
  1102.         "binary form must reproduce the above copyright notice, this list of\n"+
  1103.         "conditions and the following disclaimer in the documentation and/or other\n"+
  1104.         "materials provided with the distribution. The name of the author may not\n"+
  1105.         "be used to endorse or promote products derived from this software without\n"+
  1106.         "specific prior written permission. \n"+
  1107.         " \n"+
  1108.         "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
  1109.         "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
  1110.         "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
  1111.         "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
  1112.         "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
  1113.         "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
  1114.         "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
  1115.         "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
  1116.         "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
  1117.         "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
  1118. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement