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