1 package fi.iki.elonen; 2 3 import java.io.*; 4 import java.net.InetAddress; 5 import java.net.InetSocketAddress; 6 import java.net.ServerSocket; 7 import java.net.Socket; 8 import java.net.SocketException; 9 import java.net.SocketTimeoutException; 10 import java.net.URLDecoder; 11 import java.nio.ByteBuffer; 12 import java.nio.channels.FileChannel; 13 import java.text.SimpleDateFormat; 14 import java.util.ArrayList; 15 import java.util.Calendar; 16 import java.util.Date; 17 import java.util.HashMap; 18 import java.util.HashSet; 19 import java.util.Iterator; 20 import java.util.List; 21 import java.util.Locale; 22 import java.util.Map; 23 import java.util.Set; 24 import java.util.StringTokenizer; 25 import 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 */ 71 public 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 */ NanoHTTPD(int port)107 public NanoHTTPD(int port) { 108 this(null, port); 109 } 110 111 /** 112 * Constructs an HTTP server on given hostname and port. 113 */ NanoHTTPD(String hostname, int port)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 safeClose(Closeable closeable)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 safeClose(Socket closeable)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 safeClose(ServerSocket closeable)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 */ start()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 */ stop()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 */ registerConnection(Socket socket)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 */ unRegisterConnection(Socket socket)231 public synchronized void unRegisterConnection(Socket socket) { 232 openConnections.remove(socket); 233 } 234 235 /** 236 * Forcibly closes all connections that are open. 237 */ closeAllConnections()238 public synchronized void closeAllConnections() { 239 for (Socket socket : openConnections) { 240 safeClose(socket); 241 } 242 } 243 getListeningPort()244 public final int getListeningPort() { 245 return myServerSocket == null ? -1 : myServerSocket.getLocalPort(); 246 } 247 wasStarted()248 public final boolean wasStarted() { 249 return myServerSocket != null && myThread != null; 250 } 251 isAlive()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 serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files)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 */ serve(IHTTPSession session)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 */ decodePercent(String str)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 */ decodeParameters(Map<String, String> parms)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 */ decodeParameters(String queryString)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 */ setAsyncRunner(AsyncRunner asyncRunner)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 */ setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory)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 lookup(String method)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 { exec(Runnable code)406 void exec(Runnable code); 407 } 408 409 /** 410 * Factory to create temp file managers. 411 */ 412 public interface TempFileManagerFactory { create()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 { createTempFile()425 TempFile createTempFile() throws Exception; 426 clear()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 { open()437 OutputStream open() throws Exception; 438 delete()439 void delete() throws Exception; 440 getName()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 exec(Runnable code)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 DefaultTempFileManager()477 public DefaultTempFileManager() { 478 tmpdir = System.getProperty("java.io.tmpdir"); 479 tempFiles = new ArrayList<TempFile>(); 480 } 481 482 @Override createTempFile()483 public TempFile createTempFile() throws Exception { 484 DefaultTempFile tempFile = new DefaultTempFile(tmpdir); 485 tempFiles.add(tempFile); 486 return tempFile; 487 } 488 489 @Override clear()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 DefaultTempFile(String tempdir)511 public DefaultTempFile(String tempdir) throws IOException { 512 file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); 513 fstream = new FileOutputStream(file); 514 } 515 516 @Override open()517 public OutputStream open() throws Exception { 518 return fstream; 519 } 520 521 @Override delete()522 public void delete() throws Exception { 523 safeClose(fstream); 524 file.delete(); 525 } 526 527 @Override getName()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 */ Response(String msg)565 public Response(String msg) { 566 this(Status.OK, MIME_HTML, msg); 567 } 568 569 /** 570 * Basic constructor. 571 */ Response(IStatus status, String mimeType, InputStream data)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 */ Response(IStatus status, String mimeType, String txt)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 */ addHeader(String name, String value)594 public void addHeader(String name, String value) { 595 header.put(name, value); 596 } 597 getHeader(String name)598 public String getHeader(String name) { 599 return header.get(name); 600 } 601 602 /** 603 * Sends given response to the socket. 604 */ send(OutputStream outputStream)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 sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size)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 sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header)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 headerAlreadySent(Map<String, String> header, String name)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 sendAsChunked(OutputStream outputStream, PrintWriter pw)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 sendAsFixedLength(OutputStream outputStream, int pending)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 getStatus()701 public IStatus getStatus() { 702 return status; 703 } 704 setStatus(Status status)705 public void setStatus(Status status) { 706 this.status = status; 707 } 708 getMimeType()709 public String getMimeType() { 710 return mimeType; 711 } 712 setMimeType(String mimeType)713 public void setMimeType(String mimeType) { 714 this.mimeType = mimeType; 715 } 716 getData()717 public InputStream getData() { 718 return data; 719 } 720 setData(InputStream data)721 public void setData(InputStream data) { 722 this.data = data; 723 } 724 getRequestMethod()725 public Method getRequestMethod() { 726 return requestMethod; 727 } 728 setRequestMethod(Method requestMethod)729 public void setRequestMethod(Method requestMethod) { 730 this.requestMethod = requestMethod; 731 } 732 setChunkedTransfer(boolean chunkedTransfer)733 public void setChunkedTransfer(boolean chunkedTransfer) { 734 this.chunkedTransfer = chunkedTransfer; 735 } 736 737 public interface IStatus { getRequestStatus()738 int getRequestStatus(); getDescription()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"), 747 748 OK(200, "OK"), 749 CREATED(201, "Created"), 750 ACCEPTED(202, "Accepted"), 751 NO_CONTENT(204, "No Content"), 752 PARTIAL_CONTENT(206, "Partial Content"), 753 754 REDIRECT(301, "Moved Permanently"), 755 TEMPORARY_REDIRECT(302, "Moved Temporarily"), 756 NOT_MODIFIED(304, "Not Modified"), 757 758 BAD_REQUEST(400, "Bad Request"), 759 UNAUTHORIZED(401, "Unauthorized"), 760 FORBIDDEN(403, "Forbidden"), 761 NOT_FOUND(404, "Not Found"), 762 METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 763 RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), 764 765 INTERNAL_ERROR(500, "Internal Server Error"); 766 767 private final int requestStatus; 768 private final String description; 769 Status(int requestStatus, String description)770 Status(int requestStatus, String description) { 771 this.requestStatus = requestStatus; 772 this.description = description; 773 } 774 775 @Override getRequestStatus()776 public int getRequestStatus() { 777 return this.requestStatus; 778 } 779 780 @Override getDescription()781 public String getDescription() { 782 return "" + this.requestStatus + " " + description; 783 } 784 } 785 } 786 787 public static final class ResponseException extends Exception { 788 789 private final Response.Status status; 790 ResponseException(Response.Status status, String message)791 public ResponseException(Response.Status status, String message) { 792 super(message); 793 this.status = status; 794 } 795 ResponseException(Response.Status status, String message, Exception e)796 public ResponseException(Response.Status status, String message, Exception e) { 797 super(message, e); 798 this.status = status; 799 } 800 getStatus()801 public Response.Status getStatus() { 802 return status; 803 } 804 } 805 806 /** 807 * Default strategy for creating and cleaning up temporary files. 808 */ 809 private class DefaultTempFileManagerFactory implements TempFileManagerFactory { 810 @Override create()811 public TempFileManager create() { 812 return new DefaultTempFileManager(); 813 } 814 } 815 816 /** 817 * Handles one session, i.e. parses the HTTP request and returns the response. 818 */ 819 public interface IHTTPSession { execute()820 void execute() throws IOException; 821 getParms()822 Map<String, String> getParms(); 823 getHeaders()824 Map<String, String> getHeaders(); 825 826 /** 827 * @return the path part of the URL. 828 */ getUri()829 String getUri(); 830 getQueryParameterString()831 String getQueryParameterString(); 832 getMethod()833 Method getMethod(); 834 getInputStream()835 InputStream getInputStream(); 836 getCookies()837 CookieHandler getCookies(); 838 839 /** 840 * Adds the files in the request body to the files map. 841 * @arg files - map to modify 842 */ parseBody(Map<String, String> files)843 void parseBody(Map<String, String> files) throws IOException, ResponseException; 844 } 845 846 protected class HTTPSession implements IHTTPSession { 847 public static final int BUFSIZE = 8192; 848 private final TempFileManager tempFileManager; 849 private final OutputStream outputStream; 850 private PushbackInputStream inputStream; 851 private int splitbyte; 852 private int rlen; 853 private String uri; 854 private Method method; 855 private Map<String, String> parms; 856 private Map<String, String> headers; 857 private CookieHandler cookies; 858 private String queryParameterString; 859 HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream)860 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { 861 this.tempFileManager = tempFileManager; 862 this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); 863 this.outputStream = outputStream; 864 } 865 HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress)866 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { 867 this.tempFileManager = tempFileManager; 868 this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); 869 this.outputStream = outputStream; 870 String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); 871 headers = new HashMap<String, String>(); 872 873 headers.put("remote-addr", remoteIp); 874 headers.put("http-client-ip", remoteIp); 875 } 876 877 @Override execute()878 public void execute() throws IOException { 879 try { 880 // Read the first 8192 bytes. 881 // The full header should fit in here. 882 // Apache's default header limit is 8KB. 883 // Do NOT assume that a single read will get the entire header at once! 884 byte[] buf = new byte[BUFSIZE]; 885 splitbyte = 0; 886 rlen = 0; 887 { 888 int read = -1; 889 try { 890 read = inputStream.read(buf, 0, BUFSIZE); 891 } catch (Exception e) { 892 safeClose(inputStream); 893 safeClose(outputStream); 894 throw new SocketException("NanoHttpd Shutdown"); 895 } 896 if (read == -1) { 897 // socket was been closed 898 safeClose(inputStream); 899 safeClose(outputStream); 900 throw new SocketException("NanoHttpd Shutdown"); 901 } 902 while (read > 0) { 903 rlen += read; 904 splitbyte = findHeaderEnd(buf, rlen); 905 if (splitbyte > 0) 906 break; 907 read = inputStream.read(buf, rlen, BUFSIZE - rlen); 908 } 909 } 910 911 if (splitbyte < rlen) { 912 inputStream.unread(buf, splitbyte, rlen - splitbyte); 913 } 914 915 parms = new HashMap<String, String>(); 916 if(null == headers) { 917 headers = new HashMap<String, String>(); 918 } 919 920 // Create a BufferedReader for parsing the header. 921 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); 922 923 // Decode the header into parms and header java properties 924 Map<String, String> pre = new HashMap<String, String>(); 925 decodeHeader(hin, pre, parms, headers); 926 927 method = Method.lookup(pre.get("method")); 928 if (method == null) { 929 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); 930 } 931 932 uri = pre.get("uri"); 933 934 cookies = new CookieHandler(headers); 935 936 // Ok, now do the serve() 937 Response r = serve(this); 938 if (r == null) { 939 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); 940 } else { 941 cookies.unloadQueue(r); 942 r.setRequestMethod(method); 943 r.send(outputStream); 944 } 945 } catch (SocketException e) { 946 // throw it out to close socket object (finalAccept) 947 throw e; 948 } catch (SocketTimeoutException ste) { 949 throw ste; 950 } catch (IOException ioe) { 951 Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 952 r.send(outputStream); 953 safeClose(outputStream); 954 } catch (ResponseException re) { 955 Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); 956 r.send(outputStream); 957 safeClose(outputStream); 958 } finally { 959 tempFileManager.clear(); 960 } 961 } 962 963 @Override parseBody(Map<String, String> files)964 public void parseBody(Map<String, String> files) throws IOException, ResponseException { 965 RandomAccessFile randomAccessFile = null; 966 BufferedReader in = null; 967 try { 968 969 randomAccessFile = getTmpBucket(); 970 971 long size; 972 if (headers.containsKey("content-length")) { 973 size = Integer.parseInt(headers.get("content-length")); 974 } else if (splitbyte < rlen) { 975 size = rlen - splitbyte; 976 } else { 977 size = 0; 978 } 979 980 // Now read all the body and write it to f 981 byte[] buf = new byte[512]; 982 while (rlen >= 0 && size > 0) { 983 rlen = inputStream.read(buf, 0, (int)Math.min(size, 512)); 984 size -= rlen; 985 if (rlen > 0) { 986 randomAccessFile.write(buf, 0, rlen); 987 } 988 } 989 990 // Get the raw body as a byte [] 991 ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); 992 randomAccessFile.seek(0); 993 994 // Create a BufferedReader for easily reading it as string. 995 InputStream bin = new FileInputStream(randomAccessFile.getFD()); 996 in = new BufferedReader(new InputStreamReader(bin)); 997 998 // If the method is POST, there may be parameters 999 // in data section, too, read it: 1000 if (Method.POST.equals(method)) { 1001 String contentType = ""; 1002 String contentTypeHeader = headers.get("content-type"); 1003 1004 StringTokenizer st = null; 1005 if (contentTypeHeader != null) { 1006 st = new StringTokenizer(contentTypeHeader, ",; "); 1007 if (st.hasMoreTokens()) { 1008 contentType = st.nextToken(); 1009 } 1010 } 1011 1012 if ("multipart/form-data".equalsIgnoreCase(contentType)) { 1013 // Handle multipart/form-data 1014 if (!st.hasMoreTokens()) { 1015 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); 1016 } 1017 1018 String boundaryStartString = "boundary="; 1019 int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); 1020 String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); 1021 if (boundary.startsWith("\"") && boundary.endsWith("\"")) { 1022 boundary = boundary.substring(1, boundary.length() - 1); 1023 } 1024 1025 decodeMultipartData(boundary, fbuf, in, parms, files); 1026 } else { 1027 String postLine = ""; 1028 StringBuilder postLineBuffer = new StringBuilder(); 1029 char pbuf[] = new char[512]; 1030 int read = in.read(pbuf); 1031 while (read >= 0 && !postLine.endsWith("\r\n")) { 1032 postLine = String.valueOf(pbuf, 0, read); 1033 postLineBuffer.append(postLine); 1034 read = in.read(pbuf); 1035 } 1036 postLine = postLineBuffer.toString().trim(); 1037 // Handle application/x-www-form-urlencoded 1038 if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { 1039 decodeParms(postLine, parms); 1040 } else if (postLine.length() != 0) { 1041 // Special case for raw POST data => create a special files entry "postData" with raw content data 1042 files.put("postData", postLine); 1043 } 1044 } 1045 } else if (Method.PUT.equals(method)) { 1046 files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); 1047 } 1048 } finally { 1049 safeClose(randomAccessFile); 1050 safeClose(in); 1051 } 1052 } 1053 1054 /** 1055 * Decodes the sent headers and loads the data into Key/value pairs 1056 */ decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)1057 private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) 1058 throws ResponseException { 1059 try { 1060 // Read the request line 1061 String inLine = in.readLine(); 1062 if (inLine == null) { 1063 return; 1064 } 1065 1066 StringTokenizer st = new StringTokenizer(inLine); 1067 if (!st.hasMoreTokens()) { 1068 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); 1069 } 1070 1071 pre.put("method", st.nextToken()); 1072 1073 if (!st.hasMoreTokens()) { 1074 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); 1075 } 1076 1077 String uri = st.nextToken(); 1078 1079 // Decode parameters from the URI 1080 int qmi = uri.indexOf('?'); 1081 if (qmi >= 0) { 1082 decodeParms(uri.substring(qmi + 1), parms); 1083 uri = decodePercent(uri.substring(0, qmi)); 1084 } else { 1085 uri = decodePercent(uri); 1086 } 1087 1088 // If there's another token, it's protocol version, 1089 // followed by HTTP headers. Ignore version but parse headers. 1090 // NOTE: this now forces header names lowercase since they are 1091 // case insensitive and vary by client. 1092 if (st.hasMoreTokens()) { 1093 String line = in.readLine(); 1094 while (line != null && line.trim().length() > 0) { 1095 int p = line.indexOf(':'); 1096 if (p >= 0) 1097 headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); 1098 line = in.readLine(); 1099 } 1100 } 1101 1102 pre.put("uri", uri); 1103 } catch (IOException ioe) { 1104 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 1105 } 1106 } 1107 1108 /** 1109 * Decodes the Multipart Body data and put it into Key/Value pairs. 1110 */ decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, Map<String, String> files)1111 private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, 1112 Map<String, String> files) throws ResponseException { 1113 try { 1114 int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); 1115 int boundarycount = 1; 1116 String mpline = in.readLine(); 1117 while (mpline != null) { 1118 if (!mpline.contains(boundary)) { 1119 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"); 1120 } 1121 boundarycount++; 1122 Map<String, String> item = new HashMap<String, String>(); 1123 mpline = in.readLine(); 1124 while (mpline != null && mpline.trim().length() > 0) { 1125 int p = mpline.indexOf(':'); 1126 if (p != -1) { 1127 item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim()); 1128 } 1129 mpline = in.readLine(); 1130 } 1131 if (mpline != null) { 1132 String contentDisposition = item.get("content-disposition"); 1133 if (contentDisposition == null) { 1134 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"); 1135 } 1136 StringTokenizer st = new StringTokenizer(contentDisposition, ";"); 1137 Map<String, String> disposition = new HashMap<String, String>(); 1138 while (st.hasMoreTokens()) { 1139 String token = st.nextToken().trim(); 1140 int p = token.indexOf('='); 1141 if (p != -1) { 1142 disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim()); 1143 } 1144 } 1145 String pname = disposition.get("name"); 1146 pname = pname.substring(1, pname.length() - 1); 1147 1148 String value = ""; 1149 if (item.get("content-type") == null) { 1150 while (mpline != null && !mpline.contains(boundary)) { 1151 mpline = in.readLine(); 1152 if (mpline != null) { 1153 int d = mpline.indexOf(boundary); 1154 if (d == -1) { 1155 value += mpline; 1156 } else { 1157 value += mpline.substring(0, d - 2); 1158 } 1159 } 1160 } 1161 } else { 1162 if (boundarycount > bpositions.length) { 1163 throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); 1164 } 1165 int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); 1166 String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); 1167 files.put(pname, path); 1168 value = disposition.get("filename"); 1169 value = value.substring(1, value.length() - 1); 1170 do { 1171 mpline = in.readLine(); 1172 } while (mpline != null && !mpline.contains(boundary)); 1173 } 1174 parms.put(pname, value); 1175 } 1176 } 1177 } catch (IOException ioe) { 1178 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 1179 } 1180 } 1181 1182 /** 1183 * Find byte index separating header from body. It must be the last byte of the first two sequential new lines. 1184 */ findHeaderEnd(final byte[] buf, int rlen)1185 private int findHeaderEnd(final byte[] buf, int rlen) { 1186 int splitbyte = 0; 1187 while (splitbyte + 3 < rlen) { 1188 if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { 1189 return splitbyte + 4; 1190 } 1191 splitbyte++; 1192 } 1193 return 0; 1194 } 1195 1196 /** 1197 * Find the byte positions where multipart boundaries start. 1198 */ getBoundaryPositions(ByteBuffer b, byte[] boundary)1199 private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { 1200 int matchcount = 0; 1201 int matchbyte = -1; 1202 List<Integer> matchbytes = new ArrayList<Integer>(); 1203 for (int i = 0; i < b.limit(); i++) { 1204 if (b.get(i) == boundary[matchcount]) { 1205 if (matchcount == 0) 1206 matchbyte = i; 1207 matchcount++; 1208 if (matchcount == boundary.length) { 1209 matchbytes.add(matchbyte); 1210 matchcount = 0; 1211 matchbyte = -1; 1212 } 1213 } else { 1214 i -= matchcount; 1215 matchcount = 0; 1216 matchbyte = -1; 1217 } 1218 } 1219 int[] ret = new int[matchbytes.size()]; 1220 for (int i = 0; i < ret.length; i++) { 1221 ret[i] = matchbytes.get(i); 1222 } 1223 return ret; 1224 } 1225 1226 /** 1227 * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned. 1228 */ saveTmpFile(ByteBuffer b, int offset, int len)1229 private String saveTmpFile(ByteBuffer b, int offset, int len) { 1230 String path = ""; 1231 if (len > 0) { 1232 FileOutputStream fileOutputStream = null; 1233 try { 1234 TempFile tempFile = tempFileManager.createTempFile(); 1235 ByteBuffer src = b.duplicate(); 1236 fileOutputStream = new FileOutputStream(tempFile.getName()); 1237 FileChannel dest = fileOutputStream.getChannel(); 1238 src.position(offset).limit(offset + len); 1239 dest.write(src.slice()); 1240 path = tempFile.getName(); 1241 } catch (Exception e) { // Catch exception if any 1242 throw new Error(e); // we won't recover, so throw an error 1243 } finally { 1244 safeClose(fileOutputStream); 1245 } 1246 } 1247 return path; 1248 } 1249 getTmpBucket()1250 private RandomAccessFile getTmpBucket() { 1251 try { 1252 TempFile tempFile = tempFileManager.createTempFile(); 1253 return new RandomAccessFile(tempFile.getName(), "rw"); 1254 } catch (Exception e) { 1255 throw new Error(e); // we won't recover, so throw an error 1256 } 1257 } 1258 1259 /** 1260 * It returns the offset separating multipart file headers from the file's data. 1261 */ stripMultipartHeaders(ByteBuffer b, int offset)1262 private int stripMultipartHeaders(ByteBuffer b, int offset) { 1263 int i; 1264 for (i = offset; i < b.limit(); i++) { 1265 if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { 1266 break; 1267 } 1268 } 1269 return i + 1; 1270 } 1271 1272 /** 1273 * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and 1274 * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map. 1275 */ decodeParms(String parms, Map<String, String> p)1276 private void decodeParms(String parms, Map<String, String> p) { 1277 if (parms == null) { 1278 queryParameterString = ""; 1279 return; 1280 } 1281 1282 queryParameterString = parms; 1283 StringTokenizer st = new StringTokenizer(parms, "&"); 1284 while (st.hasMoreTokens()) { 1285 String e = st.nextToken(); 1286 int sep = e.indexOf('='); 1287 if (sep >= 0) { 1288 p.put(decodePercent(e.substring(0, sep)).trim(), 1289 decodePercent(e.substring(sep + 1))); 1290 } else { 1291 p.put(decodePercent(e).trim(), ""); 1292 } 1293 } 1294 } 1295 1296 @Override getParms()1297 public final Map<String, String> getParms() { 1298 return parms; 1299 } 1300 getQueryParameterString()1301 public String getQueryParameterString() { 1302 return queryParameterString; 1303 } 1304 1305 @Override getHeaders()1306 public final Map<String, String> getHeaders() { 1307 return headers; 1308 } 1309 1310 @Override getUri()1311 public final String getUri() { 1312 return uri; 1313 } 1314 1315 @Override getMethod()1316 public final Method getMethod() { 1317 return method; 1318 } 1319 1320 @Override getInputStream()1321 public final InputStream getInputStream() { 1322 return inputStream; 1323 } 1324 1325 @Override getCookies()1326 public CookieHandler getCookies() { 1327 return cookies; 1328 } 1329 } 1330 1331 public static class Cookie { 1332 private String n, v, e; 1333 Cookie(String name, String value, String expires)1334 public Cookie(String name, String value, String expires) { 1335 n = name; 1336 v = value; 1337 e = expires; 1338 } 1339 Cookie(String name, String value)1340 public Cookie(String name, String value) { 1341 this(name, value, 30); 1342 } 1343 Cookie(String name, String value, int numDays)1344 public Cookie(String name, String value, int numDays) { 1345 n = name; 1346 v = value; 1347 e = getHTTPTime(numDays); 1348 } 1349 getHTTPHeader()1350 public String getHTTPHeader() { 1351 String fmt = "%s=%s; expires=%s"; 1352 return String.format(fmt, n, v, e); 1353 } 1354 getHTTPTime(int days)1355 public static String getHTTPTime(int days) { 1356 Calendar calendar = Calendar.getInstance(); 1357 SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 1358 dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 1359 calendar.add(Calendar.DAY_OF_MONTH, days); 1360 return dateFormat.format(calendar.getTime()); 1361 } 1362 } 1363 1364 /** 1365 * Provides rudimentary support for cookies. 1366 * Doesn't support 'path', 'secure' nor 'httpOnly'. 1367 * Feel free to improve it and/or add unsupported features. 1368 * 1369 * @author LordFokas 1370 */ 1371 public class CookieHandler implements Iterable<String> { 1372 private HashMap<String, String> cookies = new HashMap<String, String>(); 1373 private ArrayList<Cookie> queue = new ArrayList<Cookie>(); 1374 CookieHandler(Map<String, String> httpHeaders)1375 public CookieHandler(Map<String, String> httpHeaders) { 1376 String raw = httpHeaders.get("cookie"); 1377 if (raw != null) { 1378 String[] tokens = raw.split(";"); 1379 for (String token : tokens) { 1380 String[] data = token.trim().split("="); 1381 if (data.length == 2) { 1382 cookies.put(data[0], data[1]); 1383 } 1384 } 1385 } 1386 } 1387 iterator()1388 @Override public Iterator<String> iterator() { 1389 return cookies.keySet().iterator(); 1390 } 1391 1392 /** 1393 * Read a cookie from the HTTP Headers. 1394 * 1395 * @param name The cookie's name. 1396 * @return The cookie's value if it exists, null otherwise. 1397 */ read(String name)1398 public String read(String name) { 1399 return cookies.get(name); 1400 } 1401 1402 /** 1403 * Sets a cookie. 1404 * 1405 * @param name The cookie's name. 1406 * @param value The cookie's value. 1407 * @param expires How many days until the cookie expires. 1408 */ set(String name, String value, int expires)1409 public void set(String name, String value, int expires) { 1410 queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); 1411 } 1412 set(Cookie cookie)1413 public void set(Cookie cookie) { 1414 queue.add(cookie); 1415 } 1416 1417 /** 1418 * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side. 1419 * 1420 * @param name The cookie name. 1421 */ delete(String name)1422 public void delete(String name) { 1423 set(name, "-delete-", -30); 1424 } 1425 1426 /** 1427 * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers. 1428 * 1429 * @param response The Response object to which headers the queued cookies will be added. 1430 */ unloadQueue(Response response)1431 public void unloadQueue(Response response) { 1432 for (Cookie cookie : queue) { 1433 response.addHeader("Set-Cookie", cookie.getHTTPHeader()); 1434 } 1435 } 1436 } 1437 } 1438