NanoHTTPD.java revision d06acf2b280ff10d396647e3f0f0efe4a65c74af
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 NOT_ACCEPTABLE(406, "Not Acceptable"), 1106 REQUEST_TIMEOUT(408, "Request Timeout"), 1107 CONFLICT(409, "Conflict"), 1108 RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), 1109 INTERNAL_ERROR(500, "Internal Server Error"), 1110 NOT_IMPLEMENTED(501, "Not Implemented"), 1111 UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); 1112 1113 private final int requestStatus; 1114 1115 private final String description; 1116 1117 Status(int requestStatus, String description) { 1118 this.requestStatus = requestStatus; 1119 this.description = description; 1120 } 1121 1122 @Override 1123 public String getDescription() { 1124 return "" + this.requestStatus + " " + this.description; 1125 } 1126 1127 @Override 1128 public int getRequestStatus() { 1129 return this.requestStatus; 1130 } 1131 1132 } 1133 1134 /** 1135 * Output stream that will automatically send every write to the wrapped 1136 * OutputStream according to chunked transfer: 1137 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 1138 */ 1139 private static class ChunkedOutputStream extends FilterOutputStream { 1140 1141 public ChunkedOutputStream(OutputStream out) { 1142 super(out); 1143 } 1144 1145 @Override 1146 public void write(int b) throws IOException { 1147 byte[] data = { 1148 (byte) b 1149 }; 1150 write(data, 0, 1); 1151 } 1152 1153 @Override 1154 public void write(byte[] b) throws IOException { 1155 write(b, 0, b.length); 1156 } 1157 1158 @Override 1159 public void write(byte[] b, int off, int len) throws IOException { 1160 if (len == 0) 1161 return; 1162 out.write(String.format("%x\r\n", len).getBytes()); 1163 out.write(b, off, len); 1164 out.write("\r\n".getBytes()); 1165 } 1166 1167 public void finish() throws IOException { 1168 out.write("0\r\n\r\n".getBytes()); 1169 } 1170 1171 } 1172 1173 /** 1174 * HTTP status code after processing, e.g. "200 OK", Status.OK 1175 */ 1176 private IStatus status; 1177 1178 /** 1179 * MIME type of content, e.g. "text/html" 1180 */ 1181 private String mimeType; 1182 1183 /** 1184 * Data of the response, may be null. 1185 */ 1186 private InputStream data; 1187 1188 private long contentLength; 1189 1190 /** 1191 * Headers for the HTTP response. Use addHeader() to add lines. 1192 */ 1193 private final Map<String, String> header = new HashMap<String, String>(); 1194 1195 /** 1196 * The request method that spawned this response. 1197 */ 1198 private Method requestMethod; 1199 1200 /** 1201 * Use chunkedTransfer 1202 */ 1203 private boolean chunkedTransfer; 1204 1205 private boolean encodeAsGzip; 1206 1207 private boolean keepAlive; 1208 1209 /** 1210 * Creates a fixed length response if totalBytes>=0, otherwise chunked. 1211 */ 1212 protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { 1213 this.status = status; 1214 this.mimeType = mimeType; 1215 if (data == null) { 1216 this.data = new ByteArrayInputStream(new byte[0]); 1217 this.contentLength = 0L; 1218 } else { 1219 this.data = data; 1220 this.contentLength = totalBytes; 1221 } 1222 this.chunkedTransfer = this.contentLength < 0; 1223 keepAlive = true; 1224 } 1225 1226 @Override 1227 public void close() throws IOException { 1228 if (this.data != null) { 1229 this.data.close(); 1230 } 1231 } 1232 1233 /** 1234 * Adds given line to the header. 1235 */ 1236 public void addHeader(String name, String value) { 1237 this.header.put(name, value); 1238 } 1239 1240 public InputStream getData() { 1241 return this.data; 1242 } 1243 1244 public String getHeader(String name) { 1245 for (String headerName : header.keySet()) { 1246 if (headerName.equalsIgnoreCase(name)) { 1247 return header.get(headerName); 1248 } 1249 } 1250 return null; 1251 } 1252 1253 public String getMimeType() { 1254 return this.mimeType; 1255 } 1256 1257 public Method getRequestMethod() { 1258 return this.requestMethod; 1259 } 1260 1261 public IStatus getStatus() { 1262 return this.status; 1263 } 1264 1265 public void setGzipEncoding(boolean encodeAsGzip) { 1266 this.encodeAsGzip = encodeAsGzip; 1267 } 1268 1269 public void setKeepAlive(boolean useKeepAlive) { 1270 this.keepAlive = useKeepAlive; 1271 } 1272 1273 private static boolean headerAlreadySent(Map<String, String> header, String name) { 1274 boolean alreadySent = false; 1275 for (String headerName : header.keySet()) { 1276 alreadySent |= headerName.equalsIgnoreCase(name); 1277 } 1278 return alreadySent; 1279 } 1280 1281 /** 1282 * Sends given response to the socket. 1283 */ 1284 protected void send(OutputStream outputStream) { 1285 String mime = this.mimeType; 1286 SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 1287 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); 1288 1289 try { 1290 if (this.status == null) { 1291 throw new Error("sendResponse(): Status can't be null."); 1292 } 1293 PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false); 1294 pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n"); 1295 1296 if (mime != null) { 1297 pw.print("Content-Type: " + mime + "\r\n"); 1298 } 1299 1300 if (this.header == null || this.header.get("Date") == null) { 1301 pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); 1302 } 1303 1304 if (this.header != null) { 1305 for (String key : this.header.keySet()) { 1306 String value = this.header.get(key); 1307 pw.print(key + ": " + value + "\r\n"); 1308 } 1309 } 1310 1311 if (!headerAlreadySent(header, "connection")) { 1312 pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n"); 1313 } 1314 1315 if (headerAlreadySent(this.header, "content-length")) { 1316 encodeAsGzip = false; 1317 } 1318 1319 if (encodeAsGzip) { 1320 pw.print("Content-Encoding: gzip\r\n"); 1321 setChunkedTransfer(true); 1322 } 1323 1324 long pending = this.data != null ? this.contentLength : 0; 1325 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1326 pw.print("Transfer-Encoding: chunked\r\n"); 1327 } else if (!encodeAsGzip) { 1328 pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); 1329 } 1330 pw.print("\r\n"); 1331 pw.flush(); 1332 sendBodyWithCorrectTransferAndEncoding(outputStream, pending); 1333 outputStream.flush(); 1334 safeClose(this.data); 1335 } catch (IOException ioe) { 1336 NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); 1337 } 1338 } 1339 1340 private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { 1341 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1342 ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); 1343 sendBodyWithCorrectEncoding(chunkedOutputStream, -1); 1344 chunkedOutputStream.finish(); 1345 } else { 1346 sendBodyWithCorrectEncoding(outputStream, pending); 1347 } 1348 } 1349 1350 private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { 1351 if (encodeAsGzip) { 1352 GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); 1353 sendBody(gzipOutputStream, -1); 1354 gzipOutputStream.finish(); 1355 } else { 1356 sendBody(outputStream, pending); 1357 } 1358 } 1359 1360 /** 1361 * Sends the body to the specified OutputStream. The pending parameter 1362 * limits the maximum amounts of bytes sent unless it is -1, in which 1363 * case everything is sent. 1364 * 1365 * @param outputStream 1366 * the OutputStream to send data to 1367 * @param pending 1368 * -1 to send everything, otherwise sets a max limit to the 1369 * number of bytes sent 1370 * @throws IOException 1371 * if something goes wrong while sending the data. 1372 */ 1373 private void sendBody(OutputStream outputStream, long pending) throws IOException { 1374 long BUFFER_SIZE = 16 * 1024; 1375 byte[] buff = new byte[(int) BUFFER_SIZE]; 1376 boolean sendEverything = pending == -1; 1377 while (pending > 0 || sendEverything) { 1378 long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); 1379 int read = this.data.read(buff, 0, (int) bytesToRead); 1380 if (read <= 0) { 1381 break; 1382 } 1383 outputStream.write(buff, 0, read); 1384 if (!sendEverything) { 1385 pending -= read; 1386 } 1387 } 1388 } 1389 1390 protected static long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) { 1391 for (String headerName : header.keySet()) { 1392 if (headerName.equalsIgnoreCase("content-length")) { 1393 try { 1394 return Long.parseLong(header.get(headerName)); 1395 } catch (NumberFormatException ex) { 1396 return size; 1397 } 1398 } 1399 } 1400 1401 pw.print("Content-Length: " + size + "\r\n"); 1402 return size; 1403 } 1404 1405 public void setChunkedTransfer(boolean chunkedTransfer) { 1406 this.chunkedTransfer = chunkedTransfer; 1407 } 1408 1409 public void setData(InputStream data) { 1410 this.data = data; 1411 } 1412 1413 public void setMimeType(String mimeType) { 1414 this.mimeType = mimeType; 1415 } 1416 1417 public void setRequestMethod(Method requestMethod) { 1418 this.requestMethod = requestMethod; 1419 } 1420 1421 public void setStatus(IStatus status) { 1422 this.status = status; 1423 } 1424 } 1425 1426 public static final class ResponseException extends Exception { 1427 1428 private static final long serialVersionUID = 6569838532917408380L; 1429 1430 private final Response.Status status; 1431 1432 public ResponseException(Response.Status status, String message) { 1433 super(message); 1434 this.status = status; 1435 } 1436 1437 public ResponseException(Response.Status status, String message, Exception e) { 1438 super(message, e); 1439 this.status = status; 1440 } 1441 1442 public Response.Status getStatus() { 1443 return this.status; 1444 } 1445 } 1446 1447 /** 1448 * The runnable that will be used for the main listening thread. 1449 */ 1450 public class ServerRunnable implements Runnable { 1451 1452 private final int timeout; 1453 1454 private IOException bindException; 1455 1456 private boolean hasBinded = false; 1457 1458 private ServerRunnable(int timeout) { 1459 this.timeout = timeout; 1460 } 1461 1462 @Override 1463 public void run() { 1464 try { 1465 myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); 1466 hasBinded = true; 1467 } catch (IOException e) { 1468 this.bindException = e; 1469 return; 1470 } 1471 do { 1472 try { 1473 final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); 1474 if (this.timeout > 0) { 1475 finalAccept.setSoTimeout(this.timeout); 1476 } 1477 final InputStream inputStream = finalAccept.getInputStream(); 1478 NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); 1479 } catch (IOException e) { 1480 NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 1481 } 1482 } while (!NanoHTTPD.this.myServerSocket.isClosed()); 1483 } 1484 } 1485 1486 /** 1487 * A temp file. 1488 * <p/> 1489 * <p> 1490 * Temp files are responsible for managing the actual temporary storage and 1491 * cleaning themselves up when no longer needed. 1492 * </p> 1493 */ 1494 public interface TempFile { 1495 1496 void delete() throws Exception; 1497 1498 String getName(); 1499 1500 OutputStream open() throws Exception; 1501 } 1502 1503 /** 1504 * Temp file manager. 1505 * <p/> 1506 * <p> 1507 * Temp file managers are created 1-to-1 with incoming requests, to create 1508 * and cleanup temporary files created as a result of handling the request. 1509 * </p> 1510 */ 1511 public interface TempFileManager { 1512 1513 void clear(); 1514 1515 TempFile createTempFile(String filename_hint) throws Exception; 1516 } 1517 1518 /** 1519 * Factory to create temp file managers. 1520 */ 1521 public interface TempFileManagerFactory { 1522 1523 TempFileManager create(); 1524 } 1525 1526 /** 1527 * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 1528 * This is required as the Keep-Alive HTTP connections would otherwise block 1529 * the socket reading thread forever (or as long the browser is open). 1530 */ 1531 public static final int SOCKET_READ_TIMEOUT = 5000; 1532 1533 /** 1534 * Common MIME type for dynamic content: plain text 1535 */ 1536 public static final String MIME_PLAINTEXT = "text/plain"; 1537 1538 /** 1539 * Common MIME type for dynamic content: html 1540 */ 1541 public static final String MIME_HTML = "text/html"; 1542 1543 /** 1544 * Pseudo-Parameter to use to store the actual query string in the 1545 * parameters map for later re-processing. 1546 */ 1547 private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 1548 1549 /** 1550 * logger to log to. 1551 */ 1552 private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); 1553 1554 /** 1555 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an 1556 * array of loaded KeyManagers. These objects must properly 1557 * loaded/initialized by the caller. 1558 */ 1559 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { 1560 SSLServerSocketFactory res = null; 1561 try { 1562 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1563 trustManagerFactory.init(loadedKeyStore); 1564 SSLContext ctx = SSLContext.getInstance("TLS"); 1565 ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); 1566 res = ctx.getServerSocketFactory(); 1567 } catch (Exception e) { 1568 throw new IOException(e.getMessage()); 1569 } 1570 return res; 1571 } 1572 1573 /** 1574 * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a 1575 * loaded KeyManagerFactory. These objects must properly loaded/initialized 1576 * by the caller. 1577 */ 1578 public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { 1579 SSLServerSocketFactory res = null; 1580 try { 1581 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1582 trustManagerFactory.init(loadedKeyStore); 1583 SSLContext ctx = SSLContext.getInstance("TLS"); 1584 ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1585 res = ctx.getServerSocketFactory(); 1586 } catch (Exception e) { 1587 throw new IOException(e.getMessage()); 1588 } 1589 return res; 1590 } 1591 1592 /** 1593 * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your 1594 * certificate and passphrase 1595 */ 1596 public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { 1597 SSLServerSocketFactory res = null; 1598 try { 1599 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 1600 InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); 1601 keystore.load(keystoreStream, passphrase); 1602 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1603 trustManagerFactory.init(keystore); 1604 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 1605 keyManagerFactory.init(keystore, passphrase); 1606 SSLContext ctx = SSLContext.getInstance("TLS"); 1607 ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1608 res = ctx.getServerSocketFactory(); 1609 } catch (Exception e) { 1610 throw new IOException(e.getMessage()); 1611 } 1612 return res; 1613 } 1614 1615 private static final void safeClose(Object closeable) { 1616 try { 1617 if (closeable != null) { 1618 if (closeable instanceof Closeable) { 1619 ((Closeable) closeable).close(); 1620 } else if (closeable instanceof Socket) { 1621 ((Socket) closeable).close(); 1622 } else if (closeable instanceof ServerSocket) { 1623 ((ServerSocket) closeable).close(); 1624 } else { 1625 throw new IllegalArgumentException("Unknown object to close"); 1626 } 1627 } 1628 } catch (IOException e) { 1629 NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); 1630 } 1631 } 1632 1633 private final String hostname; 1634 1635 private final int myPort; 1636 1637 private ServerSocket myServerSocket; 1638 1639 private SSLServerSocketFactory sslServerSocketFactory; 1640 1641 private Thread myThread; 1642 1643 /** 1644 * Pluggable strategy for asynchronously executing requests. 1645 */ 1646 protected AsyncRunner asyncRunner; 1647 1648 /** 1649 * Pluggable strategy for creating and cleaning up temporary files. 1650 */ 1651 private TempFileManagerFactory tempFileManagerFactory; 1652 1653 /** 1654 * Constructs an HTTP server on given port. 1655 */ 1656 public NanoHTTPD(int port) { 1657 this(null, port); 1658 } 1659 1660 // ------------------------------------------------------------------------------- 1661 // // 1662 // 1663 // Threading Strategy. 1664 // 1665 // ------------------------------------------------------------------------------- 1666 // // 1667 1668 /** 1669 * Constructs an HTTP server on given hostname and port. 1670 */ 1671 public NanoHTTPD(String hostname, int port) { 1672 this.hostname = hostname; 1673 this.myPort = port; 1674 setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 1675 setAsyncRunner(new DefaultAsyncRunner()); 1676 } 1677 1678 /** 1679 * Forcibly closes all connections that are open. 1680 */ 1681 public synchronized void closeAllConnections() { 1682 stop(); 1683 } 1684 1685 /** 1686 * create a instance of the client handler, subclasses can return a subclass 1687 * of the ClientHandler. 1688 * 1689 * @param finalAccept 1690 * the socket the cleint is connected to 1691 * @param inputStream 1692 * the input stream 1693 * @return the client handler 1694 */ 1695 protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { 1696 return new ClientHandler(inputStream, finalAccept); 1697 } 1698 1699 /** 1700 * Instantiate the server runnable, can be overwritten by subclasses to 1701 * provide a subclass of the ServerRunnable. 1702 * 1703 * @param timeout 1704 * the socet timeout to use. 1705 * @return the server runnable. 1706 */ 1707 protected ServerRunnable createServerRunnable(final int timeout) { 1708 return new ServerRunnable(timeout); 1709 } 1710 1711 /** 1712 * Decode parameters from a URL, handing the case where a single parameter 1713 * name might have been supplied several times, by return lists of values. 1714 * In general these lists will contain a single element. 1715 * 1716 * @param parms 1717 * original <b>NanoHTTPD</b> parameters values, as passed to the 1718 * <code>serve()</code> method. 1719 * @return a map of <code>String</code> (parameter name) to 1720 * <code>List<String></code> (a list of the values supplied). 1721 */ 1722 protected static Map<String, List<String>> decodeParameters(Map<String, String> parms) { 1723 return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); 1724 } 1725 1726 // ------------------------------------------------------------------------------- 1727 // // 1728 1729 /** 1730 * Decode parameters from a URL, handing the case where a single parameter 1731 * name might have been supplied several times, by return lists of values. 1732 * In general these lists will contain a single element. 1733 * 1734 * @param queryString 1735 * a query string pulled from the URL. 1736 * @return a map of <code>String</code> (parameter name) to 1737 * <code>List<String></code> (a list of the values supplied). 1738 */ 1739 protected static Map<String, List<String>> decodeParameters(String queryString) { 1740 Map<String, List<String>> parms = new HashMap<String, List<String>>(); 1741 if (queryString != null) { 1742 StringTokenizer st = new StringTokenizer(queryString, "&"); 1743 while (st.hasMoreTokens()) { 1744 String e = st.nextToken(); 1745 int sep = e.indexOf('='); 1746 String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 1747 if (!parms.containsKey(propertyName)) { 1748 parms.put(propertyName, new ArrayList<String>()); 1749 } 1750 String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; 1751 if (propertyValue != null) { 1752 parms.get(propertyName).add(propertyValue); 1753 } 1754 } 1755 } 1756 return parms; 1757 } 1758 1759 /** 1760 * Decode percent encoded <code>String</code> values. 1761 * 1762 * @param str 1763 * the percent encoded <code>String</code> 1764 * @return expanded form of the input, for example "foo%20bar" becomes 1765 * "foo bar" 1766 */ 1767 protected static String decodePercent(String str) { 1768 String decoded = null; 1769 try { 1770 decoded = URLDecoder.decode(str, "UTF8"); 1771 } catch (UnsupportedEncodingException ignored) { 1772 NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); 1773 } 1774 return decoded; 1775 } 1776 1777 /** 1778 * @return true if the gzip compression should be used if the client 1779 * accespts it. Default this option is on for text content and off 1780 * for everything. Override this for custom semantics. 1781 */ 1782 protected boolean useGzipWhenAccepted(Response r) { 1783 return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/"); 1784 } 1785 1786 public final int getListeningPort() { 1787 return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); 1788 } 1789 1790 public final boolean isAlive() { 1791 return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); 1792 } 1793 1794 /** 1795 * Call before start() to serve over HTTPS instead of HTTP 1796 */ 1797 public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) { 1798 this.sslServerSocketFactory = sslServerSocketFactory; 1799 } 1800 1801 /** 1802 * Create a response with unknown length (using HTTP 1.1 chunking). 1803 */ 1804 public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { 1805 return new Response(status, mimeType, data, -1); 1806 } 1807 1808 /** 1809 * Create a response with known length. 1810 */ 1811 public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { 1812 return new Response(status, mimeType, data, totalBytes); 1813 } 1814 1815 /** 1816 * Create a text response with known length. 1817 */ 1818 public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { 1819 if (txt == null) { 1820 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); 1821 } else { 1822 byte[] bytes; 1823 try { 1824 bytes = txt.getBytes("UTF-8"); 1825 } catch (UnsupportedEncodingException e) { 1826 NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); 1827 bytes = new byte[0]; 1828 } 1829 return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); 1830 } 1831 } 1832 1833 /** 1834 * Create a text response with known length. 1835 */ 1836 public static Response newFixedLengthResponse(String msg) { 1837 return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); 1838 } 1839 1840 /** 1841 * Override this to customize the server. 1842 * <p/> 1843 * <p/> 1844 * (By default, this returns a 404 "Not Found" plain text error response.) 1845 * 1846 * @param session 1847 * The HTTP session 1848 * @return HTTP response, see class Response for details 1849 */ 1850 public Response serve(IHTTPSession session) { 1851 Map<String, String> files = new HashMap<String, String>(); 1852 Method method = session.getMethod(); 1853 if (Method.PUT.equals(method) || Method.POST.equals(method)) { 1854 try { 1855 session.parseBody(files); 1856 } catch (IOException ioe) { 1857 return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 1858 } catch (ResponseException re) { 1859 return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 1860 } 1861 } 1862 1863 Map<String, String> parms = session.getParms(); 1864 parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); 1865 return serve(session.getUri(), method, session.getHeaders(), parms, files); 1866 } 1867 1868 /** 1869 * Override this to customize the server. 1870 * <p/> 1871 * <p/> 1872 * (By default, this returns a 404 "Not Found" plain text error response.) 1873 * 1874 * @param uri 1875 * Percent-decoded URI without parameters, for example 1876 * "/index.cgi" 1877 * @param method 1878 * "GET", "POST" etc. 1879 * @param parms 1880 * Parsed, percent decoded parameters from URI and, in case of 1881 * POST, data. 1882 * @param headers 1883 * Header entries, percent decoded 1884 * @return HTTP response, see class Response for details 1885 */ 1886 @Deprecated 1887 public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { 1888 return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); 1889 } 1890 1891 /** 1892 * Pluggable strategy for asynchronously executing requests. 1893 * 1894 * @param asyncRunner 1895 * new strategy for handling threads. 1896 */ 1897 public void setAsyncRunner(AsyncRunner asyncRunner) { 1898 this.asyncRunner = asyncRunner; 1899 } 1900 1901 /** 1902 * Pluggable strategy for creating and cleaning up temporary files. 1903 * 1904 * @param tempFileManagerFactory 1905 * new strategy for handling temp files. 1906 */ 1907 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 1908 this.tempFileManagerFactory = tempFileManagerFactory; 1909 } 1910 1911 /** 1912 * Start the server. 1913 * 1914 * @throws IOException 1915 * if the socket is in use. 1916 */ 1917 public void start() throws IOException { 1918 start(NanoHTTPD.SOCKET_READ_TIMEOUT); 1919 } 1920 1921 /** 1922 * Start the server. 1923 * 1924 * @param timeout 1925 * timeout to use for socket connections. 1926 * @param daemon 1927 * start the thread daemon or not. 1928 * @throws IOException 1929 * if the socket is in use. 1930 */ 1931 public void start(final int timeout, boolean daemon) throws IOException { 1932 if (this.sslServerSocketFactory != null) { 1933 SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); 1934 ss.setNeedClientAuth(false); 1935 this.myServerSocket = ss; 1936 } else { 1937 this.myServerSocket = new ServerSocket(); 1938 } 1939 this.myServerSocket.setReuseAddress(true); 1940 1941 ServerRunnable serverRunnable = createServerRunnable(timeout); 1942 this.myThread = new Thread(serverRunnable); 1943 this.myThread.setDaemon(daemon); 1944 this.myThread.setName("NanoHttpd Main Listener"); 1945 this.myThread.start(); 1946 while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { 1947 try { 1948 Thread.sleep(10L); 1949 } catch (Throwable e) { 1950 // on android this may not be allowed, that's why we 1951 // catch throwable the wait should be very short because we are 1952 // just waiting for the bind of the socket 1953 } 1954 } 1955 if (serverRunnable.bindException != null) { 1956 throw serverRunnable.bindException; 1957 } 1958 } 1959 1960 /** 1961 * Starts the server (in setDaemon(true) mode). 1962 */ 1963 public void start(final int timeout) throws IOException { 1964 start(timeout, true); 1965 } 1966 1967 /** 1968 * Stop the server. 1969 */ 1970 public void stop() { 1971 try { 1972 safeClose(this.myServerSocket); 1973 this.asyncRunner.closeAll(); 1974 if (this.myThread != null) { 1975 this.myThread.join(); 1976 } 1977 } catch (Exception e) { 1978 NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); 1979 } 1980 } 1981 1982 public final boolean wasStarted() { 1983 return this.myServerSocket != null && this.myThread != null; 1984 } 1985} 1986