Commit | Line | Data |
---|---|---|
f433d153 NR |
1 | package 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 | ||
36 | import java.io.BufferedInputStream; | |
37 | import java.io.BufferedReader; | |
38 | import java.io.BufferedWriter; | |
39 | import java.io.ByteArrayInputStream; | |
40 | import java.io.ByteArrayOutputStream; | |
41 | import java.io.Closeable; | |
42 | import java.io.DataOutput; | |
43 | import java.io.DataOutputStream; | |
44 | import java.io.File; | |
45 | import java.io.FileOutputStream; | |
46 | import java.io.FilterOutputStream; | |
47 | import java.io.IOException; | |
48 | import java.io.InputStream; | |
49 | import java.io.InputStreamReader; | |
50 | import java.io.OutputStream; | |
51 | import java.io.OutputStreamWriter; | |
52 | import java.io.PrintWriter; | |
53 | import java.io.RandomAccessFile; | |
54 | import java.io.UnsupportedEncodingException; | |
55 | import java.net.InetAddress; | |
56 | import java.net.InetSocketAddress; | |
57 | import java.net.ServerSocket; | |
58 | import java.net.Socket; | |
59 | import java.net.SocketException; | |
60 | import java.net.SocketTimeoutException; | |
61 | import java.net.URL; | |
62 | import java.net.URLDecoder; | |
63 | import java.nio.ByteBuffer; | |
64 | import java.nio.channels.FileChannel; | |
65 | import java.nio.charset.Charset; | |
66 | import java.nio.charset.CharsetEncoder; | |
67 | import java.security.KeyStore; | |
68 | import java.text.SimpleDateFormat; | |
69 | import java.util.ArrayList; | |
70 | import java.util.Calendar; | |
71 | import java.util.Collections; | |
72 | import java.util.Date; | |
73 | import java.util.Enumeration; | |
74 | import java.util.HashMap; | |
75 | import java.util.Iterator; | |
76 | import java.util.List; | |
77 | import java.util.Locale; | |
78 | import java.util.Map; | |
79 | import java.util.Map.Entry; | |
80 | import java.util.Properties; | |
81 | import java.util.StringTokenizer; | |
82 | import java.util.TimeZone; | |
83 | import java.util.logging.Level; | |
84 | import java.util.logging.Logger; | |
85 | import java.util.regex.Matcher; | |
86 | import java.util.regex.Pattern; | |
87 | import java.util.zip.GZIPOutputStream; | |
88 | ||
89 | import javax.net.ssl.KeyManager; | |
90 | import javax.net.ssl.KeyManagerFactory; | |
91 | import javax.net.ssl.SSLContext; | |
92 | import javax.net.ssl.SSLException; | |
93 | import javax.net.ssl.SSLServerSocket; | |
94 | import javax.net.ssl.SSLServerSocketFactory; | |
95 | import javax.net.ssl.TrustManagerFactory; | |
96 | ||
97 | import be.nikiroo.utils.NanoHTTPD.Response.IStatus; | |
98 | import 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 | */ | |
151 | public 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<String></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<String></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 | } |