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}