1 package com.ebay.carad.os.vitalsigns.util;
2
3 import java.io.*;
4 import java.util.*;
5 import java.net.*;
6
7 /***
8 * A simple, tiny, nicely embeddable HTTP 1.0 server in Java
9 *
10 * <p> NanoHTTPD version 1.1,
11 * Copyright © 2001,2005-2007 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
12 *
13 * <p><b>Features + limitations: </b><ul>
14 *
15 * <li> Only one Java file </li>
16 * <li> Java 1.1 compatible </li>
17 * <li> Released as open source, Modified BSD licence </li>
18 * <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
19 * <li> Supports parameter parsing of GET and POST methods </li>
20 * <li> Supports both dynamic content and file serving </li>
21 * <li> Never caches anything </li>
22 * <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
23 * <li> Default code serves files and shows all HTTP parameters and headers</li>
24 * <li> File server supports directory listing, index.html and index.htm </li>
25 * <li> File server does the 301 redirection trick for directories without '/'</li>
26 * <li> File server supports simple skipping for files (continue download) </li>
27 * <li> File server uses current directory as a web root </li>
28 * <li> File server serves also very long files without memory overhead </li>
29 * <li> Contains a built-in list of most common mime types </li>
30 * <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
31 *
32 * </ul>
33 *
34 * <p><b>Ways to use: </b><ul>
35 *
36 * <li> Run as a standalone app, serves files from current directory and shows requests</li>
37 * <li> Subclass serve() and embed to your own program </li>
38 * <li> Call serveFile() from serve() with your own base directory </li>
39 *
40 * </ul>
41 *
42 * See the end of the source file for distribution license
43 * (Modified BSD licence)
44 */
45 public class NanoHTTPD
46 {
47
48
49
50
51 /***
52 * Override this to customize the server.<p>
53 *
54 * (By default, this delegates to serveFile() and allows directory listing.)
55 *
56 * @parm uri Percent-decoded URI without parameters, for example "/index.cgi"
57 * @parm method "GET", "POST" etc.
58 * @parm parms Parsed, percent decoded parameters from URI and, in case of POST, data.
59 * @parm header Header entries, percent decoded
60 * @return HTTP response, see class Response for details
61 */
62 public Response serve( String uri, String method, Properties header, Properties parms )
63 {
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 return serveFile( uri, header, myFileDir, true );
82 }
83
84 /***
85 * HTTP response.
86 * Return one of these from serve().
87 */
88 public class Response
89 {
90 /***
91 * Default constructor: response = HTTP_OK, data = mime = 'null'
92 */
93 public Response()
94 {
95 this.status = HTTP_OK;
96 }
97
98 /***
99 * Basic constructor.
100 */
101 public Response( String status, String mimeType, InputStream data )
102 {
103 this.status = status;
104 this.mimeType = mimeType;
105 this.data = data;
106 }
107
108 /***
109 * Convenience method that makes an InputStream out of
110 * given text.
111 */
112 public Response( String status, String mimeType, String txt )
113 {
114 this.status = status;
115 this.mimeType = mimeType;
116 this.data = new ByteArrayInputStream( txt.getBytes());
117 }
118
119 /***
120 * Adds given line to the header.
121 */
122 public void addHeader( String name, String value )
123 {
124 header.put( name, value );
125 }
126
127 /***
128 * HTTP status code after processing, e.g. "200 OK", HTTP_OK
129 */
130 public String status;
131
132 /***
133 * MIME type of content, e.g. "text/html"
134 */
135 public String mimeType;
136
137 /***
138 * Data of the response, may be null.
139 */
140 public InputStream data;
141
142 /***
143 * Headers for the HTTP response. Use addHeader()
144 * to add lines.
145 */
146 public Properties header = new Properties();
147 }
148
149 /***
150 * Some HTTP response status codes
151 */
152 public static final String
153 HTTP_OK = "200 OK",
154 HTTP_REDIRECT = "301 Moved Permanently",
155 HTTP_FORBIDDEN = "403 Forbidden",
156 HTTP_NOTFOUND = "404 Not Found",
157 HTTP_BADREQUEST = "400 Bad Request",
158 HTTP_INTERNALERROR = "500 Internal Server Error",
159 HTTP_NOTIMPLEMENTED = "501 Not Implemented";
160
161 /***
162 * Common mime types for dynamic content
163 */
164 public static final String
165 MIME_PLAINTEXT = "text/plain",
166 MIME_HTML = "text/html",
167 MIME_DEFAULT_BINARY = "application/octet-stream";
168
169
170
171
172
173 /***
174 * Starts a HTTP server to given port.<p>
175 * Throws an IOException if the socket is already in use
176 */
177 public NanoHTTPD( int port ) throws IOException
178 {
179 myTcpPort = port;
180
181 final ServerSocket ss = new ServerSocket( myTcpPort );
182 Thread t = new Thread( new Runnable()
183 {
184 public void run()
185 {
186 try
187 {
188 while( true )
189 new HTTPSession( ss.accept());
190 }
191 catch ( IOException ioe )
192 {}
193 }
194 });
195 t.setDaemon( true );
196 t.start();
197 }
198
199 public NanoHTTPD(int port, String directory) throws IOException {
200 this(port);
201 myFileDir = new File(directory);
202 System.out.println( "Now serving files in port " + port + " from \"" +
203 myFileDir.getAbsolutePath() + "\"" );
204 }
205
206 /***
207 * Starts as a standalone file server and waits for Enter.
208 */
209 public static void main( String[] args )
210 {
211 System.out.println( "NanoHTTPD 1.1 (C) 2001,2005-2007 Jarno Elonen\n" +
212 "(Command line options: [port] [--licence])\n" );
213
214
215 int lopt = -1;
216 for ( int i=0; i<args.length; ++i )
217 if ( args[i].toLowerCase().endsWith( "licence" ))
218 {
219 lopt = i;
220 System.out.println( LICENCE + "\n" );
221 }
222
223
224 int port = 80;
225 if ( args.length > 0 && lopt != 0 )
226 port = Integer.parseInt( args[0] );
227
228 if ( args.length > 1 &&
229 args[1].toLowerCase().endsWith( "licence" ))
230 System.out.println( LICENCE + "\n" );
231
232 NanoHTTPD nh = null;
233 try
234 {
235 nh = new NanoHTTPD( port );
236 }
237 catch( IOException ioe )
238 {
239 System.err.println( "Couldn't start server:\n" + ioe );
240 System.exit( -1 );
241 }
242 nh.myFileDir = new File("");
243
244 System.out.println( "Now serving files in port " + port + " from \"" +
245 new File("").getAbsolutePath() + "\"" );
246
247
248
249 }
250
251 /***
252 * Handles one session, i.e. parses the HTTP request
253 * and returns the response.
254 */
255 private class HTTPSession implements Runnable
256 {
257 public HTTPSession( Socket s )
258 {
259 mySocket = s;
260 Thread t = new Thread( this );
261 t.setDaemon( true );
262 t.start();
263 }
264
265 public void run()
266 {
267 try
268 {
269 InputStream is = mySocket.getInputStream();
270 if ( is == null) return;
271 BufferedReader in = new BufferedReader( new InputStreamReader( is ));
272
273
274 StringTokenizer st = new StringTokenizer( in.readLine());
275 if ( !st.hasMoreTokens())
276 sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
277
278 String method = st.nextToken();
279
280 if ( !st.hasMoreTokens())
281 sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
282
283 String uri = decodePercent( st.nextToken());
284
285
286 Properties parms = new Properties();
287 int qmi = uri.indexOf( '?' );
288 if ( qmi >= 0 )
289 {
290 decodeParms( uri.substring( qmi+1 ), parms );
291 uri = decodePercent( uri.substring( 0, qmi ));
292 }
293
294
295
296
297
298 Properties header = new Properties();
299 if ( st.hasMoreTokens())
300 {
301 String line = in.readLine();
302 while ( line.trim().length() > 0 )
303 {
304 int p = line.indexOf( ':' );
305 header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
306 line = in.readLine();
307 }
308 }
309
310
311
312 if ( method.equalsIgnoreCase( "POST" ))
313 {
314 long size = 0x7FFFFFFFFFFFFFFFl;
315 String contentLength = header.getProperty("content-length");
316 if (contentLength != null)
317 {
318 try { size = Integer.parseInt(contentLength); }
319 catch (NumberFormatException ex) {}
320 }
321 String postLine = "";
322 char buf[] = new char[512];
323 int read = in.read(buf);
324 while ( read >= 0 && size > 0 && !postLine.endsWith("\r\n") )
325 {
326 size -= read;
327 postLine += String.valueOf(buf, 0, read);
328 if ( size > 0 )
329 read = in.read(buf);
330 }
331 postLine = postLine.trim();
332 decodeParms( postLine, parms );
333 }
334
335
336 Response r = serve( uri, method, header, parms );
337 if ( r == null )
338 sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
339 else
340 sendResponse( r.status, r.mimeType, r.header, r.data );
341
342 in.close();
343 }
344 catch ( IOException ioe )
345 {
346 try
347 {
348 sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
349 }
350 catch ( Throwable t ) {}
351 }
352 catch ( InterruptedException ie )
353 {
354
355 }
356 }
357
358 /***
359 * Decodes the percent encoding scheme. <br/>
360 * For example: "an+example%20string" -> "an example string"
361 */
362 private String decodePercent( String str ) throws InterruptedException
363 {
364 try
365 {
366 StringBuffer sb = new StringBuffer();
367 for( int i=0; i<str.length(); i++ )
368 {
369 char c = str.charAt( i );
370 switch ( c )
371 {
372 case '+':
373 sb.append( ' ' );
374 break;
375 case '%':
376 sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
377 i += 2;
378 break;
379 default:
380 sb.append( c );
381 break;
382 }
383 }
384 return new String( sb.toString().getBytes());
385 }
386 catch( Exception e )
387 {
388 sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
389 return null;
390 }
391 }
392
393 /***
394 * Decodes parameters in percent-encoded URI-format
395 * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
396 * adds them to given Properties.
397 */
398 private void decodeParms( String parms, Properties p )
399 throws InterruptedException
400 {
401 if ( parms == null )
402 return;
403
404 StringTokenizer st = new StringTokenizer( parms, "&" );
405 while ( st.hasMoreTokens())
406 {
407 String e = st.nextToken();
408 int sep = e.indexOf( '=' );
409 if ( sep >= 0 )
410 p.put( decodePercent( e.substring( 0, sep )).trim(),
411 decodePercent( e.substring( sep+1 )));
412 }
413 }
414
415 /***
416 * Returns an error message as a HTTP response and
417 * throws InterruptedException to stop furhter request processing.
418 */
419 private void sendError( String status, String msg ) throws InterruptedException
420 {
421 sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
422 throw new InterruptedException();
423 }
424
425 /***
426 * Sends given response to the socket.
427 */
428 private void sendResponse( String status, String mime, Properties header, InputStream data )
429 {
430 try
431 {
432 if ( status == null )
433 throw new Error( "sendResponse(): Status can't be null." );
434
435 OutputStream out = mySocket.getOutputStream();
436 PrintWriter pw = new PrintWriter( out );
437 pw.print("HTTP/1.0 " + status + " \r\n");
438
439 if ( mime != null )
440 pw.print("Content-Type: " + mime + "\r\n");
441
442 if ( header == null || header.getProperty( "Date" ) == null )
443 pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
444
445 if ( header != null )
446 {
447 Enumeration e = header.keys();
448 while ( e.hasMoreElements())
449 {
450 String key = (String)e.nextElement();
451 String value = header.getProperty( key );
452 pw.print( key + ": " + value + "\r\n");
453 }
454 }
455
456 pw.print("\r\n");
457 pw.flush();
458
459 if ( data != null )
460 {
461 byte[] buff = new byte[2048];
462 while (true)
463 {
464 int read = data.read( buff, 0, 2048 );
465 if (read <= 0)
466 break;
467 out.write( buff, 0, read );
468 }
469 }
470 out.flush();
471 out.close();
472 if ( data != null )
473 data.close();
474 }
475 catch( IOException ioe )
476 {
477
478 try { mySocket.close(); } catch( Throwable t ) {}
479 }
480 }
481
482 private Socket mySocket;
483 };
484
485 /***
486 * URL-encodes everything between "/"-characters.
487 * Encodes spaces as '%20' instead of '+'.
488 */
489 private String encodeUri( String uri )
490 {
491 String newUri = "";
492 StringTokenizer st = new StringTokenizer( uri, "/ ", true );
493 while ( st.hasMoreTokens())
494 {
495 String tok = st.nextToken();
496 if ( tok.equals( "/" ))
497 newUri += "/";
498 else if ( tok.equals( " " ))
499 newUri += "%20";
500 else
501 {
502
503
504 try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( UnsupportedEncodingException uee ) {}
505 }
506 }
507 return newUri;
508 }
509
510 private int myTcpPort;
511 File myFileDir;
512
513
514
515
516
517 /***
518 * Serves file from homeDir and its' subdirectories (only).
519 * Uses only URI, ignores all headers and HTTP parameters.
520 */
521 public Response serveFile( String uri, Properties header, File homeDir,
522 boolean allowDirectoryListing )
523 {
524
525 if ( !homeDir.isDirectory())
526 return new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
527 "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
528
529
530 uri = uri.trim().replace( File.separatorChar, '/' );
531 if ( uri.indexOf( '?' ) >= 0 )
532 uri = uri.substring(0, uri.indexOf( '?' ));
533
534
535 if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
536 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
537 "FORBIDDEN: Won't serve ../ for security reasons." );
538
539 File f = new File( homeDir, uri );
540 if ( !f.exists())
541 return new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
542 "Error 404, file not found." );
543
544
545 if ( f.isDirectory())
546 {
547
548
549 if ( !uri.endsWith( "/" ))
550 {
551 uri += "/";
552 Response r = new Response( HTTP_REDIRECT, MIME_HTML,
553 "<html><body>Redirected: <a href=\"" + uri + "\">" +
554 uri + "</a></body></html>");
555 r.addHeader( "Location", uri );
556 return r;
557 }
558
559
560 if ( new File( f, "index.html" ).exists())
561 f = new File( homeDir, uri + "/index.html" );
562 else if ( new File( f, "index.htm" ).exists())
563 f = new File( homeDir, uri + "/index.htm" );
564
565
566 else if ( allowDirectoryListing )
567 {
568 String[] files = f.list();
569 String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
570
571 if ( uri.length() > 1 )
572 {
573 String u = uri.substring( 0, uri.length()-1 );
574 int slash = u.lastIndexOf( '/' );
575 if ( slash >= 0 && slash < u.length())
576 msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
577 }
578
579 for ( int i=0; i<files.length; ++i )
580 {
581 File curFile = new File( f, files[i] );
582 boolean dir = curFile.isDirectory();
583 if ( dir )
584 {
585 msg += "<b>";
586 files[i] += "/";
587 }
588
589 msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
590 files[i] + "</a>";
591
592
593 if ( curFile.isFile())
594 {
595 long len = curFile.length();
596 msg += " <font size=2>(";
597 if ( len < 1024 )
598 msg += curFile.length() + " bytes";
599 else if ( len < 1024 * 1024 )
600 msg += curFile.length()/1024 + "." + (curFile.length()%1024/10%100) + " KB";
601 else
602 msg += curFile.length()/(1024*1024) + "." + curFile.length()%(1024*1024)/10%100 + " MB";
603
604 msg += ")</font>";
605 }
606 msg += "<br/>";
607 if ( dir ) msg += "</b>";
608 }
609 return new Response( HTTP_OK, MIME_HTML, msg );
610 }
611 else
612 {
613 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
614 "FORBIDDEN: No directory listing." );
615 }
616 }
617
618 try
619 {
620
621 String mime = null;
622 int dot = f.getCanonicalPath().lastIndexOf( '.' );
623 if ( dot >= 0 )
624 mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
625 if ( mime == null )
626 mime = MIME_DEFAULT_BINARY;
627
628
629 long startFrom = 0;
630 String range = header.getProperty( "Range" );
631 if ( range != null )
632 {
633 if ( range.startsWith( "bytes=" ))
634 {
635 range = range.substring( "bytes=".length());
636 int minus = range.indexOf( '-' );
637 if ( minus > 0 )
638 range = range.substring( 0, minus );
639 try {
640 startFrom = Long.parseLong( range );
641 }
642 catch ( NumberFormatException nfe ) {}
643 }
644 }
645
646 FileInputStream fis = new FileInputStream( f );
647 fis.skip( startFrom );
648 Response r = new Response( HTTP_OK, mime, fis );
649 r.addHeader( "Content-length", "" + (f.length() - startFrom));
650 r.addHeader( "Content-range", "" + startFrom + "-" +
651 (f.length()-1) + "/" + f.length());
652 return r;
653 }
654 catch( IOException ioe )
655 {
656 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
657 }
658 }
659
660 /***
661 * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
662 */
663 private static Hashtable theMimeTypes = new Hashtable();
664 static
665 {
666 StringTokenizer st = new StringTokenizer(
667 "htm text/html "+
668 "html text/html "+
669 "txt text/plain "+
670 "asc text/plain "+
671 "gif image/gif "+
672 "jpg image/jpeg "+
673 "jpeg image/jpeg "+
674 "png image/png "+
675 "mp3 audio/mpeg "+
676 "m3u audio/mpeg-url " +
677 "pdf application/pdf "+
678 "doc application/msword "+
679 "ogg application/x-ogg "+
680 "zip application/octet-stream "+
681 "exe application/octet-stream "+
682 "class application/octet-stream " );
683 while ( st.hasMoreTokens())
684 theMimeTypes.put( st.nextToken(), st.nextToken());
685 }
686
687 /***
688 * GMT date formatter
689 */
690 private static java.text.SimpleDateFormat gmtFrmt;
691 static
692 {
693 gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
694 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
695 }
696
697 /***
698 * The distribution licence
699 */
700 public static final String LICENCE =
701 "Copyright (C) 2001,2005 by Jarno Elonen <elonen@iki.fi>\n"+
702 "\n"+
703 "Redistribution and use in source and binary forms, with or without\n"+
704 "modification, are permitted provided that the following conditions\n"+
705 "are met:\n"+
706 "\n"+
707 "Redistributions of source code must retain the above copyright notice,\n"+
708 "this list of conditions and the following disclaimer. Redistributions in\n"+
709 "binary form must reproduce the above copyright notice, this list of\n"+
710 "conditions and the following disclaimer in the documentation and/or other\n"+
711 "materials provided with the distribution. The name of the author may not\n"+
712 "be used to endorse or promote products derived from this software without\n"+
713 "specific prior written permission. \n"+
714 " \n"+
715 "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
716 "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
717 "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
718 "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
719 "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
720 "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
721 "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
722 "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
723 "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
724 "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
725 }