Java tutorial
/* * Copyright 2007-2015 by Chris Hubick. All Rights Reserved. * * This work is licensed under the terms of the "GNU AFFERO GENERAL PUBLIC LICENSE" version 3, as published by the Free * Software Foundation <http://www.gnu.org/licenses/>, a copy of which you should have received in the file LICENSE.txt. */ package net.www_eee.portal.channels; import java.io.*; import java.net.*; import java.nio.charset.*; import java.time.*; import java.time.format.*; import java.util.*; import java.util.function.*; import java.util.logging.*; import java.util.stream.*; import javax.activation.*; import org.w3c.dom.*; import org.xml.sax.*; import org.xml.sax.ext.*; import javax.ws.rs.*; import javax.ws.rs.core.*; import org.apache.http.*; import org.apache.http.entity.*; import org.apache.http.message.*; import org.apache.http.params.*; import org.apache.http.protocol.*; import org.apache.http.client.*; import org.apache.http.client.config.*; import org.apache.http.client.methods.*; import org.apache.http.client.params.*; import org.apache.http.client.protocol.*; import org.apache.http.conn.*; import org.apache.http.impl.client.*; import org.eclipse.jdt.annotation.*; import net.www_eee.util.misc.core.*; import net.www_eee.util.misc.core.collection.*; import net.www_eee.util.misc.core.function.*; import net.www_eee.util.misc.core.io.*; import net.www_eee.util.misc.core.net.*; import net.www_eee.util.misc.core.xml.*; import net.www_eee.util.misc.core.xml.dom.*; import net.www_eee.util.misc.core.xml.html.*; import net.www_eee.util.misc.core.xml.sax.*; import net.www_eee.util.misc.core.xml.transform.sax.*; import net.www_eee.util.misc.logging.*; import net.www_eee.util.misc.http.*; import net.www_eee.util.misc.ws.rs.*; import net.www_eee.util.misc.ws.rs.properties.*; import net.www_eee.portal.*; /** * <p> * A {@link Channel} which provides content proxied via an {@link HttpClient}. * </p> * * <p> * {@linkplain #BASE_URI_PROP Provided} a {@link URL}, a <code>ProxyChannel</code> will provide access to a single * document or entire website at that location. * </p> * * <p> * This channel is implemented in a completely <em>generic</em> fashion, and is capable of rendering <em>any</em> type * of XML content into a {@link Page}. As such, you will probably desire the enhanced functionality provided by the * {@linkplain net.www_eee.portal.channelplugins.ProxyChannelHTMLSource HTML plugin}, in the likely case you will be * using the channel to proxy (X)HTML content. * </p> * * <p> * Aside from actual retrieval of the content itself, a major function of this channel is to perform * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) link rewriting} within the proxied * documents. * </p> * * <h3 id="configuration">Configuration</h3> * <p> * In addition to those inherited from the {@link Channel} class, the following {@linkplain ConfigManager configuration * properties} are supported by this class: * </p> * <ul> * <li>{@link #BASE_URI_PROP}</li> * <li>{@link #DEFAULT_PATH_PROP}</li> * <li>{@link #DEFAULT_PATH_RESTRICTION_ENABLE_PROP}</li> * <li>{@link #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP}</li> * <li>{@link #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP}</li> * <li>{@link #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP}</li> * <li>{@link #CONNECT_TIMEOUT_PROP}</li> * <li>{@link #READ_TIMEOUT_PROP}</li> * <li>{@link #FOLLOW_REDIRECTS_ENABLE_PROP}</li> * </ul> */ @NonNullByDefault public class ProxyChannel extends Channel { /** * <p> * The key to a <strong>required</strong> {@link URI#create(String) URI} property defining the * {@linkplain #getProxiedBaseURI(Page.Request) base URI} of the document or site to be proxied. * </p> * * <p> * If you wish to create a channel which will simply display a single document, proxied from a specified URL, then * this property can just contain the {@link URL} to that document (this value will essentially include the * {@linkplain #DEFAULT_PATH_PROP default path}, which is then not required). * </p> * * <p> * If you wish to create a channel which will proxy an entire website, where users can navigate between the pages of * that site within the channel {@linkplain net.www_eee.portal.Channel.Mode#VIEW view}, then this property should * contain the {@link URL} of the top-most root <em>folder</em> which contains the documents for the entire site, and * a {@linkplain #DEFAULT_PATH_PROP default path} should then also be specified. * </p> * * <p> * By default, this channel will proxy all documents within the leaf-most <em>folder</em> specified by this property, * though you can {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP restrict} it to just the document specified by * this URL (including the {@linkplain #DEFAULT_PATH_PROP default path}). Though discouraged, you may also * {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP enable} proxying of documents on the server which are outside * of this folder. * </p> * * <p> * Note that if you specify a value for this property which is not {@linkplain URI#isAbsolute() absolute}, it will be * {@linkplain ConfigManager#getContextResourceLocalHostURI(UriInfo, String, Map, String, boolean) resolved} within * the local portal context (relative to the root of the context). This is a very useful feature for creating a * self-contained portal context which includes all the documents it proxies. * </p> * * @see #getProxiedBaseURI(Page.Request) * @see #DEFAULT_PATH_PROP * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String BASE_URI_PROP = "ProxyChannel.BaseURI"; /** * <p> * The key to a String property defining a <strong>relative</strong> path under the proxied * {@linkplain #getProxiedBaseURI(Page.Request) base URI} to the document which should be displayed within this * channel by default (when {@linkplain net.www_eee.portal.Channel.Mode#VIEW viewed} as part of a * {@linkplain Page#doViewRequest(Page.Request) request} for the {@link Page}, or when no * {@linkplain net.www_eee.portal.Page.Request#getChannelLocalPath(Channel) local path} is specified). * </p> * * <p> * This property is not required if the {@linkplain #getProxiedBaseURI(Page.Request) base URI} points directly to a * document (as opposed to a folder), as then that value will be interpreted as though it includes this path. * </p> * * <p> * Note that this path is only really used during {@linkplain net.www_eee.portal.Channel.Mode#VIEW view mode} * requests, except when the {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restriction is * {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP enabled}, in which case the * {@linkplain #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) proxied file URI} will be validated * against it. * </p> * * @see #BASE_URI_PROP * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String DEFAULT_PATH_PROP = "ProxyChannel.DefaultPath"; /** * The key to a {@link Boolean#valueOf(String) Boolean} property indicating that this channel should only be * {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) enabled} to proxy the single document located at the * {@linkplain #DEFAULT_PATH_PROP default path} location, and <em>not</em> anything else within the specified * {@linkplain #getProxiedBaseURI(Page.Request) base URI} folder (as allowed by default). * * @see #isDefaultPathRestrictionEnabled(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String DEFAULT_PATH_RESTRICTION_ENABLE_PROP = "ProxyChannel.DefaultPathRestriction.Enable"; /** * The key to a {@link Boolean#valueOf(String) Boolean} property indicating the default restriction against the * proxying of documents from the source server which reside outside the {@linkplain #getProxiedBaseURI(Page.Request) * base URI} should be {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) disabled}. This option should * never really need to be used, as the base URI should generally encompass all files required by a site. * * @see #isParentFoldersRestrictionDisabled(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String PARENT_FOLDERS_RESTRICTION_DISABLE_PROP = "ProxyChannel.ParentFoldersRestriction.Disable"; /** * The key to a {@link Boolean#valueOf(String) Boolean} property which indicates that, during * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) link rewriting}, that any * <em>hyperlink</em> between this and another proxied site document should no longer be rewritten so that the client * browser {@linkplain net.www_eee.portal.Channel.Mode#VIEW views} the linked document within this portal channel ( * {@linkplain #isLinkRewritingHyperlinksToChannelDisabled(Page.Request) default behaviour}), but rather so the client * is directed outside of the portal, to display the document directly from it's * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location}. * * @see #isLinkRewritingHyperlinksToChannelDisabled(Page.Request) * @see #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP = "ProxyChannel.LinkRewriting.Hyperlinks.ToChannel.Disable"; /** * <p> * The key to a {@link Boolean#valueOf(String) Boolean} property which indicates that, during * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) link rewriting}, any relative link * from within the proxied document to an <em>external resource</em> should no longer be rewritten into an * {@linkplain URI#isAbsolute() absolute} link pointing directly back to the * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location} ( * {@linkplain #isLinkRewritingResourceLinksToChannelEnabled(Page.Request) default behaviour}), but rather so that the * client browser retrieves it as a {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource} proxied via this * channel. * </p> * * <p> * This option can be useful if there is a firewall preventing clients from direct access to an internal server * hosting the source resources, but should be exercised with care, as WWW-EEE-Portal is designed primarily for * aggregation of markup, and doesn't provide the most sophisticated HTTP proxy implementation (ie, conditional * requests <em>are</em> supported, but limited byte-range type requests against large resources are <em>not</em>). * Note that this option may be used to force resource links to be rewritten pointing back through the portal, and * then the container configured to intercept those requests and provide a more sophisticated proxy implementation * (ie, Apache <code>mod_proxy</code> and <code>mod_cache</code>) if required. * </p> * * @see #isLinkRewritingResourceLinksToChannelEnabled(Page.Request) * @see #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP = "ProxyChannel.LinkRewriting.ResourceLinksToChannel.Enable"; /** * The key to an {@link Integer#valueOf(String) Integer} property (in milliseconds) * {@linkplain #getConnectTimeout(Page.Request) used} to {@linkplain HttpParams#setIntParameter(String, int) set} the * {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT connection timeout} {@linkplain HttpClient#getParams() * parameter} on any {@link HttpClient} created to proxy documents for this channel. If this property is not specified * then the {@linkplain #DEFAULT_CONNECT_TIMEOUT_MS default} will be used. * * @see CoreConnectionPNames#CONNECTION_TIMEOUT * @see #DEFAULT_CONNECT_TIMEOUT_MS * @see #getConnectTimeout(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String CONNECT_TIMEOUT_PROP = "ProxyChannel.ConnectTimeout"; /** * Unless an explicit value is {@linkplain #CONNECT_TIMEOUT_PROP specified}, this value will be * {@linkplain #getConnectTimeout(Page.Request) used} as the default value (in milliseconds) to * {@linkplain HttpParams#setIntParameter(String, int) set} the {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT * connection timeout} {@linkplain HttpClient#getParams() parameter} on any {@link HttpClient} created to proxy * documents for this channel. * * @see #CONNECT_TIMEOUT_PROP * @see #getConnectTimeout(Page.Request) */ public static final Integer DEFAULT_CONNECT_TIMEOUT_MS = 30000; /** * The key to an {@link Integer#valueOf(String) Integer} property (in milliseconds) * {@linkplain #getReadTimeout(Page.Request) used} to {@linkplain HttpParams#setIntParameter(String, int) set} the * {@linkplain CoreConnectionPNames#SO_TIMEOUT socket timeout} {@linkplain HttpClient#getParams() parameter} on any * {@link HttpClient} created to proxy documents for this channel. If this property is not specified then the * {@linkplain #DEFAULT_READ_TIMEOUT_MS default} will be used. * * @see CoreConnectionPNames#SO_TIMEOUT * @see #DEFAULT_READ_TIMEOUT_MS * @see #getReadTimeout(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String READ_TIMEOUT_PROP = "ProxyChannel.ReadTimeout"; /** * Unless an explicit value is {@linkplain #READ_TIMEOUT_PROP specified}, this value will be * {@linkplain #getReadTimeout(Page.Request) used} as the default value (in milliseconds) to * {@linkplain HttpParams#setIntParameter(String, int) set} the {@linkplain CoreConnectionPNames#SO_TIMEOUT socket * timeout} {@linkplain HttpClient#getParams() parameter} on any {@link HttpClient} created to proxy documents for * this channel. * * @see #READ_TIMEOUT_PROP * @see #getReadTimeout(Page.Request) */ public static final Integer DEFAULT_READ_TIMEOUT_MS = 30000; /** * <p> * The key to a {@link Boolean#valueOf(String) Boolean} property {@linkplain #isFollowRedirectsEnabled(Page.Request) * used} to {@linkplain HttpParams#setBooleanParameter(String, boolean) set} the automatic * {@linkplain ClientPNames#HANDLE_REDIRECTS redirect handling} {@linkplain HttpClient#getParams() parameter} on any * {@link HttpClient} created to proxy documents for this channel. * </p> * * <p> * By default, any redirect URL returned by the proxied server will be * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) rewritten} and forwarded to the * client browser, as though it were returned as a link within a proxied document (redirects in * {@linkplain net.www_eee.portal.Channel.Mode#VIEW view mode} are treated as <em>hyperlinks</em>, redirects in * {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource mode} as <em>external resource</em> links). It is * important to note that forwarding a redirect to the client is only possible within * {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource mode}, or when the channel is being * {@linkplain net.www_eee.portal.Channel.Mode#VIEW viewed} while * {@linkplain net.www_eee.portal.Page.Request#isMaximized(Channel) maximized}, so any redirect returned by the * {@linkplain #DEFAULT_PATH_PROP default path} will generally result in a * {@link net.www_eee.portal.ConfigManager.ConfigException ConfigException}. * </p> * * <p> * Enabling this property will cause <em>this channel</em> to follow any redirections while loading proxied documents, * instead of rewriting and forwarding them to the client browser, and also allow for successful redirection by the * {@linkplain #DEFAULT_PATH_PROP default path}. * </p> * * <p> * This behavior is disabled by default as a security precaution, as when enabled, there will <strong>no longer be * <em>any</em> restrictions</strong> (ie, {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP parent folders} or * {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP default path}) imposed on the redirection target URL. You should * ensure you trust the application at the {@linkplain #BASE_URI_PROP base URL}, otherwise it could cause the portal * to load data from <em>anywhere</em> and return it to clients from within your domain, mitigating a client browser's * <a href="http://en.wikipedia.org/wiki/Same_origin_policy">same-origin policy</a> and possibly enabling <a * href="http://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF</a> type attacks against your users. * </p> * * @see ClientPNames#HANDLE_REDIRECTS * @see #isFollowRedirectsEnabled(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String FOLLOW_REDIRECTS_ENABLE_PROP = "ProxyChannel.FollowRedirects.Enable"; /** * Should the XML parser halt on {@linkplain ErrorHandler#warning(SAXParseException) warnings}? * * @see ErrorHandler#warning(SAXParseException) * @see #isParserHaltOnWarningsEnabled(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String PARSER_HALT_ON_WARNINGS_ENABLE_PROP = "ProxyChannel.Parser.Halt.Warnings.Enable"; /** * Should the XML parser halt on {@linkplain ErrorHandler#error(SAXParseException) errors}? * * @see ErrorHandler#error(SAXParseException) * @see #isParserHaltOnErrorsDisabled(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String PARSER_HALT_ON_ERRORS_DISABLE_PROP = "ProxyChannel.Parser.Halt.Errors.Disable"; /** * Should the XML parser halt on {@linkplain ErrorHandler#fatalError(SAXParseException) fatal errors}? * * @see ErrorHandler#fatalError(SAXParseException) * @see #isParserHaltOnFatalErrorsDisabled(Page.Request) * @category WWW_EEE_PORTAL_CONFIG_PROP */ public static final String PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP = "ProxyChannel.Parser.Halt.FatalErrors.Disable"; /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxyClientManager() ProxyClientManager} or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * created one. * * @see #createProxyClientManager() * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpClientConnectionManager> PROXY_CLIENT_MANAGER_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpClientConnectionManager>( ProxyChannel.class, 1, HttpClientConnectionManager.class, null); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@linkplain #getProxiedBaseURI(Page.Request) proxied base URI} or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * {@linkplain #BASE_URI_PROP provided} value. * * @see #getProxiedBaseURI(Page.Request) * @see #BASE_URI_PROP * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, URI> PROXIED_BASE_URI_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URI>( ProxyChannel.class, 2, URI.class, null); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@linkplain #getProxiedFilePathDefault(Page.Request) proxied file default path} or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * {@linkplain #DEFAULT_PATH_PROP provided} value. * * @see #getProxiedFilePathDefault(Page.Request) * @see #DEFAULT_PATH_PROP * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, URI> PROXIED_FILE_PATH_DEFAULT_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URI>( ProxyChannel.class, 3, URI.class, null); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@linkplain #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) proxied file local URI} or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * calculated value. * * @see #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, URI> PROXIED_FILE_LOCAL_URI_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URI>( ProxyChannel.class, 4, URI.class, new Class<?>[] { URI.class, Mode.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL} or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * calculated value. * * @see #getProxiedFileURL(Page.Request, Channel.Mode, boolean) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, URL> PROXIED_FILE_URL_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URL>( ProxyChannel.class, 5, URL.class, new Class<?>[] { Mode.class, Boolean.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxyClientCookieStore(Page.Request) CookieStore} to the proxy client, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * created ones. * * @see #createProxyClientCookieStore(Page.Request) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, org.apache.http.client.CookieStore> PROXY_CLIENT_COOKIE_STORE_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, org.apache.http.client.CookieStore>( ProxyChannel.class, 6, org.apache.http.client.CookieStore.class, null); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxyClient(Page.Request) HttpClient} to {@linkplain #doProxyRequest(Page.Request, Channel.Mode) * proxy} content, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * created one. * * @see #createProxyClient(Page.Request) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpClientBuilder> PROXY_CLIENT_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpClientBuilder>( ProxyChannel.class, 7, HttpClientBuilder.class, new Class<?>[] { HttpClientConnectionManager.class, org.apache.http.client.CookieStore.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) HttpUriRequest} to * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * configured one. * * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase> PROXY_REQUEST_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase>( ProxyChannel.class, 8, HttpRequestBase.class, new Class<?>[] { Mode.class, URL.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxyClientRequestConfig(Page.Request, Channel.Mode) RequestConfig} to the proxy client, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * created ones. * * @see #createProxyClientRequestConfig(Page.Request, Channel.Mode) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, RequestConfig.Builder> PROXY_CLIENT_REQUEST_CONFIG_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, RequestConfig.Builder>( ProxyChannel.class, 9, RequestConfig.Builder.class, new Class<?>[] { Mode.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest} to * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * created one. * * @see #createProxyRequestObject(Page.Request, Channel.Mode, URL) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase> PROXY_REQUEST_OBJ_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase>( ProxyChannel.class, 10, HttpRequestBase.class, new Class<?>[] { Mode.class, URL.class, RequestConfig.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * {@link HttpRequest} sent by the {@linkplain #createProxyClient(Page.Request) proxy client}. * * @see #createProxyClient(Page.Request) * @see ProxyRequestInterceptor * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpRequest> PROXY_REQUEST_INTERCEPTOR_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpRequest>( ProxyChannel.class, 11, HttpRequest.class, new Class<?>[] { HttpClientContext.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * {@link HttpResponse} received by the {@linkplain #createProxyClient(Page.Request) proxy client}. * * @see #createProxyClient(Page.Request) * @see ProxyResponseInterceptor * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpResponse> PROXY_RESPONSE_INTERCEPTOR_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpResponse>( ProxyChannel.class, 12, HttpResponse.class, new Class<?>[] { HttpClientContext.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) perform} the * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy request} on it's own, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * response. * * @see #doProxyRequest(Page.Request, Channel.Mode) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, CloseableHttpResponse> PROXY_RESPONSE_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, CloseableHttpResponse>( ProxyChannel.class, 13, CloseableHttpResponse.class, new Class<?>[] { Mode.class, HttpClientContext.class, HttpClient.class, HttpRequestBase.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@linkplain #getProxyResponseHeader(Page.Request, HttpResponse, String, Function) response header}, as if it had * {@linkplain HttpResponse#getHeaders(String) come} from the {@link #doProxyRequest(Page.Request, Channel.Mode) * proxied} response, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * returned value. * * @see #getProxyResponseHeader(Page.Request, HttpResponse, String, Function) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, Header> PROXY_RESPONSE_HEADER_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Header>( ProxyChannel.class, 14, Header.class, new Class<?>[] { HttpResponse.class, String.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link Boolean} value, to {@linkplain #isRenderedUsingXMLView(Page.Request, HttpResponse, MimeType) indicate} the * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} response should be * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered} as XML, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * calculated value. * * @see #isRenderedUsingXMLView(Page.Request, HttpResponse, MimeType) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, Boolean> IS_RENDERED_USING_XML_VIEW_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Boolean>( ProxyChannel.class, 15, Boolean.class, new Class<?>[] { HttpResponse.class, MimeType.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) TypedInputSource}, from which * to {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean) * parse} the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} content being * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered}, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * configured one. * * @see #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, TypedInputSource> PROXIED_DOC_INPUT_SOURCE_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, TypedInputSource>( ProxyChannel.class, 16, TypedInputSource.class, new Class<?>[] { HttpResponse.class, URL.class, MimeType.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) * DefaultHandler2}, to receive * {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean) * parsing} events from the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} content being * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered}, or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * created one. * * @see #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, DefaultHandler2> PROXIED_DOC_CONTENT_HANDLER_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, DefaultHandler2>( ProxyChannel.class, 17, DefaultHandler2.class, new Class<?>[] { ViewResponse.class, URL.class, TypedInputSource.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link Boolean} value, indicating the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} * {@linkplain #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) input} should * <strong>not</strong> be * {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean) * parsed} into the * {@linkplain #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) content * handler} during {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) * rendering} (ie, parsing was already handled by the plugin), or to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * calculated value. * * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, Boolean> PARSE_XML_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Boolean>( ProxyChannel.class, 18, Boolean.class, new Class<?>[] { ViewResponse.class, TypedInputSource.class, DefaultHandler2.class }); /** * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a * {@link net.www_eee.portal.Channel.Plugin Plugin} to either * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own * {@link Boolean} value, to {@linkplain #isRenderedUsingTextView(Page.Request, HttpResponse, MimeType) indicate} the * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} response should be * {@linkplain #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered} as text, or * to {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * calculated value. * * @see #isRenderedUsingTextView(Page.Request, HttpResponse, MimeType) * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK */ public static final WWWEEEPortal.PluginHook<ProxyChannel, Boolean> IS_RENDERED_USING_TEXT_VIEW_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Boolean>( ProxyChannel.class, 19, Boolean.class, new Class<?>[] { HttpResponse.class, MimeType.class }); /** * A constant used during * {@link #getProxyRequestUserAgentHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest) construction} * of the value for the "User-Agent" header to be {@link HttpUriRequest#setHeader(String, String) set} on * the {@link HttpUriRequest} being used to * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content for this channel. * * @see #getProxyRequestUserAgentHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest) */ public static final String USER_AGENT_HEADER = "WWW-EEE-Portal/1.0"; /** * The identifier used to store the {@link CloseableHttpClient} in the {@link HttpClientContext}. */ public static final String HTTP_CLIENT_CONTEXT_ID = "http.client"; /** * A {@link Function} constant for {@link URI#create(String)} which can be {@linkplain RSProperties#cache(Function) * cached}. */ protected static final Function<String, URI> STRING_TO_URI_FUNCTION = URI::create; /** * The {@link MimeType} Object for the <code>"application/*"</code> mime type. * * @see #getProxyRequestAcceptHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest) */ protected static final MimeType APPLICATION_STAR_MIME_TYPE = IOUtil.newMimeType("application", "*"); /** * The {@link MimeType} Object for the <code>"text/*"</code> mime type. * * @see #getProxyRequestAcceptHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest) */ protected static final MimeType TEXT_STAR_MIME_TYPE = IOUtil.newMimeType("text", "*"); /** * The {@link HttpClientConnectionManager} to be used by all {@link HttpClient}'s * {@linkplain #createProxyClient(Page.Request) created} by this channel. * * @see #PROXY_CLIENT_MANAGER_HOOK * @see #createProxyClientManager() * @see #createProxyClient(Page.Request) */ protected @Nullable HttpClientConnectionManager proxyClientManager = null; /** * Construct a new <code>ProxyChannel</code> instance. * * @param portal The {@link #getPortal() WWWEEEPortal} instance hosting this channel. * @param channelDef The {@linkplain net.www_eee.portal.ContentDef.Channel definition} which will be used to configure * this channel instance. * @param page The {@link Page} instance to which this * <em>{@linkplain net.www_eee.portal.ContentDef.LocalChannel local}</em> channel belongs, or <code>null</code> if * it's a {@linkplain net.www_eee.portal.ContentDef.GlobalChannel global channel}. * @throws WWWEEEPortal.Exception If a problem occurred while constructing the channel. */ public ProxyChannel(final WWWEEEPortal portal, final ContentDef.Channel<?, ? extends ProxyChannel> channelDef, final @Nullable Page page) throws WWWEEEPortal.Exception { super(portal, channelDef, page); channelDef.getProps().cache(STRING_TO_URI_FUNCTION); return; } @Override public final Class<? extends Channel> getImmediateSubClass() { return ProxyChannel.class; } /** * Optionally construct an {@link HttpClientConnectionManager} to manage the {@link HttpClient}'s being * {@linkplain #createProxyClient(Page.Request) created} to {@linkplain #doProxyRequest(Page.Request, Channel.Mode) * proxy} content. The default implementation returns <code>null</code>. * * @return The {@link HttpClientConnectionManager}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #createProxyClient(Page.Request) * @see #PROXY_CLIENT_MANAGER_HOOK */ protected @Nullable HttpClientConnectionManager createProxyClientManager() throws WWWEEEPortal.Exception { HttpClientConnectionManager proxyClientManager = PROXY_CLIENT_MANAGER_HOOK.value(plugins, null, null); proxyClientManager = PROXY_CLIENT_MANAGER_HOOK.filter(plugins, null, null, proxyClientManager); return proxyClientManager; } @Override protected void initInternal(final AnnotatedLogMessage logMessage) throws WWWEEEPortal.Exception { super.initInternal(logMessage); proxyClientManager = createProxyClientManager(); return; } @Override protected void closeInternal(final AnnotatedLogMessage logMessage) { Optional.ofNullable(proxyClientManager).ifPresent((proxyClientManager) -> proxyClientManager.shutdown()); super.closeInternal(logMessage); return; } /** * Get the {@link HttpClientConnectionManager} to be used by all {@link HttpClient}'s * {@linkplain #createProxyClient(Page.Request) created} by this channel. * * @return The {@link HttpClientConnectionManager}. * @see #PROXY_CLIENT_MANAGER_HOOK */ public HttpClientConnectionManager getProxyClientManager() { return proxyClientManager; } /** * Is the default-path restriction {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP enabled} for this channel? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If the default-path restriction is {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP enabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #DEFAULT_PATH_RESTRICTION_ENABLE_PROP */ public boolean isDefaultPathRestrictionEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(DEFAULT_PATH_RESTRICTION_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Is the parent-folders restriction {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP disabled} for this channel? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If the parent-folders restriction is {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP disabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP */ public boolean isParentFoldersRestrictionDisabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(PARENT_FOLDERS_RESTRICTION_DISABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Is the rewriting of hyperlinks within proxied documents back to this channel * {@linkplain #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP disabled}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If the rewriting of hyperlinks within proxied documents back to this channel is * {@linkplain #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP disabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP */ public boolean isLinkRewritingHyperlinksToChannelDisabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Is the rewriting of external resource links within proxied documents back to this channel * {@linkplain #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP enabled}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If the rewriting of external resource links within proxied documents back to this channel is * {@linkplain #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP enabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP */ public boolean isLinkRewritingResourceLinksToChannelEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Get the value (in milliseconds) used to {@linkplain HttpParams#setIntParameter(String, int) set} the * {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT connection timeout} {@linkplain HttpClient#getParams() * parameter} on any {@link HttpClient} created to proxy documents for this channel. If an explicit value is not * {@linkplain #CONNECT_TIMEOUT_PROP specified} then the {@linkplain #DEFAULT_CONNECT_TIMEOUT_MS default} will be * used. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return The value to be used for the {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT connection timeout}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see CoreConnectionPNames#CONNECTION_TIMEOUT * @see #CONNECT_TIMEOUT_PROP * @see #DEFAULT_CONNECT_TIMEOUT_MS */ public int getConnectTimeout(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(CONNECT_TIMEOUT_PROP, pageRequest, Integer::valueOf, DEFAULT_CONNECT_TIMEOUT_MS); } /** * Get the value (in milliseconds) used to {@linkplain HttpParams#setIntParameter(String, int) set} the * {@linkplain CoreConnectionPNames#SO_TIMEOUT socket timeout} {@linkplain HttpClient#getParams() parameter} on any * {@link HttpClient} created to proxy documents for this channel. If an explicit value is not * {@linkplain #READ_TIMEOUT_PROP specified} then the {@linkplain #DEFAULT_READ_TIMEOUT_MS default} will be used. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return The value to be used for the {@linkplain CoreConnectionPNames#SO_TIMEOUT socket timeout}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see CoreConnectionPNames#SO_TIMEOUT * @see #READ_TIMEOUT_PROP * @see #DEFAULT_READ_TIMEOUT_MS */ public int getReadTimeout(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(READ_TIMEOUT_PROP, pageRequest, Integer::valueOf, DEFAULT_READ_TIMEOUT_MS); } /** * Is the automatic handling of redirects by this channel's proxy client {@linkplain #FOLLOW_REDIRECTS_ENABLE_PROP * enabled}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If the automatic handling of redirects is {@linkplain #FOLLOW_REDIRECTS_ENABLE_PROP enabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #FOLLOW_REDIRECTS_ENABLE_PROP */ public boolean isFollowRedirectsEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(FOLLOW_REDIRECTS_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Is halting of the XML parser on {@linkplain ErrorHandler#warning(SAXParseException) warnings} * {@linkplain #PARSER_HALT_ON_WARNINGS_ENABLE_PROP enabled}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If halting of the XML parser on {@linkplain ErrorHandler#warning(SAXParseException) warnings} is * {@linkplain #PARSER_HALT_ON_WARNINGS_ENABLE_PROP enabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #PARSER_HALT_ON_WARNINGS_ENABLE_PROP * @see ErrorHandler#warning(SAXParseException) */ public boolean isParserHaltOnWarningsEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(PARSER_HALT_ON_WARNINGS_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Is halting of the XML parser on {@linkplain ErrorHandler#error(SAXParseException) errors} * {@linkplain #PARSER_HALT_ON_ERRORS_DISABLE_PROP disabled}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If halting of the XML parser on {@linkplain ErrorHandler#error(SAXParseException) errors} is * {@linkplain #PARSER_HALT_ON_ERRORS_DISABLE_PROP disabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #PARSER_HALT_ON_ERRORS_DISABLE_PROP * @see ErrorHandler#error(SAXParseException) */ public boolean isParserHaltOnErrorsDisabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(PARSER_HALT_ON_ERRORS_DISABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * Is halting of the XML parser on {@linkplain ErrorHandler#fatalError(SAXParseException) fatal errors} * {@linkplain #PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP disabled}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return If halting of the XML parser on {@linkplain ErrorHandler#fatalError(SAXParseException) fatal errors} is * {@linkplain #PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP disabled}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP * @see ErrorHandler#error(SAXParseException) */ public boolean isParserHaltOnFatalErrorsDisabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception { return getConfigPropReq(PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE); } /** * <p> * Is the supplied {@linkplain File#getPath() path} a relative path, which, if * {@linkplain java.net.URI#resolve(String) resolved} against some other path/folder, would point to a location * <em>within</em> that path's folder. * </p> * * <ul> * <li><code>"/hello/"</code> returns <code>false</code>.</li> * <li><code>"./"</code> returns <code>true</code>.</li> * <li><code>"./hello.txt"</code> returns <code>true</code>.</li> * <li><code>"hello/"</code> returns <code>true</code>.</li> * <li><code>"hello/../"</code> returns <code>true</code>.</li> * <li><code>"hello/world/../"</code> returns <code>true</code>.</li> * <li><code>"hello/world/../../"</code> returns <code>true</code>.</li> * <li><code>"hello/world/../.././"</code> returns <code>true</code>.</li> * <li><code>"hello/world/../../../"</code> returns <code>false</code>.</li> * <li><code>".."</code> returns <code>false</code>.</li> * <li><code>"../"</code> returns <code>false</code>.</li> * </ul> * * @param path The {@linkplain File#getPath() path} to some file or directory. * @return <code>true</code> if the supplied <code>path</code> is a relative subpath. * @throws IllegalArgumentException If <code>path</code> is {@linkplain String#isEmpty() empty}. */ protected static final boolean isRelativeSubPath(final String path) throws IllegalArgumentException { if (path.isEmpty()) throw new IllegalArgumentException("emtpy path"); if (path.charAt(0) == '/') return false; int depth = 0; final StringBuffer currentComponent = new StringBuffer(path.length()); for (int i = 0; i < path.length(); i++) { final char c = path.charAt(i); if (c != '/') { currentComponent.append(c); } else { final String currentComponentString = currentComponent.toString(); if (currentComponentString.equals("..")) { depth--; if (depth < 0) return false; } else if ((currentComponentString.length() > 0) && (!currentComponentString.equals("."))) { depth++; } currentComponent.setLength(0); } } if ((currentComponent.length() == 2) && (currentComponent.toString().equals("..")) && (depth <= 0)) return false; return true; } /** * <p> * Do the supplied {@link URL}'s both point to the same {@linkplain URL#getHost() host} and {@linkplain URL#getPort() * port}? * </p> * * <p> * If either of the supplied URL's don't specify a {@linkplain URL#getPort() port}, the * {@linkplain URL#getDefaultPort() default} will be used. If there is no default port, the comparison will fail * unless both have no default. * </p> * * @param url1 The first URL. * @param url2 The second URL. * @return <code>true</code> if the URL's point to the same host and port. * @throws IllegalArgumentException If <code>url1</code> or <code>url2</code> is <code>null</code>. */ protected static final boolean equalHostAndPort(final @Nullable URL url1, final @Nullable URL url2) throws IllegalArgumentException { if (url1 == null) throw new IllegalArgumentException("null url1"); if (url2 == null) throw new IllegalArgumentException("null url2"); if (!StringUtil.equal(StringUtil.mkNull(url1.getHost()), StringUtil.mkNull(url2.getHost()), false)) return false; final int url1Port = (url1.getPort() >= 0) ? url1.getPort() : url1.getDefaultPort(); final int url2Port = (url2.getPort() >= 0) ? url2.getPort() : url2.getDefaultPort(); if (url1Port != url2Port) return false; return true; } /** * Get the {@linkplain #BASE_URI_PROP base URI} of the document or site to be * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied}. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return The {@linkplain #BASE_URI_PROP base URI} of the document or site to be * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied} * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #BASE_URI_PROP * @see #getProxiedFileURL(Page.Request, Channel.Mode, boolean) * @see #PROXIED_BASE_URI_HOOK */ public URI getProxiedBaseURI(final Page.Request pageRequest) throws WWWEEEPortal.Exception { URI baseURI = PROXIED_BASE_URI_HOOK.value(plugins, null, pageRequest); if (baseURI == null) { baseURI = getConfigPropReq(BASE_URI_PROP, pageRequest, STRING_TO_URI_FUNCTION, null); } baseURI = PROXIED_BASE_URI_HOOK .requireFilteredResult(PROXIED_BASE_URI_HOOK.filter(plugins, null, pageRequest, baseURI)); return baseURI; } /** * Get the <em>relative</em> {@linkplain #DEFAULT_PATH_PROP path} (within the * {@linkplain #getProxiedBaseURI(Page.Request) proxied base URI}) to the document which should be displayed within * this channel by default. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return The {@linkplain #DEFAULT_PATH_PROP path} to the document which should be displayed within this channel by * default. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #DEFAULT_PATH_PROP * @see #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) * @see #PROXIED_FILE_PATH_DEFAULT_HOOK */ protected @Nullable URI getProxiedFilePathDefault(final Page.Request pageRequest) throws WWWEEEPortal.Exception { URI defaultPath = PROXIED_FILE_PATH_DEFAULT_HOOK.value(plugins, null, pageRequest); if (defaultPath == null) { defaultPath = getConfigPropOpt(DEFAULT_PATH_PROP, pageRequest, STRING_TO_URI_FUNCTION).orElse(null); if ((defaultPath != null) && ((defaultPath.isAbsolute()) || (defaultPath.getPath() == null) || (defaultPath.getPath().startsWith("/")))) { throw new ConfigManager.ConfigException("The '" + DEFAULT_PATH_PROP + "' property is not a relative path: '" + defaultPath.toString() + '\'', null); } } defaultPath = PROXIED_FILE_PATH_DEFAULT_HOOK.filter(plugins, null, pageRequest, defaultPath); return defaultPath; } /** * Perform any desired modifications to a link which is <em>not</em> being * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) rewritten} to point back through this * channel. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param linkURI The {@link URI} of the link to rewrite. * @param hyperlink Is the <code>linkURI</code> a hyperlink? * @param absoluteURLRequired Does the result need to be {@linkplain URI#isAbsolute() absolute}? * @param resolvedLinkURL The <code>linkURI</code> after being resolved to it's actual location. * @return The rewritten link {@link URI}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) */ protected static final URI rewriteProxiedFileLinkOutsideChannel(final Page.Request pageRequest, final URL proxiedFileURL, final @Nullable URI linkURI, final boolean hyperlink, final boolean absoluteURLRequired, final URL resolvedLinkURL) throws WWWEEEPortal.Exception { if ((linkURI != null) && (linkURI.isAbsolute())) { return linkURI; // If a document author includes an absolute link, we generally want to just leave that as-is. } try { if ((!absoluteURLRequired) && (equalHostAndPort(proxiedFileURL, pageRequest.getBaseURL()))) { // They didn't author an absolute link, we don't require one, and since the resolved link points to our host/port, we have the opportunity to return a nice short non-absolute link... final StringBuffer sb = new StringBuffer(); sb.append(resolvedLinkURL.getPath()); if (resolvedLinkURL.getQuery() != null) { sb.append('?'); sb.append(resolvedLinkURL.getQuery()); } if (resolvedLinkURL.getRef() != null) { sb.append('#'); sb.append(resolvedLinkURL.getRef()); } return new URI(sb.toString()); } return resolvedLinkURL.toURI(); } catch (URISyntaxException urise) { throw new ContentManager.ContentException("Error constructing resolved link URI", urise); } } /** * <p> * Rewrite a <code>linkURI</code> associated with a <code>proxiedFileURL</code>, if required. * </p> * * <p> * Technically, there are two types of links within a document. First, a link within a document can be to an * <em>external resource</em>, which is loaded automatically by the browser to augment that document (ie a link to an * image, style sheet, script, etc). Or, second, a link within a document can be a <em>hyperlink</em>, which, when * activated by the user, will cause the browser to stop displaying that document and navigate to displaying the * linked document instead. * </p> * * <p> * If the portal is configured to display a website to clients through this <code>ProxyChannel</code>, it is generally * expected that if the client navigates a hyperlink from one document to another within the proxied site, that the * linked document would also be rendered within the channel ({@linkplain net.www_eee.portal.Channel.Mode#VIEW view * mode}), and that any external resource links will continue to resolve correctly. Link rewriting is required for * each of these two scenarios to work. * </p> * * <p> * To continue rendering within {@linkplain net.www_eee.portal.Channel.Mode#VIEW view mode} while navigating between * website documents, any hyperlink from within a proxied document to another document within the * {@linkplain #getProxiedBaseURI(Page.Request) proxied site} will, by default, be rewritten to point back through * this channel (alternatively, hyperlinks may {@linkplain #isLinkRewritingHyperlinksToChannelDisabled(Page.Request) * optionally} be resolved into an {@linkplain URI#isAbsolute() absolute} link pointing directly back to their * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location} instead). * </p> * * <p> * If this channel were to blindly return unmodified source HTML from a proxied document for aggregation into a * {@link Page}, any relative link would break when it was incorrectly resolved relative to the * {@link net.www_eee.portal.ContentDef.Page.Key#getPageURI(UriInfo, Map, String, boolean) URL} of that page, instead * of relative to the {@linkplain #BASE_URI_PROP base URL} of the document providing it. To avoid this, any relative * link to an external resource from within a proxied document will, by default, be resolved into an * {@linkplain URI#isAbsolute() absolute} link pointing directly back to the * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location} for that resource (alternatively, resource * links may {@linkplain #isLinkRewritingResourceLinksToChannelEnabled(Page.Request) optionally} be rewritten to point * back through this channel using {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource mode} instead). * </p> * * <p> * For link rewriting to work, the <code>ProxyChannel</code> obviously needs to know which attributes of a proxied * document constitute <em>links</em>. But since the implementation is generic, and doesn't actually understand any * particular dialect of markup language on it's own, <em>including HTML</em>, you will likely want to configure this * channel alongside a plugin which does, such as the * {@linkplain net.www_eee.portal.channelplugins.ProxyChannelHTMLSource HTML plugin}. * </p> * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param linkURI The {@link URI} of the link to rewrite. * @param hyperlink Is the <code>linkURI</code> a hyperlink? * @param absoluteURLRequired Does the result need to be {@linkplain URI#isAbsolute() absolute}? * @return A Map.Entry containing the rewritten link value, and a Boolean specifying if the returned link points back * through the channel. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #isLinkRewritingHyperlinksToChannelDisabled(Page.Request) * @see #isLinkRewritingResourceLinksToChannelEnabled(Page.Request) * @see net.www_eee.portal.channelplugins.ProxyChannelHTMLSource */ public Map.Entry<URI, Boolean> rewriteProxiedFileLink(final Page.Request pageRequest, final URL proxiedFileURL, final @Nullable URI linkURI, final boolean hyperlink, final boolean absoluteURLRequired) throws WWWEEEPortal.Exception { if ((linkURI != null) && (linkURI.isOpaque())) { return new AbstractMap.SimpleImmutableEntry<URI, Boolean>(linkURI, Boolean.FALSE); // Leave all the opaque ones alone (stuff like "mailto" links, etc), as we can't do anything with those. } final @NonNull URL resolvedLinkURL; // First, resolve the URL for the link so we know what server+file we are actually talking about here. try { if (linkURI == null) { resolvedLinkURL = proxiedFileURL; // An empty (null) link is equivalent to one back to the same proxied file. } else if (linkURI.isAbsolute()) { resolvedLinkURL = linkURI.toURL(); } else { resolvedLinkURL = new URL(proxiedFileURL, linkURI.toString()); // Resolve the link relative to the file it came from. } } catch (MalformedURLException mue) { throw new ContentManager.ContentException("Error resolving proxied link URL", mue); } if (((hyperlink) && (isLinkRewritingHyperlinksToChannelDisabled(pageRequest))) || ((!hyperlink) && (!isLinkRewritingResourceLinksToChannelEnabled(pageRequest)))) { // We are configured to not write this link back through the portal. return new AbstractMap.SimpleImmutableEntry<URI, Boolean>( rewriteProxiedFileLinkOutsideChannel(pageRequest, proxiedFileURL, linkURI, hyperlink, absoluteURLRequired, resolvedLinkURL), Boolean.FALSE); } /* * At this point, in order to determine what modifications to the link might be required, we need to figure out if * the link points to something either within, or outside of, the channel base URI's folder? */ if ((linkURI != null) && (linkURI.isAbsolute()) && (!equalHostAndPort(resolvedLinkURL, proxiedFileURL))) { // This is an absolute link which doesn't even point to the same server as the proxied file. return new AbstractMap.SimpleImmutableEntry<URI, Boolean>( rewriteProxiedFileLinkOutsideChannel(pageRequest, proxiedFileURL, linkURI, hyperlink, absoluteURLRequired, resolvedLinkURL), Boolean.FALSE); } /* * At this point we know the link at least points to the same server as the proxied file, but is it within the * channel base URI's folder? */ final String resolvedLinkPath = StringUtil.toString(StringUtil.mkNull(resolvedLinkURL.getPath()), "/"); final URI baseURI = getProxiedBaseURI(pageRequest); final URI resolvedBaseURI; if (baseURI.isAbsolute()) { resolvedBaseURI = baseURI; } else { resolvedBaseURI = ConfigManager.getContextResourceLocalHostURI(pageRequest.getUriInfo(), baseURI.getPath(), NetUtil.getQueryParams(baseURI), baseURI.getFragment(), true); } final String baseURIPath = resolvedBaseURI.getPath(); final String baseURIFolder; if ((baseURIPath.length() == 1) || (baseURIPath.charAt(baseURIPath.length() - 1) == '/')) { baseURIFolder = baseURIPath; // Path is a folder. } else { final int lastSlashIndex = baseURIPath.lastIndexOf('/'); baseURIFolder = (lastSlashIndex > 0) ? baseURIPath.substring(0, lastSlashIndex + 1) : String.valueOf('/'); } if (!resolvedLinkPath.startsWith(baseURIFolder)) { // We have determined this link is not within the channel base URI folder. return new AbstractMap.SimpleImmutableEntry<URI, Boolean>( rewriteProxiedFileLinkOutsideChannel(pageRequest, proxiedFileURL, linkURI, hyperlink, absoluteURLRequired, resolvedLinkURL), Boolean.FALSE); } /* * At this point we know the link points to within the channel base URI's folder, and that we need to rewrite it to * point back through the channel. */ final String linkChannelLocalPath = StringUtil.mkNull(resolvedLinkPath.substring(baseURIFolder.length())); final Mode channelMode = ((hyperlink) && (!isMaximizationDisabled(pageRequest))) ? Mode.VIEW : Mode.RESOURCE; final ContentDef.ChannelSpec<?> channelSpec = pageRequest.getChannelSpec(this); return new AbstractMap.SimpleImmutableEntry<URI, Boolean>( channelSpec.getKey().getChannelURI(pageRequest.getUriInfo(), channelMode, linkChannelLocalPath, (linkURI != null) ? NetUtil.getQueryParams(linkURI) : null, (linkURI != null) ? linkURI.getFragment() : null, absoluteURLRequired), Boolean.TRUE); } /** * Combine the {@linkplain net.www_eee.portal.Page.Request#getChannelLocalPath(Channel) channel local path} (or the * {@linkplain #getProxiedFilePathDefault(Page.Request) default path} if that's <code>null</code>) and * {@linkplain UriInfo#getQueryParameters() query parameters} from the client <code>pageRequest</code>, to construct a * URI containing a <em>relative</em> path and query params, which can later be resolved against the * {@linkplain #getProxiedBaseURI(Page.Request) base URI} to create the * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. This method can also * <code>validate</code> the request against any {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) parent * folder} or {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restrictions. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param validate Should any {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) parent folder} or * {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restrictions be evaluated? * @return The proxied file "local {@link URI}". * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #getProxiedFileURL(Page.Request, Channel.Mode, boolean) * @see #PROXIED_FILE_LOCAL_URI_HOOK */ protected @Nullable URI getProxiedFileLocalURI(final Page.Request pageRequest, final Mode mode, final boolean validate) throws WWWEEEPortal.Exception, WebApplicationException { final URI channelLocalPath = pageRequest.getChannelLocalPath(this); final Object[] context = new Object[] { channelLocalPath, mode }; URI proxiedFileLocalURI = PROXIED_FILE_LOCAL_URI_HOOK.value(plugins, context, pageRequest); if (proxiedFileLocalURI == null) { final URI proxiedFilePath; if (channelLocalPath != null) { if ((validate) && (isDefaultPathRestrictionEnabled(pageRequest))) { // The default path restriction applies to both view-mode and resource-mode requests. final URI proxiedFilePathDefault = getProxiedFilePathDefault(pageRequest); if (!channelLocalPath.equals(proxiedFilePathDefault)) { throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) .entity("Request outside default path").type("text/plain").build()); } } if ((validate) && (!isParentFoldersRestrictionDisabled(pageRequest))) { if (!isRelativeSubPath(channelLocalPath.getPath())) { throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) .entity("Request outside base URL folder").type("text/plain").build()); } } proxiedFilePath = channelLocalPath; } else if (Mode.VIEW.equals(mode)) { proxiedFilePath = getProxiedFilePathDefault(pageRequest); } else { proxiedFilePath = null; // The default path only applies to the view, and isn't used for resource requests. } final Map<String, List<String>> reqParameters = (pageRequest.isMaximized(this)) ? pageRequest.getUriInfo().getQueryParameters() : null; if ((validate) && (isDefaultPathRestrictionEnabled(pageRequest)) && (reqParameters != null) && (!reqParameters.isEmpty())) { throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) .entity("Request outside default path").type("text/plain").build()); } if ((proxiedFilePath == null) && ((reqParameters == null) || (reqParameters.isEmpty()))) return null; final StringBuffer proxiedFileLocalURIBuffer = (proxiedFilePath != null) ? new StringBuffer(proxiedFilePath.toString()) : new StringBuffer(); if ((reqParameters != null) && (!reqParameters.isEmpty())) proxiedFileLocalURIBuffer.append(reqParameters.entrySet().stream() .flatMap((entry) -> entry.getValue().stream().<Map.Entry<String, String>>map( (i) -> new AbstractMap.SimpleImmutableEntry<String, String>(entry.getKey(), i))) .map((entry) -> NetUtil.urlEncode(entry.getKey()) + '=' + NetUtil.urlEncode(entry.getValue())) .collect(Collectors.joining("&", "?", ""))); //TODO proxied parameter blacklist try { proxiedFileLocalURI = new URI(proxiedFileLocalURIBuffer.toString()); } catch (URISyntaxException urise) { throw new WWWEEEPortal.SoftwareException(urise); } } proxiedFileLocalURI = PROXIED_FILE_LOCAL_URI_HOOK.filter(plugins, context, pageRequest, proxiedFileLocalURI); return proxiedFileLocalURI; } /** * Construct the final {@linkplain URI#isAbsolute() absolute} {@link URL} for the * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxied} file by resolving the * relative {@linkplain #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) proxied file local URI} against * the {@linkplain #getProxiedBaseURI(Page.Request) base URI}, and if the result isn't absolute, against the * {@linkplain ConfigManager#getContextResourceLocalHostURI(UriInfo, String, Map, String, boolean) local host context} * . * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param validate Should any {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) parent folder} or * {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restrictions be evaluated? * @return The proxied file {@link URL}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) * @see #PROXIED_FILE_URL_HOOK */ protected URL getProxiedFileURL(final Page.Request pageRequest, final Mode mode, final boolean validate) throws WWWEEEPortal.Exception, WebApplicationException { final Object[] context = new Object[] { mode, Boolean.valueOf(validate) }; URL proxiedFileURL = PROXIED_FILE_URL_HOOK.value(plugins, context, pageRequest); if (proxiedFileURL == null) { try { final URI proxiedFileLocalURI = getProxiedFileLocalURI(pageRequest, mode, validate); final URI baseURI = getProxiedBaseURI(pageRequest); if (proxiedFileLocalURI != null) { final URI proxiedFileURI = baseURI.resolve(proxiedFileLocalURI); if (proxiedFileURI.isAbsolute()) { proxiedFileURL = proxiedFileURI.toURL(); } else { proxiedFileURL = ConfigManager .getContextResourceLocalHostURI(pageRequest.getUriInfo(), proxiedFileURI.getPath(), NetUtil.getQueryParams(proxiedFileURI), proxiedFileURI.getFragment(), true) .toURL(); } } else { if (baseURI.isAbsolute()) { proxiedFileURL = baseURI.toURL(); } else { proxiedFileURL = ConfigManager.getContextResourceLocalHostURI(pageRequest.getUriInfo(), baseURI.getPath(), NetUtil.getQueryParams(baseURI), baseURI.getFragment(), true) .toURL(); } } } catch (MalformedURLException mue) { throw new WWWEEEPortal.SoftwareException(mue); } } proxiedFileURL = PROXIED_FILE_URL_HOOK .requireFilteredResult(PROXIED_FILE_URL_HOOK.filter(plugins, context, pageRequest, proxiedFileURL)); return proxiedFileURL; } /** * Construct the {@link org.apache.http.client.CookieStore CookieStore} to be used for * {@linkplain #createProxyClient(Page.Request) creating} the {@link HttpClient}. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return The created {@link org.apache.http.client.CookieStore CookieStore}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #createProxyClient(Page.Request) * @see #PROXY_CLIENT_COOKIE_STORE_HOOK */ protected org.apache.http.client.CookieStore createProxyClientCookieStore(final Page.Request pageRequest) throws WWWEEEPortal.Exception { org.apache.http.client.CookieStore proxyClientCookieStore = PROXY_CLIENT_COOKIE_STORE_HOOK.value(plugins, null, pageRequest); if (proxyClientCookieStore == null) { proxyClientCookieStore = new BasicCookieStore(); } proxyClientCookieStore = PROXY_CLIENT_COOKIE_STORE_HOOK.requireFilteredResult( PROXY_CLIENT_COOKIE_STORE_HOOK.filter(plugins, null, pageRequest, proxyClientCookieStore)); return proxyClientCookieStore; } /** * Construct and configure the {@link HttpClient} to be used to * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content while fulfilling the specified * <code>pageRequest</code>. This method will also configure {@linkplain ProxyRequestInterceptor request} and * {@linkplain ProxyResponseInterceptor response} interceptors on the created client. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @return The created {@link HttpClient}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #doProxyRequest(Page.Request, Channel.Mode) * @see #PROXY_CLIENT_HOOK * @see ProxyRequestInterceptor * @see ProxyResponseInterceptor */ protected CloseableHttpClient createProxyClient(final Page.Request pageRequest) throws WWWEEEPortal.Exception { final org.apache.http.client.CookieStore proxyClientCookieStore = createProxyClientCookieStore(pageRequest); Object[] context = new Object[] { proxyClientManager, proxyClientCookieStore }; HttpClientBuilder proxyClientBuilder = PROXY_CLIENT_HOOK.value(plugins, context, pageRequest); if (proxyClientBuilder == null) { final HttpClientBuilder pcb = HttpClientBuilder.create(); Optional.ofNullable(proxyClientManager) .ifPresent((proxyClientManager) -> pcb.setConnectionManager(proxyClientManager)); proxyClientBuilder = pcb; proxyClientBuilder.setDefaultCookieStore(proxyClientCookieStore); proxyClientBuilder = proxyClientBuilder.addInterceptorLast(new ProxyRequestInterceptor(pageRequest)); proxyClientBuilder = proxyClientBuilder.addInterceptorLast(new ProxyResponseInterceptor(pageRequest)); } proxyClientBuilder = PROXY_CLIENT_HOOK .requireFilteredResult(PROXY_CLIENT_HOOK.filter(plugins, context, pageRequest, proxyClientBuilder)); return proxyClientBuilder.build(); } /** * Construct and configure the {@link RequestConfig} to be used for * {@linkplain #createProxyRequestObject(Page.Request, Mode, URL) creating} the {@link HttpUriRequest} to * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content while fulfilling the specified * <code>pageRequest</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @return The created {@link RequestConfig}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #createProxyClient(Page.Request) * @see #PROXY_CLIENT_REQUEST_CONFIG_HOOK */ protected RequestConfig createProxyClientRequestConfig(final Page.Request pageRequest, final Mode mode) throws WWWEEEPortal.Exception { final Object[] context = new Object[] { mode }; RequestConfig.Builder requestConfig = PROXY_CLIENT_REQUEST_CONFIG_HOOK.value(plugins, context, pageRequest); if (requestConfig == null) { requestConfig = RequestConfig.copy(RequestConfig.DEFAULT); requestConfig = requestConfig.setConnectTimeout(getConnectTimeout(pageRequest)); requestConfig = requestConfig.setSocketTimeout(getReadTimeout(pageRequest)); requestConfig = requestConfig.setRedirectsEnabled(isFollowRedirectsEnabled(pageRequest)); requestConfig = requestConfig.setAuthenticationEnabled(false); } requestConfig = PROXY_CLIENT_REQUEST_CONFIG_HOOK.requireFilteredResult( PROXY_CLIENT_REQUEST_CONFIG_HOOK.filter(plugins, context, pageRequest, requestConfig)); return requestConfig.build(); } /** * Construct the {@link HttpUriRequest} implementation appropriate to * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} the specified * <code>pageRequest</code> to the <code>proxiedFileURL</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @return The created {@link HttpUriRequest}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) * @see #PROXY_REQUEST_OBJ_HOOK */ protected HttpRequestBase createProxyRequestObject(final Page.Request pageRequest, final Mode mode, final URL proxiedFileURL) throws WWWEEEPortal.Exception, WebApplicationException { final RequestConfig requestConfig = createProxyClientRequestConfig(pageRequest, mode); final Object[] context = new Object[] { mode, proxiedFileURL, requestConfig }; HttpRequestBase proxyRequest = PROXY_REQUEST_OBJ_HOOK.value(plugins, context, pageRequest); if (proxyRequest == null) { final URI proxiedFileURI; try { proxiedFileURI = proxiedFileURL.toURI(); } catch (URISyntaxException urise) { throw new WWWEEEPortal.SoftwareException(urise); } final String method = pageRequest.getRSRequest().getMethod(); if ((!pageRequest.isMaximized(this)) || (method.equalsIgnoreCase("GET"))) { proxyRequest = new HttpGet(proxiedFileURI); } else if (method.equalsIgnoreCase("HEAD")) { proxyRequest = new HttpHead(proxiedFileURI); } else if (method.equalsIgnoreCase("POST")) { proxyRequest = new HttpPost(proxiedFileURI); } else if (method.equalsIgnoreCase("PUT")) { proxyRequest = new HttpPut(proxiedFileURI); } else if (method.equalsIgnoreCase("DELETE")) { proxyRequest = new HttpDelete(proxiedFileURI); } else { throw new WebApplicationException( Response.status(RESTUtil.Response.Status.METHOD_NOT_ALLOWED).build()); } proxyRequest.setConfig(requestConfig); } proxyRequest = PROXY_REQUEST_OBJ_HOOK .requireFilteredResult(PROXY_REQUEST_OBJ_HOOK.filter(plugins, context, pageRequest, proxyRequest)); return proxyRequest; } /** * Construct the value for the "User-Agent" header to be {@link HttpUriRequest#setHeader(String, String) * set} on the {@link HttpUriRequest} being used to * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the * <code>proxiedFileURL</code>. This method starts with the {@linkplain javax.ws.rs.core.HttpHeaders#USER_AGENT * USER_AGENT} header provided by the client, appends the WWW-EEE-Portal {@link #USER_AGENT_HEADER}, followed by the * {@linkplain CoreProtocolPNames#USER_AGENT USER_AGENT} for the {@link HttpClient} being used. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest} * object. * @return The "User-Agent" header value. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #USER_AGENT_HEADER * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) */ protected @Nullable String getProxyRequestUserAgentHeader(final Page.Request pageRequest, final Mode mode, final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest) throws WWWEEEPortal.Exception, WebApplicationException { final StringBuffer value = new StringBuffer(); final Optional<String> clientRequestHeader = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), javax.ws.rs.core.HttpHeaders.USER_AGENT, Function.identity()); if (clientRequestHeader.isPresent()) value.append(clientRequestHeader.get()); if (value.length() > 0) value.append(' '); value.append(USER_AGENT_HEADER); //FIXME Include original User-Agent? // final String proxyRequestUserAgent = (String)proxyClient.getParams().getParameter(CoreProtocolPNames.USER_AGENT); // if (proxyRequestUserAgent != null) { // value.append(' '); // value.append(proxyRequestUserAgent); // } return value.toString(); } /** * Construct the value for the "Accept-Language" header to be * {@link HttpUriRequest#setHeader(String, String) set} on the {@link HttpUriRequest} being used to * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the * <code>proxiedFileURL</code> . This method basically just fowards the * {@link javax.ws.rs.core.HttpHeaders#getAcceptableLanguages() acceptable} to the client. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest} * object. * @return The "Accept-Language" header value. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see javax.ws.rs.core.HttpHeaders#getAcceptableLanguages() * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) */ protected @Nullable String getProxyRequestAcceptLanguageHeader(final Page.Request pageRequest, final Mode mode, final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest) throws WWWEEEPortal.Exception, WebApplicationException { final List<Locale> acceptableLanguages = pageRequest.getHttpHeaders().getAcceptableLanguages(); if ((acceptableLanguages == null) || (acceptableLanguages.isEmpty())) return null; final StringBuffer acceptLanguage = new StringBuffer(); for (Locale locale : acceptableLanguages) { if (acceptLanguage.length() > 0) acceptLanguage.append(','); acceptLanguage.append(locale.getLanguage()); if (!locale.getCountry().isEmpty()) { acceptLanguage.append('-'); acceptLanguage.append(locale.getCountry()); } } return acceptLanguage.toString(); } /** * Construct the value for the "Accept" header to be {@link HttpUriRequest#setHeader(String, String) set} on * the {@link HttpUriRequest} being used to * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the * <code>proxiedFileURL</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest} * object. * @return The "Accept" header value. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) */ protected @Nullable String getProxyRequestAcceptHeader(final Page.Request pageRequest, final Mode mode, final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest) throws WWWEEEPortal.Exception, WebApplicationException { if (Mode.VIEW.equals(mode)) { final StringBuffer accept = new StringBuffer(); accept.append(HTMLUtil.APPLICATION_XHTML_XML_MIME_TYPE); // Defaults to q=1. accept.append(','); accept.append(XMLUtil.APPLICATION_XML_MIME_TYPE); // Defaults to q=1. accept.append(','); accept.append(XMLUtil.TEXT_XML_MIME_TYPE); // They should likely be serving this as "application/something+xml" instead. accept.append(";q=0.9,"); accept.append(APPLICATION_STAR_MIME_TYPE); // Ideally this would be "application/*+xml;q=1", but since we can't do that, at least do this. accept.append(";q=0.5,"); accept.append(TEXT_STAR_MIME_TYPE); // ProxyChannel can render text, but it's not anywhere near as ideal as HTML. accept.append(";q=0.4"); return accept.toString(); } return RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "Accept", Function.identity()) .orElse(null); // The ProxyChannel doesn't really interact with content through resource mode, so just forward whatever the client wants. } /** * Construct the value for the "Authorization" header to be {@link HttpUriRequest#setHeader(String, String) * set} on the {@link HttpUriRequest} being used to * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the * <code>proxiedFileURL</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest} * object. * @return The "Authorization" header value. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) */ protected @Nullable String getProxyRequestAuthorizationHeader(final Page.Request pageRequest, final Mode mode, final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest) throws WWWEEEPortal.Exception, WebApplicationException { if ((!proxyRequest.containsHeader("Authorization")) && (RESTUtil .getFirstHeaderValue(pageRequest.getHttpHeaders(), "Authorization", Function.identity()) != null)) { return RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "Authorization", Function.identity()) .orElse(null); } return null; } /** * Construct the {@link HttpUriRequest} that will be used to {@linkplain #doProxyRequest(Page.Request, Channel.Mode) * proxy} content while fulfilling the specified <code>pageRequest</code>. This method will * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) calculate} the proxied file URL, * {@linkplain #createProxyRequestObject(Page.Request, Channel.Mode, URL) create} the appropriate type of request * object, set it's {@linkplain HttpUriRequest#setHeader(String, String) headers}, and, if this channel is * {@linkplain net.www_eee.portal.Page.Request#isMaximized(Channel) maximized}, set any * {@linkplain HttpEntityEnclosingRequest#setEntity(HttpEntity) entity} that was * {@linkplain net.www_eee.portal.Page.Request#getEntity() provided} by the <code>pageRequest</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request. * @return The proxy {@link HttpUriRequest} object. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #doProxyRequest(Page.Request, Channel.Mode) * @see #PROXY_REQUEST_HOOK */ protected HttpRequestBase createProxyRequest(final Page.Request pageRequest, final Mode mode, final CloseableHttpClient proxyClient) throws WWWEEEPortal.Exception, WebApplicationException { final URL proxiedFileURL = getProxiedFileURL(pageRequest, mode, true); final Object[] context = new Object[] { mode, proxiedFileURL }; HttpRequestBase proxyRequest = PROXY_REQUEST_HOOK.value(plugins, context, pageRequest); if (proxyRequest == null) { proxyRequest = createProxyRequestObject(pageRequest, mode, proxiedFileURL); final String userAgent = getProxyRequestUserAgentHeader(pageRequest, mode, proxyClient, proxiedFileURL, proxyRequest); if (userAgent != null) proxyRequest.setHeader("User-Agent", userAgent); final String acceptLanguage = getProxyRequestAcceptLanguageHeader(pageRequest, mode, proxyClient, proxiedFileURL, proxyRequest); if (acceptLanguage != null) proxyRequest.setHeader("Accept-Language", acceptLanguage); final String accept = getProxyRequestAcceptHeader(pageRequest, mode, proxyClient, proxiedFileURL, proxyRequest); if (accept != null) proxyRequest.setHeader("Accept", accept); final String authorization = getProxyRequestAuthorizationHeader(pageRequest, mode, proxyClient, proxiedFileURL, proxyRequest); if (authorization != null) proxyRequest.setHeader("Authorization", authorization); if (Mode.RESOURCE.equals(mode)) { final Optional<String> ifMatch = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "If-Match", Function.identity()); if (ifMatch.isPresent()) proxyRequest.setHeader("If-Match", ifMatch.get()); final Optional<String> ifModifiedSince = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "If-Modified-Since", Function.identity()); if (ifModifiedSince.isPresent()) proxyRequest.setHeader("If-Modified-Since", ifModifiedSince.get()); final Optional<String> ifNoneMatch = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "If-None-Match", Function.identity()); if (ifNoneMatch.isPresent()) proxyRequest.setHeader("If-None-Match", ifNoneMatch.get()); final Optional<String> ifUnmodifiedSince = RESTUtil.getFirstHeaderValue( pageRequest.getHttpHeaders(), "If-Unmodified-Since", Function.identity()); if (ifUnmodifiedSince.isPresent()) proxyRequest.setHeader("If-Unmodified-Since", ifUnmodifiedSince.get()); } if (!isCacheControlClientDirectivesDisabled(pageRequest)) { final Optional<CacheControl> cacheControl = RESTUtil .getFirstHeaderValue(pageRequest.getHttpHeaders(), "Cache-Control", CacheControl::valueOf); if (cacheControl.isPresent()) proxyRequest.setHeader("Cache-Control", cacheControl.get().toString()); } if (pageRequest.isMaximized(this)) { final MediaType contentType = pageRequest.getHttpHeaders().getMediaType(); if (contentType != null) proxyRequest.setHeader("Content-Type", contentType.toString()); final DataSource entity = pageRequest.getEntity(); if ((entity != null) && (proxyRequest instanceof HttpEntityEnclosingRequest)) { try { final Optional<String> contentLengthString = RESTUtil.getFirstHeaderValue( pageRequest.getHttpHeaders(), "Content-Length", Function.identity()); final HttpEntity httpEntity = new InputStreamEntity(entity.getInputStream(), (contentLengthString.isPresent()) ? Long.parseLong(contentLengthString.get()) : -1); ((HttpEntityEnclosingRequest) proxyRequest).setEntity(httpEntity); } catch (NumberFormatException nfe) { throw new WebApplicationException(nfe, Response.Status.INTERNAL_SERVER_ERROR); } catch (IOException ioe) { throw new WWWEEEPortal.OperationalException(ioe); } } } // if (pageRequest.isMaximized(this)) } // if (proxyRequest == null) proxyRequest = PROXY_REQUEST_HOOK .requireFilteredResult(PROXY_REQUEST_HOOK.filter(plugins, context, pageRequest, proxyRequest)); return proxyRequest; } /** * Construct a {@link Reader} for the {@linkplain HttpResponse#getEntity() entity} within the supplied * <code>proxyResponse</code>. This method will attempt to use the given <code>responseContentType</code> to * {@linkplain IOUtil#getCharsetParameter(MimeType) determine} the correct {@link Charset} to use for * {@linkplain InputStreamReader#InputStreamReader(InputStream, Charset) decoding} the * {@linkplain HttpEntity#getContent() byte stream}. * * @param proxyResponse The {@link HttpResponse} containing the proxied file. * @param responseContentType The {@link MimeType} of the proxied file. * @return A {@link Reader} for the proxied content. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. */ protected @Nullable Reader createProxiedFileReader(final HttpResponse proxyResponse, final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception { if (proxyResponse.getEntity() == null) return null; final Optional<Charset> charset; try { charset = (responseContentType != null) ? IOUtil.getCharsetParameter(responseContentType) : Optional.empty(); } catch (UnsupportedCharsetException uce) { throw new ContentManager.ContentException("Proxied file has unsupported charset", uce); } try { return (charset.isPresent()) ? new InputStreamReader(proxyResponse.getEntity().getContent(), charset.get()) : new InputStreamReader(proxyResponse.getEntity().getContent()); } catch (IOException ioe) { throw new WWWEEEPortal.OperationalException(ioe); } } /** * Examine the {@linkplain HttpResponse#getStatusLine() status} {@link StatusLine#getStatusCode() code} from the * response to the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy request} and throw an exception if * something went wrong. * * @param proxyContext The {@link HttpClientContext} containing the {@linkplain ExecutionContext#HTTP_RESPONSE * response} from the proxied server. * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @return The supplied <code>proxyClientContext</code> argument. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #doProxyRequest(Page.Request, Channel.Mode) */ protected HttpClientContext validateProxyResponse(final HttpClientContext proxyContext, final Page.Request pageRequest, final Mode mode) throws WWWEEEPortal.Exception, WebApplicationException { final HttpResponse proxyResponse = proxyContext.getResponse(); final int responseCode = proxyResponse.getStatusLine().getStatusCode(); if (responseCode == Response.Status.OK.getStatusCode()) return proxyContext; try { if (responseCode == Response.Status.NOT_MODIFIED.getStatusCode()) { throw new WebApplicationException(Response.Status.NOT_MODIFIED); } else if (((responseCode >= Response.Status.MOVED_PERMANENTLY.getStatusCode()) && (responseCode <= Response.Status.SEE_OTHER.getStatusCode())) || (responseCode == Response.Status.TEMPORARY_REDIRECT.getStatusCode())) { // Moved Permanently, Found, See Other, Temporary Redirect if (pageRequest.isMaximized(this)) { final URI fixedLocation; try { final Optional<URI> locationURI = HttpUtil.getValue(proxyResponse.getLastHeader("Location"), URI::create); final URL proxiedFileURL = HttpUtil.getRequestTargetURL(proxyContext); fixedLocation = rewriteProxiedFileLink(pageRequest, proxiedFileURL, locationURI.orElse(null), Mode.VIEW.equals(mode), true).getKey(); } catch (Exception e) { throw new ContentManager.ContentException("Error rewriting 'Location' header", e); } throw new WebApplicationException( Response.status(RESTUtil.Response.Status.fromStatusCode(responseCode)) .location(fixedLocation).build()); } } else if (responseCode == Response.Status.UNAUTHORIZED.getStatusCode()) { if (pageRequest.isMaximized(this)) { throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) .header("WWW-Authenticate", HttpUtil .getValue(proxyResponse.getLastHeader("WWW-Authenticate"), Function.identity()) .orElse(null)) .build()); } } else if ((responseCode == Response.Status.NOT_FOUND.getStatusCode()) || (responseCode == Response.Status.GONE.getStatusCode())) { final URI channelLocalPath = pageRequest.getChannelLocalPath(this); if (channelLocalPath != null) { throw new WebApplicationException(Response.Status.fromStatusCode(responseCode)); } } else if (responseCode == RESTUtil.Response.Status.METHOD_NOT_ALLOWED.getStatusCode()) { final URI channelLocalPath = pageRequest.getChannelLocalPath(this); if (channelLocalPath != null) { throw new WebApplicationException( Response.status(RESTUtil.Response.Status.METHOD_NOT_ALLOWED) .header("Allow", HttpUtil .getValue(proxyResponse.getLastHeader("Allow"), Function.identity()) .orElse(null)) .build()); } } else if (responseCode == RESTUtil.Response.Status.REQUEST_TIMEOUT.getStatusCode()) { // we didn't send it to the proxied server fast enough!? throw new WWWEEEPortal.OperationalException(new WebApplicationException(responseCode)); } else if (responseCode == Response.Status.BAD_REQUEST.getStatusCode()) { throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).build()); } else if ((responseCode >= 400) && (responseCode < 500)) { // All other 4XX errors if (pageRequest.isMaximized(this)) { throw new WebApplicationException( Response.status(RESTUtil.Response.Status.fromStatusCode(responseCode)).build()); } } else if ((responseCode >= RESTUtil.Response.Status.BAD_GATEWAY.getStatusCode()) && (responseCode <= RESTUtil.Response.Status.GATEWAY_TIMEOUT.getStatusCode())) { // Bad Gateway, Service Unavailable, Gateway Timeout throw new WWWEEEPortal.OperationalException(new WebApplicationException(responseCode)); } final Response.StatusType statusType = RESTUtil.Response.Status.fromStatusCode(responseCode); final String codePhrase = (statusType != null) ? " (" + statusType.getReasonPhrase() + ")" : ""; final String responsePhrase = (proxyResponse.getStatusLine().getReasonPhrase() != null) ? ": " + proxyResponse.getStatusLine().getReasonPhrase() : ""; final ConfigManager.ConfigException configurationException = new ConfigManager.ConfigException( "The proxied file server returned code '" + responseCode + "'" + codePhrase + responsePhrase, null); if (getLogger().isLoggable(Level.FINE)) { try { final Reader reader = createProxiedFileReader(proxyResponse, getProxyResponseHeader(pageRequest, proxyResponse, "Content-Type", IOUtil::newMimeType)); LogAnnotation.annotate(configurationException, "ProxiedFileResponseContent", (reader != null) ? IOUtil.toString(reader) : null, Level.FINE, false); } catch (Exception e) { } } throw configurationException; } catch (WWWEEEPortal.Exception wpe) { try { LogAnnotation.annotate(wpe, "ProxiedFileURL", HttpUtil.getRequestTargetURL(proxyContext), null, false); } catch (Exception e) { } LogAnnotation.annotate(wpe, "ProxiedFileResponseCode", responseCode, null, false); LogAnnotation.annotate(wpe, "ProxiedFileResponseCodeReasonPhrase", RESTUtil.Response.Status.fromStatusCode(responseCode), null, false); LogAnnotation.annotate(wpe, "ProxiedFileResponseReasonPhrase", proxyResponse.getStatusLine().getReasonPhrase(), null, false); throw wpe; } } /** * {@linkplain #createProxyClient(Page.Request) Create} an {@link HttpClient} and use it to * {@linkplain HttpClient#execute(HttpUriRequest, HttpContext) execute} a * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy request} to retrieve the * content to be returned/rendered in response to the supplied <code>pageRequest</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request. * @return An {@link HttpClientContext} containing the {@linkplain ExecutionContext#HTTP_RESPONSE response} from the * proxied server. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. * @see #PROXY_RESPONSE_HOOK */ protected HttpClientContext doProxyRequest(final Page.Request pageRequest, final Mode mode) throws WWWEEEPortal.Exception, WebApplicationException { final HttpClientContext proxyContext = HttpClientContext.create(); final CloseableHttpClient proxyClient = createProxyClient(pageRequest); proxyContext.setAttribute(HTTP_CLIENT_CONTEXT_ID, proxyClient); try { CloseableHttpResponse proxyResponse = null; while (proxyResponse == null) { // Keep trying again if a plugin filter null's out the previous response. final HttpRequestBase proxyRequest = createProxyRequest(pageRequest, mode, proxyClient); try { final Object[] context = new Object[] { mode, proxyContext, proxyClient, proxyRequest }; proxyResponse = PROXY_RESPONSE_HOOK.value(plugins, context, pageRequest); if (proxyResponse == null) { try { proxyResponse = proxyClient.execute(proxyRequest, proxyContext); } catch (UnknownHostException uhe) { throw new ConfigManager.ConfigException(uhe); } catch (IOException ioe) { throw new WWWEEEPortal.OperationalException(ioe); } } proxyResponse = PROXY_RESPONSE_HOOK.filter(plugins, context, pageRequest, proxyResponse); } catch (WWWEEEPortal.Exception wpe) { LogAnnotation.annotate(wpe, "ProxyRequest", proxyRequest, null, false); LogAnnotation.annotate(wpe, "ProxyResponse", proxyResponse, null, false); LogAnnotation.annotate(wpe, "ProxiedFileURL", proxyRequest.getURI(), null, false); throw wpe; } } // while (proxyResponse == null) return validateProxyResponse(proxyContext, pageRequest, mode); } catch (WWWEEEPortal.Exception wpe) { LogAnnotation.annotate(wpe, "ProxyContext", proxyContext, null, false); try { LogAnnotation.annotate(wpe, "ProxiedFileURL", HttpUtil.getRequestTargetURL(proxyContext), null, false); // This wouldn't be necessary if any of the previous annotations could actually toString() themselves usefully. } catch (Exception e) { } throw wpe; } } /** * {@linkplain HttpResponse#getHeaders(String) Retrieve} the value of the named header (multiple values will be * {@link HttpUtil#consolidate(Header[]) consolidated}), using the supplied <code>converter</code> to handle the data * type. * * @param <R> The type of value returned for this header. * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param proxyResponse The {@link HttpResponse} received for the proxied file. * @param headerName The name of the response header desired. * @param headerReadingFunction A {@link Function} capable of converting the header value to the desired result type. * @return The converted value of the header. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #PROXY_RESPONSE_HEADER_HOOK */ public <R> @Nullable R getProxyResponseHeader(final Page.Request pageRequest, final HttpResponse proxyResponse, final String headerName, final Function<String, R> headerReadingFunction) throws WWWEEEPortal.Exception { final Object[] context = new Object[] { proxyResponse, headerName }; Header header = PROXY_RESPONSE_HEADER_HOOK.value(plugins, context, pageRequest); if (header == null) { header = HttpUtil.consolidate(proxyResponse.getHeaders(headerName)).orElse(null); } if (header == null) header = new BasicHeader(headerName, null); // Give filters access to the headerName header = PROXY_RESPONSE_HEADER_HOOK.filter(plugins, context, pageRequest, header); return HttpUtil .getValue(header, FuncUtil.mapEx(headerReadingFunction, ContentManager.ContentException::new)) .orElse(null); } /** * Should the content contained within the supplied <code>proxyResponse</code> having the supplied * <code>responseContentType</code> be * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the XML * view}? By default, this method will return <code>true</code> for any {@linkplain XMLUtil#isXML(MimeType) XML} * MimeType. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param proxyResponse The {@link HttpResponse} received for the proxied file. * @param responseContentType The {@link MimeType} of the proxied file. * @return If the <code>proxyResponse</code> content should be * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the XML * view}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) * @see #IS_RENDERED_USING_XML_VIEW_HOOK */ protected boolean isRenderedUsingXMLView(final Page.Request pageRequest, final HttpResponse proxyResponse, final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception { final Object[] context = new Object[] { proxyResponse, responseContentType }; Boolean isRenderedUsingXMLView = IS_RENDERED_USING_XML_VIEW_HOOK.value(plugins, context, pageRequest); if (isRenderedUsingXMLView == null) { isRenderedUsingXMLView = Boolean.valueOf(XMLUtil.isXML(responseContentType)); } return Boolean.TRUE.equals( IS_RENDERED_USING_XML_VIEW_HOOK.filter(plugins, context, pageRequest, isRenderedUsingXMLView)); } /** * Create a {@link #createProxiedFileReader(HttpResponse, MimeType) Reader} for the <code>proxyResponse</code> and * wrap it in a {@link TypedInputSource}. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param proxyResponse The {@link HttpResponse} received for the proxied file. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param responseContentType The {@link MimeType} of the proxied file. * @return A {@link TypedInputSource} containing the proxied document. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) * @see #PROXIED_DOC_INPUT_SOURCE_HOOK */ protected TypedInputSource createProxiedDocumentInputSource(final Page.Request pageRequest, final HttpResponse proxyResponse, final URL proxiedFileURL, final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception { final Object[] context = new Object[] { proxyResponse, proxiedFileURL, responseContentType }; TypedInputSource proxiedDocumentInputSource = PROXIED_DOC_INPUT_SOURCE_HOOK.value(plugins, context, pageRequest); if (proxiedDocumentInputSource == null) { final Reader proxiedFileReader = createProxiedFileReader(proxyResponse, responseContentType); proxiedDocumentInputSource = new TypedInputSource(new InputSource(proxiedFileReader), responseContentType); } proxiedDocumentInputSource = PROXIED_DOC_INPUT_SOURCE_HOOK.requireFilteredResult( PROXIED_DOC_INPUT_SOURCE_HOOK.filter(plugins, context, pageRequest, proxiedDocumentInputSource)); return proxiedDocumentInputSource; } /** * Create a {@linkplain DOMBuildingHandler} to add the parsed XML to the channel * {@linkplain net.www_eee.portal.Channel.ViewResponse#getContentContainerElement() content}. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param proxiedDocumentInputSource The * {@linkplain #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) input source} containing * the proxied file content. * @return A {@link DefaultHandler2} to handle the parsing events for the proxied content. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) * @see #PROXIED_DOC_CONTENT_HANDLER_HOOK */ protected DefaultHandler2 createProxiedDocumentContentHandler(final Page.Request pageRequest, final ViewResponse viewResponse, final URL proxiedFileURL, final TypedInputSource proxiedDocumentInputSource) throws WWWEEEPortal.Exception { final Object[] context = new Object[] { viewResponse, proxiedFileURL, proxiedDocumentInputSource }; DefaultHandler2 contentHandler = PROXIED_DOC_CONTENT_HANDLER_HOOK.value(plugins, context, pageRequest); if (contentHandler == null) { contentHandler = new DOMBuildingHandler((DefaultHandler2) null, viewResponse.getContentContainerElement()); } contentHandler = PROXIED_DOC_CONTENT_HANDLER_HOOK.requireFilteredResult( PROXIED_DOC_CONTENT_HANDLER_HOOK.filter(plugins, context, pageRequest, contentHandler)); return contentHandler; } /** * Create an {@linkplain #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) input source}, * {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean) feed} * it to an XML parser, and * {@linkplain #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) handle} * the results. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated. * @param proxyResponse The {@link HttpResponse} received for the proxied file. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param responseContentType The {@link MimeType} of the proxied file. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #isRenderedUsingXMLView(Page.Request, HttpResponse, MimeType) * @see #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) * @see MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean) * @see #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) * @see #PARSE_XML_HOOK */ protected void renderXMLView(final Page.Request pageRequest, final ViewResponse viewResponse, final HttpResponse proxyResponse, final URL proxiedFileURL, final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception { final TypedInputSource proxiedDocumentInputSource = createProxiedDocumentInputSource(pageRequest, proxyResponse, proxiedFileURL, responseContentType); final DefaultHandler2 contentHandler = createProxiedDocumentContentHandler(pageRequest, viewResponse, proxiedFileURL, proxiedDocumentInputSource); final Object[] context = new Object[] { viewResponse, proxiedDocumentInputSource, contentHandler }; Boolean parsedXML = PARSE_XML_HOOK.value(plugins, context, pageRequest); if (!Boolean.TRUE.equals(parsedXML)) { MarkupManager.parseXMLDocument(proxiedDocumentInputSource.getInputSource(), contentHandler, isParserHaltOnWarningsEnabled(pageRequest), !isParserHaltOnErrorsDisabled(pageRequest), !isParserHaltOnFatalErrorsDisabled(pageRequest), false); } PARSE_XML_HOOK.filter(plugins, context, pageRequest, Boolean.TRUE); final Optional<Element> contentRootElement = DOMUtil .getChildElement(viewResponse.getContentContainerElement(), null, null); if (contentRootElement.isPresent()) { final Locale contentLocale = DOMUtil.getXMLLangAttr(contentRootElement.get(), Locale.ROOT); if (!Locale.ROOT.equals(contentLocale)) viewResponse.setLocale(contentLocale); } return; } /** * Should the content contained within the supplied <code>proxyResponse</code> having the supplied * <code>responseContentType</code> be * {@linkplain #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the * Text view}? * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param proxyResponse The {@link HttpResponse} received for the proxied file. * @param responseContentType The {@link MimeType} of the proxied file. * @return If the <code>proxyResponse</code> content should be * {@linkplain #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the * Text view}. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) * @see #IS_RENDERED_USING_TEXT_VIEW_HOOK */ protected boolean isRenderedUsingTextView(final Page.Request pageRequest, final HttpResponse proxyResponse, final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception { final Object[] context = new Object[] { proxyResponse, responseContentType }; Boolean isRenderedUsingTextView = IS_RENDERED_USING_TEXT_VIEW_HOOK.value(plugins, context, pageRequest); if (isRenderedUsingTextView == null) { isRenderedUsingTextView = Boolean.valueOf((responseContentType != null) && (IOUtil.TEXT_PLAIN_MIME_TYPE.getBaseType().equals(responseContentType.getBaseType()))); } return Boolean.TRUE.equals( IS_RENDERED_USING_TEXT_VIEW_HOOK.filter(plugins, context, pageRequest, isRenderedUsingTextView)); } /** * {@link #createProxiedFileReader(HttpResponse, MimeType) Read} the <code>proxyResponse</code> into a * {@link IOUtil#toString(Reader) String} and add it to the channel * {@link net.www_eee.portal.Channel.ViewResponse#getContentContainerElement() content} within a * <code><pre></code> element. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated. * @param proxyResponse The {@link HttpResponse} received for the proxied file. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param responseContentType The {@link MimeType} of the proxied file. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @see #isRenderedUsingTextView(Page.Request, HttpResponse, MimeType) * @see #createProxiedFileReader(HttpResponse, MimeType) */ protected void renderTextView(final Page.Request pageRequest, final ViewResponse viewResponse, final HttpResponse proxyResponse, final URL proxiedFileURL, final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception { final Reader proxiedDocumentReader = createProxiedFileReader(proxyResponse, responseContentType); final String textString = (proxiedDocumentReader != null) ? IOUtil.toString(proxiedDocumentReader) : null; final Element channelContentContainerElement = viewResponse.getContentContainerElement(); final Element preElement = DOMUtil.createElement(HTMLUtil.HTML_NS_URI, HTMLUtil.HTML_NS_PREFIX, "pre", channelContentContainerElement); setIDAndClassAttrs(preElement, Arrays.asList("proxy", "text"), null, null); DOMUtil.createAttr(XMLUtil.XML_NS_URI, "xml", "space", "preserve", preElement); if ((textString != null) && (!textString.isEmpty())) DOMUtil.appendText(preElement, textString); return; } /** * Add an <code><object></code> element to the channel * {@link net.www_eee.portal.Channel.ViewResponse#getContentContainerElement() content} creating an external resource * reference to the content at the given <code>proxiedFileURL</code> (link * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) rewritten} if necessary). * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated. * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. * @param contentType The {@link MimeType} of the proxied file. * @throws WWWEEEPortal.Exception If a problem occurred while determining the result. * @throws WebApplicationException If a problem occurred while determining the result. */ protected void renderResourceReferenceView(final Page.Request pageRequest, final ViewResponse viewResponse, final URL proxiedFileURL, final @Nullable MimeType contentType) throws WWWEEEPortal.Exception, WebApplicationException { final Element contentContainerElement = viewResponse.getContentContainerElement(); final Document contentContainerDocument = DOMUtil.getDocument(contentContainerElement); final Element objectElement = DOMUtil.createElement(HTMLUtil.HTML_NS_URI, HTMLUtil.HTML_NS_PREFIX, "object", contentContainerElement, contentContainerDocument, true, true); final String[][] extraObjectClasses; if (contentType != null) { final String contentTypeString = contentType.toString(); DOMUtil.createAttr(null, null, "type", contentTypeString, objectElement); final StringBuffer safeTypeClass = new StringBuffer(); for (int i = 0; i < contentTypeString.length(); i++) { char c = contentTypeString.charAt(i); if (Character.isLetterOrDigit(c)) { safeTypeClass.append(c); } else { safeTypeClass.append('_'); } } extraObjectClasses = new String[][] { new String[] { portal.getPortalID(), "channel", channelDef.getID(), "resource", "type", safeTypeClass.toString() } }; } else { extraObjectClasses = null; } setIDAndClassAttrs(objectElement, Arrays.asList("proxy", "resource"), extraObjectClasses, null); final URI resourceURI = rewriteProxiedFileLink(pageRequest, proxiedFileURL, null, false, false).getKey(); DOMUtil.createAttr(null, null, "data", resourceURI.toString(), objectElement); return; } /** * This method will create a new {@link CacheControl} by intelligently merging the values from the * <code>newCacheControl</code> into the <code>defaultCacheControl</code> values, removing a default 'public' * directive if the input specifies a 'private' one, etc. * * @param defaultCacheControl The base {@link CacheControl} values. * @param newCacheControl The {@link CacheControl} to use to update the <code>defaultCacheControl</code> values with. * @return A merged {@link CacheControl}. */ protected static final @Nullable CacheControl mergeCacheControl( final @Nullable CacheControl defaultCacheControl, final @Nullable CacheControl newCacheControl) { if ((defaultCacheControl == null) || (defaultCacheControl.equals(newCacheControl))) return newCacheControl; final CacheControl mergedCacheControl = RESTUtil.copy(defaultCacheControl); if (newCacheControl == null) return mergedCacheControl; if (newCacheControl.getCacheExtension().containsKey("public")) { mergedCacheControl.getCacheExtension().put("public", null); mergedCacheControl.setPrivate(false); mergedCacheControl.getPrivateFields().clear(); mergedCacheControl.setNoStore(false); mergedCacheControl.setNoCache(false); mergedCacheControl.getNoCacheFields().clear(); } if (newCacheControl.isPrivate()) { mergedCacheControl.setPrivate(true); mergedCacheControl.getPrivateFields().clear(); mergedCacheControl.getPrivateFields().addAll(newCacheControl.getPrivateFields()); mergedCacheControl.getCacheExtension().remove("public"); } if (newCacheControl.isNoCache()) { mergedCacheControl.setNoCache(true); mergedCacheControl.getNoCacheFields().clear(); mergedCacheControl.getNoCacheFields().addAll(newCacheControl.getPrivateFields()); mergedCacheControl.getCacheExtension().remove("public"); mergedCacheControl.setMaxAge(-1); mergedCacheControl.setSMaxAge(-1); } if (newCacheControl.isNoStore()) { mergedCacheControl.setNoStore(true); mergedCacheControl.getCacheExtension().remove("public"); mergedCacheControl.setMaxAge(-1); mergedCacheControl.setSMaxAge(-1); } if (newCacheControl.isNoTransform()) { mergedCacheControl.setNoTransform(true); } if (newCacheControl.isMustRevalidate()) { mergedCacheControl.setMustRevalidate(true); } if (newCacheControl.isProxyRevalidate()) { mergedCacheControl.setProxyRevalidate(true); } if (newCacheControl.getMaxAge() >= 0) { mergedCacheControl.setMaxAge(newCacheControl.getMaxAge()); mergedCacheControl.setNoCache(false); mergedCacheControl.setNoStore(false); } if (newCacheControl.getSMaxAge() >= 0) { mergedCacheControl.setSMaxAge(newCacheControl.getSMaxAge()); mergedCacheControl.setNoCache(false); mergedCacheControl.setNoStore(false); } return mergedCacheControl; } @Override protected void doViewRequestImpl(final Page.Request pageRequest, final ViewResponse viewResponse) throws WWWEEEPortal.Exception, WebApplicationException { final HttpClientContext proxyContext = doProxyRequest(pageRequest, Mode.VIEW); final @NonNull HttpResponse proxyResponse = proxyContext.getResponse(); final URL proxiedFileURL = HttpUtil.getRequestTargetURL(proxyContext); try (final CloseableHttpClient proxyClient = Objects .requireNonNull((CloseableHttpClient) proxyContext.getAttribute(HTTP_CLIENT_CONTEXT_ID))) { final MimeType responseContentType = getProxyResponseHeader(pageRequest, proxyResponse, "Content-Type", IOUtil::newMimeType); viewResponse.setContentType( (responseContentType != null) ? RESTUtil.getMediaType(responseContentType) : null); viewResponse.setLastModified(getProxyResponseHeader(pageRequest, proxyResponse, "Last-Modified", (s) -> ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant())); viewResponse.setCacheControl(mergeCacheControl(viewResponse.getCacheControl(), getProxyResponseHeader(pageRequest, proxyResponse, "Cache-Control", CacheControl::valueOf))); viewResponse.setExpires(getProxyResponseHeader(pageRequest, proxyResponse, "Expires", (s) -> ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant())); viewResponse .setEntityTag(getProxyResponseHeader(pageRequest, proxyResponse, "ETag", EntityTag::valueOf)); viewResponse.setLocale(getProxyResponseHeader(pageRequest, proxyResponse, "Content-Language", (h) -> Locale.forLanguageTag( CollUtil.first(Arrays.asList(StringUtil.COMMA_SEPARATED_PATTERN.split(h))).get()))); if (isRenderedUsingXMLView(pageRequest, proxyResponse, responseContentType)) { renderXMLView(pageRequest, viewResponse, proxyResponse, proxiedFileURL, responseContentType); } else if (isRenderedUsingTextView(pageRequest, proxyResponse, responseContentType)) { renderTextView(pageRequest, viewResponse, proxyResponse, proxiedFileURL, responseContentType); } else { renderResourceReferenceView(pageRequest, viewResponse, proxiedFileURL, responseContentType); } } catch (WWWEEEPortal.Exception wpe) { LogAnnotation.annotate(wpe, "ProxyContext", proxyContext, null, false); LogAnnotation.annotate(wpe, "ProxyResponse", proxyResponse, null, false); LogAnnotation.annotate(wpe, "ProxiedFileURL", proxiedFileURL, null, false); // This wouldn't be necessary if any of the previous annotations could actually toString() themselves usefully. throw wpe; } catch (IOException ioe) { throw new WWWEEEPortal.OperationalException(ioe); } return; } @Override protected Response doResourceRequestImpl(final Page.Request pageRequest) throws WWWEEEPortal.Exception, WebApplicationException { final HttpClientContext proxyContext = doProxyRequest(pageRequest, Mode.RESOURCE); @SuppressWarnings("resource") final CloseableHttpClient proxyClient = Objects .requireNonNull((CloseableHttpClient) proxyContext.getAttribute(HTTP_CLIENT_CONTEXT_ID)); final @NonNull HttpResponse proxyResponse = proxyContext.getResponse(); try { final Response.ResponseBuilder responseBuilder = Response.ok(); responseBuilder.lastModified(getProxyResponseHeader(pageRequest, proxyResponse, "Last-Modified", (s) -> Date.from(ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant()))); final MimeType responseContentType = getProxyResponseHeader(pageRequest, proxyResponse, "Content-Type", IOUtil::newMimeType); responseBuilder.type((responseContentType != null) ? RESTUtil.getMediaType(responseContentType) : null); responseBuilder.cacheControl(mergeCacheControl(getCacheControlDefault().orElse(null), getProxyResponseHeader(pageRequest, proxyResponse, "Cache-Control", CacheControl::valueOf))); responseBuilder.expires(getProxyResponseHeader(pageRequest, proxyResponse, "Expires", (s) -> Date.from(ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant()))); responseBuilder.tag(getProxyResponseHeader(pageRequest, proxyResponse, "ETag", EntityTag::valueOf)); responseBuilder.language(getProxyResponseHeader(pageRequest, proxyResponse, "Content-Language", (h) -> Locale.forLanguageTag(StringUtil.COMMA_SEPARATED_PATTERN.split("")[0]).toString())); final HttpEntity proxyResponseEntity = proxyResponse.getEntity(); final Long contentLength = (proxyResponseEntity != null) ? Long.valueOf(proxyResponseEntity.getContentLength()) : null; responseBuilder.header("Content-Length", contentLength); if (proxyResponseEntity != null) { responseBuilder.entity(HttpUtil.getDataSource(proxyResponseEntity, proxyClient)); } else { try { proxyClient.close(); } catch (IOException ioe) { throw new WWWEEEPortal.OperationalException(ioe); } } return responseBuilder.build(); } catch (WWWEEEPortal.Exception wpe) { LogAnnotation.annotate(wpe, "ProxyContext", proxyContext, null, false); LogAnnotation.annotate(wpe, "ProxyResponse", proxyResponse, null, false); try { LogAnnotation.annotate(wpe, "ProxiedFileURL", HttpUtil.getRequestTargetURL(proxyContext), null, false); // This wouldn't be necessary if any of the previous annotations could actually toString() themselves usefully. } catch (Exception e) { } throw wpe; } } /** * An {@link HttpRequestInterceptor} which is {@link AbstractHttpClient#addRequestInterceptor(HttpRequestInterceptor) * added} to all {@link HttpClient}'s {@linkplain ProxyChannel#createProxyClient(Page.Request) created} to * {@linkplain ProxyChannel#doProxyRequest(Page.Request, Channel.Mode) proxy} content for this channel, which, when * {@linkplain #process(HttpRequest, HttpContext) processed}, allows {@link net.www_eee.portal.Channel.Plugin * plugin's} a {@linkplain #PROXY_REQUEST_INTERCEPTOR_HOOK hook} they can use to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * {@link HttpRequest}. * * @see ProxyChannel#PROXY_REQUEST_INTERCEPTOR_HOOK */ public final class ProxyRequestInterceptor implements HttpRequestInterceptor { /** * The {@link net.www_eee.portal.Page.Request Request} currently being processed. */ protected final Page.Request pageRequest; /** * Construct a new <code>ProxyRequestInterceptor</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. */ public ProxyRequestInterceptor(final Page.Request pageRequest) { this.pageRequest = pageRequest; return; } @Override @SuppressWarnings("synthetic-access") public void process(final HttpRequest proxyRequest, final HttpContext proxyContext) throws HttpException, IOException { try { PROXY_REQUEST_INTERCEPTOR_HOOK.filter(plugins, new Object[] { HttpClientContext.adapt(proxyContext) }, pageRequest, proxyRequest); } catch (WWWEEEPortal.Exception wpe) { final HttpException httpException = new HttpException(); httpException.initCause(wpe); throw httpException; } return; } } // ProxyRequestInterceptor /** * An {@link HttpResponseInterceptor} which is * {@link AbstractHttpClient#addResponseInterceptor(HttpResponseInterceptor) added} to all {@link HttpClient}'s * {@linkplain ProxyChannel#createProxyClient(Page.Request) created} to * {@linkplain ProxyChannel#doProxyRequest(Page.Request, Channel.Mode) proxy} content for this channel, which, when * {@linkplain #process(HttpResponse, HttpContext) processed}, allows {@link net.www_eee.portal.Channel.Plugin * plugin's} a {@linkplain ProxyChannel#PROXY_RESPONSE_INTERCEPTOR_HOOK hook} they can use to * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the * {@link HttpResponse}. * * @see ProxyChannel#PROXY_RESPONSE_INTERCEPTOR_HOOK */ public final class ProxyResponseInterceptor implements HttpResponseInterceptor { /** * The {@link net.www_eee.portal.Page.Request Request} currently being processed. */ protected final Page.Request pageRequest; /** * Construct a new <code>ProxyResponseInterceptor</code>. * * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed. */ public ProxyResponseInterceptor(final Page.Request pageRequest) { this.pageRequest = pageRequest; return; } @Override @SuppressWarnings("synthetic-access") public void process(final HttpResponse proxyResponse, final HttpContext proxyContext) throws HttpException, IOException { try { PROXY_RESPONSE_INTERCEPTOR_HOOK.filter(plugins, new Object[] { HttpClientContext.adapt(proxyContext) }, pageRequest, proxyResponse); } catch (WWWEEEPortal.Exception wpe) { final HttpException httpException = new HttpException(); httpException.initCause(wpe); throw httpException; } return; } } // ProxyResponseInterceptor }