NanoHTTPD.java revision 8cdc631dca8d05f64ea8da466ecfe759a2f6e429
1package fi.iki.elonen; 2 3import java.io.*; 4import java.net.InetSocketAddress; 5import java.net.ServerSocket; 6import java.net.Socket; 7import java.net.URLDecoder; 8import java.nio.ByteBuffer; 9import java.nio.channels.FileChannel; 10import java.text.SimpleDateFormat; 11import java.util.*; 12 13/** 14 * A simple, tiny, nicely embeddable HTTP server in Java 15 * <p/> 16 * <p/> 17 * NanoHTTPD 18 * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p> 19 * <p/> 20 * <p/> 21 * <b>Features + limitations: </b> 22 * <ul> 23 * <p/> 24 * <li>Only one Java file</li> 25 * <li>Java 5 compatible</li> 26 * <li>Released as open source, Modified BSD licence</li> 27 * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li> 28 * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li> 29 * <li>Supports both dynamic content and file serving</li> 30 * <li>Supports file upload (since version 1.2, 2010)</li> 31 * <li>Supports partial content (streaming)</li> 32 * <li>Supports ETags</li> 33 * <li>Never caches anything</li> 34 * <li>Doesn't limit bandwidth, request time or simultaneous connections</li> 35 * <li>Default code serves files and shows all HTTP parameters and headers</li> 36 * <li>File server supports directory listing, index.html and index.htm</li> 37 * <li>File server supports partial content (streaming)</li> 38 * <li>File server supports ETags</li> 39 * <li>File server does the 301 redirection trick for directories without '/'</li> 40 * <li>File server supports simple skipping for files (continue download)</li> 41 * <li>File server serves also very long files without memory overhead</li> 42 * <li>Contains a built-in list of most common mime types</li> 43 * <li>All header names are converted lowercase so they don't vary between browsers/clients</li> 44 * <p/> 45 * </ul> 46 * <p/> 47 * <p/> 48 * <b>How to use: </b> 49 * <ul> 50 * <p/> 51 * <li>Subclass and implement serve() and embed to your own program</li> 52 * <p/> 53 * </ul> 54 * <p/> 55 * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence) 56 */ 57public abstract class NanoHTTPD { 58 /** 59 * Common mime type for dynamic content: plain text 60 */ 61 public static final String MIME_PLAINTEXT = "text/plain"; 62 /** 63 * Common mime type for dynamic content: html 64 */ 65 public static final String MIME_HTML = "text/html"; 66 /** 67 * Common mime type for dynamic content: binary 68 */ 69 public static final String MIME_DEFAULT_BINARY = "application/octet-stream"; 70 private final String hostname; 71 private final int myPort; 72 private ServerSocket myServerSocket; 73 private Thread myThread; 74 /** 75 * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing. 76 */ 77 private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 78 79 /** 80 * Constructs an HTTP server on given port. 81 */ 82 public NanoHTTPD(int port) { 83 this(null, port); 84 } 85 86 /** 87 * Constructs an HTTP server on given hostname and port. 88 */ 89 public NanoHTTPD(String hostname, int port) { 90 this.hostname = hostname; 91 this.myPort = port; 92 setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 93 setAsyncRunner(new DefaultAsyncRunner()); 94 } 95 96 /** 97 * Start the server. 98 * @throws IOException if the socket is in use. 99 */ 100 public void start() throws IOException { 101 myServerSocket = new ServerSocket(); 102 myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); 103 104 myThread = new Thread(new Runnable() { 105 @Override 106 public void run() { 107 do { 108 try { 109 final Socket finalAccept = myServerSocket.accept(); 110 final InputStream inputStream = finalAccept.getInputStream(); 111 if (inputStream == null) { 112 safeClose(finalAccept); 113 } else { 114 asyncRunner.exec(new Runnable() { 115 @Override 116 public void run() { 117 OutputStream outputStream = null; 118 try { 119 outputStream = finalAccept.getOutputStream(); 120 TempFileManager tempFileManager = tempFileManagerFactory.create(); 121 HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream); 122 session.execute(); 123 } catch (IOException e) { 124 } finally { 125 safeClose(outputStream); 126 safeClose(inputStream); 127 safeClose(finalAccept); 128 } 129 } 130 }); 131 } 132 } catch (IOException e) { 133 } 134 } while (!myServerSocket.isClosed()); 135 } 136 }); 137 myThread.setDaemon(true); 138 myThread.setName("NanoHttpd Main Listener"); 139 myThread.start(); 140 } 141 142 /** 143 * Stop the server. 144 */ 145 public void stop() { 146 try { 147 safeClose(myServerSocket); 148 myThread.join(); 149 } catch (Exception e) { 150 e.printStackTrace(); 151 } 152 } 153 154 /** 155 * Override this to customize the server. 156 * <p/> 157 * <p/> 158 * (By default, this delegates to serveFile() and allows directory listing.) 159 * 160 * @param uri Percent-decoded URI without parameters, for example "/index.cgi" 161 * @param method "GET", "POST" etc. 162 * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. 163 * @param headers Header entries, percent decoded 164 * @return HTTP response, see class Response for details 165 */ 166 public abstract Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, 167 Map<String, String> files); 168 169 /** 170 * Override this to customize the server. 171 * <p/> 172 * <p/> 173 * (By default, this delegates to serveFile() and allows directory listing.) 174 * 175 * @param session The HTTP session 176 * @return HTTP response, see class Response for details 177 */ 178 protected Response serve(HTTPSession session) { 179 Map<String, String> files = new HashMap<String, String>(); 180 181 try { 182 session.parseBody(files); 183 } catch (IOException ioe) { 184 return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 185 } catch (ResponseException re) { 186 return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); 187 } 188 189 String uri = session.getUri(); 190 Method method = session.getMethod(); 191 Map<String, String> parms = session.getParms(); 192 Map<String, String> headers = session.getHeaders(); 193 return serve(uri, method, headers, parms, files); 194 } 195 196 /** 197 * Decode percent encoded <code>String</code> values. 198 * @param str the percent encoded <code>String</code> 199 * @return expanded form of the input, for example "foo%20bar" becomes "foo bar" 200 */ 201 protected String decodePercent(String str) { 202 String decoded = null; 203 try { 204 decoded = URLDecoder.decode(str, "UTF8"); 205 } catch (UnsupportedEncodingException ignored) { 206 } 207 return decoded; 208 } 209 210 /** 211 * Decode parameters from a URL, handing the case where a single parameter name might have been 212 * supplied several times, by return lists of values. In general these lists will contain a single 213 * element. 214 * 215 * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method. 216 * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). 217 */ 218 protected Map<String, List<String>> decodeParameters(Map<String, String> parms) { 219 return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER)); 220 } 221 222 /** 223 * Decode parameters from a URL, handing the case where a single parameter name might have been 224 * supplied several times, by return lists of values. In general these lists will contain a single 225 * element. 226 * 227 * @param queryString a query string pulled from the URL. 228 * @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied). 229 */ 230 protected Map<String, List<String>> decodeParameters(String queryString) { 231 Map<String, List<String>> parms = new HashMap<String, List<String>>(); 232 if (queryString != null) { 233 StringTokenizer st = new StringTokenizer(queryString, "&"); 234 while (st.hasMoreTokens()) { 235 String e = st.nextToken(); 236 int sep = e.indexOf('='); 237 String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 238 if (!parms.containsKey(propertyName)) { 239 parms.put(propertyName, new ArrayList<String>()); 240 } 241 String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null; 242 if (propertyValue != null) { 243 parms.get(propertyName).add(propertyValue); 244 } 245 } 246 } 247 return parms; 248 } 249 250 /** 251 * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value. 252 */ 253 public enum Method { 254 GET, PUT, POST, DELETE, HEAD; 255 256 static Method lookup(String method) { 257 for (Method m : Method.values()) { 258 if (m.toString().equalsIgnoreCase(method)) { 259 return m; 260 } 261 } 262 return null; 263 } 264 } 265 266 // ------------------------------------------------------------------------------- // 267 // 268 // Threading Strategy. 269 // 270 // ------------------------------------------------------------------------------- // 271 272 /** 273 * Pluggable strategy for asynchronously executing requests. 274 */ 275 private AsyncRunner asyncRunner; 276 277 /** 278 * Pluggable strategy for asynchronously executing requests. 279 * @param asyncRunner new strategy for handling threads. 280 */ 281 public void setAsyncRunner(AsyncRunner asyncRunner) { 282 this.asyncRunner = asyncRunner; 283 } 284 285 /** 286 * Pluggable strategy for asynchronously executing requests. 287 */ 288 public interface AsyncRunner { 289 void exec(Runnable code); 290 } 291 292 /** 293 * Default threading strategy for NanoHttpd. 294 * 295 * <p>By default, the server spawns a new Thread for every incoming request. These are set 296 * to <i>daemon</i> status, and named according to the request number. The name is 297 * useful when profiling the application.</p> 298 */ 299 public static class DefaultAsyncRunner implements AsyncRunner { 300 private long requestCount; 301 @Override 302 public void exec(Runnable code) { 303 ++requestCount; 304 Thread t = new Thread(code); 305 t.setDaemon(true); 306 t.setName("NanoHttpd Request Processor (#" + requestCount + ")"); 307 t.start(); 308 } 309 } 310 311 // ------------------------------------------------------------------------------- // 312 // 313 // Temp file handling strategy. 314 // 315 // ------------------------------------------------------------------------------- // 316 317 /** 318 * Pluggable strategy for creating and cleaning up temporary files. 319 */ 320 private TempFileManagerFactory tempFileManagerFactory; 321 322 /** 323 * Pluggable strategy for creating and cleaning up temporary files. 324 * @param tempFileManagerFactory new strategy for handling temp files. 325 */ 326 public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 327 this.tempFileManagerFactory = tempFileManagerFactory; 328 } 329 330 /** 331 * Factory to create temp file managers. 332 */ 333 public interface TempFileManagerFactory { 334 TempFileManager create(); 335 } 336 337 /** 338 * Temp file manager. 339 * 340 * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup 341 * temporary files created as a result of handling the request.</p> 342 */ 343 public interface TempFileManager { 344 TempFile createTempFile() throws Exception; 345 346 void clear(); 347 } 348 349 /** 350 * A temp file. 351 * 352 * <p>Temp files are responsible for managing the actual temporary storage and cleaning 353 * themselves up when no longer needed.</p> 354 */ 355 public interface TempFile { 356 OutputStream open() throws Exception; 357 358 void delete() throws Exception; 359 360 String getName(); 361 } 362 363 /** 364 * Default strategy for creating and cleaning up temporary files. 365 */ 366 private class DefaultTempFileManagerFactory implements TempFileManagerFactory { 367 @Override 368 public TempFileManager create() { 369 return new DefaultTempFileManager(); 370 } 371 } 372 373 /** 374 * Default strategy for creating and cleaning up temporary files. 375 * 376 * <p></p>This class stores its files in the standard location (that is, 377 * wherever <code>java.io.tmpdir</code> points to). Files are added 378 * to an internal list, and deleted when no longer needed (that is, 379 * when <code>clear()</code> is invoked at the end of processing a 380 * request).</p> 381 */ 382 public static class DefaultTempFileManager implements TempFileManager { 383 private final String tmpdir; 384 private final List<TempFile> tempFiles; 385 386 public DefaultTempFileManager() { 387 tmpdir = System.getProperty("java.io.tmpdir"); 388 tempFiles = new ArrayList<TempFile>(); 389 } 390 391 @Override 392 public TempFile createTempFile() throws Exception { 393 DefaultTempFile tempFile = new DefaultTempFile(tmpdir); 394 tempFiles.add(tempFile); 395 return tempFile; 396 } 397 398 @Override 399 public void clear() { 400 for (TempFile file : tempFiles) { 401 try { 402 file.delete(); 403 } catch (Exception ignored) { 404 } 405 } 406 tempFiles.clear(); 407 } 408 } 409 410 /** 411 * Default strategy for creating and cleaning up temporary files. 412 * 413 * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in 414 * the directory specified.</p> 415 */ 416 public static class DefaultTempFile implements TempFile { 417 private File file; 418 private OutputStream fstream; 419 420 public DefaultTempFile(String tempdir) throws IOException { 421 file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); 422 fstream = new FileOutputStream(file); 423 } 424 425 @Override 426 public OutputStream open() throws Exception { 427 return fstream; 428 } 429 430 @Override 431 public void delete() throws Exception { 432 safeClose(fstream); 433 file.delete(); 434 } 435 436 @Override 437 public String getName() { 438 return file.getAbsolutePath(); 439 } 440 } 441 442 // ------------------------------------------------------------------------------- // 443 444 /** 445 * HTTP response. Return one of these from serve(). 446 */ 447 public static class Response { 448 /** 449 * HTTP status code after processing, e.g. "200 OK", HTTP_OK 450 */ 451 private Status status; 452 /** 453 * MIME type of content, e.g. "text/html" 454 */ 455 private String mimeType; 456 /** 457 * Data of the response, may be null. 458 */ 459 private InputStream data; 460 /** 461 * Headers for the HTTP response. Use addHeader() to add lines. 462 */ 463 private Map<String, String> header = new HashMap<String, String>(); 464 /** 465 * The request method that spawned this response. 466 */ 467 private Method requestMethod; 468 /** 469 * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message 470 */ 471 public Response(String msg) { 472 this(Status.OK, MIME_HTML, msg); 473 } 474 475 /** 476 * Basic constructor. 477 */ 478 public Response(Status status, String mimeType, InputStream data) { 479 this.status = status; 480 this.mimeType = mimeType; 481 this.data = data; 482 } 483 484 /** 485 * Convenience method that makes an InputStream out of given text. 486 */ 487 public Response(Status status, String mimeType, String txt) { 488 this.status = status; 489 this.mimeType = mimeType; 490 try { 491 this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null; 492 } catch (java.io.UnsupportedEncodingException uee) { 493 uee.printStackTrace(); 494 } 495 } 496 497 /** 498 * Adds given line to the header. 499 */ 500 public void addHeader(String name, String value) { 501 header.put(name, value); 502 } 503 504 /** 505 * Sends given response to the socket. 506 */ 507 private void send(OutputStream outputStream) { 508 String mime = mimeType; 509 SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 510 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); 511 512 try { 513 if (status == null) { 514 throw new Error("sendResponse(): Status can't be null."); 515 } 516 PrintWriter pw = new PrintWriter(outputStream); 517 pw.print("HTTP/1.0 " + status.getDescription() + " \r\n"); 518 519 if (mime != null) { 520 pw.print("Content-Type: " + mime + "\r\n"); 521 } 522 523 if (header == null || header.get("Date") == null) { 524 pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); 525 } 526 527 if (header != null) { 528 for (String key : header.keySet()) { 529 String value = header.get(key); 530 pw.print(key + ": " + value + "\r\n"); 531 } 532 } 533 534 pw.print("\r\n"); 535 pw.flush(); 536 537 if (requestMethod != Method.HEAD && data != null) { 538 int pending = data.available(); // This is to support partial sends, see serveFile() 539 int BUFFER_SIZE = 16 * 1024; 540 byte[] buff = new byte[BUFFER_SIZE]; 541 while (pending > 0) { 542 int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending)); 543 if (read <= 0) { 544 break; 545 } 546 outputStream.write(buff, 0, read); 547 548 pending -= read; 549 } 550 } 551 outputStream.flush(); 552 safeClose(outputStream); 553 safeClose(data); 554 } catch (IOException ioe) { 555 // Couldn't write? No can do. 556 } 557 } 558 559 public Status getStatus() { 560 return status; 561 } 562 563 public void setStatus(Status status) { 564 this.status = status; 565 } 566 567 public String getMimeType() { 568 return mimeType; 569 } 570 571 public void setMimeType(String mimeType) { 572 this.mimeType = mimeType; 573 } 574 575 public InputStream getData() { 576 return data; 577 } 578 579 public void setData(InputStream data) { 580 this.data = data; 581 } 582 583 public Method getRequestMethod() { 584 return requestMethod; 585 } 586 587 public void setRequestMethod(Method requestMethod) { 588 this.requestMethod = requestMethod; 589 } 590 591 /** 592 * Some HTTP response status codes 593 */ 594 public enum Status { 595 OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301, 596 "Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, 597 "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), RANGE_NOT_SATISFIABLE(416, 598 "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error"); 599 private final int requestStatus; 600 private final String description; 601 602 Status(int requestStatus, String description) { 603 this.requestStatus = requestStatus; 604 this.description = description; 605 } 606 607 public int getRequestStatus() { 608 return this.requestStatus; 609 } 610 611 public String getDescription() { 612 return "" + this.requestStatus + " " + description; 613 } 614 } 615 } 616 617 /** 618 * Handles one session, i.e. parses the HTTP request and returns the response. 619 */ 620 protected class HTTPSession { 621 public static final int BUFSIZE = 8192; 622 623 private final TempFileManager tempFileManager; 624 private final InputStream inputStream; 625 private final OutputStream outputStream; 626 627 private byte[] buf; 628 private int splitbyte; 629 private int rlen; 630 631 private String uri; 632 private Method method; 633 private Map<String, String> parms; 634 private Map<String, String> headers; 635 636 public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { 637 this.tempFileManager = tempFileManager; 638 this.inputStream = inputStream; 639 this.outputStream = outputStream; 640 } 641 642 public void execute() { 643 try { 644 // Read the first 8192 bytes. 645 // The full header should fit in here. 646 // Apache's default header limit is 8KB. 647 // Do NOT assume that a single read will get the entire header at once! 648 buf = new byte[BUFSIZE]; 649 { 650 int read = inputStream.read(buf, 0, BUFSIZE); 651 while (read > 0) { 652 rlen += read; 653 splitbyte = findHeaderEnd(buf, rlen); 654 if (splitbyte > 0) 655 break; 656 read = inputStream.read(buf, rlen, BUFSIZE - rlen); 657 } 658 } 659 660 parms = new HashMap<String, String>(); 661 headers = new HashMap<String, String>(); 662 663 // Create a BufferedReader for parsing the header. 664 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); 665 666 // Decode the header into parms and header java properties 667 Map<String, String> pre = new HashMap<String, String>(); 668 decodeHeader(hin, pre, parms, headers); 669 670 method = Method.lookup(pre.get("method")); 671 if (method == null) { 672 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); 673 } 674 675 uri = pre.get("uri"); 676 677 // Ok, now do the serve() 678 Response r = serve(this); 679 if (r == null) { 680 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); 681 } else { 682 r.setRequestMethod(method); 683 r.send(outputStream); 684 } 685 } catch (IOException ioe) { 686 Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 687 r.send(outputStream); 688 } catch (ResponseException re) { 689 Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); 690 r.send(outputStream); 691 } 692 } 693 694 private void parseBody(Map<String, String> files) throws IOException, ResponseException { 695 696 RandomAccessFile randomAccessFile = null; 697 BufferedReader in = null; 698 try { 699 700 long size = extractContentLength(headers); 701 702 // Write the part of body already read to ByteArrayOutputStream f 703 randomAccessFile = getTmpBucket(); 704 if (splitbyte < rlen) { 705 randomAccessFile.write(buf, splitbyte, rlen - splitbyte); 706 } 707 708 // While Firefox sends on the first read all the data fitting 709 // our buffer, Chrome and Opera send only the headers even if 710 // there is data for the body. We do some magic here to find 711 // out whether we have already consumed part of body, if we 712 // have reached the end of the data to be sent or we should 713 // expect the first byte of the body at the next read. 714 if (splitbyte < rlen) { 715 size -= rlen - splitbyte + 1; 716 } else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl) { 717 size = 0; 718 } 719 720 // Now read all the body and write it to f 721 buf = new byte[512]; 722 splitbyte = 0; 723 rlen = 0; 724 while (rlen >= 0 && size > 0) { 725 rlen = inputStream.read(buf, 0, 512); 726 size -= rlen; 727 if (rlen > 0) { 728 randomAccessFile.write(buf, 0, rlen); 729 } 730 } 731 732 // Get the raw body as a byte [] 733 ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); 734 randomAccessFile.seek(0); 735 736 // Create a BufferedReader for easily reading it as string. 737 InputStream bin = new FileInputStream(randomAccessFile.getFD()); 738 in = new BufferedReader(new InputStreamReader(bin)); 739 740 // If the method is POST, there may be parameters 741 // in data section, too, read it: 742 if (Method.POST.equals(method)) { 743 String contentType = ""; 744 String contentTypeHeader = headers.get("content-type"); 745 746 StringTokenizer st = null; 747 if (contentTypeHeader != null) { 748 st = new StringTokenizer(contentTypeHeader, ",; "); 749 if (st.hasMoreTokens()) { 750 contentType = st.nextToken(); 751 } 752 } 753 754 if ("multipart/form-data".equalsIgnoreCase(contentType)) { 755 // Handle multipart/form-data 756 if (!st.hasMoreTokens()) { 757 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); 758 } 759 760 String boundaryStartString = "boundary="; 761 int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); 762 String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); 763 if (boundary.startsWith("\"") && boundary.startsWith("\"")) { 764 boundary = boundary.substring(1, boundary.length() - 1); 765 } 766 767 decodeMultipartData(boundary, fbuf, in, parms, files); 768 } else { 769 // Handle application/x-www-form-urlencoded 770 String postLine = ""; 771 char pbuf[] = new char[512]; 772 int read = in.read(pbuf); 773 while (read >= 0 && !postLine.endsWith("\r\n")) { 774 postLine += String.valueOf(pbuf, 0, read); 775 read = in.read(pbuf); 776 } 777 postLine = postLine.trim(); 778 decodeParms(postLine, parms); 779 } 780 } else if (Method.PUT.equals(method)) { 781 files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); 782 } 783 } finally { 784 safeClose(randomAccessFile); 785 tempFileManager.clear(); 786 safeClose(in); 787 } 788 } 789 790 private long extractContentLength(Map<String, String> headers) { 791 long size = 0x7FFFFFFFFFFFFFFFl; 792 String contentLength = headers.get("content-length"); 793 if (contentLength != null) { 794 try { 795 size = Integer.parseInt(contentLength); 796 } catch (NumberFormatException ex) { 797 ex.printStackTrace(); 798 } 799 } 800 return size; 801 } 802 803 public String getHeader(String key) { 804 return headers.get(key.toLowerCase()); 805 } 806 807 /** 808 * Decodes the sent headers and loads the data into Key/value pairs 809 */ 810 private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) 811 throws ResponseException { 812 try { 813 // Read the request line 814 String inLine = in.readLine(); 815 if (inLine == null) { 816 return; 817 } 818 819 StringTokenizer st = new StringTokenizer(inLine); 820 if (!st.hasMoreTokens()) { 821 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); 822 } 823 824 pre.put("method", st.nextToken()); 825 826 if (!st.hasMoreTokens()) { 827 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); 828 } 829 830 String uri = st.nextToken(); 831 832 // Decode parameters from the URI 833 int qmi = uri.indexOf('?'); 834 if (qmi >= 0) { 835 decodeParms(uri.substring(qmi + 1), parms); 836 uri = decodePercent(uri.substring(0, qmi)); 837 } else { 838 uri = decodePercent(uri); 839 } 840 841 // If there's another token, it's protocol version, 842 // followed by HTTP headers. Ignore version but parse headers. 843 // NOTE: this now forces header names lowercase since they are 844 // case insensitive and vary by client. 845 if (st.hasMoreTokens()) { 846 String line = in.readLine(); 847 while (line != null && line.trim().length() > 0) { 848 int p = line.indexOf(':'); 849 if (p >= 0) 850 headers.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim()); 851 line = in.readLine(); 852 } 853 } 854 855 pre.put("uri", uri); 856 } catch (IOException ioe) { 857 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 858 } 859 } 860 861 /** 862 * Decodes the Multipart Body data and put it into Key/Value pairs. 863 */ 864 private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms, 865 Map<String, String> files) throws ResponseException { 866 try { 867 int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); 868 int boundarycount = 1; 869 String mpline = in.readLine(); 870 while (mpline != null) { 871 if (!mpline.contains(boundary)) { 872 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html"); 873 } 874 boundarycount++; 875 Map<String, String> item = new HashMap<String, String>(); 876 mpline = in.readLine(); 877 while (mpline != null && mpline.trim().length() > 0) { 878 int p = mpline.indexOf(':'); 879 if (p != -1) { 880 item.put(mpline.substring(0, p).trim().toLowerCase(), mpline.substring(p + 1).trim()); 881 } 882 mpline = in.readLine(); 883 } 884 if (mpline != null) { 885 String contentDisposition = item.get("content-disposition"); 886 if (contentDisposition == null) { 887 throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html"); 888 } 889 StringTokenizer st = new StringTokenizer(contentDisposition, "; "); 890 Map<String, String> disposition = new HashMap<String, String>(); 891 while (st.hasMoreTokens()) { 892 String token = st.nextToken(); 893 int p = token.indexOf('='); 894 if (p != -1) { 895 disposition.put(token.substring(0, p).trim().toLowerCase(), token.substring(p + 1).trim()); 896 } 897 } 898 String pname = disposition.get("name"); 899 pname = pname.substring(1, pname.length() - 1); 900 901 String value = ""; 902 if (item.get("content-type") == null) { 903 while (mpline != null && !mpline.contains(boundary)) { 904 mpline = in.readLine(); 905 if (mpline != null) { 906 int d = mpline.indexOf(boundary); 907 if (d == -1) { 908 value += mpline; 909 } else { 910 value += mpline.substring(0, d - 2); 911 } 912 } 913 } 914 } else { 915 if (boundarycount > bpositions.length) { 916 throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); 917 } 918 int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); 919 String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); 920 files.put(pname, path); 921 value = disposition.get("filename"); 922 value = value.substring(1, value.length() - 1); 923 do { 924 mpline = in.readLine(); 925 } while (mpline != null && !mpline.contains(boundary)); 926 } 927 parms.put(pname, value); 928 } 929 } 930 } catch (IOException ioe) { 931 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 932 } 933 } 934 935 /** 936 * Find byte index separating header from body. It must be the last byte of the first two sequential new lines. 937 */ 938 private int findHeaderEnd(final byte[] buf, int rlen) { 939 int splitbyte = 0; 940 while (splitbyte + 3 < rlen) { 941 if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { 942 return splitbyte + 4; 943 } 944 splitbyte++; 945 } 946 return 0; 947 } 948 949 /** 950 * Find the byte positions where multipart boundaries start. 951 */ 952 private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { 953 int matchcount = 0; 954 int matchbyte = -1; 955 List<Integer> matchbytes = new ArrayList<Integer>(); 956 for (int i=0; i<b.limit(); i++) { 957 if (b.get(i) == boundary[matchcount]) { 958 if (matchcount == 0) 959 matchbyte = i; 960 matchcount++; 961 if (matchcount == boundary.length) { 962 matchbytes.add(matchbyte); 963 matchcount = 0; 964 matchbyte = -1; 965 } 966 } else { 967 i -= matchcount; 968 matchcount = 0; 969 matchbyte = -1; 970 } 971 } 972 int[] ret = new int[matchbytes.size()]; 973 for (int i = 0; i < ret.length; i++) { 974 ret[i] = matchbytes.get(i); 975 } 976 return ret; 977 } 978 979 /** 980 * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned. 981 */ 982 private String saveTmpFile(ByteBuffer b, int offset, int len) { 983 String path = ""; 984 if (len > 0) { 985 FileOutputStream outputStream = null; 986 try { 987 TempFile tempFile = tempFileManager.createTempFile(); 988 ByteBuffer src = b.duplicate(); 989 outputStream = new FileOutputStream(tempFile.getName()); 990 FileChannel dest = outputStream.getChannel(); 991 src.position(offset).limit(offset + len); 992 dest.write(src.slice()); 993 path = tempFile.getName(); 994 } catch (Exception e) { // Catch exception if any 995 System.err.println("Error: " + e.getMessage()); 996 } finally { 997 safeClose(outputStream); 998 } 999 } 1000 return path; 1001 } 1002 1003 private RandomAccessFile getTmpBucket() { 1004 try { 1005 TempFile tempFile = tempFileManager.createTempFile(); 1006 return new RandomAccessFile(tempFile.getName(), "rw"); 1007 } catch (Exception e) { 1008 System.err.println("Error: " + e.getMessage()); 1009 } 1010 return null; 1011 } 1012 1013 /** 1014 * It returns the offset separating multipart file headers from the file's data. 1015 */ 1016 private int stripMultipartHeaders(ByteBuffer b, int offset) { 1017 int i; 1018 for (i=offset; i<b.limit(); i++) { 1019 if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { 1020 break; 1021 } 1022 } 1023 return i + 1; 1024 } 1025 1026 /** 1027 * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and 1028 * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map. 1029 */ 1030 private void decodeParms(String parms, Map<String, String> p) { 1031 if (parms == null) { 1032 p.put(QUERY_STRING_PARAMETER, ""); 1033 return; 1034 } 1035 1036 p.put(QUERY_STRING_PARAMETER, parms); 1037 StringTokenizer st = new StringTokenizer(parms, "&"); 1038 while (st.hasMoreTokens()) { 1039 String e = st.nextToken(); 1040 int sep = e.indexOf('='); 1041 if (sep >= 0) { 1042 p.put(decodePercent(e.substring(0, sep)).trim(), 1043 decodePercent(e.substring(sep + 1))); 1044 } else { 1045 p.put(decodePercent(e).trim(), ""); 1046 } 1047 } 1048 } 1049 1050 public final Map<String, String> getParms() { 1051 return parms; 1052 } 1053 1054 public final Map<String, String> getHeaders() { 1055 return headers; 1056 } 1057 1058 public final String getUri() { 1059 return uri; 1060 } 1061 1062 public final Method getMethod() { 1063 return method; 1064 } 1065 1066 public final InputStream getInputStream() { 1067 return inputStream; 1068 } 1069 } 1070 1071 private static final class ResponseException extends Exception { 1072 1073 private final Response.Status status; 1074 1075 public ResponseException(Response.Status status, String message) { 1076 super(message); 1077 this.status = status; 1078 } 1079 1080 public ResponseException(Response.Status status, String message, Exception e) { 1081 super(message, e); 1082 this.status = status; 1083 } 1084 1085 public Response.Status getStatus() { 1086 return status; 1087 } 1088 } 1089 1090 private static final void safeClose(ServerSocket serverSocket) { 1091 if (serverSocket != null) { 1092 try { 1093 serverSocket.close(); 1094 } 1095 catch(IOException e) { 1096 } 1097 } 1098 } 1099 1100 private static final void safeClose(Socket socket) { 1101 if (socket != null) { 1102 try { 1103 socket.close(); 1104 } 1105 catch(IOException e) { 1106 } 1107 } 1108 } 1109 1110 private static final void safeClose(Closeable closeable) { 1111 if (closeable != null) { 1112 try { 1113 closeable.close(); 1114 } 1115 catch(IOException e) { 1116 } 1117 } 1118 } 1119 1120 public final int getListeningPort() { 1121 return myServerSocket == null ? -1 : myServerSocket.getLocalPort(); 1122 } 1123 1124 public final boolean wasStarted() { 1125 return myServerSocket != null && myThread != null; 1126 } 1127 1128 public final boolean isAlive() { 1129 return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive(); 1130 } 1131} 1132