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