1 package fi.iki.elonen;
2 
3 /*
4  * #%L
5  * NanoHttpd-Webserver
6  * %%
7  * Copyright (C) 2012 - 2015 nanohttpd
8  * %%
9  * Redistribution and use in source and binary forms, with or without modification,
10  * are permitted provided that the following conditions are met:
11  *
12  * 1. Redistributions of source code must retain the above copyright notice, this
13  *    list of conditions and the following disclaimer.
14  *
15  * 2. Redistributions in binary form must reproduce the above copyright notice,
16  *    this list of conditions and the following disclaimer in the documentation
17  *    and/or other materials provided with the distribution.
18  *
19  * 3. Neither the name of the nanohttpd nor the names of its contributors
20  *    may be used to endorse or promote products derived from this software without
21  *    specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
26  * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
31  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32  * OF THE POSSIBILITY OF SUCH DAMAGE.
33  * #L%
34  */
35 import java.io.ByteArrayOutputStream;
36 import java.io.File;
37 import java.io.FileInputStream;
38 import java.io.FileNotFoundException;
39 import java.io.FilenameFilter;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.UnsupportedEncodingException;
43 import java.net.URLEncoder;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.Iterator;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.ServiceLoader;
52 import java.util.StringTokenizer;
53 
54 import fi.iki.elonen.NanoHTTPD.Response.IStatus;
55 import fi.iki.elonen.util.ServerRunner;
56 
57 public class SimpleWebServer extends NanoHTTPD {
58 
59     /**
60      * Default Index file names.
61      */
62     @SuppressWarnings("serial")
63     public static final List<String> INDEX_FILE_NAMES = new ArrayList<String>() {
64 
65         {
66             add("index.html");
67             add("index.htm");
68         }
69     };
70 
71     /**
72      * The distribution licence
73      */
74     private static final String LICENCE;
75     static {
mimeTypes()76         mimeTypes();
77         InputStream stream = SimpleWebServer.class.getResourceAsStream("/LICENSE.txt");
78         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
79         byte[] buffer = new byte[1024];
80         int count;
81         String text;
82         try {
83             while ((count = stream.read(buffer)) >= 0) {
bytes.write(buffer, 0, count)84                 bytes.write(buffer, 0, count);
85             }
86             text = bytes.toString("UTF-8");
87         } catch (IOException e) {
88             text = "unknown";
89         }
90         LICENCE = text;
91     }
92 
93     private static Map<String, WebServerPlugin> mimeTypeHandlers = new HashMap<String, WebServerPlugin>();
94 
95     /**
96      * Starts as a standalone file server and waits for Enter.
97      */
main(String[] args)98     public static void main(String[] args) {
99         // Defaults
100         int port = 8080;
101 
102         String host = null; // bind to all interfaces by default
103         List<File> rootDirs = new ArrayList<File>();
104         boolean quiet = false;
105         String cors = null;
106         Map<String, String> options = new HashMap<String, String>();
107 
108         // Parse command-line, with short and long versions of the options.
109         for (int i = 0; i < args.length; ++i) {
110             if (args[i].equalsIgnoreCase("-h") || args[i].equalsIgnoreCase("--host")) {
111                 host = args[i + 1];
112             } else if (args[i].equalsIgnoreCase("-p") || args[i].equalsIgnoreCase("--port")) {
113                 port = Integer.parseInt(args[i + 1]);
114             } else if (args[i].equalsIgnoreCase("-q") || args[i].equalsIgnoreCase("--quiet")) {
115                 quiet = true;
116             } else if (args[i].equalsIgnoreCase("-d") || args[i].equalsIgnoreCase("--dir")) {
117                 rootDirs.add(new File(args[i + 1]).getAbsoluteFile());
118             } else if (args[i].startsWith("--cors")) {
119                 cors = "*";
120                 int equalIdx = args[i].indexOf('=');
121                 if (equalIdx > 0) {
122                     cors = args[i].substring(equalIdx + 1);
123                 }
124             } else if (args[i].equalsIgnoreCase("--licence")) {
125                 System.out.println(SimpleWebServer.LICENCE + "\n");
126             } else if (args[i].startsWith("-X:")) {
127                 int dot = args[i].indexOf('=');
128                 if (dot > 0) {
129                     String name = args[i].substring(0, dot);
130                     String value = args[i].substring(dot + 1, args[i].length());
131                     options.put(name, value);
132                 }
133             }
134         }
135 
136         if (rootDirs.isEmpty()) {
137             rootDirs.add(new File(".").getAbsoluteFile());
138         }
139         options.put("host", host);
140         options.put("port", "" + port);
141         options.put("quiet", String.valueOf(quiet));
142         StringBuilder sb = new StringBuilder();
143         for (File dir : rootDirs) {
144             if (sb.length() > 0) {
145                 sb.append(":");
146             }
147             try {
148                 sb.append(dir.getCanonicalPath());
149             } catch (IOException ignored) {
150             }
151         }
152         options.put("home", sb.toString());
153         ServiceLoader<WebServerPluginInfo> serviceLoader = ServiceLoader.load(WebServerPluginInfo.class);
154         for (WebServerPluginInfo info : serviceLoader) {
155             String[] mimeTypes = info.getMimeTypes();
156             for (String mime : mimeTypes) {
157                 String[] indexFiles = info.getIndexFilesForMimeType(mime);
158                 if (!quiet) {
159                     System.out.print("# Found plugin for Mime type: \"" + mime + "\"");
160                     if (indexFiles != null) {
161                         System.out.print(" (serving index files: ");
162                         for (String indexFile : indexFiles) {
163                             System.out.print(indexFile + " ");
164                         }
165                     }
166                     System.out.println(").");
167                 }
168                 registerPluginForMimeType(indexFiles, mime, info.getWebServerPlugin(mime), options);
169             }
170         }
171         ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet, cors));
172     }
173 
registerPluginForMimeType(String[] indexFiles, String mimeType, WebServerPlugin plugin, Map<String, String> commandLineOptions)174     protected static void registerPluginForMimeType(String[] indexFiles, String mimeType, WebServerPlugin plugin, Map<String, String> commandLineOptions) {
175         if (mimeType == null || plugin == null) {
176             return;
177         }
178 
179         if (indexFiles != null) {
180             for (String filename : indexFiles) {
181                 int dot = filename.lastIndexOf('.');
182                 if (dot >= 0) {
183                     String extension = filename.substring(dot + 1).toLowerCase();
184                     mimeTypes().put(extension, mimeType);
185                 }
186             }
187             SimpleWebServer.INDEX_FILE_NAMES.addAll(Arrays.asList(indexFiles));
188         }
189         SimpleWebServer.mimeTypeHandlers.put(mimeType, plugin);
190         plugin.initialize(commandLineOptions);
191     }
192 
193     private final boolean quiet;
194 
195     private final String cors;
196 
197     protected List<File> rootDirs;
198 
SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors)199     public SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors) {
200         this(host, port, Collections.singletonList(wwwroot), quiet, cors);
201     }
202 
SimpleWebServer(String host, int port, File wwwroot, boolean quiet)203     public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
204         this(host, port, Collections.singletonList(wwwroot), quiet, null);
205     }
206 
SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet)207     public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) {
208         this(host, port, wwwroots, quiet, null);
209     }
210 
SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet, String cors)211     public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet, String cors) {
212         super(host, port);
213         this.quiet = quiet;
214         this.cors = cors;
215         this.rootDirs = new ArrayList<File>(wwwroots);
216 
217         init();
218     }
219 
canServeUri(String uri, File homeDir)220     private boolean canServeUri(String uri, File homeDir) {
221         boolean canServeUri;
222         File f = new File(homeDir, uri);
223         canServeUri = f.exists();
224         if (!canServeUri) {
225             WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(getMimeTypeForFile(uri));
226             if (plugin != null) {
227                 canServeUri = plugin.canServeUri(uri, homeDir);
228             }
229         }
230         return canServeUri;
231     }
232 
233     /**
234      * URL-encodes everything between "/"-characters. Encodes spaces as '%20'
235      * instead of '+'.
236      */
encodeUri(String uri)237     private String encodeUri(String uri) {
238         String newUri = "";
239         StringTokenizer st = new StringTokenizer(uri, "/ ", true);
240         while (st.hasMoreTokens()) {
241             String tok = st.nextToken();
242             if (tok.equals("/")) {
243                 newUri += "/";
244             } else if (tok.equals(" ")) {
245                 newUri += "%20";
246             } else {
247                 try {
248                     newUri += URLEncoder.encode(tok, "UTF-8");
249                 } catch (UnsupportedEncodingException ignored) {
250                 }
251             }
252         }
253         return newUri;
254     }
255 
findIndexFileInDirectory(File directory)256     private String findIndexFileInDirectory(File directory) {
257         for (String fileName : SimpleWebServer.INDEX_FILE_NAMES) {
258             File indexFile = new File(directory, fileName);
259             if (indexFile.isFile()) {
260                 return fileName;
261             }
262         }
263         return null;
264     }
265 
getForbiddenResponse(String s)266     protected Response getForbiddenResponse(String s) {
267         return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
268     }
269 
getInternalErrorResponse(String s)270     protected Response getInternalErrorResponse(String s) {
271         return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
272     }
273 
getNotFoundResponse()274     protected Response getNotFoundResponse() {
275         return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
276     }
277 
278     /**
279      * Used to initialize and customize the server.
280      */
init()281     public void init() {
282     }
283 
listDirectory(String uri, File f)284     protected String listDirectory(String uri, File f) {
285         String heading = "Directory " + uri;
286         StringBuilder msg =
287                 new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n"
288                         + "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>");
289 
290         String up = null;
291         if (uri.length() > 1) {
292             String u = uri.substring(0, uri.length() - 1);
293             int slash = u.lastIndexOf('/');
294             if (slash >= 0 && slash < u.length()) {
295                 up = uri.substring(0, slash + 1);
296             }
297         }
298 
299         List<String> files = Arrays.asList(f.list(new FilenameFilter() {
300 
301             @Override
302             public boolean accept(File dir, String name) {
303                 return new File(dir, name).isFile();
304             }
305         }));
306         Collections.sort(files);
307         List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
308 
309             @Override
310             public boolean accept(File dir, String name) {
311                 return new File(dir, name).isDirectory();
312             }
313         }));
314         Collections.sort(directories);
315         if (up != null || directories.size() + files.size() > 0) {
316             msg.append("<ul>");
317             if (up != null || directories.size() > 0) {
318                 msg.append("<section class=\"directories\">");
319                 if (up != null) {
320                     msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></b></li>");
321                 }
322                 for (String directory : directories) {
323                     String dir = directory + "/";
324                     msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir)
325                             .append("</span></a></b></li>");
326                 }
327                 msg.append("</section>");
328             }
329             if (files.size() > 0) {
330                 msg.append("<section class=\"files\">");
331                 for (String file : files) {
332                     msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
333                     File curFile = new File(f, file);
334                     long len = curFile.length();
335                     msg.append("&nbsp;<span class=\"filesize\">(");
336                     if (len < 1024) {
337                         msg.append(len).append(" bytes");
338                     } else if (len < 1024 * 1024) {
339                         msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
340                     } else {
341                         msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
342                     }
343                     msg.append(")</span></li>");
344                 }
345                 msg.append("</section>");
346             }
347             msg.append("</ul>");
348         }
349         msg.append("</body></html>");
350         return msg.toString();
351     }
352 
newFixedLengthResponse(IStatus status, String mimeType, String message)353     public static Response newFixedLengthResponse(IStatus status, String mimeType, String message) {
354         Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message);
355         response.addHeader("Accept-Ranges", "bytes");
356         return response;
357     }
358 
respond(Map<String, String> headers, IHTTPSession session, String uri)359     private Response respond(Map<String, String> headers, IHTTPSession session, String uri) {
360         // First let's handle CORS OPTION query
361         Response r;
362         if (cors != null && Method.OPTIONS.equals(session.getMethod())) {
363             r = new NanoHTTPD.Response(Response.Status.OK, MIME_PLAINTEXT, null, 0);
364         } else {
365             r = defaultRespond(headers, session, uri);
366         }
367 
368         if (cors != null) {
369             r = addCORSHeaders(headers, r, cors);
370         }
371         return r;
372     }
373 
defaultRespond(Map<String, String> headers, IHTTPSession session, String uri)374     private Response defaultRespond(Map<String, String> headers, IHTTPSession session, String uri) {
375         // Remove URL arguments
376         uri = uri.trim().replace(File.separatorChar, '/');
377         if (uri.indexOf('?') >= 0) {
378             uri = uri.substring(0, uri.indexOf('?'));
379         }
380 
381         // Prohibit getting out of current directory
382         if (uri.contains("../")) {
383             return getForbiddenResponse("Won't serve ../ for security reasons.");
384         }
385 
386         boolean canServeUri = false;
387         File homeDir = null;
388         for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) {
389             homeDir = this.rootDirs.get(i);
390             canServeUri = canServeUri(uri, homeDir);
391         }
392         if (!canServeUri) {
393             return getNotFoundResponse();
394         }
395 
396         // Browsers get confused without '/' after the directory, send a
397         // redirect.
398         File f = new File(homeDir, uri);
399         if (f.isDirectory() && !uri.endsWith("/")) {
400             uri += "/";
401             Response res =
402                     newFixedLengthResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri + "</a></body></html>");
403             res.addHeader("Location", uri);
404             return res;
405         }
406 
407         if (f.isDirectory()) {
408             // First look for index files (index.html, index.htm, etc) and if
409             // none found, list the directory if readable.
410             String indexFile = findIndexFileInDirectory(f);
411             if (indexFile == null) {
412                 if (f.canRead()) {
413                     // No index file, list the directory if it is readable
414                     return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f));
415                 } else {
416                     return getForbiddenResponse("No directory listing.");
417                 }
418             } else {
419                 return respond(headers, session, uri + indexFile);
420             }
421         }
422         String mimeTypeForFile = getMimeTypeForFile(uri);
423         WebServerPlugin plugin = SimpleWebServer.mimeTypeHandlers.get(mimeTypeForFile);
424         Response response = null;
425         if (plugin != null && plugin.canServeUri(uri, homeDir)) {
426             response = plugin.serveFile(uri, headers, session, f, mimeTypeForFile);
427             if (response != null && response instanceof InternalRewrite) {
428                 InternalRewrite rewrite = (InternalRewrite) response;
429                 return respond(rewrite.getHeaders(), session, rewrite.getUri());
430             }
431         } else {
432             response = serveFile(uri, headers, f, mimeTypeForFile);
433         }
434         return response != null ? response : getNotFoundResponse();
435     }
436 
437     @Override
serve(IHTTPSession session)438     public Response serve(IHTTPSession session) {
439         Map<String, String> header = session.getHeaders();
440         Map<String, String> parms = session.getParms();
441         String uri = session.getUri();
442 
443         if (!this.quiet) {
444             System.out.println(session.getMethod() + " '" + uri + "' ");
445 
446             Iterator<String> e = header.keySet().iterator();
447             while (e.hasNext()) {
448                 String value = e.next();
449                 System.out.println("  HDR: '" + value + "' = '" + header.get(value) + "'");
450             }
451             e = parms.keySet().iterator();
452             while (e.hasNext()) {
453                 String value = e.next();
454                 System.out.println("  PRM: '" + value + "' = '" + parms.get(value) + "'");
455             }
456         }
457 
458         for (File homeDir : this.rootDirs) {
459             // Make sure we won't die of an exception later
460             if (!homeDir.isDirectory()) {
461                 return getInternalErrorResponse("given path is not a directory (" + homeDir + ").");
462             }
463         }
464         return respond(Collections.unmodifiableMap(header), session, uri);
465     }
466 
467     /**
468      * Serves file from homeDir and its' subdirectories (only). Uses only URI,
469      * ignores all headers and HTTP parameters.
470      */
serveFile(String uri, Map<String, String> header, File file, String mime)471     Response serveFile(String uri, Map<String, String> header, File file, String mime) {
472         Response res;
473         try {
474             // Calculate etag
475             String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode());
476 
477             // Support (simple) skipping:
478             long startFrom = 0;
479             long endAt = -1;
480             String range = header.get("range");
481             if (range != null) {
482                 if (range.startsWith("bytes=")) {
483                     range = range.substring("bytes=".length());
484                     int minus = range.indexOf('-');
485                     try {
486                         if (minus > 0) {
487                             startFrom = Long.parseLong(range.substring(0, minus));
488                             endAt = Long.parseLong(range.substring(minus + 1));
489                         }
490                     } catch (NumberFormatException ignored) {
491                     }
492                 }
493             }
494 
495             // get if-range header. If present, it must match etag or else we
496             // should ignore the range request
497             String ifRange = header.get("if-range");
498             boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange));
499 
500             String ifNoneMatch = header.get("if-none-match");
501             boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && (ifNoneMatch.equals("*") || ifNoneMatch.equals(etag));
502 
503             // Change return code and add Content-Range header when skipping is
504             // requested
505             long fileLen = file.length();
506 
507             if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) {
508                 // range request that matches current etag
509                 // and the startFrom of the range is satisfiable
510                 if (headerIfNoneMatchPresentAndMatching) {
511                     // range request that matches current etag
512                     // and the startFrom of the range is satisfiable
513                     // would return range from file
514                     // respond with not-modified
515                     res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
516                     res.addHeader("ETag", etag);
517                 } else {
518                     if (endAt < 0) {
519                         endAt = fileLen - 1;
520                     }
521                     long newLen = endAt - startFrom + 1;
522                     if (newLen < 0) {
523                         newLen = 0;
524                     }
525 
526                     FileInputStream fis = new FileInputStream(file);
527                     fis.skip(startFrom);
528 
529                     res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen);
530                     res.addHeader("Accept-Ranges", "bytes");
531                     res.addHeader("Content-Length", "" + newLen);
532                     res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
533                     res.addHeader("ETag", etag);
534                 }
535             } else {
536 
537                 if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) {
538                     // return the size of the file
539                     // 4xx responses are not trumped by if-none-match
540                     res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
541                     res.addHeader("Content-Range", "bytes */" + fileLen);
542                     res.addHeader("ETag", etag);
543                 } else if (range == null && headerIfNoneMatchPresentAndMatching) {
544                     // full-file-fetch request
545                     // would return entire file
546                     // respond with not-modified
547                     res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
548                     res.addHeader("ETag", etag);
549                 } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) {
550                     // range request that doesn't match current etag
551                     // would return entire (different) file
552                     // respond with not-modified
553 
554                     res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
555                     res.addHeader("ETag", etag);
556                 } else {
557                     // supply the file
558                     res = newFixedFileResponse(file, mime);
559                     res.addHeader("Content-Length", "" + fileLen);
560                     res.addHeader("ETag", etag);
561                 }
562             }
563         } catch (IOException ioe) {
564             res = getForbiddenResponse("Reading file failed.");
565         }
566 
567         return res;
568     }
569 
newFixedFileResponse(File file, String mime)570     private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException {
571         Response res;
572         res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length());
573         res.addHeader("Accept-Ranges", "bytes");
574         return res;
575     }
576 
addCORSHeaders(Map<String, String> queryHeaders, Response resp, String cors)577     protected Response addCORSHeaders(Map<String, String> queryHeaders, Response resp, String cors) {
578         resp.addHeader("Access-Control-Allow-Origin", cors);
579         resp.addHeader("Access-Control-Allow-Headers", calculateAllowHeaders(queryHeaders));
580         resp.addHeader("Access-Control-Allow-Credentials", "true");
581         resp.addHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
582         resp.addHeader("Access-Control-Max-Age", "" + MAX_AGE);
583 
584         return resp;
585     }
586 
calculateAllowHeaders(Map<String, String> queryHeaders)587     private String calculateAllowHeaders(Map<String, String> queryHeaders) {
588         // here we should use the given asked headers
589         // but NanoHttpd uses a Map whereas it is possible for requester to send
590         // several time the same header
591         // let's just use default values for this version
592         return System.getProperty(ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, DEFAULT_ALLOWED_HEADERS);
593     }
594 
595     private final static String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, HEAD";
596 
597     private final static int MAX_AGE = 42 * 60 * 60;
598 
599     // explicitly relax visibility to package for tests purposes
600     final static String DEFAULT_ALLOWED_HEADERS = "origin,accept,content-type";
601 
602     public final static String ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME = "AccessControlAllowHeader";
603 }
604