NanoHTTPD.java revision de2bb1bc9fa3959846741e1fd14076971e17b90c
1package fi.iki.elonen; 2 3/* 4 * #%L 5 * NanoHttpd-Core 6 * %% 7 * Copyright (C) 2012 - 2015 nanohttpd 8 * %% 9 * Redistribution and use in source and binary forms, with or without modification, 10 * are permitted provided that the following conditions are met: 11 * 12 * 1. Redistributions of source code must retain the above copyright notice, this 13 * list of conditions and the following disclaimer. 14 * 15 * 2. Redistributions in binary form must reproduce the above copyright notice, 16 * this list of conditions and the following disclaimer in the documentation 17 * and/or other materials provided with the distribution. 18 * 19 * 3. Neither the name of the nanohttpd nor the names of its contributors 20 * may be used to endorse or promote products derived from this software without 21 * specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 31 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 * OF THE POSSIBILITY OF SUCH DAMAGE. 33 * #L% 34 */ 35 36import java.io.BufferedReader; 37import java.io.BufferedWriter; 38import java.io.ByteArrayInputStream; 39import java.io.Closeable; 40import java.io.File; 41import java.io.FileInputStream; 42import java.io.FileOutputStream; 43import java.io.IOException; 44import java.io.InputStream; 45import java.io.InputStreamReader; 46import java.io.OutputStream; 47import java.io.OutputStreamWriter; 48import java.io.PrintWriter; 49import java.io.PushbackInputStream; 50import java.io.RandomAccessFile; 51import java.io.UnsupportedEncodingException; 52import java.net.InetAddress; 53import java.net.InetSocketAddress; 54import java.net.ServerSocket; 55import java.net.Socket; 56import java.net.SocketException; 57import java.net.SocketTimeoutException; 58import java.net.URLDecoder; 59import java.nio.ByteBuffer; 60import java.nio.channels.FileChannel; 61import java.security.KeyStore; 62import java.text.SimpleDateFormat; 63import java.util.ArrayList; 64import java.util.Calendar; 65import java.util.Collections; 66import java.util.Date; 67import java.util.HashMap; 68import java.util.Iterator; 69import java.util.List; 70import java.util.Locale; 71import java.util.Map; 72import java.util.StringTokenizer; 73import java.util.TimeZone; 74import java.util.logging.Level; 75import java.util.logging.Logger; 76 77import javax.net.ssl.KeyManager; 78import javax.net.ssl.KeyManagerFactory; 79import javax.net.ssl.SSLContext; 80import javax.net.ssl.SSLServerSocket; 81import javax.net.ssl.SSLServerSocketFactory; 82import javax.net.ssl.TrustManagerFactory; 83 84import fi.iki.elonen.NanoHTTPD.Response.IStatus; 85import fi.iki.elonen.NanoHTTPD.Response.Status; 86 87/** 88 * A simple, tiny, nicely embeddable HTTP server in Java 89 * <p/> 90 * <p/> 91 * NanoHTTPD 92 * <p> 93 * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 94 * 2010 by Konstantinos Togias 95 * </p> 96 * <p/> 97 * <p/> 98 * <b>Features + limitations: </b> 99 * <ul> 100 * <p/> 101 * <li>Only one Java file</li> 102 * <li>Java 5 compatible</li> 103 * <li>Released as open source, Modified BSD licence</li> 104 * <li>No fixed config files, logging, authorization etc. (Implement yourself if 105 * you need them.)</li> 106 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT 107 * support in 1.25)</li> 108 * <li>Supports both dynamic content and file serving</li> 109 * <li>Supports file upload (since version 1.2, 2010)</li> 110 * <li>Supports partial content (streaming)</li> 111 * <li>Supports ETags</li> 112 * <li>Never caches anything</li> 113 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li> 114 * <li>Default code serves files and shows all HTTP parameters and headers</li> 115 * <li>File server supports directory listing, index.html and index.htm</li> 116 * <li>File server supports partial content (streaming)</li> 117 * <li>File server supports ETags</li> 118 * <li>File server does the 301 redirection trick for directories without '/'</li> 119 * <li>File server supports simple skipping for files (continue download)</li> 120 * <li>File server serves also very long files without memory overhead</li> 121 * <li>Contains a built-in list of most common MIME types</li> 122 * <li>All header names are converted to lower case so they don't vary between 123 * browsers/clients</li> 124 * <p/> 125 * </ul> 126 * <p/> 127 * <p/> 128 * <b>How to use: </b> 129 * <ul> 130 * <p/> 131 * <li>Subclass and implement serve() and embed to your own program</li> 132 * <p/> 133 * </ul> 134 * <p/> 135 * See the separate "LICENSE.md" file for the distribution license (Modified BSD 136 * licence) 137 */ 138public abstract class NanoHTTPD { 139 140 /** 141 * Pluggable strategy for asynchronously executing requests. 142 */ 143 public interface AsyncRunner { 144 145 void closeAll(); 146 147 void closed(ClientHandler clientHandler); 148 149 void exec(ClientHandler code); 150 } 151 152 /** 153 * The runnable that will be used for every new client connection. 154 */ 155 public class ClientHandler implements Runnable { 156 157 private final InputStream inputStream; 158 159 private final Socket acceptSocket; 160 161 private ClientHandler(InputStream inputStream, Socket acceptSocket) { 162 this.inputStream = inputStream; 163 this.acceptSocket = acceptSocket; 164 } 165 166 public void close() { 167 safeClose(this.inputStream); 168 safeClose(this.acceptSocket); 169 } 170 171 @Override 172 public void run() { 173 OutputStream outputStream = null; 174 try { 175 outputStream = this.acceptSocket.getOutputStream(); 176 TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); 177 HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); 178 while (!this.acceptSocket.isClosed()) { 179 session.execute(); 180 } 181 } catch (Exception e) { 182 // When the socket is closed by the client, 183 // we throw our own SocketException 184 // to break the "keep alive" loop above. If 185 // the exception was anything other 186 // than the expected SocketException OR a 187 // SocketTimeoutException, print the 188 // stacktrace 189 if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) { 190 NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 191 } 192 } finally { 193 safeClose(outputStream); 194 safeClose(this.inputStream); 195 safeClose(this.acceptSocket); 196 NanoHTTPD.this.asyncRunner.closed(this); 197 } 198 } 199 } 200 201 public static class Cookie { 202 203 public static String getHTTPTime(int days) { 204 Calendar calendar = Calendar.getInstance(); 205 SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 206 dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 207 calendar.add(Calendar.DAY_OF_MONTH, days); 208 return dateFormat.format(calendar.getTime()); 209 } 210 211 private final String n, v, e; 212 213 public Cookie(String name, String value) { 214 this(name, value, 30); 215 } 216 217 public Cookie(String name, String value, int numDays) { 218 this.n = name; 219 this.v = value; 220 this.e = getHTTPTime(numDays); 221 } 222 223 public Cookie(String name, String value, String expires) { 224 this.n = name; 225 this.v = value; 226 this.e = expires; 227 } 228 229 public String getHTTPHeader() { 230 String fmt = "%s=%s; expires=%s"; 231 return String.format(fmt, this.n, this.v, this.e); 232 } 233 } 234 235 /** 236 * Provides rudimentary support for cookies. Doesn't support 'path', 237 * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported 238 * features. 239 * 240 * @author LordFokas 241 */ 242 public class CookieHandler implements Iterable<String> { 243 244 private final HashMap<String, String> cookies = new HashMap<String, String>(); 245 246 private final ArrayList<Cookie> queue = new ArrayList<Cookie>(); 247 248 public CookieHandler(Map<String, String> httpHeaders) { 249 String raw = httpHeaders.get("cookie"); 250 if (raw != null) { 251 String[] tokens = raw.split(";"); 252 for (String token : tokens) { 253 String[] data = token.trim().split("="); 254 if (data.length == 2) { 255 this.cookies.put(data[0], data[1]); 256 } 257 } 258 } 259 } 260 261 /** 262 * Set a cookie with an expiration date from a month ago, effectively 263 * deleting it on the client side. 264 * 265 * @param name 266 * The cookie name. 267 */ 268 public void delete(String name) { 269 set(name, "-delete-", -30); 270 } 271 272 @Override 273 public Iterator<String> iterator() { 274 return this.cookies.keySet().iterator(); 275 } 276 277 /** 278 * Read a cookie from the HTTP Headers. 279 * 280 * @param name 281 * The cookie's name. 282 * @return The cookie's value if it exists, null otherwise. 283 */ 284 public String read(String name) { 285 return this.cookies.get(name); 286 } 287 288 public void set(Cookie cookie) { 289 this.queue.add(cookie); 290 } 291 292 /** 293 * Sets a cookie. 294 * 295 * @param name 296 * The cookie's name. 297 * @param value 298 * The cookie's value. 299 * @param expires 300 * How many days until the cookie expires. 301 */ 302 public void set(String name, String value, int expires) { 303 this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); 304 } 305 306 /** 307 * Internally used by the webserver to add all queued cookies into the 308 * Response's HTTP Headers. 309 * 310 * @param response 311 * The Response object to which headers the queued cookies 312 * will be added. 313 */ 314 public void unloadQueue(Response response) { 315 for (Cookie cookie : this.queue) { 316 response.addHeader("Set-Cookie", cookie.getHTTPHeader()); 317 } 318 } 319 } 320 321 /** 322 * Default threading strategy for NanoHTTPD. 323 * <p/> 324 * <p> 325 * By default, the server spawns a new Thread for every incoming request. 326 * These are set to <i>daemon</i> status, and named according to the request 327 * number. The name is useful when profiling the application. 328 * </p> 329 */ 330 public static class DefaultAsyncRunner implements AsyncRunner { 331 332 private long requestCount; 333 334 private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<NanoHTTPD.ClientHandler>()); 335 336 /** 337 * @return a list with currently running clients. 338 */ 339 public List<ClientHandler> getRunning() { 340 return running; 341 } 342 343 @Override 344 public void closeAll() { 345 // copy of the list for concurrency 346 for (ClientHandler clientHandler : new ArrayList<ClientHandler>(this.running)) { 347 clientHandler.close(); 348 } 349 } 350 351 @Override 352 public void closed(ClientHandler clientHandler) { 353 this.running.remove(clientHandler); 354 } 355 356 @Override 357 public void exec(ClientHandler clientHandler) { 358 ++this.requestCount; 359 Thread t = new Thread(clientHandler); 360 t.setDaemon(true); 361 t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); 362 this.running.add(clientHandler); 363 t.start(); 364 } 365 } 366 367 /** 368 * Default strategy for creating and cleaning up temporary files. 369 * <p/> 370 * <p> 371 * By default, files are created by <code>File.createTempFile()</code> in 372 * the directory specified. 373 * </p> 374 */ 375 public static class DefaultTempFile implements TempFile { 376 377 private final File file; 378 379 private final OutputStream fstream; 380 381 public DefaultTempFile(String tempdir) throws IOException { 382 this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); 383 this.fstream = new FileOutputStream(this.file); 384 } 385 386 @Override 387 public void delete() throws Exception { 388 safeClose(this.fstream); 389 if (!this.file.delete()) { 390 throw new Exception("could not delete temporary file"); 391 } 392 } 393 394 @Override 395 public String getName() { 396 return this.file.getAbsolutePath(); 397 } 398 399 @Override 400 public OutputStream open() throws Exception { 401 return this.fstream; 402 } 403 } 404 405 /** 406 * Default strategy for creating and cleaning up temporary files. 407 * <p/> 408 * <p> 409 * This class stores its files in the standard location (that is, wherever 410 * <code>java.io.tmpdir</code> points to). Files are added to an internal 411 * list, and deleted when no longer needed (that is, when 412 * <code>clear()</code> is invoked at the end of processing a request). 413 * </p> 414 */ 415 public static class DefaultTempFileManager implements TempFileManager { 416 417 private final String tmpdir; 418 419 private final List<TempFile> tempFiles; 420 421 public DefaultTempFileManager() { 422 this.tmpdir = System.getProperty("java.io.tmpdir"); 423 this.tempFiles = new ArrayList<TempFile>(); 424 } 425 426 @Override 427 public void clear() { 428 for (TempFile file : this.tempFiles) { 429 try { 430 file.delete(); 431 } catch (Exception ignored) { 432 NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); 433 } 434 } 435 this.tempFiles.clear(); 436 } 437 438 @Override 439 public TempFile createTempFile() throws Exception { 440 DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); 441 this.tempFiles.add(tempFile); 442 return tempFile; 443 } 444 } 445 446 /** 447 * Default strategy for creating and cleaning up temporary files. 448 */ 449 private class DefaultTempFileManagerFactory implements TempFileManagerFactory { 450 451 @Override 452 public TempFileManager create() { 453 return new DefaultTempFileManager(); 454 } 455 } 456 457 protected class HTTPSession implements IHTTPSession { 458 459 public static final int BUFSIZE = 8192; 460 461 private final TempFileManager tempFileManager; 462 463 private final OutputStream outputStream; 464 465 private final PushbackInputStream inputStream; 466 467 private int splitbyte; 468 469 private int rlen; 470 471 private String uri; 472 473 private Method method; 474 475 private Map<String, String> parms; 476 477 private Map<String, String> headers; 478 479 private CookieHandler cookies; 480 481 private String queryParameterString; 482 483 private String remoteIp; 484 485 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { 486 this.tempFileManager = tempFileManager; 487 this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE); 488 this.outputStream = outputStream; 489 } 490 491 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { 492 this.tempFileManager = tempFileManager; 493 this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE); 494 this.outputStream = outputStream; 495 this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); 496 this.headers = new HashMap<String, String>(); 497 } 498 499 /** 500 * Decodes the sent headers and loads the data into Key/value pairs 501 */ 502 private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) throws ResponseException { 503 try { 504 // Read the request line 505 String inLine = in.readLine(); 506 if (inLine == null) { 507 return; 508 } 509 510 StringTokenizer st = new StringTokenizer(inLine); 511 if (!st.hasMoreTokens()) { 512 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); 513 } 514 515 pre.put("method", st.nextToken()); 516 517 if (!st.hasMoreTokens()) { 518 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); 519 } 520 521 String uri = st.nextToken(); 522 523 // Decode parameters from the URI 524 int qmi = uri.indexOf('?'); 525 if (qmi >= 0) { 526 decodeParms(uri.substring(qmi + 1), parms); 527 uri = decodePercent(uri.substring(0, qmi)); 528 } else { 529 uri = decodePercent(uri); 530 } 531 532 // If there's another token, its protocol version, 533 // followed by HTTP headers. Ignore version but parse headers. 534 // NOTE: this now forces header names lower case since they are 535 // case insensitive and vary by client. 536 if (st.hasMoreTokens()) { 537 if (!st.nextToken().equals("HTTP/1.1")) { 538 throw new ResponseException(Response.Status.UNSUPPORTED_HTTP_VERSION, "Only HTTP/1.1 is supported."); 539 } 540 } else { 541 NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange.."); 542 } 543 String line = in.readLine(); 544 while (line != null && line.trim().length() > 0) { 545 int p = line.indexOf(':'); 546 if (p >= 0) { 547 headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); 548 } 549 line = in.readLine(); 550 } 551 552 pre.put("uri", uri); 553 } catch (IOException ioe) { 554 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 555 } 556 } 557 558 /** 559 * Decodes the Multipart Body data and put it into Key/Value pairs. 560 */ 561 private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, Map<String, String> files) throws ResponseException { 562 try { 563 int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); 564 int boundarycount = 1; 565 String mpline = in.readLine(); 566 while (mpline != null) { 567 if (!mpline.contains(boundary)) { 568 throw new ResponseException(Response.Status.BAD_REQUEST, 569 "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html"); 570 } 571 boundarycount++; 572 Map<String, String> item = new HashMap<String, String>(); 573 mpline = in.readLine(); 574 while (mpline != null && mpline.trim().length() > 0) { 575 int p = mpline.indexOf(':'); 576 if (p != -1) { 577 item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim()); 578 } 579 mpline = in.readLine(); 580 } 581 if (mpline != null) { 582 String contentDisposition = item.get("content-disposition"); 583 if (contentDisposition == null) { 584 throw new ResponseException(Response.Status.BAD_REQUEST, 585 "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html"); 586 } 587 StringTokenizer st = new StringTokenizer(contentDisposition, ";"); 588 Map<String, String> disposition = new HashMap<String, String>(); 589 while (st.hasMoreTokens()) { 590 String token = st.nextToken().trim(); 591 int p = token.indexOf('='); 592 if (p != -1) { 593 disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim()); 594 } 595 } 596 String pname = disposition.get("name"); 597 pname = pname.substring(1, pname.length() - 1); 598 599 String value = ""; 600 if (item.get("content-type") == null) { 601 while (mpline != null && !mpline.contains(boundary)) { 602 mpline = in.readLine(); 603 if (mpline != null) { 604 int d = mpline.indexOf(boundary); 605 if (d == -1) { 606 value += mpline; 607 } else { 608 value += mpline.substring(0, d - 2); 609 } 610 } 611 } 612 } else { 613 if (boundarycount > bpositions.length) { 614 throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); 615 } 616 int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); 617 String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); 618 if (!files.containsKey(pname)) { 619 files.put(pname, path); 620 } else { 621 int count = 2; 622 while (files.containsKey(pname + count)) { 623 count++; 624 } 625 files.put(pname + count, path); 626 } 627 value = disposition.get("filename"); 628 value = value.substring(1, value.length() - 1); 629 do { 630 mpline = in.readLine(); 631 } while (mpline != null && !mpline.contains(boundary)); 632 } 633 parms.put(pname, value); 634 } 635 } 636 } catch (IOException ioe) { 637 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 638 } 639 } 640 641 /** 642 * Decodes parameters in percent-encoded URI-format ( e.g. 643 * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given 644 * Map. NOTE: this doesn't support multiple identical keys due to the 645 * simplicity of Map. 646 */ 647 private void decodeParms(String parms, Map<String, String> p) { 648 if (parms == null) { 649 this.queryParameterString = ""; 650 return; 651 } 652 653 this.queryParameterString = parms; 654 StringTokenizer st = new StringTokenizer(parms, "&"); 655 while (st.hasMoreTokens()) { 656 String e = st.nextToken(); 657 int sep = e.indexOf('='); 658 if (sep >= 0) { 659 p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1))); 660 } else { 661 p.put(decodePercent(e).trim(), ""); 662 } 663 } 664 } 665 666 @Override 667 public void execute() throws IOException { 668 try { 669 // Read the first 8192 bytes. 670 // The full header should fit in here. 671 // Apache's default header limit is 8KB. 672 // Do NOT assume that a single read will get the entire header 673 // at once! 674 byte[] buf = new byte[HTTPSession.BUFSIZE]; 675 this.splitbyte = 0; 676 this.rlen = 0; 677 678 int read = -1; 679 try { 680 read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); 681 } catch (Exception e) { 682 safeClose(this.inputStream); 683 safeClose(this.outputStream); 684 throw new SocketException("NanoHttpd Shutdown"); 685 } 686 if (read == -1) { 687 // socket was been closed 688 safeClose(this.inputStream); 689 safeClose(this.outputStream); 690 throw new SocketException("NanoHttpd Shutdown"); 691 } 692 while (read > 0) { 693 this.rlen += read; 694 this.splitbyte = findHeaderEnd(buf, this.rlen); 695 if (this.splitbyte > 0) { 696 break; 697 } 698 read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen); 699 } 700 701 if (this.splitbyte < this.rlen) { 702 this.inputStream.unread(buf, this.splitbyte, this.rlen - this.splitbyte); 703 } 704 705 this.parms = new HashMap<String, String>(); 706 if (null == this.headers) { 707 this.headers = new HashMap<String, String>(); 708 } else { 709 this.headers.clear(); 710 } 711 712 if (null != this.remoteIp) { 713 this.headers.put("remote-addr", this.remoteIp); 714 this.headers.put("http-client-ip", this.remoteIp); 715 } 716 717 // Create a BufferedReader for parsing the header. 718 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); 719 720 // Decode the header into parms and header java properties 721 Map<String, String> pre = new HashMap<String, String>(); 722 decodeHeader(hin, pre, this.parms, this.headers); 723 724 this.method = Method.lookup(pre.get("method")); 725 if (this.method == null) { 726 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); 727 } 728 729 this.uri = pre.get("uri"); 730 731 this.cookies = new CookieHandler(this.headers); 732 733 // Ok, now do the serve() 734 Response r = serve(this); 735 if (r == null) { 736 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); 737 } else { 738 this.cookies.unloadQueue(r); 739 r.setRequestMethod(this.method); 740 r.send(this.outputStream); 741 } 742 } catch (SocketException e) { 743 // throw it out to close socket object (finalAccept) 744 throw e; 745 } catch (SocketTimeoutException ste) { 746 // treat socket timeouts the same way we treat socket exceptions 747 // i.e. close the stream & finalAccept object by throwing the 748 // exception up the call stack. 749 throw ste; 750 } catch (IOException ioe) { 751 Response r = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 752 r.send(this.outputStream); 753 safeClose(this.outputStream); 754 } catch (ResponseException re) { 755 Response r = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 756 r.send(this.outputStream); 757 safeClose(this.outputStream); 758 } finally { 759 this.tempFileManager.clear(); 760 } 761 } 762 763 /** 764 * Find byte index separating header from body. It must be the last byte 765 * of the first two sequential new lines. 766 */ 767 private int findHeaderEnd(final byte[] buf, int rlen) { 768 int splitbyte = 0; 769 while (splitbyte + 3 < rlen) { 770 if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { 771 return splitbyte + 4; 772 } 773 splitbyte++; 774 } 775 return 0; 776 } 777 778 /** 779 * Find the byte positions where multipart boundaries start. 780 */ 781 private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { 782 int matchcount = 0; 783 int matchbyte = -1; 784 List<Integer> matchbytes = new ArrayList<Integer>(); 785 for (int i = 0; i < b.limit(); i++) { 786 if (b.get(i) == boundary[matchcount]) { 787 if (matchcount == 0) { 788 matchbyte = i; 789 } 790 matchcount++; 791 if (matchcount == boundary.length) { 792 matchbytes.add(matchbyte); 793 matchcount = 0; 794 matchbyte = -1; 795 } 796 } else { 797 i -= matchcount; 798 matchcount = 0; 799 matchbyte = -1; 800 } 801 } 802 int[] ret = new int[matchbytes.size()]; 803 for (int i = 0; i < ret.length; i++) { 804 ret[i] = matchbytes.get(i); 805 } 806 return ret; 807 } 808 809 @Override 810 public CookieHandler getCookies() { 811 return this.cookies; 812 } 813 814 @Override 815 public final Map<String, String> getHeaders() { 816 return this.headers; 817 } 818 819 @Override 820 public final InputStream getInputStream() { 821 return this.inputStream; 822 } 823 824 @Override 825 public final Method getMethod() { 826 return this.method; 827 } 828 829 @Override 830 public final Map<String, String> getParms() { 831 return this.parms; 832 } 833 834 @Override 835 public String getQueryParameterString() { 836 return this.queryParameterString; 837 } 838 839 private RandomAccessFile getTmpBucket() { 840 try { 841 TempFile tempFile = this.tempFileManager.createTempFile(); 842 return new RandomAccessFile(tempFile.getName(), "rw"); 843 } catch (Exception e) { 844 throw new Error(e); // we won't recover, so throw an error 845 } 846 } 847 848 @Override 849 public final String getUri() { 850 return this.uri; 851 } 852 853 @Override 854 public void parseBody(Map<String, String> files) throws IOException, ResponseException { 855 RandomAccessFile randomAccessFile = null; 856 BufferedReader in = null; 857 try { 858 859 randomAccessFile = getTmpBucket(); 860 861 long size; 862 if (this.headers.containsKey("content-length")) { 863 size = Integer.parseInt(this.headers.get("content-length")); 864 } else if (this.splitbyte < this.rlen) { 865 size = this.rlen - this.splitbyte; 866 } else { 867 size = 0; 868 } 869 870 // Now read all the body and write it to f 871 byte[] buf = new byte[512]; 872 while (this.rlen >= 0 && size > 0) { 873 this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, 512)); 874 size -= this.rlen; 875 if (this.rlen > 0) { 876 randomAccessFile.write(buf, 0, this.rlen); 877 } 878 } 879 880 // Get the raw body as a byte [] 881 ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); 882 randomAccessFile.seek(0); 883 884 // Create a BufferedReader for easily reading it as string. 885 InputStream bin = new FileInputStream(randomAccessFile.getFD()); 886 in = new BufferedReader(new InputStreamReader(bin)); 887 888 // If the method is POST, there may be parameters 889 // in data section, too, read it: 890 if (Method.POST.equals(this.method)) { 891 String contentType = ""; 892 String contentTypeHeader = this.headers.get("content-type"); 893 894 StringTokenizer st = null; 895 if (contentTypeHeader != null) { 896 st = new StringTokenizer(contentTypeHeader, ",; "); 897 if (st.hasMoreTokens()) { 898 contentType = st.nextToken(); 899 } 900 } 901 902 if ("multipart/form-data".equalsIgnoreCase(contentType)) { 903 // Handle multipart/form-data 904 if (!st.hasMoreTokens()) { 905 throw new ResponseException(Response.Status.BAD_REQUEST, 906 "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); 907 } 908 909 String boundaryStartString = "boundary="; 910 int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); 911 String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); 912 if (boundary.startsWith("\"") && boundary.endsWith("\"")) { 913 boundary = boundary.substring(1, boundary.length() - 1); 914 } 915 916 decodeMultipartData(boundary, fbuf, in, this.parms, files); 917 } else { 918 String postLine = ""; 919 StringBuilder postLineBuffer = new StringBuilder(); 920 char pbuf[] = new char[512]; 921 int read = in.read(pbuf); 922 while (read >= 0) { 923 postLine = String.valueOf(pbuf, 0, read); 924 postLineBuffer.append(postLine); 925 read = in.read(pbuf); 926 } 927 postLine = postLineBuffer.toString().trim(); 928 // Handle application/x-www-form-urlencoded 929 if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { 930 decodeParms(postLine, this.parms); 931 } else if (postLine.length() != 0) { 932 // Special case for raw POST data => create a 933 // special files entry "postData" with raw content 934 // data 935 files.put("postData", postLine); 936 } 937 } 938 } else if (Method.PUT.equals(this.method)) { 939 files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); 940 } 941 } finally { 942 safeClose(randomAccessFile); 943 safeClose(in); 944 } 945 } 946 947 /** 948 * Retrieves the content of a sent file and saves it to a temporary 949 * file. The full path to the saved file is returned. 950 */ 951 private String saveTmpFile(ByteBuffer b, int offset, int len) { 952 String path = ""; 953 if (len > 0) { 954 FileOutputStream fileOutputStream = null; 955 try { 956 TempFile tempFile = this.tempFileManager.createTempFile(); 957 ByteBuffer src = b.duplicate(); 958 fileOutputStream = new FileOutputStream(tempFile.getName()); 959 FileChannel dest = fileOutputStream.getChannel(); 960 src.position(offset).limit(offset + len); 961 dest.write(src.slice()); 962 path = tempFile.getName(); 963 } catch (Exception e) { // Catch exception if any 964 throw new Error(e); // we won't recover, so throw an error 965 } finally { 966 safeClose(fileOutputStream); 967 } 968 } 969 return path; 970 } 971 972 /** 973 * It returns the offset separating multipart file headers from the 974 * file's data. 975 */ 976 private int stripMultipartHeaders(ByteBuffer b, int offset) { 977 int i; 978 for (i = offset; i < b.limit(); i++) { 979 if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { 980 break; 981 } 982 } 983 return i + 1; 984 } 985 } 986 987 /** 988 * Handles one session, i.e. parses the HTTP request and returns the 989 * response. 990 */ 991 public interface IHTTPSession { 992 993 void execute() throws IOException; 994 995 CookieHandler getCookies(); 996 997 Map<String, String> getHeaders(); 998 999 InputStream getInputStream(); 1000 1001 Method getMethod(); 1002 1003 Map<String, String> getParms(); 1004 1005 String getQueryParameterString(); 1006 1007 /** 1008 * @return the path part of the URL. 1009 */ 1010 String getUri(); 1011 1012 /** 1013 * Adds the files in the request body to the files map. 1014 * 1015 * @param files 1016 * map to modify 1017 */ 1018 void parseBody(Map<String, String> files) throws IOException, ResponseException; 1019 } 1020 1021 /** 1022 * HTTP Request methods, with the ability to decode a <code>String</code> 1023 * back to its enum value. 1024 */ 1025 public enum Method { 1026 GET, 1027 PUT, 1028 POST, 1029 DELETE, 1030 HEAD, 1031 OPTIONS; 1032 1033 static Method lookup(String method) { 1034 for (Method m : Method.values()) { 1035 if (m.toString().equalsIgnoreCase(method)) { 1036 return m; 1037 } 1038 } 1039 return null; 1040 } 1041 } 1042 1043 /** 1044 * HTTP response. Return one of these from serve(). 1045 */ 1046 public static class Response { 1047 1048 public interface IStatus { 1049 1050 String getDescription(); 1051 1052 int getRequestStatus(); 1053 } 1054 1055 /** 1056 * Some HTTP response status codes 1057 */ 1058 public enum Status implements IStatus { 1059 SWITCH_PROTOCOL(101, "Switching Protocols"), 1060 OK(200, "OK"), 1061 CREATED(201, "Created"), 1062 ACCEPTED(202, "Accepted"), 1063 NO_CONTENT(204, "No Content"), 1064 PARTIAL_CONTENT(206, "Partial Content"), 1065 REDIRECT(301, "Moved Permanently"), 1066 NOT_MODIFIED(304, "Not Modified"), 1067 BAD_REQUEST(400, "Bad Request"), 1068 UNAUTHORIZED(401, "Unauthorized"), 1069 FORBIDDEN(403, "Forbidden"), 1070 NOT_FOUND(404, "Not Found"), 1071 METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 1072 RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), 1073 INTERNAL_ERROR(500, "Internal Server Error"), 1074 UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); 1075 1076 private final int requestStatus; 1077 1078 private final String description; 1079 1080 Status(int requestStatus, String description) { 1081 this.requestStatus = requestStatus; 1082 this.description = description; 1083 } 1084 1085 @Override 1086 public String getDescription() { 1087 return "" + this.requestStatus + " " + this.description; 1088 } 1089 1090 @Override 1091 public int getRequestStatus() { 1092 return this.requestStatus; 1093 } 1094 } 1095 1096 /** 1097 * HTTP status code after processing, e.g. "200 OK", Status.OK 1098 */ 1099 private IStatus status; 1100 1101 /** 1102 * MIME type of content, e.g. "text/html" 1103 */ 1104 private String mimeType; 1105 1106 /** 1107 * Data of the response, may be null. 1108 */ 1109 private InputStream data; 1110 1111 private long contentLength; 1112 1113 /** 1114 * Headers for the HTTP response. Use addHeader() to add lines. 1115 */ 1116 private final Map<String, String> header = new HashMap<String, String>(); 1117 1118 /** 1119 * The request method that spawned this response. 1120 */ 1121 private Method requestMethod; 1122 1123 /** 1124 * Use chunkedTransfer 1125 */ 1126 private boolean chunkedTransfer; 1127 1128 /** 1129 * Creates a fixed length response if totalBytes>=0, otherwise chunked. 1130 */ 1131 protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { 1132 this.status = status; 1133 this.mimeType = mimeType; 1134 if (data == null) { 1135 this.data = new ByteArrayInputStream(new byte[0]); 1136 this.contentLength = 0L; 1137 } else { 1138 this.data = data; 1139 this.contentLength = totalBytes; 1140 } 1141 this.chunkedTransfer = this.contentLength < 0; 1142 } 1143 1144 /** 1145 * Adds given line to the header. 1146 */ 1147 public void addHeader(String name, String value) { 1148 this.header.put(name, value); 1149 } 1150 1151 public InputStream getData() { 1152 return this.data; 1153 } 1154 1155 public String getHeader(String name) { 1156 return this.header.get(name); 1157 } 1158 1159 public String getMimeType() { 1160 return this.mimeType; 1161 } 1162 1163 public Method getRequestMethod() { 1164 return this.requestMethod; 1165 } 1166 1167 public IStatus getStatus() { 1168 return this.status; 1169 } 1170 1171 private boolean headerAlreadySent(Map<String, String> header, String name) { 1172 boolean alreadySent = false; 1173 for (String headerName : header.keySet()) { 1174 alreadySent |= headerName.equalsIgnoreCase(name); 1175 } 1176 return alreadySent; 1177 } 1178 1179 /** 1180 * Sends given response to the socket. 1181 */ 1182 protected void send(OutputStream outputStream) { 1183 String mime = this.mimeType; 1184 SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 1185 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); 1186 1187 try { 1188 if (this.status == null) { 1189 throw new Error("sendResponse(): Status can't be null."); 1190 } 1191 PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false); 1192 pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n"); 1193 1194 if (mime != null) { 1195 pw.print("Content-Type: " + mime + "\r\n"); 1196 } 1197 1198 if (this.header == null || this.header.get("Date") == null) { 1199 pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); 1200 } 1201 1202 if (this.header != null) { 1203 for (String key : this.header.keySet()) { 1204 String value = this.header.get(key); 1205 pw.print(key + ": " + value + "\r\n"); 1206 } 1207 } 1208 1209 sendConnectionHeaderIfNotAlreadyPresent(pw, this.header); 1210 1211 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1212 sendAsChunked(outputStream, pw); 1213 } else { 1214 long pending = this.data != null ? this.contentLength : 0; 1215 pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); 1216 pw.print("\r\n"); 1217 pw.flush(); 1218 sendAsFixedLength(outputStream, pending); 1219 } 1220 outputStream.flush(); 1221 safeClose(this.data); 1222 } catch (IOException ioe) { 1223 NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); 1224 } 1225 } 1226 1227 private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException { 1228 pw.print("Transfer-Encoding: chunked\r\n"); 1229 pw.print("\r\n"); 1230 pw.flush(); 1231 int BUFFER_SIZE = 16 * 1024; 1232 byte[] CRLF = "\r\n".getBytes(); 1233 byte[] buff = new byte[BUFFER_SIZE]; 1234 int read; 1235 while ((read = this.data.read(buff)) > 0) { 1236 outputStream.write(String.format("%x\r\n", read).getBytes()); 1237 outputStream.write(buff, 0, read); 1238 outputStream.write(CRLF); 1239 } 1240 outputStream.write(String.format("0\r\n\r\n").getBytes()); 1241 } 1242 1243 private void sendAsFixedLength(OutputStream outputStream, long pending) throws IOException { 1244 if (this.requestMethod != Method.HEAD && this.data != null) { 1245 long BUFFER_SIZE = 16 * 1024; 1246 byte[] buff = new byte[(int) BUFFER_SIZE]; 1247 while (pending > 0) { 1248 int read = this.data.read(buff, 0, (int) (pending > BUFFER_SIZE ? BUFFER_SIZE : pending)); 1249 if (read <= 0) { 1250 break; 1251 } 1252 outputStream.write(buff, 0, read); 1253 pending -= read; 1254 } 1255 } 1256 } 1257 1258 protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) { 1259 if (!headerAlreadySent(header, "connection")) { 1260 pw.print("Connection: keep-alive\r\n"); 1261 } 1262 } 1263 1264 protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) { 1265 for (String headerName : header.keySet()) { 1266 if (headerName.equalsIgnoreCase("content-length")) { 1267 try { 1268 return Long.parseLong(header.get(headerName)); 1269 } catch (NumberFormatException ex) { 1270 return size; 1271 } 1272 } 1273 } 1274 1275 pw.print("Content-Length: " + size + "\r\n"); 1276 return size; 1277 } 1278 1279 public void setChunkedTransfer(boolean chunkedTransfer) { 1280 this.chunkedTransfer = chunkedTransfer; 1281 } 1282 1283 public void setData(InputStream data) { 1284 this.data = data; 1285 } 1286 1287 public void setMimeType(String mimeType) { 1288 this.mimeType = mimeType; 1289 } 1290 1291 public void setRequestMethod(Method requestMethod) { 1292 this.requestMethod = requestMethod; 1293 } 1294 1295 public void setStatus(IStatus status) { 1296 this.status = status; 1297 } 1298 } 1299 1300 public static final class ResponseException extends Exception { 1301 1302 private static final long serialVersionUID = 6569838532917408380L; 1303 1304 private final Response.Status status; 1305 1306 public ResponseException(Response.Status status, String message) { 1307 super(message); 1308 this.status = status; 1309 } 1310 1311 public ResponseException(Response.Status status, String message, Exception e) { 1312 super(message, e); 1313 this.status = status; 1314 } 1315 1316 public Response.Status getStatus() { 1317 return this.status; 1318 } 1319 } 1320 1321 /** 1322 * The runnable that will be used for the main listening thread. 1323 */ 1324 public class ServerRunnable implements Runnable { 1325 1326 private final int timeout; 1327 1328 private ServerRunnable(int timeout) { 1329 this.timeout = timeout; 1330 } 1331 1332 @Override 1333 public void run() { 1334 do { 1335 try { 1336 final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); 1337 if (this.timeout > 0) { 1338 finalAccept.setSoTimeout(this.timeout); 1339 } 1340 final InputStream inputStream = finalAccept.getInputStream(); 1341 NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); 1342 } catch (IOException e) { 1343 NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 1344 } 1345 } while (!NanoHTTPD.this.myServerSocket.isClosed()); 1346 } 1347 } 1348 1349 /** 1350 * A temp file. 1351 * <p/> 1352 * <p> 1353 * Temp files are responsible for managing the actual temporary storage and 1354 * cleaning themselves up when no longer needed. 1355 * </p> 1356 */ 1357 public interface TempFile { 1358 1359 void delete() throws Exception; 1360 1361 String getName(); 1362 1363 OutputStream open() throws Exception; 1364 } 1365 1366 /** 1367 * Temp file manager. 1368 * <p/> 1369 * <p> 1370 * Temp file managers are created 1-to-1 with incoming requests, to create 1371 * and cleanup temporary files created as a result of handling the request. 1372 * </p> 1373 */ 1374 public interface TempFileManager { 1375 1376 void clear(); 1377 1378 TempFile createTempFile() throws Exception; 1379 } 1380 1381 /** 1382 * Factory to create temp file managers. 1383 */ 1384 public interface TempFileManagerFactory { 1385 1386 TempFileManager create(); 1387 } 1388 1389 /** 1390 * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 1391 * This is required as the Keep-Alive HTTP connections would otherwise block 1392 * the socket reading thread forever (or as long the browser is open). 1393 */ 1394 public static final int SOCKET_READ_TIMEOUT = 5000; 1395 1396 /** 1397 * Common MIME type for dynamic content: plain text 1398 */ 1399 public static final String MIME_PLAINTEXT = "text/plain"; 1400 1401 /** 1402 * Common MIME type for dynamic content: html 1403 */ 1404 public static final String MIME_HTML = "text/html"; 1405 1406 /** 1407 * Pseudo-Parameter to use to store the actual query string in the 1408 * parameters map for later re-processing. 1409 */ 1410 private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 1411 1412 /** 1413 * logger to log to. 1414 */ 1415 private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); 1416 1417 /** 1418 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an 1419 * array of loaded KeyManagers. These objects must properly 1420 * loaded/initialized by the caller. 1421 */ 1422 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { 1423 SSLServerSocketFactory res = null; 1424 try { 1425 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1426 trustManagerFactory.init(loadedKeyStore); 1427 SSLContext ctx = SSLContext.getInstance("TLS"); 1428 ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); 1429 res = ctx.getServerSocketFactory(); 1430 } catch (Exception e) { 1431 throw new IOException(e.getMessage()); 1432 } 1433 return res; 1434 } 1435 1436 /** 1437 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a 1438 * loaded KeyManagerFactory. These objects must properly loaded/initialized 1439 * by the caller. 1440 */ 1441 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { 1442 SSLServerSocketFactory res = null; 1443 try { 1444 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1445 trustManagerFactory.init(loadedKeyStore); 1446 SSLContext ctx = SSLContext.getInstance("TLS"); 1447 ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1448 res = ctx.getServerSocketFactory(); 1449 } catch (Exception e) { 1450 throw new IOException(e.getMessage()); 1451 } 1452 return res; 1453 } 1454 1455 /** 1456 * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your 1457 * certificate and passphrase 1458 */ 1459 public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { 1460 SSLServerSocketFactory res = null; 1461 try { 1462 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 1463 InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); 1464 keystore.load(keystoreStream, passphrase); 1465 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1466 trustManagerFactory.init(keystore); 1467 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 1468 keyManagerFactory.init(keystore, passphrase); 1469 SSLContext ctx = SSLContext.getInstance("TLS"); 1470 ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1471 res = ctx.getServerSocketFactory(); 1472 } catch (Exception e) { 1473 throw new IOException(e.getMessage()); 1474 } 1475 return res; 1476 } 1477 1478 private static final void safeClose(Closeable closeable) { 1479 if (closeable != null) { 1480 try { 1481 closeable.close(); 1482 } catch (IOException e) { 1483 NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); 1484 } 1485 } 1486 } 1487 1488 private final String hostname; 1489 1490 private final int myPort; 1491 1492 private ServerSocket myServerSocket; 1493 1494 private SSLServerSocketFactory sslServerSocketFactory; 1495 1496 private Thread myThread; 1497 1498 /** 1499 * Pluggable strategy for asynchronously executing requests. 1500 */ 1501 protected AsyncRunner asyncRunner; 1502 1503 /** 1504 * Pluggable strategy for creating and cleaning up temporary files. 1505 */ 1506 private TempFileManagerFactory tempFileManagerFactory; 1507 1508 /** 1509 * Constructs an HTTP server on given port. 1510 */ 1511 public NanoHTTPD(int port) { 1512 this(null, port); 1513 } 1514 1515 // ------------------------------------------------------------------------------- 1516 // // 1517 // 1518 // Threading Strategy. 1519 // 1520 // ------------------------------------------------------------------------------- 1521 // // 1522 1523 /** 1524 * Constructs an HTTP server on given hostname and port. 1525 */ 1526 public NanoHTTPD(String hostname, int port) { 1527 this.hostname = hostname; 1528 this.myPort = port; 1529 setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 1530 setAsyncRunner(new DefaultAsyncRunner()); 1531 } 1532 1533 /** 1534 * Forcibly closes all connections that are open. 1535 */ 1536 public synchronized void closeAllConnections() { 1537 stop(); 1538 } 1539 1540 /** 1541 * create a instance of the client handler, subclasses can return a subclass 1542 * of the ClientHandler. 1543 * 1544 * @param finalAccept 1545 * the socket the cleint is connected to 1546 * @param inputStream 1547 * the input stream 1548 * @return the client handler 1549 */ 1550 protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { 1551 return new ClientHandler(inputStream, finalAccept); 1552 } 1553 1554 /** 1555 * Instantiate the server runnable, can be overwritten by subclasses to 1556 * provide a subclass of the ServerRunnable. 1557 * 1558 * @param timeout 1559 * the socet timeout to use. 1560 * @return the server runnable. 1561 */ 1562 protected ServerRunnable createServerRunnable(final int timeout) { 1563 return new ServerRunnable(timeout); 1564 } 1565 1566 /** 1567 * Decode parameters from a URL, handing the case where a single parameter 1568 * name might have been supplied several times, by return lists of values. 1569 * In general these lists will contain a single element. 1570 * 1571 * @param parms 1572 * original <b>NanoHTTPD</b> parameters values, as passed to the 1573 * <code>serve()</code> method. 1574 * @return a map of <code>String</code> (parameter name) to 1575 * <code>List<String></code> (a list of the values supplied). 1576 */ 1577 protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { 1578 return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); 1579 } 1580 1581 // ------------------------------------------------------------------------------- 1582 // // 1583 1584 /** 1585 * Decode parameters from a URL, handing the case where a single parameter 1586 * name might have been supplied several times, by return lists of values. 1587 * In general these lists will contain a single element. 1588 * 1589 * @param queryString 1590 * a query string pulled from the URL. 1591 * @return a map of <code>String</code> (parameter name) to 1592 * <code>List<String></code> (a list of the values supplied). 1593 */ 1594 protected Map<String, List<String>> decodeParameters(String queryString) { 1595 Map<String, List<String>> parms = new HashMap<String, List<String>>(); 1596 if (queryString != null) { 1597 StringTokenizer st = new StringTokenizer(queryString, "&"); 1598 while (st.hasMoreTokens()) { 1599 String e = st.nextToken(); 1600 int sep = e.indexOf('='); 1601 String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 1602 if (!parms.containsKey(propertyName)) { 1603 parms.put(propertyName, new ArrayList<String>()); 1604 } 1605 String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; 1606 if (propertyValue != null) { 1607 parms.get(propertyName).add(propertyValue); 1608 } 1609 } 1610 } 1611 return parms; 1612 } 1613 1614 /** 1615 * Decode percent encoded <code>String</code> values. 1616 * 1617 * @param str 1618 * the percent encoded <code>String</code> 1619 * @return expanded form of the input, for example "foo%20bar" becomes 1620 * "foo bar" 1621 */ 1622 protected String decodePercent(String str) { 1623 String decoded = null; 1624 try { 1625 decoded = URLDecoder.decode(str, "UTF8"); 1626 } catch (UnsupportedEncodingException ignored) { 1627 NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); 1628 } 1629 return decoded; 1630 } 1631 1632 public final int getListeningPort() { 1633 return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); 1634 } 1635 1636 public final boolean isAlive() { 1637 return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); 1638 } 1639 1640 /** 1641 * Call before start() to serve over HTTPS instead of HTTP 1642 */ 1643 public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) { 1644 this.sslServerSocketFactory = sslServerSocketFactory; 1645 } 1646 1647 /** 1648 * Create a response with unknown length (using HTTP 1.1 chunking). 1649 */ 1650 public Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { 1651 return new Response(status, mimeType, data, -1); 1652 } 1653 1654 /** 1655 * Create a response with known length. 1656 */ 1657 public Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { 1658 return new Response(status, mimeType, data, totalBytes); 1659 } 1660 1661 /** 1662 * Create a text response with known length. 1663 */ 1664 public Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { 1665 if (txt == null) { 1666 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); 1667 } else { 1668 byte[] bytes; 1669 try { 1670 bytes = txt.getBytes("UTF-8"); 1671 } catch (UnsupportedEncodingException e) { 1672 NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); 1673 bytes = new byte[0]; 1674 } 1675 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); 1676 } 1677 } 1678 1679 /** 1680 * Create a text response with known length. 1681 */ 1682 public Response newFixedLengthResponse(String msg) { 1683 return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); 1684 } 1685 1686 /** 1687 * Override this to customize the server. 1688 * <p/> 1689 * <p/> 1690 * (By default, this returns a 404 "Not Found" plain text error response.) 1691 * 1692 * @param session 1693 * The HTTP session 1694 * @return HTTP response, see class Response for details 1695 */ 1696 public Response serve(IHTTPSession session) { 1697 Map<String, String> files = new HashMap<String, String>(); 1698 Method method = session.getMethod(); 1699 if (Method.PUT.equals(method) || Method.POST.equals(method)) { 1700 try { 1701 session.parseBody(files); 1702 } catch (IOException ioe) { 1703 return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 1704 } catch (ResponseException re) { 1705 return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 1706 } 1707 } 1708 1709 Map<String, String> parms = session.getParms(); 1710 parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); 1711 return serve(session.getUri(), method, session.getHeaders(), parms, files); 1712 } 1713 1714 /** 1715 * Override this to customize the server. 1716 * <p/> 1717 * <p/> 1718 * (By default, this returns a 404 "Not Found" plain text error response.) 1719 * 1720 * @param uri 1721 * Percent-decoded URI without parameters, for example 1722 * "/index.cgi" 1723 * @param method 1724 * "GET", "POST" etc. 1725 * @param parms 1726 * Parsed, percent decoded parameters from URI and, in case of 1727 * POST, data. 1728 * @param headers 1729 * Header entries, percent decoded 1730 * @return HTTP response, see class Response for details 1731 */ 1732 @Deprecated 1733 public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { 1734 return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); 1735 } 1736 1737 /** 1738 * Pluggable strategy for asynchronously executing requests. 1739 * 1740 * @param asyncRunner 1741 * new strategy for handling threads. 1742 */ 1743 public void setAsyncRunner(AsyncRunner asyncRunner) { 1744 this.asyncRunner = asyncRunner; 1745 } 1746 1747 /** 1748 * Pluggable strategy for creating and cleaning up temporary files. 1749 * 1750 * @param tempFileManagerFactory 1751 * new strategy for handling temp files. 1752 */ 1753 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 1754 this.tempFileManagerFactory = tempFileManagerFactory; 1755 } 1756 1757 /** 1758 * Start the server. 1759 * 1760 * @throws IOException 1761 * if the socket is in use. 1762 */ 1763 public void start() throws IOException { 1764 start(NanoHTTPD.SOCKET_READ_TIMEOUT); 1765 } 1766 1767 /** 1768 * Start the server. 1769 * 1770 * @param timeout 1771 * timeout to use for socket connections. 1772 * @throws IOException 1773 * if the socket is in use. 1774 */ 1775 public void start(final int timeout) throws IOException { 1776 if (this.sslServerSocketFactory != null) { 1777 SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); 1778 ss.setNeedClientAuth(false); 1779 this.myServerSocket = ss; 1780 } else { 1781 this.myServerSocket = new ServerSocket(); 1782 } 1783 this.myServerSocket.setReuseAddress(true); 1784 this.myServerSocket.bind(this.hostname != null ? new InetSocketAddress(this.hostname, this.myPort) : new InetSocketAddress(this.myPort)); 1785 1786 this.myThread = new Thread(createServerRunnable(timeout)); 1787 this.myThread.setDaemon(true); 1788 this.myThread.setName("NanoHttpd Main Listener"); 1789 this.myThread.start(); 1790 } 1791 1792 /** 1793 * Stop the server. 1794 */ 1795 public void stop() { 1796 try { 1797 safeClose(this.myServerSocket); 1798 this.asyncRunner.closeAll(); 1799 if (this.myThread != null) { 1800 this.myThread.join(); 1801 } 1802 } catch (Exception e) { 1803 NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); 1804 } 1805 } 1806 1807 public final boolean wasStarted() { 1808 return this.myServerSocket != null && this.myThread != null; 1809 } 1810 1811} 1812