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(" <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