AI2 Component  (Version nb184)
NanoHTTPD.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>
3 // and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>
4 // See Details at end of file.
5 package com.google.appinventor.components.runtime.util;
6 import java.io.BufferedReader;
7 import java.io.ByteArrayInputStream;
8 import java.io.File;
9 import java.io.FileInputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.InputStreamReader;
13 import java.io.OutputStream;
14 import java.io.PrintStream;
15 import java.io.PrintWriter;
16 import java.net.ServerSocket;
17 import java.net.Socket;
18 import java.net.URLEncoder;
19 import java.util.concurrent.SynchronousQueue;
20 import java.util.concurrent.ThreadFactory;
21 import java.util.concurrent.ThreadPoolExecutor;
22 import java.util.concurrent.TimeUnit;
23 import java.util.Date;
24 import java.util.Enumeration;
25 import java.util.Vector;
26 import java.util.Hashtable;
27 import java.util.Locale;
28 import java.util.Properties;
29 import java.util.StringTokenizer;
30 import java.util.TimeZone;
31 
32 import java.io.ByteArrayOutputStream;
33 import java.io.FileOutputStream;
34 
35 import android.util.Log;
36 
82 public class NanoHTTPD
83 {
84  private static final String LOG_TAG = "AppInvHTTPD"; // using this tag on purpose
85  // when filtering logcat for
86  // interesting message, will
87  // use this tag to search
88 
89  // ==================================================
90  // API parts
91  // ==================================================
92 
104  public Response serve( String uri, String method, Properties header, Properties parms, Properties files, Socket mySocket )
105  {
106  myOut.println( method + " '" + uri + "' " );
107 
108  Enumeration e = header.propertyNames();
109  while ( e.hasMoreElements())
110  {
111  String value = (String)e.nextElement();
112  myOut.println( " HDR: '" + value + "' = '" +
113  header.getProperty( value ) + "'" );
114  }
115  e = parms.propertyNames();
116  while ( e.hasMoreElements())
117  {
118  String value = (String)e.nextElement();
119  myOut.println( " PRM: '" + value + "' = '" +
120  parms.getProperty( value ) + "'" );
121  }
122  e = files.propertyNames();
123  while ( e.hasMoreElements())
124  {
125  String value = (String)e.nextElement();
126  myOut.println( " UPLOADED: '" + value + "' = '" +
127  files.getProperty( value ) + "'" );
128  }
129 
130  return serveFile( uri, header, myRootDir, true );
131  }
132 
137  public class Response
138  {
142  public Response()
143  {
144  this.status = HTTP_OK;
145  }
146 
150  public Response( String status, String mimeType, InputStream data )
151  {
152  this.status = status;
153  this.mimeType = mimeType;
154  this.data = data;
155  }
156 
161  public Response( String status, String mimeType, String txt )
162  {
163  this.status = status;
164  this.mimeType = mimeType;
165  try
166  {
167  this.data = new ByteArrayInputStream( txt.getBytes("UTF-8"));
168  }
169  catch ( java.io.UnsupportedEncodingException uee )
170  {
171  uee.printStackTrace();
172  }
173  }
174 
178  public void addHeader( String name, String value )
179  {
180  header.put( name, value );
181  }
182 
186  public String status;
187 
191  public String mimeType;
192 
196  public InputStream data;
197 
202  public Properties header = new Properties();
203  }
204 
208  public static final String
209  HTTP_OK = "200 OK",
210  HTTP_PARTIALCONTENT = "206 Partial Content",
211  HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable",
212  HTTP_REDIRECT = "301 Moved Permanently",
213  HTTP_NOTMODIFIED = "304 Not Modified",
214  HTTP_FORBIDDEN = "403 Forbidden",
215  HTTP_NOTFOUND = "404 Not Found",
216  HTTP_BADREQUEST = "400 Bad Request",
217  HTTP_INTERNALERROR = "500 Internal Server Error",
218  HTTP_NOTIMPLEMENTED = "501 Not Implemented";
219 
223  public static final String
224  MIME_PLAINTEXT = "text/plain",
225  MIME_HTML = "text/html",
226  MIME_DEFAULT_BINARY = "application/octet-stream",
227  MIME_XML = "text/xml";
228 
229  // ==================================================
230  // Socket & server code
231  // ==================================================
232 
233  private static final int REPL_STACK_SIZE = 256*1024;
234 
240  public NanoHTTPD( int port, File wwwroot ) throws IOException
241  {
242  myTcpPort = port;
243  this.myRootDir = wwwroot;
244  myServerSocket = new ServerSocket( myTcpPort );
245  myThread = new Thread(new Runnable()
246  {
247  public void run()
248  {
249  try
250  {
251  while( true )
252  new HTTPSession( myServerSocket.accept());
253  }
254  catch ( IOException ioe )
255  {}
256  }
257  });
258  myThread.setDaemon( true );
259  myThread.start();
260  }
261 
265  public void stop()
266  {
267  try
268  {
269  myServerSocket.close();
270  myThread.join();
271  }
272  catch ( IOException ioe ) {}
273  catch ( InterruptedException e ) {}
274  }
275 
276 
280  public static void main( String[] args )
281  {
282  myOut.println( "NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" +
283  "(Command line options: [-p port] [-d root-dir] [--licence])\n" );
284 
285  // Defaults
286  int port = 80;
287  File wwwroot = new File(".").getAbsoluteFile();
288 
289  // Show licence if requested
290  for ( int i=0; i<args.length; ++i )
291  if(args[i].equalsIgnoreCase("-p"))
292  port = Integer.parseInt( args[i+1] );
293  else if(args[i].equalsIgnoreCase("-d"))
294  wwwroot = new File( args[i+1] ).getAbsoluteFile();
295  else if ( args[i].toLowerCase().endsWith( "licence" ))
296  {
297  myOut.println( LICENCE + "\n" );
298  break;
299  }
300 
301  try
302  {
303  new NanoHTTPD( port, wwwroot );
304  }
305  catch( IOException ioe )
306  {
307  myErr.println( "Couldn't start server:\n" + ioe );
308  System.exit( -1 );
309  }
310 
311  myOut.println( "Now serving files in port " + port + " from \"" + wwwroot + "\"" );
312  myOut.println( "Hit Enter to stop.\n" );
313 
314  try { System.in.read(); } catch( Throwable t ) {}
315  }
316 
317  private class myThreadFactory implements ThreadFactory {
318 
319  public Thread newThread(Runnable r) {
320  Thread retval = new Thread(new ThreadGroup("biggerstack"), r, "HTTPD Session", REPL_STACK_SIZE);
321  retval.setDaemon(true);
322  return retval;
323  }
324  }
325 
333  private ThreadPoolExecutor myExecutor = new ThreadPoolExecutor(2, 10, 5,
334  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new myThreadFactory());
335 
340  private class HTTPSession implements Runnable
341  {
342  public HTTPSession( Socket s )
343  {
344  mySocket = s;
345  Log.d(LOG_TAG, "NanoHTTPD: getPoolSize() = " + myExecutor.getPoolSize());
346  myExecutor.execute(this);
347  }
348 
349  public void run()
350  {
351  try
352  {
353  InputStream is = mySocket.getInputStream();
354  if ( is == null) return;
355 
356  // Read the first 8192 bytes.
357  // The full header should fit in here.
358  // Apache's default header limit is 8KB.
359  int bufsize = 8192;
360  byte[] buf = new byte[bufsize];
361  int rlen = is.read(buf, 0, bufsize);
362  if (rlen <= 0) return;
363 
364  // Create a BufferedReader for parsing the header.
365  ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
366  BufferedReader hin = new BufferedReader( new InputStreamReader( hbis ));
367  Properties pre = new Properties();
368  Properties parms = new Properties();
369  Properties header = new Properties();
370  Properties files = new Properties();
371 
372  // Decode the header into parms and header java properties
373  decodeHeader(hin, pre, parms, header);
374  String method = pre.getProperty("method");
375  String uri = pre.getProperty("uri");
376 
377  long size = 0x7FFFFFFFFFFFFFFFl;
378  String contentLength = header.getProperty("content-length");
379  if (contentLength != null)
380  {
381  try { size = Integer.parseInt(contentLength); }
382  catch (NumberFormatException ex) {}
383  }
384 
385  // We are looking for the byte separating header from body.
386  // It must be the last byte of the first two sequential new lines.
387  int splitbyte = 0;
388  boolean sbfound = false;
389  while (splitbyte < rlen)
390  {
391  if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') {
392  sbfound = true;
393  break;
394  }
395  splitbyte++;
396  }
397  splitbyte++;
398 
399  // Write the part of body already read to ByteArrayOutputStream f
400  OutputStream f;
401  if ( method.equalsIgnoreCase( "PUT" ) )
402  {
403  File tmpfile = File.createTempFile("upload", "bin");
404  tmpfile.deleteOnExit();
405  f = new FileOutputStream(tmpfile);
406  files.put("content", tmpfile.getAbsolutePath());
407  }
408  else
409  {
410  f = new ByteArrayOutputStream();
411  }
412  if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte);
413 
414  // While Firefox sends on the first read all the data fitting
415  // our buffer, Chrome and Opera sends only the headers even if
416  // there is data for the body. So we do some magic here to find
417  // out whether we have already consumed part of body, if we
418  // have reached the end of the data to be sent or we should
419  // expect the first byte of the body at the next read.
420  if (splitbyte < rlen)
421  size -= rlen - splitbyte +1;
422  else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
423  size = 0;
424 
425  // Now read all the body and write it to f
426  buf = new byte[512];
427  while ( rlen >= 0 && size > 0 )
428  {
429  rlen = is.read(buf, 0, 512);
430  size -= rlen;
431  if (rlen > 0)
432  f.write(buf, 0, rlen);
433  }
434 
435  // If the method is POST, there may be parameters
436  // in data section, too, read it:
437  if ( method.equalsIgnoreCase( "POST" ))
438  {
439  // Get the raw body as a byte []
440  byte [] fbuf = ((ByteArrayOutputStream)f).toByteArray();
441 
442  // Create a BufferedReader for easily reading it as string.
443  ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
444  BufferedReader in = new BufferedReader( new InputStreamReader(bin));
445 
446  String contentType = "";
447  String contentTypeHeader = header.getProperty("content-type");
448  StringTokenizer st = new StringTokenizer( contentTypeHeader , "; " );
449  if ( st.hasMoreTokens()) {
450  contentType = st.nextToken();
451  }
452 
453  if (contentType.equalsIgnoreCase("multipart/form-data"))
454  {
455  // Handle multipart/form-data
456  if ( !st.hasMoreTokens())
457  sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" );
458  String boundaryExp = st.nextToken();
459  st = new StringTokenizer( boundaryExp , "=" );
460  if (st.countTokens() != 2)
461  sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" );
462  st.nextToken();
463  String boundary = st.nextToken();
464 
465  decodeMultipartData(boundary, fbuf, in, parms, files);
466  }
467  else
468  {
469  // Handle application/x-www-form-urlencoded
470  String postLine = "";
471  char pbuf[] = new char[512];
472  int read = in.read(pbuf);
473  while ( read >= 0 && !postLine.endsWith("\r\n") )
474  {
475  postLine += String.valueOf(pbuf, 0, read);
476  read = in.read(pbuf);
477  }
478  postLine = postLine.trim();
479  decodeParms( postLine, parms );
480  }
481  in.close();
482  }
483  else if ( method.equalsIgnoreCase( "PUT " ) )
484  {
485  f.close(); // Close open file
486  }
487 
488  // Ok, now do the serve()
489  Response r = serve( uri, method, header, parms, files, mySocket );
490  if ( r == null )
491  sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
492  else
493  sendResponse( r.status, r.mimeType, r.header, r.data );
494 
495  is.close();
496  }
497  catch ( IOException ioe )
498  {
499  try
500  {
501  sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
502  }
503  catch ( Throwable t ) {}
504  }
505  catch ( InterruptedException ie )
506  {
507  // Thrown by sendError, ignore and exit the thread.
508  }
509  }
510 
515  private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header)
516  throws InterruptedException
517  {
518  try {
519  // Read the request line
520  String inLine = in.readLine();
521  if (inLine == null) return;
522  StringTokenizer st = new StringTokenizer( inLine );
523  if ( !st.hasMoreTokens())
524  sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
525 
526  String method = st.nextToken();
527  pre.put("method", method);
528 
529  if ( !st.hasMoreTokens())
530  sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
531 
532  String uri = st.nextToken();
533 
534  // Decode parameters from the URI
535  int qmi = uri.indexOf( '?' );
536  if ( qmi >= 0 )
537  {
538  decodeParms( uri.substring( qmi+1 ), parms );
539  uri = decodePercent( uri.substring( 0, qmi ));
540  }
541  else uri = decodePercent(uri);
542 
543  // If there's another token, it's protocol version,
544  // followed by HTTP headers. Ignore version but parse headers.
545  // NOTE: this now forces header names lowercase since they are
546  // case insensitive and vary by client.
547  if ( st.hasMoreTokens())
548  {
549  String line = in.readLine();
550  while ( line != null && line.trim().length() > 0 )
551  {
552  int p = line.indexOf( ':' );
553  if ( p >= 0 )
554  header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
555  line = in.readLine();
556  }
557  }
558 
559  pre.put("uri", uri);
560  }
561  catch ( IOException ioe )
562  {
563  sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
564  }
565  }
566 
571  private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files)
572  throws InterruptedException
573  {
574  try
575  {
576  int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes());
577  int boundarycount = 1;
578  String mpline = in.readLine();
579  while ( mpline != null )
580  {
581  if (mpline.indexOf(boundary) == -1)
582  sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" );
583  boundarycount++;
584  Properties item = new Properties();
585  mpline = in.readLine();
586  while (mpline != null && mpline.trim().length() > 0)
587  {
588  int p = mpline.indexOf( ':' );
589  if (p != -1)
590  item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim());
591  mpline = in.readLine();
592  }
593  if (mpline != null)
594  {
595  String contentDisposition = item.getProperty("content-disposition");
596  if (contentDisposition == null)
597  {
598  sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" );
599  }
600  StringTokenizer st = new StringTokenizer( contentDisposition , "; " );
601  Properties disposition = new Properties();
602  while ( st.hasMoreTokens())
603  {
604  String token = st.nextToken();
605  int p = token.indexOf( '=' );
606  if (p!=-1)
607  disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim());
608  }
609  String pname = disposition.getProperty("name");
610  pname = pname.substring(1,pname.length()-1);
611 
612  String value = "";
613  if (item.getProperty("content-type") == null) {
614  while (mpline != null && mpline.indexOf(boundary) == -1)
615  {
616  mpline = in.readLine();
617  if ( mpline != null)
618  {
619  int d = mpline.indexOf(boundary);
620  if (d == -1)
621  value+=mpline;
622  else
623  value+=mpline.substring(0,d-2);
624  }
625  }
626  }
627  else
628  {
629  if (boundarycount> bpositions.length)
630  sendError( HTTP_INTERNALERROR, "Error processing request" );
631  int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]);
632  String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4);
633  files.put(pname, path);
634  value = disposition.getProperty("filename");
635  value = value.substring(1,value.length()-1);
636  do {
637  mpline = in.readLine();
638  } while (mpline != null && mpline.indexOf(boundary) == -1);
639  }
640  parms.put(pname, value);
641  }
642  }
643  }
644  catch ( IOException ioe )
645  {
646  sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
647  }
648  }
649 
653  public int[] getBoundaryPositions(byte[] b, byte[] boundary)
654  {
655  int matchcount = 0;
656  int matchbyte = -1;
657  Vector matchbytes = new Vector();
658  for (int i=0; i<b.length; i++)
659  {
660  if (b[i] == boundary[matchcount])
661  {
662  if (matchcount == 0)
663  matchbyte = i;
664  matchcount++;
665  if (matchcount==boundary.length)
666  {
667  matchbytes.addElement(new Integer(matchbyte));
668  matchcount = 0;
669  matchbyte = -1;
670  }
671  }
672  else
673  {
674  i -= matchcount;
675  matchcount = 0;
676  matchbyte = -1;
677  }
678  }
679  int[] ret = new int[matchbytes.size()];
680  for (int i=0; i < ret.length; i++)
681  {
682  ret[i] = ((Integer)matchbytes.elementAt(i)).intValue();
683  }
684  return ret;
685  }
686 
692  private String saveTmpFile(byte[] b, int offset, int len)
693  {
694  String path = "";
695  if (len > 0)
696  {
697  String tmpdir = System.getProperty("java.io.tmpdir");
698  try {
699  File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
700  OutputStream fstream = new FileOutputStream(temp);
701  fstream.write(b, offset, len);
702  fstream.close();
703  path = temp.getAbsolutePath();
704  } catch (Exception e) { // Catch exception if any
705  myErr.println("Error: " + e.getMessage());
706  }
707  }
708  return path;
709  }
710 
711 
716  private int stripMultipartHeaders(byte[] b, int offset)
717  {
718  int i = 0;
719  for (i=offset; i<b.length; i++)
720  {
721  if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n')
722  break;
723  }
724  return i+1;
725  }
726 
731  private String decodePercent( String str ) throws InterruptedException
732  {
733  try
734  {
735  StringBuffer sb = new StringBuffer();
736  for( int i=0; i<str.length(); i++ )
737  {
738  char c = str.charAt( i );
739  switch ( c )
740  {
741  case '+':
742  sb.append( ' ' );
743  break;
744  case '%':
745  sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
746  i += 2;
747  break;
748  default:
749  sb.append( c );
750  break;
751  }
752  }
753  return sb.toString();
754  }
755  catch( Exception e )
756  {
757  sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
758  return null;
759  }
760  }
761 
769  private void decodeParms( String parms, Properties p )
770  throws InterruptedException
771  {
772  if ( parms == null )
773  return;
774 
775  StringTokenizer st = new StringTokenizer( parms, "&" );
776  while ( st.hasMoreTokens())
777  {
778  String e = st.nextToken();
779  int sep = e.indexOf( '=' );
780  if ( sep >= 0 )
781  p.put( decodePercent( e.substring( 0, sep )).trim(),
782  decodePercent( e.substring( sep+1 )));
783  }
784  }
785 
790  private void sendError( String status, String msg ) throws InterruptedException
791  {
792  sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
793  throw new InterruptedException();
794  }
795 
799  private void sendResponse( String status, String mime, Properties header, InputStream data )
800  {
801  try
802  {
803  if ( status == null )
804  throw new Error( "sendResponse(): Status can't be null." );
805 
806  OutputStream out = mySocket.getOutputStream();
807  PrintWriter pw = new PrintWriter( out );
808  pw.print("HTTP/1.0 " + status + " \r\n");
809 
810  if ( mime != null )
811  pw.print("Content-Type: " + mime + "\r\n");
812 
813  if ( header == null || header.getProperty( "Date" ) == null )
814  pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
815 
816  if ( header != null )
817  {
818  Enumeration e = header.keys();
819  while ( e.hasMoreElements())
820  {
821  String key = (String)e.nextElement();
822  String value = header.getProperty( key );
823  pw.print( key + ": " + value + "\r\n");
824  }
825  }
826 
827  pw.print("\r\n");
828  pw.flush();
829 
830  if ( data != null )
831  {
832  int pending = data.available(); // This is to support partial sends, see serveFile()
833  byte[] buff = new byte[theBufferSize];
834  while (pending>0)
835  {
836  int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending ));
837  if (read <= 0) break;
838  out.write( buff, 0, read );
839  pending -= read;
840  }
841  }
842  out.flush();
843  out.close();
844  if ( data != null )
845  data.close();
846  }
847  catch( IOException ioe )
848  {
849  // Couldn't write? No can do.
850  try { mySocket.close(); } catch( Throwable t ) {}
851  }
852  }
853 
854  private Socket mySocket;
855  }
856 
861  private String encodeUri( String uri )
862  {
863  String newUri = "";
864  StringTokenizer st = new StringTokenizer( uri, "/ ", true );
865  while ( st.hasMoreTokens())
866  {
867  String tok = st.nextToken();
868  if ( tok.equals( "/" ))
869  newUri += "/";
870  else if ( tok.equals( " " ))
871  newUri += "%20";
872  else
873  {
874  newUri += URLEncoder.encode( tok );
875  // For Java 1.4 you'll want to use this instead:
876  // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {}
877  }
878  }
879  return newUri;
880  }
881 
882  private int myTcpPort;
883  private final ServerSocket myServerSocket;
884  private Thread myThread;
885  private File myRootDir;
886 
887  // ==================================================
888  // File server code
889  // ==================================================
890 
895  public Response serveFile( String uri, Properties header, File homeDir,
896  boolean allowDirectoryListing )
897  {
898  Response res = null;
899 
900  // Make sure we won't die of an exception later
901  if ( !homeDir.isDirectory())
902  res = new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
903  "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
904 
905  if ( res == null )
906  {
907  // Remove URL arguments
908  uri = uri.trim().replace( File.separatorChar, '/' );
909  if ( uri.indexOf( '?' ) >= 0 )
910  uri = uri.substring(0, uri.indexOf( '?' ));
911 
912  // Prohibit getting out of current directory
913  if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
914  res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
915  "FORBIDDEN: Won't serve ../ for security reasons." );
916  }
917 
918  File f = new File( homeDir, uri );
919  if ( res == null && !f.exists())
920  res = new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
921  "Error 404, file not found." );
922 
923  // List the directory, if necessary
924  if ( res == null && f.isDirectory())
925  {
926  // Browsers get confused without '/' after the
927  // directory, send a redirect.
928  if ( !uri.endsWith( "/" ))
929  {
930  uri += "/";
931  res = new Response( HTTP_REDIRECT, MIME_HTML,
932  "<html><body>Redirected: <a href=\"" + uri + "\">" +
933  uri + "</a></body></html>");
934  res.addHeader( "Location", uri );
935  }
936 
937  if ( res == null )
938  {
939  // First try index.html and index.htm
940  if ( new File( f, "index.html" ).exists())
941  f = new File( homeDir, uri + "/index.html" );
942  else if ( new File( f, "index.htm" ).exists())
943  f = new File( homeDir, uri + "/index.htm" );
944  // No index file, list the directory if it is readable
945  else if ( allowDirectoryListing && f.canRead() )
946  {
947  String[] files = f.list();
948  String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
949 
950  if ( uri.length() > 1 )
951  {
952  String u = uri.substring( 0, uri.length()-1 );
953  int slash = u.lastIndexOf( '/' );
954  if ( slash >= 0 && slash < u.length())
955  msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
956  }
957 
958  if (files!=null)
959  {
960  for ( int i=0; i<files.length; ++i )
961  {
962  File curFile = new File( f, files[i] );
963  boolean dir = curFile.isDirectory();
964  if ( dir )
965  {
966  msg += "<b>";
967  files[i] += "/";
968  }
969 
970  msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
971  files[i] + "</a>";
972 
973  // Show file size
974  if ( curFile.isFile())
975  {
976  long len = curFile.length();
977  msg += " &nbsp;<font size=2>(";
978  if ( len < 1024 )
979  msg += len + " bytes";
980  else if ( len < 1024 * 1024 )
981  msg += len/1024 + "." + (len%1024/10%100) + " KB";
982  else
983  msg += len/(1024*1024) + "." + len%(1024*1024)/10%100 + " MB";
984 
985  msg += ")</font>";
986  }
987  msg += "<br/>";
988  if ( dir ) msg += "</b>";
989  }
990  }
991  msg += "</body></html>";
992  res = new Response( HTTP_OK, MIME_HTML, msg );
993  }
994  else
995  {
996  res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
997  "FORBIDDEN: No directory listing." );
998  }
999  }
1000  }
1001 
1002  try
1003  {
1004  if ( res == null )
1005  {
1006  // Get MIME type from file name extension, if possible
1007  String mime = null;
1008  int dot = f.getCanonicalPath().lastIndexOf( '.' );
1009  if ( dot >= 0 )
1010  mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
1011  if ( mime == null )
1012  mime = MIME_DEFAULT_BINARY;
1013 
1014  // Calculate etag
1015  String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
1016 
1017  // Support (simple) skipping:
1018  long startFrom = 0;
1019  long endAt = -1;
1020  String range = header.getProperty( "range" );
1021  if ( range != null )
1022  {
1023  if ( range.startsWith( "bytes=" ))
1024  {
1025  range = range.substring( "bytes=".length());
1026  int minus = range.indexOf( '-' );
1027  try {
1028  if ( minus > 0 )
1029  {
1030  startFrom = Long.parseLong( range.substring( 0, minus ));
1031  endAt = Long.parseLong( range.substring( minus+1 ));
1032  }
1033  }
1034  catch ( NumberFormatException nfe ) {}
1035  }
1036  }
1037 
1038  // Change return code and add Content-Range header when skipping is requested
1039  long fileLen = f.length();
1040  if (range != null && startFrom >= 0)
1041  {
1042  if ( startFrom >= fileLen)
1043  {
1044  res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" );
1045  res.addHeader( "Content-Range", "bytes 0-0/" + fileLen);
1046  res.addHeader( "ETag", etag);
1047  }
1048  else
1049  {
1050  if ( endAt < 0 )
1051  endAt = fileLen-1;
1052  long newLen = endAt - startFrom + 1;
1053  if ( newLen < 0 ) newLen = 0;
1054 
1055  final long dataLen = newLen;
1056  FileInputStream fis = new FileInputStream( f ) {
1057  public int available() throws IOException { return (int)dataLen; }
1058  };
1059  fis.skip( startFrom );
1060 
1061  res = new Response( HTTP_PARTIALCONTENT, mime, fis );
1062  res.addHeader( "Content-Length", "" + dataLen);
1063  res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
1064  res.addHeader( "ETag", etag);
1065  }
1066  }
1067  else
1068  {
1069  if (etag.equals(header.getProperty("if-none-match")))
1070  res = new Response( HTTP_NOTMODIFIED, mime, "");
1071  else
1072  {
1073  res = new Response( HTTP_OK, mime, new FileInputStream( f ));
1074  res.addHeader( "Content-Length", "" + fileLen);
1075  res.addHeader( "ETag", etag);
1076  }
1077  }
1078  }
1079  }
1080  catch( IOException ioe )
1081  {
1082  res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
1083  }
1084 
1085  res.addHeader( "Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
1086  return res;
1087  }
1088 
1092  private static Hashtable theMimeTypes = new Hashtable();
1093  static
1094  {
1095  StringTokenizer st = new StringTokenizer(
1096  "css text/css "+
1097  "htm text/html "+
1098  "html text/html "+
1099  "xml text/xml "+
1100  "txt text/plain "+
1101  "asc text/plain "+
1102  "gif image/gif "+
1103  "jpg image/jpeg "+
1104  "jpeg image/jpeg "+
1105  "png image/png "+
1106  "mp3 audio/mpeg "+
1107  "m3u audio/mpeg-url " +
1108  "mp4 video/mp4 " +
1109  "ogv video/ogg " +
1110  "flv video/x-flv " +
1111  "mov video/quicktime " +
1112  "swf application/x-shockwave-flash " +
1113  "js application/javascript "+
1114  "pdf application/pdf "+
1115  "doc application/msword "+
1116  "ogg application/x-ogg "+
1117  "zip application/octet-stream "+
1118  "exe application/octet-stream "+
1119  "class application/octet-stream " );
1120  while ( st.hasMoreTokens())
1121  theMimeTypes.put( st.nextToken(), st.nextToken());
1122  }
1123 
1124  private static int theBufferSize = 16 * 1024;
1125 
1126  // Change these if you want to log to somewhere else than stdout
1127  protected static PrintStream myOut = System.out;
1128  protected static PrintStream myErr = System.err;
1129 
1133  private static java.text.SimpleDateFormat gmtFrmt;
1134  static
1135  {
1136  gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
1137  gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
1138  }
1139 
1143  private static final String LICENCE =
1144  "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+
1145  "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"+
1146  "\n"+
1147  "Redistribution and use in source and binary forms, with or without\n"+
1148  "modification, are permitted provided that the following conditions\n"+
1149  "are met:\n"+
1150  "\n"+
1151  "Redistributions of source code must retain the above copyright notice,\n"+
1152  "this list of conditions and the following disclaimer. Redistributions in\n"+
1153  "binary form must reproduce the above copyright notice, this list of\n"+
1154  "conditions and the following disclaimer in the documentation and/or other\n"+
1155  "materials provided with the distribution. The name of the author may not\n"+
1156  "be used to endorse or promote products derived from this software without\n"+
1157  "specific prior written permission. \n"+
1158  " \n"+
1159  "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
1160  "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
1161  "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
1162  "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
1163  "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
1164  "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
1165  "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
1166  "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
1167  "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
1168  "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
1169 }
1170 
com.google.appinventor.components.runtime.util.NanoHTTPD.MIME_PLAINTEXT
static final String MIME_PLAINTEXT
Definition: NanoHTTPD.java:224
com.google.appinventor.components.runtime.util.NanoHTTPD
Definition: NanoHTTPD.java:82
com.google.appinventor.components.runtime.util.NanoHTTPD.Response
Definition: NanoHTTPD.java:137
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.Response
Response()
Definition: NanoHTTPD.java:142
com.google.appinventor.components.runtime.util.NanoHTTPD.myOut
static PrintStream myOut
Definition: NanoHTTPD.java:1127
com.google.appinventor.components.runtime.util.NanoHTTPD.NanoHTTPD
NanoHTTPD(int port, File wwwroot)
Definition: NanoHTTPD.java:240
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.mimeType
String mimeType
Definition: NanoHTTPD.java:191
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.Response
Response(String status, String mimeType, String txt)
Definition: NanoHTTPD.java:161
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.addHeader
void addHeader(String name, String value)
Definition: NanoHTTPD.java:178
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.status
String status
Definition: NanoHTTPD.java:186
com.google.appinventor.components.runtime.util.NanoHTTPD.stop
void stop()
Definition: NanoHTTPD.java:265
com.google.appinventor.components.runtime.util.NanoHTTPD.main
static void main(String[] args)
Definition: NanoHTTPD.java:280
com.google.appinventor.components.runtime.util.NanoHTTPD.serveFile
Response serveFile(String uri, Properties header, File homeDir, boolean allowDirectoryListing)
Definition: NanoHTTPD.java:895
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.Response
Response(String status, String mimeType, InputStream data)
Definition: NanoHTTPD.java:150
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.data
InputStream data
Definition: NanoHTTPD.java:196
com.google.appinventor.components.runtime.util.NanoHTTPD.myErr
static PrintStream myErr
Definition: NanoHTTPD.java:1128
com.google.appinventor.components.runtime.util.NanoHTTPD.serve
Response serve(String uri, String method, Properties header, Properties parms, Properties files, Socket mySocket)
Definition: NanoHTTPD.java:104
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.header
Properties header
Definition: NanoHTTPD.java:202
com.google.appinventor.components.runtime.util.NanoHTTPD.HTTP_OK
static final String HTTP_OK
Definition: NanoHTTPD.java:209