/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.web.servlet; import java.io.IOException; import java.net.URI; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.config.Settings; import org.structr.common.AccessMode; import org.structr.common.SecurityContext; import org.structr.common.ThreadLocalMatcher; import org.structr.core.app.StructrApp; import org.structr.core.auth.Authenticator; import org.structr.core.entity.AbstractNode; import org.structr.core.entity.Principal; import org.structr.core.graph.Tx; import org.structr.rest.service.HttpServiceServlet; import org.structr.rest.service.StructrHttpServiceConfig; import org.structr.web.auth.UiAuthenticator; import org.structr.rest.common.HttpHelper; import org.structr.web.entity.User; import org.structr.web.entity.dom.Page; //~--- classes ---------------------------------------------------------------- /** * Servlet for proxy requests. * * * */ public class ProxyServlet extends HttpServlet implements HttpServiceServlet { private static final Logger logger = LoggerFactory.getLogger(ProxyServlet.class.getName()); public static final String CONFIRM_REGISTRATION_PAGE = "/confirm_registration"; public static final String RESET_PASSWORD_PAGE = "/reset-password"; public static final String POSSIBLE_ENTRY_POINTS_KEY = "possibleEntryPoints"; public static final String DOWNLOAD_AS_FILENAME_KEY = "filename"; public static final String RANGE_KEY = "range"; public static final String DOWNLOAD_AS_DATA_URL_KEY = "as-data-url"; public static final String CONFIRM_KEY_KEY = "key"; public static final String TARGET_PAGE_KEY = "target"; public static final String ERROR_PAGE_KEY = "onerror"; public static final String CUSTOM_RESPONSE_HEADERS = "HtmlServlet.customResponseHeaders"; public static final String OBJECT_RESOLUTION_PROPERTIES = "HtmlServlet.resolveProperties"; private static final String defaultCustomResponseHeaders = "Strict-Transport-Security:max-age=60," + "X-Content-Type-Options:nosniff," + "X-Frame-Options:SAMEORIGIN," + "X-XSS-Protection:1;mode=block"; private static List<String> customResponseHeaders = Collections.EMPTY_LIST; private static final ThreadLocalMatcher threadLocalUUIDMatcher = new ThreadLocalMatcher("[a-fA-F0-9]{32}"); private static final ExecutorService threadPool = Executors.newCachedThreadPool(); private final StructrHttpServiceConfig config = new StructrHttpServiceConfig(); private final Set<String> possiblePropertyNamesForEntityResolving = new LinkedHashSet<>(); private boolean isAsync = false; @Override public StructrHttpServiceConfig getConfig() { return config; } public ProxyServlet() { String customResponseHeadersString = Settings.HtmlCustomResponseHeaders.getValue(); if (StringUtils.isBlank(customResponseHeadersString)) { customResponseHeadersString = defaultCustomResponseHeaders; } if (StringUtils.isNotBlank(customResponseHeadersString)) { customResponseHeaders = Arrays.asList(customResponseHeadersString.split("[ ,]+")); } // resolving properties final String resolvePropertiesSource = Settings.HtmlResolveProperties.getValue(); for (final String src : resolvePropertiesSource.split("[, ]+")) { final String name = src.trim(); if (StringUtils.isNotBlank(name)) { possiblePropertyNamesForEntityResolving.add(name); } } this.isAsync = Settings.Async.getValue(); } @Override public void destroy() { } @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) { final Authenticator auth = getConfig().getAuthenticator(); SecurityContext securityContext; String content; if (auth == null) { final String errorMessage = "No authenticator class found. Check log for 'Missing authenticator key " + this.getClass().getSimpleName() + ".authenticator'"; logger.error(errorMessage); try { final ServletOutputStream out = response.getOutputStream(); content = errorPage(new Throwable(errorMessage)); IOUtils.write(content, out); } catch (IOException ex) { logger.error("Could not write to response", ex); } return; } try { // isolate request authentication in a transaction try (final Tx tx = StructrApp.getInstance().tx()) { securityContext = auth.initializeAndExamineRequest(request, response); tx.success(); } // Ensure access mode is frontend securityContext.setAccessMode(AccessMode.Frontend); String address = request.getParameter("url"); final URI url = URI.create(address); String proxyUrl = request.getParameter("proxyUrl"); String proxyUsername = request.getParameter("proxyUsername"); String proxyPassword = request.getParameter("proxyPassword"); String authUsername = request.getParameter("authUsername"); String authPassword = request.getParameter("authPassword"); String cookie = request.getParameter("cookie"); final Principal user = securityContext.getCachedUser(); if (user != null && StringUtils.isBlank(proxyUrl)) { proxyUrl = user.getProperty(User.proxyUrl); proxyUsername = user.getProperty(User.proxyUsername); proxyPassword = user.getProperty(User.proxyPassword); } content = HttpHelper.get(address, authUsername, authPassword, proxyUrl, proxyUsername, proxyPassword, cookie, Collections.EMPTY_MAP).replace("<head>", "<head>\n <base href=\"" + url + "\">"); } catch (Throwable t) { logger.error("Exception while processing request", t); content = errorPage(t); } try { final ServletOutputStream out = response.getOutputStream(); IOUtils.write(content, out); } catch (IOException ex) { logger.error("Could not write to response", ex); } } @Override protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } @Override protected void doHead(final HttpServletRequest request, final HttpServletResponse response) { try { String path = request.getPathInfo(); } catch (Throwable t) { logger.error("Exception while processing request", t); UiAuthenticator.writeInternalServerError(response); } } @Override protected void doOptions(final HttpServletRequest request, final HttpServletResponse response) { final Authenticator auth = config.getAuthenticator(); try { response.setContentLength(0); response.setHeader("Allow", "GET,HEAD,OPTIONS"); } catch (Throwable t) { logger.error("Exception while processing request", t); UiAuthenticator.writeInternalServerError(response); } } //~--- set methods ---------------------------------------------------- public static void setNoCacheHeaders(final HttpServletResponse response) { response.setHeader("Cache-Control", "private, max-age=0, s-maxage=0, no-cache, no-store, must-revalidate"); // HTTP 1.1. response.setHeader("Pragma", "no-cache, no-store"); // HTTP 1.0. response.setDateHeader("Expires", 0); } private static void setCustomResponseHeaders(final HttpServletResponse response) { for (final String header : customResponseHeaders) { final String[] keyValuePair = header.split("[ :]+"); response.setHeader(keyValuePair[0], keyValuePair[1]); logger.debug("Set custom response header: {} {}", new Object[]{keyValuePair[0], keyValuePair[1]}); } } private static boolean notModifiedSince(final HttpServletRequest request, HttpServletResponse response, final AbstractNode node, final boolean dontCache) { boolean notModified = false; final Date lastModified = node.getLastModifiedDate(); // add some caching directives to header // see http://weblogs.java.net/blog/2007/08/08/expires-http-header-magic-number-yslow final DateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); httpDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); response.setHeader("Date", httpDateFormat.format(new Date())); final Calendar cal = new GregorianCalendar(); final Integer seconds = node.getProperty(Page.cacheForSeconds); if (!dontCache && seconds != null) { cal.add(Calendar.SECOND, seconds); response.setHeader("Cache-Control", "max-age=" + seconds + ", s-maxage=" + seconds + ""); response.setHeader("Expires", httpDateFormat.format(cal.getTime())); } else { if (!dontCache) { response.setHeader("Cache-Control", "no-cache, must-revalidate, proxy-revalidate"); } else { response.setHeader("Cache-Control", "private, no-cache, no-store, max-age=0, s-maxage=0, must-revalidate, proxy-revalidate"); } } if (lastModified != null) { final Date roundedLastModified = DateUtils.round(lastModified, Calendar.SECOND); response.setHeader("Last-Modified", httpDateFormat.format(roundedLastModified)); final String ifModifiedSince = request.getHeader("If-Modified-Since"); if (StringUtils.isNotBlank(ifModifiedSince)) { try { Date ifModSince = httpDateFormat.parse(ifModifiedSince); // Note that ifModSince has not ms resolution, so the last digits are always 000 // That requires the lastModified to be rounded to seconds if ((ifModSince != null) && (roundedLastModified.equals(ifModSince) || roundedLastModified.before(ifModSince))) { notModified = true; response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setHeader("Vary", "Accept-Encoding"); } } catch (ParseException ex) { logger.warn("Could not parse If-Modified-Since header", ex); } } } return notModified; } private String errorPage(final Throwable t) { return "<html><head><title>Error in Structr Proxy</title></head><body><h1>Error in Proxy</h1><p>" + t.getMessage() + "</p>\n<!--" + ExceptionUtils.getStackTrace(t) + "--></body></html>"; } }