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