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