NanoHTTPD.java revision 770aaf0c6567d8b62a6eef18c8f0359208427c77
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 final int MEMORY_STORE_LIMIT = 1024; 884 RandomAccessFile randomAccessFile = null; 885 try { 886 long size; 887 if (this.headers.containsKey("content-length")) { 888 size = Integer.parseInt(this.headers.get("content-length")); 889 } else if (this.splitbyte < this.rlen) { 890 size = this.rlen - this.splitbyte; 891 } else { 892 size = 0; 893 } 894 895 ByteArrayOutputStream baos = null; 896 DataOutput request_data_output = null; 897 898 // Store the request in memory or a file, depending on size 899 if (size < MEMORY_STORE_LIMIT) { 900 baos = new ByteArrayOutputStream(); 901 request_data_output = new DataOutputStream(baos); 902 } else { 903 randomAccessFile = getTmpBucket(); 904 request_data_output = randomAccessFile; 905 } 906 907 // Read all the body and write it to request_data_output 908 byte[] buf = new byte[REQUEST_BUFFER_LEN]; 909 while (this.rlen >= 0 && size > 0) { 910 this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); 911 size -= this.rlen; 912 if (this.rlen > 0) { 913 request_data_output.write(buf, 0, this.rlen); 914 } 915 } 916 917 ByteBuffer fbuf = null; 918 if (baos != null) { 919 fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); 920 } else { 921 fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); 922 randomAccessFile.seek(0); 923 } 924 925 // If the method is POST, there may be parameters 926 // in data section, too, read it: 927 if (Method.POST.equals(this.method)) { 928 String contentType = ""; 929 String contentTypeHeader = this.headers.get("content-type"); 930 931 StringTokenizer st = null; 932 if (contentTypeHeader != null) { 933 st = new StringTokenizer(contentTypeHeader, ",; "); 934 if (st.hasMoreTokens()) { 935 contentType = st.nextToken(); 936 } 937 } 938 939 if ("multipart/form-data".equalsIgnoreCase(contentType)) { 940 // Handle multipart/form-data 941 if (!st.hasMoreTokens()) { 942 throw new ResponseException(Response.Status.BAD_REQUEST, 943 "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); 944 } 945 946 String boundaryStartString = "boundary="; 947 int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); 948 String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); 949 if (boundary.startsWith("\"") && boundary.endsWith("\"")) { 950 boundary = boundary.substring(1, boundary.length() - 1); 951 } 952 953 decodeMultipartFormData(boundary, fbuf, this.parms, files); 954 } else { 955 byte[] postBytes = new byte[fbuf.remaining()]; 956 fbuf.get(postBytes); 957 String postLine = new String(postBytes).trim(); 958 // Handle application/x-www-form-urlencoded 959 if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { 960 decodeParms(postLine, this.parms); 961 } else if (postLine.length() != 0) { 962 // Special case for raw POST data => create a 963 // special files entry "postData" with raw content 964 // data 965 files.put("postData", postLine); 966 } 967 } 968 } else if (Method.PUT.equals(this.method)) { 969 files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); 970 } 971 } finally { 972 safeClose(randomAccessFile); 973 } 974 } 975 976 /** 977 * Retrieves the content of a sent file and saves it to a temporary 978 * file. The full path to the saved file is returned. 979 */ 980 private String saveTmpFile(ByteBuffer b, int offset, int len) { 981 String path = ""; 982 if (len > 0) { 983 FileOutputStream fileOutputStream = null; 984 try { 985 TempFile tempFile = this.tempFileManager.createTempFile(); 986 ByteBuffer src = b.duplicate(); 987 fileOutputStream = new FileOutputStream(tempFile.getName()); 988 FileChannel dest = fileOutputStream.getChannel(); 989 src.position(offset).limit(offset + len); 990 dest.write(src.slice()); 991 path = tempFile.getName(); 992 } catch (Exception e) { // Catch exception if any 993 throw new Error(e); // we won't recover, so throw an error 994 } finally { 995 safeClose(fileOutputStream); 996 } 997 } 998 return path; 999 } 1000 } 1001 1002 /** 1003 * Handles one session, i.e. parses the HTTP request and returns the 1004 * response. 1005 */ 1006 public interface IHTTPSession { 1007 1008 void execute() throws IOException; 1009 1010 CookieHandler getCookies(); 1011 1012 Map<String, String> getHeaders(); 1013 1014 InputStream getInputStream(); 1015 1016 Method getMethod(); 1017 1018 Map<String, String> getParms(); 1019 1020 String getQueryParameterString(); 1021 1022 /** 1023 * @return the path part of the URL. 1024 */ 1025 String getUri(); 1026 1027 /** 1028 * Adds the files in the request body to the files map. 1029 * 1030 * @param files 1031 * map to modify 1032 */ 1033 void parseBody(Map<String, String> files) throws IOException, ResponseException; 1034 } 1035 1036 /** 1037 * HTTP Request methods, with the ability to decode a <code>String</code> 1038 * back to its enum value. 1039 */ 1040 public enum Method { 1041 GET, 1042 PUT, 1043 POST, 1044 DELETE, 1045 HEAD, 1046 OPTIONS; 1047 1048 static Method lookup(String method) { 1049 for (Method m : Method.values()) { 1050 if (m.toString().equalsIgnoreCase(method)) { 1051 return m; 1052 } 1053 } 1054 return null; 1055 } 1056 } 1057 1058 /** 1059 * HTTP response. Return one of these from serve(). 1060 */ 1061 public static class Response { 1062 1063 public interface IStatus { 1064 1065 String getDescription(); 1066 1067 int getRequestStatus(); 1068 } 1069 1070 /** 1071 * Some HTTP response status codes 1072 */ 1073 public enum Status implements IStatus { 1074 SWITCH_PROTOCOL(101, "Switching Protocols"), 1075 OK(200, "OK"), 1076 CREATED(201, "Created"), 1077 ACCEPTED(202, "Accepted"), 1078 NO_CONTENT(204, "No Content"), 1079 PARTIAL_CONTENT(206, "Partial Content"), 1080 REDIRECT(301, "Moved Permanently"), 1081 NOT_MODIFIED(304, "Not Modified"), 1082 BAD_REQUEST(400, "Bad Request"), 1083 UNAUTHORIZED(401, "Unauthorized"), 1084 FORBIDDEN(403, "Forbidden"), 1085 NOT_FOUND(404, "Not Found"), 1086 METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 1087 RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), 1088 INTERNAL_ERROR(500, "Internal Server Error"), 1089 UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); 1090 1091 private final int requestStatus; 1092 1093 private final String description; 1094 1095 Status(int requestStatus, String description) { 1096 this.requestStatus = requestStatus; 1097 this.description = description; 1098 } 1099 1100 @Override 1101 public String getDescription() { 1102 return "" + this.requestStatus + " " + this.description; 1103 } 1104 1105 @Override 1106 public int getRequestStatus() { 1107 return this.requestStatus; 1108 } 1109 1110 } 1111 1112 /** 1113 * Output stream that will automatically send every write to the wrapped 1114 * OutputStream according to chunked transfer: 1115 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 1116 */ 1117 private static class ChunkedOutputStream extends FilterOutputStream { 1118 1119 public ChunkedOutputStream(OutputStream out) { 1120 super(out); 1121 } 1122 1123 @Override 1124 public void write(int b) throws IOException { 1125 byte[] data = { 1126 (byte) b 1127 }; 1128 write(data, 0, 1); 1129 } 1130 1131 @Override 1132 public void write(byte[] b) throws IOException { 1133 write(b, 0, b.length); 1134 } 1135 1136 @Override 1137 public void write(byte[] b, int off, int len) throws IOException { 1138 if (len == 0) 1139 return; 1140 out.write(String.format("%x\r\n", len).getBytes()); 1141 out.write(b, off, len); 1142 out.write("\r\n".getBytes()); 1143 } 1144 1145 public void finish() throws IOException { 1146 out.write("0\r\n\r\n".getBytes()); 1147 } 1148 1149 } 1150 1151 /** 1152 * HTTP status code after processing, e.g. "200 OK", Status.OK 1153 */ 1154 private IStatus status; 1155 1156 /** 1157 * MIME type of content, e.g. "text/html" 1158 */ 1159 private String mimeType; 1160 1161 /** 1162 * Data of the response, may be null. 1163 */ 1164 private InputStream data; 1165 1166 private long contentLength; 1167 1168 /** 1169 * Headers for the HTTP response. Use addHeader() to add lines. 1170 */ 1171 private final Map<String, String> header = new HashMap<String, String>(); 1172 1173 /** 1174 * The request method that spawned this response. 1175 */ 1176 private Method requestMethod; 1177 1178 /** 1179 * Use chunkedTransfer 1180 */ 1181 private boolean chunkedTransfer; 1182 1183 private boolean encodeAsGzip; 1184 1185 private boolean keepAlive; 1186 1187 /** 1188 * Creates a fixed length response if totalBytes>=0, otherwise chunked. 1189 */ 1190 protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { 1191 this.status = status; 1192 this.mimeType = mimeType; 1193 if (data == null) { 1194 this.data = new ByteArrayInputStream(new byte[0]); 1195 this.contentLength = 0L; 1196 } else { 1197 this.data = data; 1198 this.contentLength = totalBytes; 1199 } 1200 this.chunkedTransfer = this.contentLength < 0; 1201 keepAlive = true; 1202 } 1203 1204 /** 1205 * Adds given line to the header. 1206 */ 1207 public void addHeader(String name, String value) { 1208 this.header.put(name, value); 1209 } 1210 1211 public InputStream getData() { 1212 return this.data; 1213 } 1214 1215 public String getHeader(String name) { 1216 for (String headerName : header.keySet()) { 1217 if (headerName.equalsIgnoreCase(name)) { 1218 return header.get(headerName); 1219 } 1220 } 1221 return null; 1222 } 1223 1224 public String getMimeType() { 1225 return this.mimeType; 1226 } 1227 1228 public Method getRequestMethod() { 1229 return this.requestMethod; 1230 } 1231 1232 public IStatus getStatus() { 1233 return this.status; 1234 } 1235 1236 public void setGzipEncoding(boolean encodeAsGzip) { 1237 this.encodeAsGzip = encodeAsGzip; 1238 } 1239 1240 public void setKeepAlive(boolean useKeepAlive) { 1241 this.keepAlive = useKeepAlive; 1242 } 1243 1244 private boolean headerAlreadySent(Map<String, String> header, String name) { 1245 boolean alreadySent = false; 1246 for (String headerName : header.keySet()) { 1247 alreadySent |= headerName.equalsIgnoreCase(name); 1248 } 1249 return alreadySent; 1250 } 1251 1252 /** 1253 * Sends given response to the socket. 1254 */ 1255 protected void send(OutputStream outputStream) { 1256 String mime = this.mimeType; 1257 SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 1258 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); 1259 1260 try { 1261 if (this.status == null) { 1262 throw new Error("sendResponse(): Status can't be null."); 1263 } 1264 PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false); 1265 pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n"); 1266 1267 if (mime != null) { 1268 pw.print("Content-Type: " + mime + "\r\n"); 1269 } 1270 1271 if (this.header == null || this.header.get("Date") == null) { 1272 pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); 1273 } 1274 1275 if (this.header != null) { 1276 for (String key : this.header.keySet()) { 1277 String value = this.header.get(key); 1278 pw.print(key + ": " + value + "\r\n"); 1279 } 1280 } 1281 1282 if (!headerAlreadySent(header, "connection")) { 1283 pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n"); 1284 } 1285 1286 if (headerAlreadySent(this.header, "content-length")) { 1287 encodeAsGzip = false; 1288 } 1289 1290 if (encodeAsGzip) { 1291 pw.print("Content-Encoding: gzip\r\n"); 1292 } 1293 1294 long pending = this.data != null ? this.contentLength : 0; 1295 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1296 pw.print("Transfer-Encoding: chunked\r\n"); 1297 } else if (!encodeAsGzip) { 1298 pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); 1299 } 1300 pw.print("\r\n"); 1301 pw.flush(); 1302 sendBodyWithCorrectTransferAndEncoding(outputStream, pending); 1303 outputStream.flush(); 1304 safeClose(this.data); 1305 } catch (IOException ioe) { 1306 NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); 1307 } 1308 } 1309 1310 private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { 1311 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1312 ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); 1313 sendBodyWithCorrectEncoding(chunkedOutputStream, -1); 1314 chunkedOutputStream.finish(); 1315 } else { 1316 sendBodyWithCorrectEncoding(outputStream, pending); 1317 } 1318 } 1319 1320 private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { 1321 if (encodeAsGzip) { 1322 GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); 1323 sendBody(gzipOutputStream, -1); 1324 gzipOutputStream.finish(); 1325 } else { 1326 sendBody(outputStream, pending); 1327 } 1328 } 1329 1330 /** 1331 * Sends the body to the specified OutputStream. The pending parameter 1332 * limits the maximum amounts of bytes sent unless it is -1, in which 1333 * case everything is sent. 1334 * 1335 * @param outputStream 1336 * the OutputStream to send data to 1337 * @param pending 1338 * -1 to send everything, otherwise sets a max limit to the 1339 * number of bytes sent 1340 * @throws IOException 1341 * if something goes wrong while sending the data. 1342 */ 1343 private void sendBody(OutputStream outputStream, long pending) throws IOException { 1344 long BUFFER_SIZE = 16 * 1024; 1345 byte[] buff = new byte[(int) BUFFER_SIZE]; 1346 boolean sendEverything = pending == -1; 1347 while (pending > 0 || sendEverything) { 1348 long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); 1349 int read = this.data.read(buff, 0, (int) bytesToRead); 1350 if (read <= 0) { 1351 break; 1352 } 1353 outputStream.write(buff, 0, read); 1354 if (!sendEverything) { 1355 pending -= read; 1356 } 1357 } 1358 } 1359 1360 protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) { 1361 for (String headerName : header.keySet()) { 1362 if (headerName.equalsIgnoreCase("content-length")) { 1363 try { 1364 return Long.parseLong(header.get(headerName)); 1365 } catch (NumberFormatException ex) { 1366 return size; 1367 } 1368 } 1369 } 1370 1371 pw.print("Content-Length: " + size + "\r\n"); 1372 return size; 1373 } 1374 1375 public void setChunkedTransfer(boolean chunkedTransfer) { 1376 this.chunkedTransfer = chunkedTransfer; 1377 } 1378 1379 public void setData(InputStream data) { 1380 this.data = data; 1381 } 1382 1383 public void setMimeType(String mimeType) { 1384 this.mimeType = mimeType; 1385 } 1386 1387 public void setRequestMethod(Method requestMethod) { 1388 this.requestMethod = requestMethod; 1389 } 1390 1391 public void setStatus(IStatus status) { 1392 this.status = status; 1393 } 1394 } 1395 1396 public static final class ResponseException extends Exception { 1397 1398 private static final long serialVersionUID = 6569838532917408380L; 1399 1400 private final Response.Status status; 1401 1402 public ResponseException(Response.Status status, String message) { 1403 super(message); 1404 this.status = status; 1405 } 1406 1407 public ResponseException(Response.Status status, String message, Exception e) { 1408 super(message, e); 1409 this.status = status; 1410 } 1411 1412 public Response.Status getStatus() { 1413 return this.status; 1414 } 1415 } 1416 1417 /** 1418 * The runnable that will be used for the main listening thread. 1419 */ 1420 public class ServerRunnable implements Runnable { 1421 1422 private final int timeout; 1423 1424 private IOException bindException; 1425 1426 private boolean hasBinded = false; 1427 1428 private ServerRunnable(int timeout) { 1429 this.timeout = timeout; 1430 } 1431 1432 @Override 1433 public void run() { 1434 try { 1435 myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); 1436 hasBinded = true; 1437 } catch (IOException e) { 1438 this.bindException = e; 1439 return; 1440 } 1441 do { 1442 try { 1443 final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); 1444 if (this.timeout > 0) { 1445 finalAccept.setSoTimeout(this.timeout); 1446 } 1447 final InputStream inputStream = finalAccept.getInputStream(); 1448 NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); 1449 } catch (IOException e) { 1450 NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 1451 } 1452 } while (!NanoHTTPD.this.myServerSocket.isClosed()); 1453 } 1454 } 1455 1456 /** 1457 * A temp file. 1458 * <p/> 1459 * <p> 1460 * Temp files are responsible for managing the actual temporary storage and 1461 * cleaning themselves up when no longer needed. 1462 * </p> 1463 */ 1464 public interface TempFile { 1465 1466 void delete() throws Exception; 1467 1468 String getName(); 1469 1470 OutputStream open() throws Exception; 1471 } 1472 1473 /** 1474 * Temp file manager. 1475 * <p/> 1476 * <p> 1477 * Temp file managers are created 1-to-1 with incoming requests, to create 1478 * and cleanup temporary files created as a result of handling the request. 1479 * </p> 1480 */ 1481 public interface TempFileManager { 1482 1483 void clear(); 1484 1485 TempFile createTempFile() throws Exception; 1486 } 1487 1488 /** 1489 * Factory to create temp file managers. 1490 */ 1491 public interface TempFileManagerFactory { 1492 1493 TempFileManager create(); 1494 } 1495 1496 /** 1497 * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 1498 * This is required as the Keep-Alive HTTP connections would otherwise block 1499 * the socket reading thread forever (or as long the browser is open). 1500 */ 1501 public static final int SOCKET_READ_TIMEOUT = 5000; 1502 1503 /** 1504 * Common MIME type for dynamic content: plain text 1505 */ 1506 public static final String MIME_PLAINTEXT = "text/plain"; 1507 1508 /** 1509 * Common MIME type for dynamic content: html 1510 */ 1511 public static final String MIME_HTML = "text/html"; 1512 1513 /** 1514 * Pseudo-Parameter to use to store the actual query string in the 1515 * parameters map for later re-processing. 1516 */ 1517 private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 1518 1519 /** 1520 * logger to log to. 1521 */ 1522 private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); 1523 1524 /** 1525 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an 1526 * array of loaded KeyManagers. These objects must properly 1527 * loaded/initialized by the caller. 1528 */ 1529 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { 1530 SSLServerSocketFactory res = null; 1531 try { 1532 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1533 trustManagerFactory.init(loadedKeyStore); 1534 SSLContext ctx = SSLContext.getInstance("TLS"); 1535 ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); 1536 res = ctx.getServerSocketFactory(); 1537 } catch (Exception e) { 1538 throw new IOException(e.getMessage()); 1539 } 1540 return res; 1541 } 1542 1543 /** 1544 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a 1545 * loaded KeyManagerFactory. These objects must properly loaded/initialized 1546 * by the caller. 1547 */ 1548 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { 1549 SSLServerSocketFactory res = null; 1550 try { 1551 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1552 trustManagerFactory.init(loadedKeyStore); 1553 SSLContext ctx = SSLContext.getInstance("TLS"); 1554 ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1555 res = ctx.getServerSocketFactory(); 1556 } catch (Exception e) { 1557 throw new IOException(e.getMessage()); 1558 } 1559 return res; 1560 } 1561 1562 /** 1563 * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your 1564 * certificate and passphrase 1565 */ 1566 public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { 1567 SSLServerSocketFactory res = null; 1568 try { 1569 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 1570 InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); 1571 keystore.load(keystoreStream, passphrase); 1572 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1573 trustManagerFactory.init(keystore); 1574 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 1575 keyManagerFactory.init(keystore, passphrase); 1576 SSLContext ctx = SSLContext.getInstance("TLS"); 1577 ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1578 res = ctx.getServerSocketFactory(); 1579 } catch (Exception e) { 1580 throw new IOException(e.getMessage()); 1581 } 1582 return res; 1583 } 1584 1585 private static final void safeClose(Object closeable) { 1586 try { 1587 if (closeable != null) { 1588 if (closeable instanceof Closeable) { 1589 ((Closeable) closeable).close(); 1590 } else if (closeable instanceof Socket) { 1591 ((Socket) closeable).close(); 1592 } else if (closeable instanceof ServerSocket) { 1593 ((ServerSocket) closeable).close(); 1594 } else { 1595 throw new IllegalArgumentException("Unknown object to close"); 1596 } 1597 } 1598 } catch (IOException e) { 1599 NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); 1600 } 1601 } 1602 1603 private final String hostname; 1604 1605 private final int myPort; 1606 1607 private ServerSocket myServerSocket; 1608 1609 private SSLServerSocketFactory sslServerSocketFactory; 1610 1611 private Thread myThread; 1612 1613 /** 1614 * Pluggable strategy for asynchronously executing requests. 1615 */ 1616 protected AsyncRunner asyncRunner; 1617 1618 /** 1619 * Pluggable strategy for creating and cleaning up temporary files. 1620 */ 1621 private TempFileManagerFactory tempFileManagerFactory; 1622 1623 /** 1624 * Constructs an HTTP server on given port. 1625 */ 1626 public NanoHTTPD(int port) { 1627 this(null, port); 1628 } 1629 1630 // ------------------------------------------------------------------------------- 1631 // // 1632 // 1633 // Threading Strategy. 1634 // 1635 // ------------------------------------------------------------------------------- 1636 // // 1637 1638 /** 1639 * Constructs an HTTP server on given hostname and port. 1640 */ 1641 public NanoHTTPD(String hostname, int port) { 1642 this.hostname = hostname; 1643 this.myPort = port; 1644 setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 1645 setAsyncRunner(new DefaultAsyncRunner()); 1646 } 1647 1648 /** 1649 * Forcibly closes all connections that are open. 1650 */ 1651 public synchronized void closeAllConnections() { 1652 stop(); 1653 } 1654 1655 /** 1656 * create a instance of the client handler, subclasses can return a subclass 1657 * of the ClientHandler. 1658 * 1659 * @param finalAccept 1660 * the socket the cleint is connected to 1661 * @param inputStream 1662 * the input stream 1663 * @return the client handler 1664 */ 1665 protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { 1666 return new ClientHandler(inputStream, finalAccept); 1667 } 1668 1669 /** 1670 * Instantiate the server runnable, can be overwritten by subclasses to 1671 * provide a subclass of the ServerRunnable. 1672 * 1673 * @param timeout 1674 * the socet timeout to use. 1675 * @return the server runnable. 1676 */ 1677 protected ServerRunnable createServerRunnable(final int timeout) { 1678 return new ServerRunnable(timeout); 1679 } 1680 1681 /** 1682 * Decode parameters from a URL, handing the case where a single parameter 1683 * name might have been supplied several times, by return lists of values. 1684 * In general these lists will contain a single element. 1685 * 1686 * @param parms 1687 * original <b>NanoHTTPD</b> parameters values, as passed to the 1688 * <code>serve()</code> method. 1689 * @return a map of <code>String</code> (parameter name) to 1690 * <code>List<String></code> (a list of the values supplied). 1691 */ 1692 protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { 1693 return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); 1694 } 1695 1696 // ------------------------------------------------------------------------------- 1697 // // 1698 1699 /** 1700 * Decode parameters from a URL, handing the case where a single parameter 1701 * name might have been supplied several times, by return lists of values. 1702 * In general these lists will contain a single element. 1703 * 1704 * @param queryString 1705 * a query string pulled from the URL. 1706 * @return a map of <code>String</code> (parameter name) to 1707 * <code>List<String></code> (a list of the values supplied). 1708 */ 1709 protected Map<String, List<String>> decodeParameters(String queryString) { 1710 Map<String, List<String>> parms = new HashMap<String, List<String>>(); 1711 if (queryString != null) { 1712 StringTokenizer st = new StringTokenizer(queryString, "&"); 1713 while (st.hasMoreTokens()) { 1714 String e = st.nextToken(); 1715 int sep = e.indexOf('='); 1716 String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 1717 if (!parms.containsKey(propertyName)) { 1718 parms.put(propertyName, new ArrayList<String>()); 1719 } 1720 String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; 1721 if (propertyValue != null) { 1722 parms.get(propertyName).add(propertyValue); 1723 } 1724 } 1725 } 1726 return parms; 1727 } 1728 1729 /** 1730 * Decode percent encoded <code>String</code> values. 1731 * 1732 * @param str 1733 * the percent encoded <code>String</code> 1734 * @return expanded form of the input, for example "foo%20bar" becomes 1735 * "foo bar" 1736 */ 1737 protected String decodePercent(String str) { 1738 String decoded = null; 1739 try { 1740 decoded = URLDecoder.decode(str, "UTF8"); 1741 } catch (UnsupportedEncodingException ignored) { 1742 NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); 1743 } 1744 return decoded; 1745 } 1746 1747 public final int getListeningPort() { 1748 return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); 1749 } 1750 1751 public final boolean isAlive() { 1752 return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); 1753 } 1754 1755 /** 1756 * Call before start() to serve over HTTPS instead of HTTP 1757 */ 1758 public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) { 1759 this.sslServerSocketFactory = sslServerSocketFactory; 1760 } 1761 1762 /** 1763 * Create a response with unknown length (using HTTP 1.1 chunking). 1764 */ 1765 public Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { 1766 return new Response(status, mimeType, data, -1); 1767 } 1768 1769 /** 1770 * Create a response with known length. 1771 */ 1772 public Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { 1773 return new Response(status, mimeType, data, totalBytes); 1774 } 1775 1776 /** 1777 * Create a text response with known length. 1778 */ 1779 public Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { 1780 if (txt == null) { 1781 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); 1782 } else { 1783 byte[] bytes; 1784 try { 1785 bytes = txt.getBytes("UTF-8"); 1786 } catch (UnsupportedEncodingException e) { 1787 NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); 1788 bytes = new byte[0]; 1789 } 1790 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); 1791 } 1792 } 1793 1794 /** 1795 * Create a text response with known length. 1796 */ 1797 public Response newFixedLengthResponse(String msg) { 1798 return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); 1799 } 1800 1801 /** 1802 * Override this to customize the server. 1803 * <p/> 1804 * <p/> 1805 * (By default, this returns a 404 "Not Found" plain text error response.) 1806 * 1807 * @param session 1808 * The HTTP session 1809 * @return HTTP response, see class Response for details 1810 */ 1811 public Response serve(IHTTPSession session) { 1812 Map<String, String> files = new HashMap<String, String>(); 1813 Method method = session.getMethod(); 1814 if (Method.PUT.equals(method) || Method.POST.equals(method)) { 1815 try { 1816 session.parseBody(files); 1817 } catch (IOException ioe) { 1818 return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 1819 } catch (ResponseException re) { 1820 return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 1821 } 1822 } 1823 1824 Map<String, String> parms = session.getParms(); 1825 parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); 1826 return serve(session.getUri(), method, session.getHeaders(), parms, files); 1827 } 1828 1829 /** 1830 * Override this to customize the server. 1831 * <p/> 1832 * <p/> 1833 * (By default, this returns a 404 "Not Found" plain text error response.) 1834 * 1835 * @param uri 1836 * Percent-decoded URI without parameters, for example 1837 * "/index.cgi" 1838 * @param method 1839 * "GET", "POST" etc. 1840 * @param parms 1841 * Parsed, percent decoded parameters from URI and, in case of 1842 * POST, data. 1843 * @param headers 1844 * Header entries, percent decoded 1845 * @return HTTP response, see class Response for details 1846 */ 1847 @Deprecated 1848 public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { 1849 return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); 1850 } 1851 1852 /** 1853 * Pluggable strategy for asynchronously executing requests. 1854 * 1855 * @param asyncRunner 1856 * new strategy for handling threads. 1857 */ 1858 public void setAsyncRunner(AsyncRunner asyncRunner) { 1859 this.asyncRunner = asyncRunner; 1860 } 1861 1862 /** 1863 * Pluggable strategy for creating and cleaning up temporary files. 1864 * 1865 * @param tempFileManagerFactory 1866 * new strategy for handling temp files. 1867 */ 1868 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 1869 this.tempFileManagerFactory = tempFileManagerFactory; 1870 } 1871 1872 /** 1873 * Start the server. 1874 * 1875 * @throws IOException 1876 * if the socket is in use. 1877 */ 1878 public void start() throws IOException { 1879 start(NanoHTTPD.SOCKET_READ_TIMEOUT); 1880 } 1881 1882 /** 1883 * Start the server. 1884 * 1885 * @param timeout 1886 * timeout to use for socket connections. 1887 * @throws IOException 1888 * if the socket is in use. 1889 */ 1890 public void start(final int timeout) throws IOException { 1891 if (this.sslServerSocketFactory != null) { 1892 SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); 1893 ss.setNeedClientAuth(false); 1894 this.myServerSocket = ss; 1895 } else { 1896 this.myServerSocket = new ServerSocket(); 1897 } 1898 this.myServerSocket.setReuseAddress(true); 1899 1900 ServerRunnable serverRunnable = createServerRunnable(timeout); 1901 this.myThread = new Thread(serverRunnable); 1902 this.myThread.setDaemon(true); 1903 this.myThread.setName("NanoHttpd Main Listener"); 1904 this.myThread.start(); 1905 while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { 1906 try { 1907 Thread.sleep(10L); 1908 } catch (Throwable e) { 1909 // on android this may not be allowed, that's why we 1910 // catch throwable the wait should be very short because we are 1911 // just waiting for the bind of the socket 1912 } 1913 } 1914 if (serverRunnable.bindException != null) { 1915 throw serverRunnable.bindException; 1916 } 1917 } 1918 1919 /** 1920 * Stop the server. 1921 */ 1922 public void stop() { 1923 try { 1924 safeClose(this.myServerSocket); 1925 this.asyncRunner.closeAll(); 1926 if (this.myThread != null) { 1927 this.myThread.join(); 1928 } 1929 } catch (Exception e) { 1930 NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); 1931 } 1932 } 1933 1934 public final boolean wasStarted() { 1935 return this.myServerSocket != null && this.myThread != null; 1936 } 1937 1938} 1939