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