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