001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.requests; 019 020import io.stallion.exceptions.ClientException; 021import io.stallion.settings.Settings; 022import io.stallion.settings.childSections.CorsSettings; 023import org.apache.commons.io.FilenameUtils; 024 025import java.util.List; 026import java.util.Set; 027import java.util.regex.Pattern; 028 029import static io.stallion.utils.Literals.*; 030 031 032public class CorsResponseHandler { 033 034 public void handleIfNecessary(IRequest request, StResponse response) { 035 String origin = request.getHeader("Origin"); 036 if (empty(origin)) { 037 return; 038 } 039 if (request.getMethod().toUpperCase().equals("OPTIONS")) { 040 handlePreflight(request, response); 041 } else { 042 handleSimpleRequest(request, response); 043 } 044 045 } 046 047 protected void handlePreflight(IRequest request, StResponse response) { 048 049 //Access-Control-Allow-Methods: GET, POST, PUT 050 //Access-Control-Allow-Headers: X-Custom-Header 051 //Content-Type: text/html; charset=utf-8 052 handleOriginAllowed(request, response); 053 CorsSettings cors = Settings.instance().getCors(); 054 response.addHeader("Access-Control-Allow-Credentials", ((Boolean)cors.isAllowCredentials()).toString().toLowerCase()); 055 response.addHeader("Access-Control-Allow-Methods", cors.getAllowedMethodsString()); 056 057 List<String> allowHeaders = list(); 058 for(String requestHeader: or(request.getHeader("Access-Control-Request-Headers"), "").split(",")) { 059 requestHeader = requestHeader.trim(); 060 String requestHeaderLower = requestHeader.toLowerCase(); 061 if (cors.getAllowHeaders().contains(requestHeaderLower)) { 062 allowHeaders.add(requestHeader); 063 } 064 } 065 if (allowHeaders.size() > 0) { 066 response.addHeader("Access-Control-Allow-Headers", String.join(",", allowHeaders)); 067 } 068 069 response.addHeader("Access-Control-Max-Age", cors.getPreflightMaxAge().toString()); 070 071 response.setContentType("text/html; charset=utf-8"); 072 response.setStatus(200); 073 throw new ResponseComplete(); 074 } 075 076 077 protected void handleSimpleRequest(IRequest request, StResponse response) { 078 handleOriginAllowed(request, response); 079 CorsSettings cors = Settings.instance().getCors(); 080 if (!empty(cors.getExposeHeadersString())) { 081 response.addHeader("Access-Control-Expose-Headers", cors.getExposeHeadersString()); 082 } 083 084 response.addHeader("Access-Control-Allow-Credentials", ((Boolean)cors.isAllowCredentials()).toString().toLowerCase()); 085 //Access-Control-Expose-Headers: FooBar 086 } 087 088 /** 089 * Adds the Access-Control-Allow-Origin header if allowed, it not, raise an exception 090 * @param request 091 * @param response 092 */ 093 private void handleOriginAllowed(IRequest request, StResponse response) { 094 CorsSettings cors = Settings.instance().getCors(); 095 096 097 String origin = request.getHeader("Origin"); 098 boolean matches = false; 099 String baseUrl = request.getScheme() + "://" + request.getHost(); 100 101 if (isFontRequest(request) && cors.isAllowAllForFonts()) { 102 response.addHeader("Access-Control-Allow-Origin", "*"); 103 return; 104 } 105 106 if (baseUrl.equals(origin)) { 107 return; 108 } 109 110 111 if (cors.isAllowAll()) { 112 response.addHeader("Access-Control-Allow-Origin", "*"); 113 matches = true; 114 } else if (cors.getOriginWhitelist().contains(origin)) { 115 response.addHeader("Access-Control-Allow-Origin", request.getScheme() + "://" + origin); 116 matches = true; 117 } else { 118 for (Pattern pattern: cors.getOriginPatternWhitelist()) { 119 if (pattern.matcher(origin).matches()) { 120 response.addHeader("Access-Control-Allow-Origin", request.getScheme() + "://" + origin); 121 matches = true; 122 break; 123 } 124 } 125 } 126 127 if (!matches) { 128 throw new ClientException("CORS request not allowed for this origin"); 129 } 130 131 if (cors.getUrlPattern() != null) { 132 if (!cors.getUrlPattern().matcher(request.getPath()).matches()) { 133 throw new ClientException("CORS request not allowed for this URL path"); 134 } 135 } 136 137 } 138 139 public boolean isFontRequest(IRequest request) { 140 String ext = FilenameUtils.getExtension(request.getRequestUri().getPath()); 141 if (fontExtensions.contains(ext)) { 142 return true; 143 } 144 return false; 145 } 146 147 private static final Set<String> fontExtensions = set("tff", "woff", "eot", "svg", "otf"); 148 149}