NanoHTTPD.java revision f713632b4a7ad3fa9c7581dff3df6a5abf7de395
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(useGzipWhenAccepted() && 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 setChunkedTransfer(true); 1293 } 1294 1295 long pending = this.data != null ? this.contentLength : 0; 1296 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1297 pw.print("Transfer-Encoding: chunked\r\n"); 1298 } else if (!encodeAsGzip) { 1299 pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); 1300 } 1301 pw.print("\r\n"); 1302 pw.flush(); 1303 sendBodyWithCorrectTransferAndEncoding(outputStream, pending); 1304 outputStream.flush(); 1305 safeClose(this.data); 1306 } catch (IOException ioe) { 1307 NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); 1308 } 1309 } 1310 1311 private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { 1312 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1313 ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); 1314 sendBodyWithCorrectEncoding(chunkedOutputStream, -1); 1315 chunkedOutputStream.finish(); 1316 } else { 1317 sendBodyWithCorrectEncoding(outputStream, pending); 1318 } 1319 } 1320 1321 private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { 1322 if (encodeAsGzip) { 1323 GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); 1324 sendBody(gzipOutputStream, -1); 1325 gzipOutputStream.finish(); 1326 } else { 1327 sendBody(outputStream, pending); 1328 } 1329 } 1330 1331 /** 1332 * Sends the body to the specified OutputStream. The pending parameter 1333 * limits the maximum amounts of bytes sent unless it is -1, in which 1334 * case everything is sent. 1335 * 1336 * @param outputStream 1337 * the OutputStream to send data to 1338 * @param pending 1339 * -1 to send everything, otherwise sets a max limit to the 1340 * number of bytes sent 1341 * @throws IOException 1342 * if something goes wrong while sending the data. 1343 */ 1344 private void sendBody(OutputStream outputStream, long pending) throws IOException { 1345 long BUFFER_SIZE = 16 * 1024; 1346 byte[] buff = new byte[(int) BUFFER_SIZE]; 1347 boolean sendEverything = pending == -1; 1348 while (pending > 0 || sendEverything) { 1349 long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); 1350 int read = this.data.read(buff, 0, (int) bytesToRead); 1351 if (read <= 0) { 1352 break; 1353 } 1354 outputStream.write(buff, 0, read); 1355 if (!sendEverything) { 1356 pending -= read; 1357 } 1358 } 1359 } 1360 1361 protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) { 1362 for (String headerName : header.keySet()) { 1363 if (headerName.equalsIgnoreCase("content-length")) { 1364 try { 1365 return Long.parseLong(header.get(headerName)); 1366 } catch (NumberFormatException ex) { 1367 return size; 1368 } 1369 } 1370 } 1371 1372 pw.print("Content-Length: " + size + "\r\n"); 1373 return size; 1374 } 1375 1376 public void setChunkedTransfer(boolean chunkedTransfer) { 1377 this.chunkedTransfer = chunkedTransfer; 1378 } 1379 1380 public void setData(InputStream data) { 1381 this.data = data; 1382 } 1383 1384 public void setMimeType(String mimeType) { 1385 this.mimeType = mimeType; 1386 } 1387 1388 public void setRequestMethod(Method requestMethod) { 1389 this.requestMethod = requestMethod; 1390 } 1391 1392 public void setStatus(IStatus status) { 1393 this.status = status; 1394 } 1395 } 1396 1397 public static final class ResponseException extends Exception { 1398 1399 private static final long serialVersionUID = 6569838532917408380L; 1400 1401 private final Response.Status status; 1402 1403 public ResponseException(Response.Status status, String message) { 1404 super(message); 1405 this.status = status; 1406 } 1407 1408 public ResponseException(Response.Status status, String message, Exception e) { 1409 super(message, e); 1410 this.status = status; 1411 } 1412 1413 public Response.Status getStatus() { 1414 return this.status; 1415 } 1416 } 1417 1418 /** 1419 * The runnable that will be used for the main listening thread. 1420 */ 1421 public class ServerRunnable implements Runnable { 1422 1423 private final int timeout; 1424 1425 private IOException bindException; 1426 1427 private boolean hasBinded = false; 1428 1429 private ServerRunnable(int timeout) { 1430 this.timeout = timeout; 1431 } 1432 1433 @Override 1434 public void run() { 1435 try { 1436 myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); 1437 hasBinded = true; 1438 } catch (IOException e) { 1439 this.bindException = e; 1440 return; 1441 } 1442 do { 1443 try { 1444 final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); 1445 if (this.timeout > 0) { 1446 finalAccept.setSoTimeout(this.timeout); 1447 } 1448 final InputStream inputStream = finalAccept.getInputStream(); 1449 NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); 1450 } catch (IOException e) { 1451 NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 1452 } 1453 } while (!NanoHTTPD.this.myServerSocket.isClosed()); 1454 } 1455 } 1456 1457 /** 1458 * A temp file. 1459 * <p/> 1460 * <p> 1461 * Temp files are responsible for managing the actual temporary storage and 1462 * cleaning themselves up when no longer needed. 1463 * </p> 1464 */ 1465 public interface TempFile { 1466 1467 void delete() throws Exception; 1468 1469 String getName(); 1470 1471 OutputStream open() throws Exception; 1472 } 1473 1474 /** 1475 * Temp file manager. 1476 * <p/> 1477 * <p> 1478 * Temp file managers are created 1-to-1 with incoming requests, to create 1479 * and cleanup temporary files created as a result of handling the request. 1480 * </p> 1481 */ 1482 public interface TempFileManager { 1483 1484 void clear(); 1485 1486 TempFile createTempFile() throws Exception; 1487 } 1488 1489 /** 1490 * Factory to create temp file managers. 1491 */ 1492 public interface TempFileManagerFactory { 1493 1494 TempFileManager create(); 1495 } 1496 1497 /** 1498 * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 1499 * This is required as the Keep-Alive HTTP connections would otherwise block 1500 * the socket reading thread forever (or as long the browser is open). 1501 */ 1502 public static final int SOCKET_READ_TIMEOUT = 5000; 1503 1504 /** 1505 * Common MIME type for dynamic content: plain text 1506 */ 1507 public static final String MIME_PLAINTEXT = "text/plain"; 1508 1509 /** 1510 * Common MIME type for dynamic content: html 1511 */ 1512 public static final String MIME_HTML = "text/html"; 1513 1514 /** 1515 * Pseudo-Parameter to use to store the actual query string in the 1516 * parameters map for later re-processing. 1517 */ 1518 private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 1519 1520 /** 1521 * logger to log to. 1522 */ 1523 private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); 1524 1525 /** 1526 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an 1527 * array of loaded KeyManagers. These objects must properly 1528 * loaded/initialized by the caller. 1529 */ 1530 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { 1531 SSLServerSocketFactory res = null; 1532 try { 1533 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1534 trustManagerFactory.init(loadedKeyStore); 1535 SSLContext ctx = SSLContext.getInstance("TLS"); 1536 ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); 1537 res = ctx.getServerSocketFactory(); 1538 } catch (Exception e) { 1539 throw new IOException(e.getMessage()); 1540 } 1541 return res; 1542 } 1543 1544 /** 1545 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a 1546 * loaded KeyManagerFactory. These objects must properly loaded/initialized 1547 * by the caller. 1548 */ 1549 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { 1550 SSLServerSocketFactory res = null; 1551 try { 1552 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1553 trustManagerFactory.init(loadedKeyStore); 1554 SSLContext ctx = SSLContext.getInstance("TLS"); 1555 ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1556 res = ctx.getServerSocketFactory(); 1557 } catch (Exception e) { 1558 throw new IOException(e.getMessage()); 1559 } 1560 return res; 1561 } 1562 1563 /** 1564 * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your 1565 * certificate and passphrase 1566 */ 1567 public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { 1568 SSLServerSocketFactory res = null; 1569 try { 1570 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 1571 InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); 1572 keystore.load(keystoreStream, passphrase); 1573 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1574 trustManagerFactory.init(keystore); 1575 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 1576 keyManagerFactory.init(keystore, passphrase); 1577 SSLContext ctx = SSLContext.getInstance("TLS"); 1578 ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1579 res = ctx.getServerSocketFactory(); 1580 } catch (Exception e) { 1581 throw new IOException(e.getMessage()); 1582 } 1583 return res; 1584 } 1585 1586 private static final void safeClose(Object closeable) { 1587 try { 1588 if (closeable != null) { 1589 if (closeable instanceof Closeable) { 1590 ((Closeable) closeable).close(); 1591 } else if (closeable instanceof Socket) { 1592 ((Socket) closeable).close(); 1593 } else if (closeable instanceof ServerSocket) { 1594 ((ServerSocket) closeable).close(); 1595 } else { 1596 throw new IllegalArgumentException("Unknown object to close"); 1597 } 1598 } 1599 } catch (IOException e) { 1600 NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); 1601 } 1602 } 1603 1604 private final String hostname; 1605 1606 private final int myPort; 1607 1608 private ServerSocket myServerSocket; 1609 1610 private SSLServerSocketFactory sslServerSocketFactory; 1611 1612 private Thread myThread; 1613 1614 /** 1615 * Pluggable strategy for asynchronously executing requests. 1616 */ 1617 protected AsyncRunner asyncRunner; 1618 1619 /** 1620 * Pluggable strategy for creating and cleaning up temporary files. 1621 */ 1622 private TempFileManagerFactory tempFileManagerFactory; 1623 1624 /** 1625 * Constructs an HTTP server on given port. 1626 */ 1627 public NanoHTTPD(int port) { 1628 this(null, port); 1629 } 1630 1631 // ------------------------------------------------------------------------------- 1632 // // 1633 // 1634 // Threading Strategy. 1635 // 1636 // ------------------------------------------------------------------------------- 1637 // // 1638 1639 /** 1640 * Constructs an HTTP server on given hostname and port. 1641 */ 1642 public NanoHTTPD(String hostname, int port) { 1643 this.hostname = hostname; 1644 this.myPort = port; 1645 setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 1646 setAsyncRunner(new DefaultAsyncRunner()); 1647 } 1648 1649 /** 1650 * Forcibly closes all connections that are open. 1651 */ 1652 public synchronized void closeAllConnections() { 1653 stop(); 1654 } 1655 1656 /** 1657 * create a instance of the client handler, subclasses can return a subclass 1658 * of the ClientHandler. 1659 * 1660 * @param finalAccept 1661 * the socket the cleint is connected to 1662 * @param inputStream 1663 * the input stream 1664 * @return the client handler 1665 */ 1666 protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { 1667 return new ClientHandler(inputStream, finalAccept); 1668 } 1669 1670 /** 1671 * Instantiate the server runnable, can be overwritten by subclasses to 1672 * provide a subclass of the ServerRunnable. 1673 * 1674 * @param timeout 1675 * the socet timeout to use. 1676 * @return the server runnable. 1677 */ 1678 protected ServerRunnable createServerRunnable(final int timeout) { 1679 return new ServerRunnable(timeout); 1680 } 1681 1682 /** 1683 * Decode parameters from a URL, handing the case where a single parameter 1684 * name might have been supplied several times, by return lists of values. 1685 * In general these lists will contain a single element. 1686 * 1687 * @param parms 1688 * original <b>NanoHTTPD</b> parameters values, as passed to the 1689 * <code>serve()</code> method. 1690 * @return a map of <code>String</code> (parameter name) to 1691 * <code>List<String></code> (a list of the values supplied). 1692 */ 1693 protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { 1694 return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); 1695 } 1696 1697 // ------------------------------------------------------------------------------- 1698 // // 1699 1700 /** 1701 * Decode parameters from a URL, handing the case where a single parameter 1702 * name might have been supplied several times, by return lists of values. 1703 * In general these lists will contain a single element. 1704 * 1705 * @param queryString 1706 * a query string pulled from the URL. 1707 * @return a map of <code>String</code> (parameter name) to 1708 * <code>List<String></code> (a list of the values supplied). 1709 */ 1710 protected Map<String, List<String>> decodeParameters(String queryString) { 1711 Map<String, List<String>> parms = new HashMap<String, List<String>>(); 1712 if (queryString != null) { 1713 StringTokenizer st = new StringTokenizer(queryString, "&"); 1714 while (st.hasMoreTokens()) { 1715 String e = st.nextToken(); 1716 int sep = e.indexOf('='); 1717 String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 1718 if (!parms.containsKey(propertyName)) { 1719 parms.put(propertyName, new ArrayList<String>()); 1720 } 1721 String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; 1722 if (propertyValue != null) { 1723 parms.get(propertyName).add(propertyValue); 1724 } 1725 } 1726 } 1727 return parms; 1728 } 1729 1730 /** 1731 * Decode percent encoded <code>String</code> values. 1732 * 1733 * @param str 1734 * the percent encoded <code>String</code> 1735 * @return expanded form of the input, for example "foo%20bar" becomes 1736 * "foo bar" 1737 */ 1738 protected String decodePercent(String str) { 1739 String decoded = null; 1740 try { 1741 decoded = URLDecoder.decode(str, "UTF8"); 1742 } catch (UnsupportedEncodingException ignored) { 1743 NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); 1744 } 1745 return decoded; 1746 } 1747 1748 /** 1749 * @return true if the gzip compression should be used if the client 1750 * accespts it. 1751 */ 1752 protected boolean useGzipWhenAccepted() { 1753 return true; 1754 } 1755 1756 public final int getListeningPort() { 1757 return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); 1758 } 1759 1760 public final boolean isAlive() { 1761 return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); 1762 } 1763 1764 /** 1765 * Call before start() to serve over HTTPS instead of HTTP 1766 */ 1767 public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) { 1768 this.sslServerSocketFactory = sslServerSocketFactory; 1769 } 1770 1771 /** 1772 * Create a response with unknown length (using HTTP 1.1 chunking). 1773 */ 1774 public Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { 1775 return new Response(status, mimeType, data, -1); 1776 } 1777 1778 /** 1779 * Create a response with known length. 1780 */ 1781 public Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { 1782 return new Response(status, mimeType, data, totalBytes); 1783 } 1784 1785 /** 1786 * Create a text response with known length. 1787 */ 1788 public Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { 1789 if (txt == null) { 1790 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); 1791 } else { 1792 byte[] bytes; 1793 try { 1794 bytes = txt.getBytes("UTF-8"); 1795 } catch (UnsupportedEncodingException e) { 1796 NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); 1797 bytes = new byte[0]; 1798 } 1799 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); 1800 } 1801 } 1802 1803 /** 1804 * Create a text response with known length. 1805 */ 1806 public Response newFixedLengthResponse(String msg) { 1807 return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); 1808 } 1809 1810 /** 1811 * Override this to customize the server. 1812 * <p/> 1813 * <p/> 1814 * (By default, this returns a 404 "Not Found" plain text error response.) 1815 * 1816 * @param session 1817 * The HTTP session 1818 * @return HTTP response, see class Response for details 1819 */ 1820 public Response serve(IHTTPSession session) { 1821 Map<String, String> files = new HashMap<String, String>(); 1822 Method method = session.getMethod(); 1823 if (Method.PUT.equals(method) || Method.POST.equals(method)) { 1824 try { 1825 session.parseBody(files); 1826 } catch (IOException ioe) { 1827 return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 1828 } catch (ResponseException re) { 1829 return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 1830 } 1831 } 1832 1833 Map<String, String> parms = session.getParms(); 1834 parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); 1835 return serve(session.getUri(), method, session.getHeaders(), parms, files); 1836 } 1837 1838 /** 1839 * Override this to customize the server. 1840 * <p/> 1841 * <p/> 1842 * (By default, this returns a 404 "Not Found" plain text error response.) 1843 * 1844 * @param uri 1845 * Percent-decoded URI without parameters, for example 1846 * "/index.cgi" 1847 * @param method 1848 * "GET", "POST" etc. 1849 * @param parms 1850 * Parsed, percent decoded parameters from URI and, in case of 1851 * POST, data. 1852 * @param headers 1853 * Header entries, percent decoded 1854 * @return HTTP response, see class Response for details 1855 */ 1856 @Deprecated 1857 public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { 1858 return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); 1859 } 1860 1861 /** 1862 * Pluggable strategy for asynchronously executing requests. 1863 * 1864 * @param asyncRunner 1865 * new strategy for handling threads. 1866 */ 1867 public void setAsyncRunner(AsyncRunner asyncRunner) { 1868 this.asyncRunner = asyncRunner; 1869 } 1870 1871 /** 1872 * Pluggable strategy for creating and cleaning up temporary files. 1873 * 1874 * @param tempFileManagerFactory 1875 * new strategy for handling temp files. 1876 */ 1877 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 1878 this.tempFileManagerFactory = tempFileManagerFactory; 1879 } 1880 1881 /** 1882 * Start the server. 1883 * 1884 * @throws IOException 1885 * if the socket is in use. 1886 */ 1887 public void start() throws IOException { 1888 start(NanoHTTPD.SOCKET_READ_TIMEOUT); 1889 } 1890 1891 /** 1892 * Start the server. 1893 * 1894 * @param timeout 1895 * timeout to use for socket connections. 1896 * @throws IOException 1897 * if the socket is in use. 1898 */ 1899 public void start(final int timeout) throws IOException { 1900 if (this.sslServerSocketFactory != null) { 1901 SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); 1902 ss.setNeedClientAuth(false); 1903 this.myServerSocket = ss; 1904 } else { 1905 this.myServerSocket = new ServerSocket(); 1906 } 1907 this.myServerSocket.setReuseAddress(true); 1908 1909 ServerRunnable serverRunnable = createServerRunnable(timeout); 1910 this.myThread = new Thread(serverRunnable); 1911 this.myThread.setDaemon(true); 1912 this.myThread.setName("NanoHttpd Main Listener"); 1913 this.myThread.start(); 1914 while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { 1915 try { 1916 Thread.sleep(10L); 1917 } catch (Throwable e) { 1918 // on android this may not be allowed, that's why we 1919 // catch throwable the wait should be very short because we are 1920 // just waiting for the bind of the socket 1921 } 1922 } 1923 if (serverRunnable.bindException != null) { 1924 throw serverRunnable.bindException; 1925 } 1926 } 1927 1928 /** 1929 * Stop the server. 1930 */ 1931 public void stop() { 1932 try { 1933 safeClose(this.myServerSocket); 1934 this.asyncRunner.closeAll(); 1935 if (this.myThread != null) { 1936 this.myThread.join(); 1937 } 1938 } catch (Exception e) { 1939 NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); 1940 } 1941 } 1942 1943 public final boolean wasStarted() { 1944 return this.myServerSocket != null && this.myThread != null; 1945 } 1946} 1947