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