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