test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/NanoHTTPD.java in calabash-android-0.4.7.pre4 vs test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/NanoHTTPD.java in calabash-android-0.4.7.pre6

- old
+ new

@@ -1,38 +1,36 @@ package sh.calaba.instrumentationbackend.actions; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; 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.io.UnsupportedEncodingException; 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.util.Vector; -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 &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) + * <p> NanoHTTPD version 1.27, + * Copyright &copy; 2001,2005-2013 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) * and Copyright &copy; 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr) * * <p><b>Features + limitations: </b><ul> * * <li> Only one Java file </li> @@ -215,35 +213,32 @@ // 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 ) + public NanoHTTPD( int port, File wwwroot ) throws IOException { - try { - myTcpPort = port; - this.myRootDir = wwwroot; - myServerSocket = new ServerSocket( myTcpPort ); - myThread = new Thread( new Runnable() + myTcpPort = port; + this.myRootDir = wwwroot; + myServerSocket = new ServerSocket( myTcpPort ); + myThread = new Thread( new Runnable() + { + public void run() { - public void run() + try { - try - { - while( true ) - new HTTPSession( myServerSocket.accept()); - } - catch ( IOException ioe ) - {} + while( true ) + new HTTPSession( myServerSocket.accept()); } - }); - myThread.setDaemon( true ); - myThread.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } + catch ( IOException ioe ) + {} + } + }); + myThread.setDaemon( true ); + myThread.start(); } /** * Stops the server. */ @@ -256,11 +251,52 @@ } 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.27 (C) 2001,2005-2013 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 { @@ -280,14 +316,26 @@ 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; + // Do NOT assume that a single read will get the entire header at once! + final int bufsize = 8192; byte[] buf = new byte[bufsize]; - int rlen = is.read(buf, 0, bufsize); - if (rlen <= 0) return; + int splitbyte = 0; + int rlen = 0; + { + int read = is.read(buf, 0, bufsize); + while (read > 0) + { + rlen += read; + splitbyte = findHeaderEnd(buf, rlen); + if (splitbyte > 0) + break; + read = is.read(buf, rlen, bufsize - rlen); + } + } // 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(); @@ -306,37 +354,24 @@ { 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); + 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 + // our buffer, Chrome and Opera send only the headers even if + // there is data for the body. 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 -= rlen-splitbyte; + else if (splitbyte==0 || size == 0x7FFFFFFFFFFFFFFFl) size = 0; // Now read all the body and write it to f buf = new byte[512]; while ( rlen >= 0 && size > 0 ) @@ -350,25 +385,24 @@ // 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, "UTF-8")); + 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 = null; - if (contentTypeHeader != null) { - st = new StringTokenizer(contentTypeHeader, ",; "); - if (st.hasMoreTokens()) { - contentType = st.nextToken(); - } + if( contentTypeHeader != null) { + st = new StringTokenizer( contentTypeHeader , "; " ); + if ( st.hasMoreTokens()) { + contentType = st.nextToken(); + } } if (contentType.equalsIgnoreCase("multipart/form-data")) { // Handle multipart/form-data @@ -381,17 +415,17 @@ st.nextToken(); String boundary = st.nextToken(); decodeMultipartData(boundary, fbuf, in, parms, files); } - else if (contentType.toLowerCase().startsWith("application/json")) + else if (contentType.toLowerCase().startsWith("application/json")) { StringBuffer sb = new StringBuffer(); String line = null; - while ((line = in.readLine()) != null) + while ((line = in.readLine()) != null) { - sb.append(line + "\n"); + sb.append(line + "\n"); } parms.put("json", sb.toString()); } else { @@ -523,15 +557,15 @@ 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 , "; " ); + StringTokenizer st = new StringTokenizer( contentDisposition , ";" ); Properties disposition = new Properties(); while ( st.hasMoreTokens()) { - String token = st.nextToken(); + String token = st.nextToken().trim(); 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"); @@ -574,10 +608,26 @@ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); } } /** + * Find byte index separating header from body. + * It must be the last byte of the first two sequential new lines. + **/ + private int findHeaderEnd(final byte[] buf, int rlen) + { + int splitbyte = 0; + while (splitbyte + 3 < rlen) + { + if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') + return splitbyte + 4; + splitbyte++; + } + return 0; + } + + /** * Find the byte positions where multipart boundaries start. **/ public int[] getBoundaryPositions(byte[] b, byte[] boundary) { int matchcount = 0; @@ -658,29 +708,30 @@ */ private String decodePercent( String str ) throws InterruptedException { try { - StringBuffer sb = new StringBuffer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); for( int i=0; i<str.length(); i++ ) { char c = str.charAt( i ); switch ( c ) { case '+': - sb.append( ' ' ); + baos.write( (int)' ' ); break; case '%': - sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 )); + baos.write(Integer.parseInt( str.substring(i+1,i+3), 16 )); i += 2; break; default: - sb.append( c ); + baos.write( (int)c ); break; } } - return sb.toString(); + + return new String( baos.toByteArray(), "UTF-8"); } catch( Exception e ) { sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." ); return null; @@ -706,10 +757,12 @@ String e = st.nextToken(); int sep = e.indexOf( '=' ); if ( sep >= 0 ) p.put( decodePercent( e.substring( 0, sep )).trim(), decodePercent( e.substring( sep+1 ))); + else + p.put( decodePercent( e ).trim(), "" ); } } /** * Returns an error message as a HTTP response and @@ -969,10 +1022,12 @@ { if ( startFrom >= fileLen) { res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" ); res.addHeader( "Content-Range", "bytes 0-0/" + fileLen); + if ( mime.startsWith( "application/" )) + res.addHeader( "Content-Disposition", "attachment; filename=\"" + f.getName() + "\""); res.addHeader( "ETag", etag); } else { if ( endAt < 0 ) @@ -987,10 +1042,12 @@ fis.skip( startFrom ); res = new Response( HTTP_PARTIALCONTENT, mime, fis ); res.addHeader( "Content-Length", "" + dataLen); res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); + if ( mime.startsWith( "application/" )) + res.addHeader( "Content-Disposition", "attachment; filename=\"" + f.getName() + "\""); res.addHeader( "ETag", etag); } } else { @@ -998,10 +1055,12 @@ res = new Response( HTTP_NOTMODIFIED, mime, ""); else { res = new Response( HTTP_OK, mime, new FileInputStream( f )); res.addHeader( "Content-Length", "" + fileLen); + if ( mime.startsWith( "application/" )) + res.addHeader( "Content-Disposition", "attachment; filename=\"" + f.getName() + "\""); res.addHeader( "ETag", etag); } } } } @@ -1036,11 +1095,11 @@ "mp4 video/mp4 " + "ogv video/ogg " + "flv video/x-flv " + "mov video/quicktime " + "swf application/x-shockwave-flash " + - "js application/javascript "+ + "js application/javascript "+ "pdf application/pdf "+ "doc application/msword "+ "ogg application/x-ogg "+ "zip application/octet-stream "+ "exe application/octet-stream "+ @@ -1067,10 +1126,10 @@ /** * The distribution licence */ private static final String LICENCE = - "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+ + "Copyright (C) 2001,2005-2013 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"+