Home | History | Annotate | Download | only in elonen
      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 {
     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) {
     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      */
     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 
    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 
    199     public SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors) {
    200         this(host, port, Collections.singletonList(wwwroot), quiet, cors);
    201     }
    202 
    203     public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) {
    204         this(host, port, Collections.singletonList(wwwroot), quiet, null);
    205     }
    206 
    207     public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) {
    208         this(host, port, wwwroots, quiet, null);
    209     }
    210 
    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 
    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      */
    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 
    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 
    266     protected Response getForbiddenResponse(String s) {
    267         return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
    268     }
    269 
    270     protected Response getInternalErrorResponse(String s) {
    271         return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
    272     }
    273 
    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      */
    281     public void init() {
    282     }
    283 
    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 
    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 
    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 
    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
    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      */
    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 
    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 
    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 
    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