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 | } |