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