NanoHTTPD.java revision 6cfcb8e672366ca723d6c0a55cfb4cf865a78cb8
1package fi.iki.elonen; 2 3import java.io.*; 4import java.net.InetAddress; 5import java.net.InetSocketAddress; 6import java.net.ServerSocket; 7import java.net.Socket; 8import java.net.SocketException; 9import java.net.SocketTimeoutException; 10import java.net.URLDecoder; 11import java.nio.ByteBuffer; 12import java.nio.channels.FileChannel; 13import java.text.SimpleDateFormat; 14import java.util.ArrayList; 15import java.util.Calendar; 16import java.util.Date; 17import java.util.HashMap; 18import java.util.HashSet; 19import java.util.Iterator; 20import java.util.List; 21import java.util.Locale; 22import java.util.Map; 23import java.util.Set; 24import java.util.StringTokenizer; 25import java.util.TimeZone; 26 27/** 28 * A simple, tiny, nicely embeddable HTTP server in Java 29 * <p/> 30 * <p/> 31 * NanoHTTPD 32 * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p> 33 * <p/> 34 * <p/> 35 * <b>Features + limitations: </b> 36 * <ul> 37 * <p/> 38 * <li>Only one Java file</li> 39 * <li>Java 5 compatible</li> 40 * <li>Released as open source, Modified BSD licence</li> 41 * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li> 42 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li> 43 * <li>Supports both dynamic content and file serving</li> 44 * <li>Supports file upload (since version 1.2, 2010)</li> 45 * <li>Supports partial content (streaming)</li> 46 * <li>Supports ETags</li> 47 * <li>Never caches anything</li> 48 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li> 49 * <li>Default code serves files and shows all HTTP parameters and headers</li> 50 * <li>File server supports directory listing, index.html and index.htm</li> 51 * <li>File server supports partial content (streaming)</li> 52 * <li>File server supports ETags</li> 53 * <li>File server does the 301 redirection trick for directories without '/'</li> 54 * <li>File server supports simple skipping for files (continue download)</li> 55 * <li>File server serves also very long files without memory overhead</li> 56 * <li>Contains a built-in list of most common mime types</li> 57 * <li>All header names are converted lowercase so they don't vary between browsers/clients</li> 58 * <p/> 59 * </ul> 60 * <p/> 61 * <p/> 62 * <b>How to use: </b> 63 * <ul> 64 * <p/> 65 * <li>Subclass and implement serve() and embed to your own program</li> 66 * <p/> 67 * </ul> 68 * <p/> 69 * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence) 70 */ 71public abstract class NanoHTTPD { 72 /** 73 * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 74 * This is required as the Keep-Alive HTTP connections would otherwise 75 * block the socket reading thread forever (or as long the browser is open). 76 */ 77 public static final int SOCKET_READ_TIMEOUT = 5000; 78 /** 79 * Common mime type for dynamic content: plain text 80 */ 81 public static final String MIME_PLAINTEXT = "text/plain"; 82 /** 83 * Common mime type for dynamic content: html 84 */ 85 public static final String MIME_HTML = "text/html"; 86 /** 87 * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing. 88 */ 89 private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 90 private final String hostname; 91 private final int myPort; 92 private ServerSocket myServerSocket; 93 private Set<Socket> openConnections = new HashSet<Socket>(); 94 private Thread myThread; 95 /** 96 * Pluggable strategy for asynchronously executing requests. 97 */ 98 private AsyncRunner asyncRunner; 99 /** 100 * Pluggable strategy for creating and cleaning up temporary files. 101 */ 102 private TempFileManagerFactory tempFileManagerFactory; 103 104 /** 105 * Constructs an HTTP server on given port. 106 */ 107 public NanoHTTPD(int port) { 108 this(null, port); 109 } 110 111 /** 112 * Constructs an HTTP server on given hostname and port. 113 */ 114 public NanoHTTPD(String hostname, int port) { 115 this.hostname = hostname; 116 this.myPort = port; 117 setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 118 setAsyncRunner(new DefaultAsyncRunner()); 119 } 120 121 private static final void safeClose(Closeable closeable) { 122 if (closeable != null) { 123 try { 124 closeable.close(); 125 } catch (IOException e) { 126 } 127 } 128 } 129 130 private static final void safeClose(Socket closeable) { 131 if (closeable != null) { 132 try { 133 closeable.close(); 134 } catch (IOException e) { 135 } 136 } 137 } 138 139 private static final void safeClose(ServerSocket closeable) { 140 if (closeable != null) { 141 try { 142 closeable.close(); 143 } catch (IOException e) { 144 } 145 } 146 } 147 148 /** 149 * Start the server. 150 * 151 * @throws IOException if the socket is in use. 152 */ 153 public void start() throws IOException { 154 myServerSocket = new ServerSocket(); 155 myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); 156 157 myThread = new Thread(new Runnable() { 158 @Override 159 public void run() { 160 do { 161 try { 162 final Socket finalAccept = myServerSocket.accept(); 163 registerConnection(finalAccept); 164 finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT); 165 final InputStream inputStream = finalAccept.getInputStream(); 166 asyncRunner.exec(new Runnable() { 167 @Override 168 public void run() { 169 OutputStream outputStream = null; 170 try { 171 outputStream = finalAccept.getOutputStream(); 172 TempFileManager tempFileManager = tempFileManagerFactory.create(); 173 HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress()); 174 while (!finalAccept.isClosed()) { 175 session.execute(); 176 } 177 } catch (Exception e) { 178 // When the socket is closed by the client, we throw our own SocketException 179 // to break the "keep alive" loop above. 180 if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) { 181 e.printStackTrace(); 182 } 183 } finally { 184 safeClose(outputStream); 185 safeClose(inputStream); 186 safeClose(finalAccept); 187 unRegisterConnection(finalAccept); 188 } 189 } 190 }); 191 } catch (IOException e) { 192 } 193 } while (!myServerSocket.isClosed()); 194 } 195 }); 196 myThread.setDaemon(true); 197 myThread.setName("NanoHttpd Main Listener"); 198 myThread.start(); 199 } 200 201 /** 202 * Stop the server. 203 */ 204 public void stop() { 205 try { 206 safeClose(myServerSocket); 207 closeAllConnections(); 208 if (myThread != null) { 209 myThread.join(); 210 } 211 } catch (Exception e) { 212 e.printStackTrace(); 213 } 214 } 215 216 /** 217 * Registers that a new connection has been set up. 218 * 219 * @param socket the {@link Socket} for the connection. 220 */ 221 public synchronized void registerConnection(Socket socket) { 222 openConnections.add(socket); 223 } 224 225 /** 226 * Registers that a connection has been closed 227 * 228 * @param socket 229 * the {@link Socket} for the connection. 230 */ 231 public synchronized void unRegisterConnection(Socket socket) { 232 openConnections.remove(socket); 233 } 234 235 /** 236 * Forcibly closes all connections that are open. 237 */ 238 public synchronized void closeAllConnections() { 239 for (Socket socket : openConnections) { 240 safeClose(socket); 241 } 242 } 243 244 public final int getListeningPort() { 245 return myServerSocket == null ? -1 : myServerSocket.getLocalPort(); 246 } 247 248 public final boolean wasStarted() { 249 return myServerSocket != null && myThread != null; 250 } 251 252 public final boolean isAlive() { 253 return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive(); 254 } 255 256 /** 257 * Override this to customize the server. 258 * <p/> 259 * <p/> 260 * (By default, this delegates to serveFile() and allows directory listing.) 261 * 262 * @param uri Percent-decoded URI without parameters, for example "/index.cgi" 263 * @param method "GET", "POST" etc. 264 * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. 265 * @param headers Header entries, percent decoded 266 * @return HTTP response, see class Response for details 267 */ 268 @Deprecated 269 public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, 270 Map<String, String> files) { 271 return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); 272 } 273 274 /** 275 * Override this to customize the server. 276 * <p/> 277 * <p/> 278 * (By default, this delegates to serveFile() and allows directory listing.) 279 * 280 * @param session The HTTP session 281 * @return HTTP response, see class Response for details 282 */ 283 public Response serve(IHTTPSession session) { 284 Map<String, String> files = new HashMap<String, String>(); 285 Method method = session.getMethod(); 286 if (Method.PUT.equals(method) || Method.POST.equals(method)) { 287 try { 288 session.parseBody(files); 289 } catch (IOException ioe) { 290 return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 291 } catch (ResponseException re) { 292 return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); 293 } 294 } 295 296 Map<String, String> parms = session.getParms(); 297 parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString()); 298 return serve(session.getUri(), method, session.getHeaders(), parms, files); 299 } 300 301 /** 302 * Decode percent encoded <code>String</code> values. 303 * 304 * @param str the percent encoded <code>String</code> 305 * @return expanded form of the input, for example "foo%20bar" becomes "foo bar" 306 */ 307 protected String decodePercent(String str) { 308 String decoded = null; 309 try { 310 decoded = URLDecoder.decode(str, "UTF8"); 311 } catch (UnsupportedEncodingException ignored) { 312 } 313 return decoded; 314 } 315 316 /** 317 * Decode parameters from a URL, handing the case where a single parameter name might have been 318 * supplied several times, by return lists of values. In general these lists will contain a single 319 * element. 320 * 321 * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method. 322 * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). 323 */ 324 protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { 325 return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER)); 326 } 327 328 /** 329 * Decode parameters from a URL, handing the case where a single parameter name might have been 330 * supplied several times, by return lists of values. In general these lists will contain a single 331 * element. 332 * 333 * @param queryString a query string pulled from the URL. 334 * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). 335 */ 336 protected Map<String, List<String>> decodeParameters(String queryString) { 337 Map<String, List<String>> parms = new HashMap<String, List<String>>(); 338 if (queryString != null) { 339 StringTokenizer st = new StringTokenizer(queryString, "&"); 340 while (st.hasMoreTokens()) { 341 String e = st.nextToken(); 342 int sep = e.indexOf('='); 343 String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 344 if (!parms.containsKey(propertyName)) { 345 parms.put(propertyName, new ArrayList<String>()); 346 } 347 String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null; 348 if (propertyValue != null) { 349 parms.get(propertyName).add(propertyValue); 350 } 351 } 352 } 353 return parms; 354 } 355 356 // ------------------------------------------------------------------------------- // 357 // 358 // Threading Strategy. 359 // 360 // ------------------------------------------------------------------------------- // 361 362 /** 363 * Pluggable strategy for asynchronously executing requests. 364 * 365 * @param asyncRunner new strategy for handling threads. 366 */ 367 public void setAsyncRunner(AsyncRunner asyncRunner) { 368 this.asyncRunner = asyncRunner; 369 } 370 371 // ------------------------------------------------------------------------------- // 372 // 373 // Temp file handling strategy. 374 // 375 // ------------------------------------------------------------------------------- // 376 377 /** 378 * Pluggable strategy for creating and cleaning up temporary files. 379 * 380 * @param tempFileManagerFactory new strategy for handling temp files. 381 */ 382 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 383 this.tempFileManagerFactory = tempFileManagerFactory; 384 } 385 386 /** 387 * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value. 388 */ 389 public enum Method { 390 GET, PUT, POST, DELETE, HEAD, OPTIONS; 391 392 static Method lookup(String method) { 393 for (Method m : Method.values()) { 394 if (m.toString().equalsIgnoreCase(method)) { 395 return m; 396 } 397 } 398 return null; 399 } 400 } 401 402 /** 403 * Pluggable strategy for asynchronously executing requests. 404 */ 405 public interface AsyncRunner { 406 void exec(Runnable code); 407 } 408 409 /** 410 * Factory to create temp file managers. 411 */ 412 public interface TempFileManagerFactory { 413 TempFileManager create(); 414 } 415 416 // ------------------------------------------------------------------------------- // 417 418 /** 419 * Temp file manager. 420 * <p/> 421 * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup 422 * temporary files created as a result of handling the request.</p> 423 */ 424 public interface TempFileManager { 425 TempFile createTempFile() throws Exception; 426 427 void clear(); 428 } 429 430 /** 431 * A temp file. 432 * <p/> 433 * <p>Temp files are responsible for managing the actual temporary storage and cleaning 434 * themselves up when no longer needed.</p> 435 */ 436 public interface TempFile { 437 OutputStream open() throws Exception; 438 439 void delete() throws Exception; 440 441 String getName(); 442 } 443 444 /** 445 * Default threading strategy for NanoHttpd. 446 * <p/> 447 * <p>By default, the server spawns a new Thread for every incoming request. These are set 448 * to <i>daemon</i> status, and named according to the request number. The name is 449 * useful when profiling the application.</p> 450 */ 451 public static class DefaultAsyncRunner implements AsyncRunner { 452 private long requestCount; 453 454 @Override 455 public void exec(Runnable code) { 456 ++requestCount; 457 Thread t = new Thread(code); 458 t.setDaemon(true); 459 t.setName("NanoHttpd Request Processor (#" + requestCount + ")"); 460 t.start(); 461 } 462 } 463 464 /** 465 * Default strategy for creating and cleaning up temporary files. 466 * <p/> 467 * <p></p>This class stores its files in the standard location (that is, 468 * wherever <code>java.io.tmpdir</code> points to). Files are added 469 * to an internal list, and deleted when no longer needed (that is, 470 * when <code>clear()</code> is invoked at the end of processing a 471 * request).</p> 472 */ 473 public static class DefaultTempFileManager implements TempFileManager { 474 private final String tmpdir; 475 private final List<TempFile> tempFiles; 476 477 public DefaultTempFileManager() { 478 tmpdir = System.getProperty("java.io.tmpdir"); 479 tempFiles = new ArrayList<TempFile>(); 480 } 481 482 @Override 483 public TempFile createTempFile() throws Exception { 484 DefaultTempFile tempFile = new DefaultTempFile(tmpdir); 485 tempFiles.add(tempFile); 486 return tempFile; 487 } 488 489 @Override 490 public void clear() { 491 for (TempFile file : tempFiles) { 492 try { 493 file.delete(); 494 } catch (Exception ignored) { 495 } 496 } 497 tempFiles.clear(); 498 } 499 } 500 501 /** 502 * Default strategy for creating and cleaning up temporary files. 503 * <p/> 504 * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in 505 * the directory specified.</p> 506 */ 507 public static class DefaultTempFile implements TempFile { 508 private File file; 509 private OutputStream fstream; 510 511 public DefaultTempFile(String tempdir) throws IOException { 512 file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); 513 fstream = new FileOutputStream(file); 514 } 515 516 @Override 517 public OutputStream open() throws Exception { 518 return fstream; 519 } 520 521 @Override 522 public void delete() throws Exception { 523 safeClose(fstream); 524 file.delete(); 525 } 526 527 @Override 528 public String getName() { 529 return file.getAbsolutePath(); 530 } 531 } 532 533 /** 534 * HTTP response. Return one of these from serve(). 535 */ 536 public static class Response { 537 /** 538 * HTTP status code after processing, e.g. "200 OK", HTTP_OK 539 */ 540 private IStatus status; 541 /** 542 * MIME type of content, e.g. "text/html" 543 */ 544 private String mimeType; 545 /** 546 * Data of the response, may be null. 547 */ 548 private InputStream data; 549 /** 550 * Headers for the HTTP response. Use addHeader() to add lines. 551 */ 552 private Map<String, String> header = new HashMap<String, String>(); 553 /** 554 * The request method that spawned this response. 555 */ 556 private Method requestMethod; 557 /** 558 * Use chunkedTransfer 559 */ 560 private boolean chunkedTransfer; 561 562 /** 563 * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message 564 */ 565 public Response(String msg) { 566 this(Status.OK, MIME_HTML, msg); 567 } 568 569 /** 570 * Basic constructor. 571 */ 572 public Response(IStatus status, String mimeType, InputStream data) { 573 this.status = status; 574 this.mimeType = mimeType; 575 this.data = data; 576 } 577 578 /** 579 * Convenience method that makes an InputStream out of given text. 580 */ 581 public Response(IStatus status, String mimeType, String txt) { 582 this.status = status; 583 this.mimeType = mimeType; 584 try { 585 this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null; 586 } catch (java.io.UnsupportedEncodingException uee) { 587 uee.printStackTrace(); 588 } 589 } 590 591 /** 592 * Adds given line to the header. 593 */ 594 public void addHeader(String name, String value) { 595 header.put(name, value); 596 } 597 598 public String getHeader(String name) { 599 return header.get(name); 600 } 601 602 /** 603 * Sends given response to the socket. 604 */ 605 protected void send(OutputStream outputStream) { 606 String mime = mimeType; 607 SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 608 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); 609 610 try { 611 if (status == null) { 612 throw new Error("sendResponse(): Status can't be null."); 613 } 614 PrintWriter pw = new PrintWriter(outputStream); 615 pw.print("HTTP/1.1 " + status.getDescription() + " \r\n"); 616 617 if (mime != null) { 618 pw.print("Content-Type: " + mime + "\r\n"); 619 } 620 621 if (header == null || header.get("Date") == null) { 622 pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); 623 } 624 625 if (header != null) { 626 for (String key : header.keySet()) { 627 String value = header.get(key); 628 pw.print(key + ": " + value + "\r\n"); 629 } 630 } 631 632 sendConnectionHeaderIfNotAlreadyPresent(pw, header); 633 634 if (requestMethod != Method.HEAD && chunkedTransfer) { 635 sendAsChunked(outputStream, pw); 636 } else { 637 int pending = data != null ? data.available() : 0; 638 sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending); 639 pw.print("\r\n"); 640 pw.flush(); 641 sendAsFixedLength(outputStream, pending); 642 } 643 outputStream.flush(); 644 safeClose(data); 645 } catch (IOException ioe) { 646 // Couldn't write? No can do. 647 } 648 } 649 650 protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) { 651 if (!headerAlreadySent(header, "content-length")) { 652 pw.print("Content-Length: "+ size +"\r\n"); 653 } 654 } 655 656 protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) { 657 if (!headerAlreadySent(header, "connection")) { 658 pw.print("Connection: keep-alive\r\n"); 659 } 660 } 661 662 private boolean headerAlreadySent(Map<String, String> header, String name) { 663 boolean alreadySent = false; 664 for (String headerName : header.keySet()) { 665 alreadySent |= headerName.equalsIgnoreCase(name); 666 } 667 return alreadySent; 668 } 669 670 private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException { 671 pw.print("Transfer-Encoding: chunked\r\n"); 672 pw.print("\r\n"); 673 pw.flush(); 674 int BUFFER_SIZE = 16 * 1024; 675 byte[] CRLF = "\r\n".getBytes(); 676 byte[] buff = new byte[BUFFER_SIZE]; 677 int read; 678 while ((read = data.read(buff)) > 0) { 679 outputStream.write(String.format("%x\r\n", read).getBytes()); 680 outputStream.write(buff, 0, read); 681 outputStream.write(CRLF); 682 } 683 outputStream.write(String.format("0\r\n\r\n").getBytes()); 684 } 685 686 private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException { 687 if (requestMethod != Method.HEAD && data != null) { 688 int BUFFER_SIZE = 16 * 1024; 689 byte[] buff = new byte[BUFFER_SIZE]; 690 while (pending > 0) { 691 int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending)); 692 if (read <= 0) { 693 break; 694 } 695 outputStream.write(buff, 0, read); 696 pending -= read; 697 } 698 } 699 } 700 701 public IStatus getStatus() { 702 return status; 703 } 704 705 public void setStatus(Status status) { 706 this.status = status; 707 } 708 709 public String getMimeType() { 710 return mimeType; 711 } 712 713 public void setMimeType(String mimeType) { 714 this.mimeType = mimeType; 715 } 716 717 public InputStream getData() { 718 return data; 719 } 720 721 public void setData(InputStream data) { 722 this.data = data; 723 } 724 725 public Method getRequestMethod() { 726 return requestMethod; 727 } 728 729 public void setRequestMethod(Method requestMethod) { 730 this.requestMethod = requestMethod; 731 } 732 733 public void setChunkedTransfer(boolean chunkedTransfer) { 734 this.chunkedTransfer = chunkedTransfer; 735 } 736 737 public interface IStatus { 738 int getRequestStatus(); 739 String getDescription(); 740 } 741 742 /** 743 * Some HTTP response status codes 744 */ 745 public enum Status implements IStatus { 746 SWITCH_PROTOCOL(101, "Switching Protocols"), OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301, 747 "Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, 748 "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), RANGE_NOT_SATISFIABLE(416, 749 "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error"); 750 private final int requestStatus; 751 private final String description; 752 753 Status(int requestStatus, String description) { 754 this.requestStatus = requestStatus; 755 this.description = description; 756 } 757 758 @Override 759 public int getRequestStatus() { 760 return this.requestStatus; 761 } 762 763 @Override 764 public String getDescription() { 765 return "" + this.requestStatus + " " + description; 766 } 767 } 768 } 769 770 public static final class ResponseException extends Exception { 771 private static final long serialVersionUID = 6569838532917408380L; 772 773 private final Response.Status status; 774 775 public ResponseException(Response.Status status, String message) { 776 super(message); 777 this.status = status; 778 } 779 780 public ResponseException(Response.Status status, String message, Exception e) { 781 super(message, e); 782 this.status = status; 783 } 784 785 public Response.Status getStatus() { 786 return status; 787 } 788 } 789 790 /** 791 * Default strategy for creating and cleaning up temporary files. 792 */ 793 private class DefaultTempFileManagerFactory implements TempFileManagerFactory { 794 @Override 795 public TempFileManager create() { 796 return new DefaultTempFileManager(); 797 } 798 } 799 800 /** 801 * Handles one session, i.e. parses the HTTP request and returns the response. 802 */ 803 public interface IHTTPSession { 804 void execute() throws IOException; 805 806 Map<String, String> getParms(); 807 808 Map<String, String> getHeaders(); 809 810 /** 811 * @return the path part of the URL. 812 */ 813 String getUri(); 814 815 String getQueryParameterString(); 816 817 Method getMethod(); 818 819 InputStream getInputStream(); 820 821 CookieHandler getCookies(); 822 823 /** 824 * Adds the files in the request body to the files map. 825 * @arg files - map to modify 826 */ 827 void parseBody(Map<String, String> files) throws IOException, ResponseException; 828 } 829 830 protected class HTTPSession implements IHTTPSession { 831 public static final int BUFSIZE = 8192; 832 private final TempFileManager tempFileManager; 833 private final OutputStream outputStream; 834 private PushbackInputStream inputStream; 835 private int splitbyte; 836 private int rlen; 837 private String uri; 838 private Method method; 839 private Map<String, String> parms; 840 private Map<String, String> headers; 841 private CookieHandler cookies; 842 private String queryParameterString; 843 844 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { 845 this.tempFileManager = tempFileManager; 846 this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); 847 this.outputStream = outputStream; 848 } 849 850 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { 851 this.tempFileManager = tempFileManager; 852 this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); 853 this.outputStream = outputStream; 854 String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); 855 headers = new HashMap<String, String>(); 856 857 headers.put("remote-addr", remoteIp); 858 headers.put("http-client-ip", remoteIp); 859 } 860 861 @Override 862 public void execute() throws IOException { 863 try { 864 // Read the first 8192 bytes. 865 // The full header should fit in here. 866 // Apache's default header limit is 8KB. 867 // Do NOT assume that a single read will get the entire header at once! 868 byte[] buf = new byte[BUFSIZE]; 869 splitbyte = 0; 870 rlen = 0; 871 { 872 int read = -1; 873 try { 874 read = inputStream.read(buf, 0, BUFSIZE); 875 } catch (Exception e) { 876 safeClose(inputStream); 877 safeClose(outputStream); 878 throw new SocketException("NanoHttpd Shutdown"); 879 } 880 if (read == -1) { 881 // socket was been closed 882 safeClose(inputStream); 883 safeClose(outputStream); 884 throw new SocketException("NanoHttpd Shutdown"); 885 } 886 while (read > 0) { 887 rlen += read; 888 splitbyte = findHeaderEnd(buf, rlen); 889 if (splitbyte > 0) 890 break; 891 read = inputStream.read(buf, rlen, BUFSIZE - rlen); 892 } 893 } 894 895 if (splitbyte < rlen) { 896 inputStream.unread(buf, splitbyte, rlen - splitbyte); 897 } 898 899 parms = new HashMap<String, String>(); 900 if(null == headers) { 901 headers = new HashMap<String, String>(); 902 } 903 904 // Create a BufferedReader for parsing the header. 905 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); 906 907 // Decode the header into parms and header java properties 908 Map<String, String> pre = new HashMap<String, String>(); 909 decodeHeader(hin, pre, parms, headers); 910 911 method = Method.lookup(pre.get("method")); 912 if (method == null) { 913 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); 914 } 915 916 uri = pre.get("uri"); 917 918 cookies = new CookieHandler(headers); 919 920 // Ok, now do the serve() 921 Response r = serve(this); 922 if (r == null) { 923 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); 924 } else { 925 cookies.unloadQueue(r); 926 r.setRequestMethod(method); 927 r.send(outputStream); 928 } 929 } catch (SocketException e) { 930 // throw it out to close socket object (finalAccept) 931 throw e; 932 } catch (SocketTimeoutException ste) { 933 throw ste; 934 } catch (IOException ioe) { 935 Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 936 r.send(outputStream); 937 safeClose(outputStream); 938 } catch (ResponseException re) { 939 Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); 940 r.send(outputStream); 941 safeClose(outputStream); 942 } finally { 943 tempFileManager.clear(); 944 } 945 } 946 947 @Override 948 public void parseBody(Map<String, String> files) throws IOException, ResponseException { 949 RandomAccessFile randomAccessFile = null; 950 BufferedReader in = null; 951 try { 952 953 randomAccessFile = getTmpBucket(); 954 955 long size; 956 if (headers.containsKey("content-length")) { 957 size = Integer.parseInt(headers.get("content-length")); 958 } else if (splitbyte < rlen) { 959 size = rlen - splitbyte; 960 } else { 961 size = 0; 962 } 963 964 // Now read all the body and write it to f 965 byte[] buf = new byte[512]; 966 while (rlen >= 0 && size > 0) { 967 rlen = inputStream.read(buf, 0, (int)Math.min(size, 512)); 968 size -= rlen; 969 if (rlen > 0) { 970 randomAccessFile.write(buf, 0, rlen); 971 } 972 } 973 974 // Get the raw body as a byte [] 975 ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); 976 randomAccessFile.seek(0); 977 978 // Create a BufferedReader for easily reading it as string. 979 InputStream bin = new FileInputStream(randomAccessFile.getFD()); 980 in = new BufferedReader(new InputStreamReader(bin)); 981 982 // If the method is POST, there may be parameters 983 // in data section, too, read it: 984 if (Method.POST.equals(method)) { 985 String contentType = ""; 986 String contentTypeHeader = headers.get("content-type"); 987 988 StringTokenizer st = null; 989 if (contentTypeHeader != null) { 990 st = new StringTokenizer(contentTypeHeader, ",; "); 991 if (st.hasMoreTokens()) { 992 contentType = st.nextToken(); 993 } 994 } 995 996 if ("multipart/form-data".equalsIgnoreCase(contentType)) { 997 // Handle multipart/form-data 998 if (!st.hasMoreTokens()) { 999 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); 1000 } 1001 1002 String boundaryStartString = "boundary="; 1003 int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); 1004 String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); 1005 if (boundary.startsWith("\"") && boundary.endsWith("\"")) { 1006 boundary = boundary.substring(1, boundary.length() - 1); 1007 } 1008 1009 decodeMultipartData(boundary, fbuf, in, parms, files); 1010 } else { 1011 String postLine = ""; 1012 StringBuilder postLineBuffer = new StringBuilder(); 1013 char pbuf[] = new char[512]; 1014 int read = in.read(pbuf); 1015 while (read >= 0 && !postLine.endsWith("\r\n")) { 1016 postLine = String.valueOf(pbuf, 0, read); 1017 postLineBuffer.append(postLine); 1018 read = in.read(pbuf); 1019 } 1020 postLine = postLineBuffer.toString().trim(); 1021 // Handle application/x-www-form-urlencoded 1022 if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { 1023 decodeParms(postLine, parms); 1024 } else if (postLine.length() != 0) { 1025 // Special case for raw POST data => create a special files entry "postData" with raw content data 1026 files.put("postData", postLine); 1027 } 1028 } 1029 } else if (Method.PUT.equals(method)) { 1030 files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); 1031 } 1032 } finally { 1033 safeClose(randomAccessFile); 1034 safeClose(in); 1035 } 1036 } 1037 1038 /** 1039 * Decodes the sent headers and loads the data into Key/value pairs 1040 */ 1041 private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) 1042 throws ResponseException { 1043 try { 1044 // Read the request line 1045 String inLine = in.readLine(); 1046 if (inLine == null) { 1047 return; 1048 } 1049 1050 StringTokenizer st = new StringTokenizer(inLine); 1051 if (!st.hasMoreTokens()) { 1052 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); 1053 } 1054 1055 pre.put("method", st.nextToken()); 1056 1057 if (!st.hasMoreTokens()) { 1058 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); 1059 } 1060 1061 String uri = st.nextToken(); 1062 1063 // Decode parameters from the URI 1064 int qmi = uri.indexOf('?'); 1065 if (qmi >= 0) { 1066 decodeParms(uri.substring(qmi + 1), parms); 1067 uri = decodePercent(uri.substring(0, qmi)); 1068 } else { 1069 uri = decodePercent(uri); 1070 } 1071 1072 // If there's another token, it's protocol version, 1073 // followed by HTTP headers. Ignore version but parse headers. 1074 // NOTE: this now forces header names lowercase since they are 1075 // case insensitive and vary by client. 1076 if (st.hasMoreTokens()) { 1077 String line = in.readLine(); 1078 while (line != null && line.trim().length() > 0) { 1079 int p = line.indexOf(':'); 1080 if (p >= 0) 1081 headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); 1082 line = in.readLine(); 1083 } 1084 } 1085 1086 pre.put("uri", uri); 1087 } catch (IOException ioe) { 1088 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 1089 } 1090 } 1091 1092 /** 1093 * Decodes the Multipart Body data and put it into Key/Value pairs. 1094 */ 1095 private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, 1096 Map<String, String> files) throws ResponseException { 1097 try { 1098 int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); 1099 int boundarycount = 1; 1100 String mpline = in.readLine(); 1101 while (mpline != null) { 1102 if (!mpline.contains(boundary)) { 1103 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html"); 1104 } 1105 boundarycount++; 1106 Map<String, String> item = new HashMap<String, String>(); 1107 mpline = in.readLine(); 1108 while (mpline != null && mpline.trim().length() > 0) { 1109 int p = mpline.indexOf(':'); 1110 if (p != -1) { 1111 item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim()); 1112 } 1113 mpline = in.readLine(); 1114 } 1115 if (mpline != null) { 1116 String contentDisposition = item.get("content-disposition"); 1117 if (contentDisposition == null) { 1118 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html"); 1119 } 1120 StringTokenizer st = new StringTokenizer(contentDisposition, ";"); 1121 Map<String, String> disposition = new HashMap<String, String>(); 1122 while (st.hasMoreTokens()) { 1123 String token = st.nextToken().trim(); 1124 int p = token.indexOf('='); 1125 if (p != -1) { 1126 disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim()); 1127 } 1128 } 1129 String pname = disposition.get("name"); 1130 pname = pname.substring(1, pname.length() - 1); 1131 1132 String value = ""; 1133 if (item.get("content-type") == null) { 1134 while (mpline != null && !mpline.contains(boundary)) { 1135 mpline = in.readLine(); 1136 if (mpline != null) { 1137 int d = mpline.indexOf(boundary); 1138 if (d == -1) { 1139 value += mpline; 1140 } else { 1141 value += mpline.substring(0, d - 2); 1142 } 1143 } 1144 } 1145 } else { 1146 if (boundarycount > bpositions.length) { 1147 throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); 1148 } 1149 int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); 1150 String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); 1151 files.put(pname, path); 1152 value = disposition.get("filename"); 1153 value = value.substring(1, value.length() - 1); 1154 do { 1155 mpline = in.readLine(); 1156 } while (mpline != null && !mpline.contains(boundary)); 1157 } 1158 parms.put(pname, value); 1159 } 1160 } 1161 } catch (IOException ioe) { 1162 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 1163 } 1164 } 1165 1166 /** 1167 * Find byte index separating header from body. It must be the last byte of the first two sequential new lines. 1168 */ 1169 private int findHeaderEnd(final byte[] buf, int rlen) { 1170 int splitbyte = 0; 1171 while (splitbyte + 3 < rlen) { 1172 if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { 1173 return splitbyte + 4; 1174 } 1175 splitbyte++; 1176 } 1177 return 0; 1178 } 1179 1180 /** 1181 * Find the byte positions where multipart boundaries start. 1182 */ 1183 private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { 1184 int matchcount = 0; 1185 int matchbyte = -1; 1186 List<Integer> matchbytes = new ArrayList<Integer>(); 1187 for (int i = 0; i < b.limit(); i++) { 1188 if (b.get(i) == boundary[matchcount]) { 1189 if (matchcount == 0) 1190 matchbyte = i; 1191 matchcount++; 1192 if (matchcount == boundary.length) { 1193 matchbytes.add(matchbyte); 1194 matchcount = 0; 1195 matchbyte = -1; 1196 } 1197 } else { 1198 i -= matchcount; 1199 matchcount = 0; 1200 matchbyte = -1; 1201 } 1202 } 1203 int[] ret = new int[matchbytes.size()]; 1204 for (int i = 0; i < ret.length; i++) { 1205 ret[i] = matchbytes.get(i); 1206 } 1207 return ret; 1208 } 1209 1210 /** 1211 * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned. 1212 */ 1213 private String saveTmpFile(ByteBuffer b, int offset, int len) { 1214 String path = ""; 1215 if (len > 0) { 1216 FileOutputStream fileOutputStream = null; 1217 try { 1218 TempFile tempFile = tempFileManager.createTempFile(); 1219 ByteBuffer src = b.duplicate(); 1220 fileOutputStream = new FileOutputStream(tempFile.getName()); 1221 FileChannel dest = fileOutputStream.getChannel(); 1222 src.position(offset).limit(offset + len); 1223 dest.write(src.slice()); 1224 path = tempFile.getName(); 1225 } catch (Exception e) { // Catch exception if any 1226 throw new Error(e); // we won't recover, so throw an error 1227 } finally { 1228 safeClose(fileOutputStream); 1229 } 1230 } 1231 return path; 1232 } 1233 1234 private RandomAccessFile getTmpBucket() { 1235 try { 1236 TempFile tempFile = tempFileManager.createTempFile(); 1237 return new RandomAccessFile(tempFile.getName(), "rw"); 1238 } catch (Exception e) { 1239 throw new Error(e); // we won't recover, so throw an error 1240 } 1241 } 1242 1243 /** 1244 * It returns the offset separating multipart file headers from the file's data. 1245 */ 1246 private int stripMultipartHeaders(ByteBuffer b, int offset) { 1247 int i; 1248 for (i = offset; i < b.limit(); i++) { 1249 if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { 1250 break; 1251 } 1252 } 1253 return i + 1; 1254 } 1255 1256 /** 1257 * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and 1258 * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map. 1259 */ 1260 private void decodeParms(String parms, Map<String, String> p) { 1261 if (parms == null) { 1262 queryParameterString = ""; 1263 return; 1264 } 1265 1266 queryParameterString = parms; 1267 StringTokenizer st = new StringTokenizer(parms, "&"); 1268 while (st.hasMoreTokens()) { 1269 String e = st.nextToken(); 1270 int sep = e.indexOf('='); 1271 if (sep >= 0) { 1272 p.put(decodePercent(e.substring(0, sep)).trim(), 1273 decodePercent(e.substring(sep + 1))); 1274 } else { 1275 p.put(decodePercent(e).trim(), ""); 1276 } 1277 } 1278 } 1279 1280 @Override 1281 public final Map<String, String> getParms() { 1282 return parms; 1283 } 1284 1285 public String getQueryParameterString() { 1286 return queryParameterString; 1287 } 1288 1289 @Override 1290 public final Map<String, String> getHeaders() { 1291 return headers; 1292 } 1293 1294 @Override 1295 public final String getUri() { 1296 return uri; 1297 } 1298 1299 @Override 1300 public final Method getMethod() { 1301 return method; 1302 } 1303 1304 @Override 1305 public final InputStream getInputStream() { 1306 return inputStream; 1307 } 1308 1309 @Override 1310 public CookieHandler getCookies() { 1311 return cookies; 1312 } 1313 } 1314 1315 public static class Cookie { 1316 private String n, v, e; 1317 1318 public Cookie(String name, String value, String expires) { 1319 n = name; 1320 v = value; 1321 e = expires; 1322 } 1323 1324 public Cookie(String name, String value) { 1325 this(name, value, 30); 1326 } 1327 1328 public Cookie(String name, String value, int numDays) { 1329 n = name; 1330 v = value; 1331 e = getHTTPTime(numDays); 1332 } 1333 1334 public String getHTTPHeader() { 1335 String fmt = "%s=%s; expires=%s"; 1336 return String.format(fmt, n, v, e); 1337 } 1338 1339 public static String getHTTPTime(int days) { 1340 Calendar calendar = Calendar.getInstance(); 1341 SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 1342 dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 1343 calendar.add(Calendar.DAY_OF_MONTH, days); 1344 return dateFormat.format(calendar.getTime()); 1345 } 1346 } 1347 1348 /** 1349 * Provides rudimentary support for cookies. 1350 * Doesn't support 'path', 'secure' nor 'httpOnly'. 1351 * Feel free to improve it and/or add unsupported features. 1352 * 1353 * @author LordFokas 1354 */ 1355 public class CookieHandler implements Iterable<String> { 1356 private HashMap<String, String> cookies = new HashMap<String, String>(); 1357 private ArrayList<Cookie> queue = new ArrayList<Cookie>(); 1358 1359 public CookieHandler(Map<String, String> httpHeaders) { 1360 String raw = httpHeaders.get("cookie"); 1361 if (raw != null) { 1362 String[] tokens = raw.split(";"); 1363 for (String token : tokens) { 1364 String[] data = token.trim().split("="); 1365 if (data.length == 2) { 1366 cookies.put(data[0], data[1]); 1367 } 1368 } 1369 } 1370 } 1371 1372 @Override public Iterator<String> iterator() { 1373 return cookies.keySet().iterator(); 1374 } 1375 1376 /** 1377 * Read a cookie from the HTTP Headers. 1378 * 1379 * @param name The cookie's name. 1380 * @return The cookie's value if it exists, null otherwise. 1381 */ 1382 public String read(String name) { 1383 return cookies.get(name); 1384 } 1385 1386 /** 1387 * Sets a cookie. 1388 * 1389 * @param name The cookie's name. 1390 * @param value The cookie's value. 1391 * @param expires How many days until the cookie expires. 1392 */ 1393 public void set(String name, String value, int expires) { 1394 queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); 1395 } 1396 1397 public void set(Cookie cookie) { 1398 queue.add(cookie); 1399 } 1400 1401 /** 1402 * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side. 1403 * 1404 * @param name The cookie name. 1405 */ 1406 public void delete(String name) { 1407 set(name, "-delete-", -30); 1408 } 1409 1410 /** 1411 * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers. 1412 * 1413 * @param response The Response object to which headers the queued cookies will be added. 1414 */ 1415 public void unloadQueue(Response response) { 1416 for (Cookie cookie : queue) { 1417 response.addHeader("Set-Cookie", cookie.getHTTPHeader()); 1418 } 1419 } 1420 } 1421} 1422