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