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