/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.esigate.extension.surrogate; import static org.apache.commons.lang3.ArrayUtils.contains; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.join; import static org.apache.commons.lang3.StringUtils.split; import static org.apache.commons.lang3.StringUtils.strip; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.esigate.Driver; import org.esigate.Parameters; import org.esigate.events.Event; import org.esigate.events.EventDefinition; import org.esigate.events.EventManager; import org.esigate.events.IEventListener; import org.esigate.events.impl.FetchEvent; import org.esigate.events.impl.FragmentEvent; import org.esigate.events.impl.ProxyEvent; import org.esigate.extension.Extension; import org.esigate.extension.parallelesi.Esi; import org.esigate.extension.surrogate.http.Capability; import org.esigate.extension.surrogate.http.SurrogateCapabilities; import org.esigate.extension.surrogate.http.SurrogateCapabilitiesHeader; import org.esigate.http.DeleteResponseHeader; import org.esigate.http.MoveResponseHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implementation of Edge-Arch specification 1.0. * * <p> * See : * <ul> * <li>http://www.w3.org/TR/edge-arch</li> * <li>http://docs.oracle.com/cd/E17904_01/web.1111/e10143/esi.htm</li> * </ul> * * <p> * This extension allows control of esigate features from the provider application. * * <p> * This extension cooperates with other extensions to get the currently supported capabilities. To participate to * capabilities collection, and extension should register to EVENT_SURROGATE_CAPABILITIES and provide capabilities when * the event is fired. * <p> * * <pre> * driver.getEventManager().register(Surrogate.EVENT_SURROGATE_CAPABILITIES, new IEventListener() { * public boolean event(EventDefinition id, Event event) { * CapabilitiesEvent capEvent = (CapabilitiesEvent) event; * capEvent.capabilities.add("ESI/1.0"); * capEvent.capabilities.add("ESI-Inline/1.0"); * capEvent.capabilities.add("ESIGATE/4.0"); * return true; * } * }); * </pre> * <p> * The provider may respond with control directive. In that case, the extension should ensure that its capability are * requested and do not process response otherwise. * <p> * * <pre> * if (renderEvent.httpResponse.containsHeader(Surrogate.H_X_ENABLED_CAPABILITIES)) { * String capabilities = renderEvent.httpResponse.getFirstHeader(Surrogate.H_X_ENABLED_CAPABILITIES).getValue(); * * if (!containsIgnoreCase(capabilities, "ESI/1.0") { * // Cancel processing * } * } * </pre> * * <p> * Targeting is supported. * * <p> * NOTE: * <ul> * <li>no-store-remote is not honored since esigate is generally not used as a CDN.</li> * </ul> * * @author Nicolas Richeton * */ public class Surrogate implements Extension, IEventListener { private static final String H_SURROGATE_CONTROL = "Surrogate-Control"; private static final String H_SURROGATE_CAPABILITIES = "Surrogate-Capabilities"; private static final Logger LOG = LoggerFactory.getLogger(Surrogate.class); /** * This is an internal header, used to track the requested capabilities. * <p> * Value is the value of the content directive from the Surrogate-Control header. */ public static final String H_X_ENABLED_CAPABILITIES = "X-Esigate-Internal-Enabled-Capabilities"; /** * This header is used to flag the presence of another Surrogate in front of esigate. */ private static final String H_X_SURROGATE = "X-Esigate-Internal-Surrogate"; private static final String H_X_ORIGINAL_CACHE_CONTROL = "X-Esigate-Int-Surrogate-OCC"; /** * This header is used to store the esigate instance id for the current request. */ private static final String H_X_SURROGATE_ID = "X-Esigate-Int-Surrogate-Id"; /** * This is an internal header used to store the value of the Surrogate-Control header which will be set to the next * surrogate. This is based on the original header, but with the removal of Capabilities processed on the current * hop. */ private static final String H_X_NEXT_SURROGATE_CONTROL = "X-Esigate-Int-Surrogate-NSC"; private String[] capabilities; /** * Pre-generated esigate token including all capabilities reported by extensions. */ private String esigateToken; /** * This event is fired on startup to collect all installed capabilities. */ public static final EventDefinition EVENT_SURROGATE_CAPABILITIES = new EventDefinition( "org.esigate.surrogate.capabilities", EventDefinition.TYPE_DEFAULT); private static final String CAP_SURROGATE = "Surrogate/1.0"; @Override public void init(Driver driver, Properties properties) { CapabilitiesEvent capEvent = new CapabilitiesEvent(); capEvent.getCapabilities().add(CAP_SURROGATE); // Build all supported capabilities driver.getEventManager().fire(EVENT_SURROGATE_CAPABILITIES, capEvent); List<String> var = capEvent.getCapabilities(); this.capabilities = var.toArray(new String[var.size()]); LOG.info("Surrogate capabilities: {}", join(this.capabilities, " ")); // Build esigate token this.esigateToken = "=\"" + join(this.capabilities, " ") + "\""; // Register for events. driver.getEventManager().register(EventManager.EVENT_FETCH_PRE, this); driver.getEventManager().register(EventManager.EVENT_FETCH_POST, this); driver.getEventManager().register(EventManager.EVENT_PROXY_PRE, this); driver.getEventManager().register(EventManager.EVENT_PROXY_POST, this); driver.getEventManager().register(EventManager.EVENT_FRAGMENT_PRE, this); // Restore original Cache-Control header driver.getEventManager().register(EventManager.EVENT_FRAGMENT_POST, new MoveResponseHeader(H_X_ORIGINAL_CACHE_CONTROL, "Cache-Control")); // Delete internal header driver.getEventManager().register(EventManager.EVENT_PROXY_POST, new DeleteResponseHeader(H_X_ENABLED_CAPABILITIES)); } /** * Return a new token, unique for the current Surrogate-Capability header. * <p> * Uses "esigate" and appends a number if necessary. * * @param currentCapabilitiesHeader * existing header which may contains tokens of other proxies (including other esigate instances). * @return unique token */ private static String getUniqueToken(String currentCapabilitiesHeader) { String token = "esigate"; if (currentCapabilitiesHeader != null && currentCapabilitiesHeader.contains(token + "=\"")) { int id = 2; while (currentCapabilitiesHeader.contains(token + id + "=\"")) { id++; } token = token + id; } return token; } @Override public boolean event(EventDefinition id, Event event) { if (EventManager.EVENT_FETCH_PRE.equals(id)) { FetchEvent e = (FetchEvent) event; // This header is used internally, and should not be forwarded. e.getHttpRequest().removeHeaders(H_X_SURROGATE); } else if (EventManager.EVENT_FETCH_POST.equals(id)) { onPostFetch(event); } else if (EventManager.EVENT_FRAGMENT_PRE.equals(id)) { // Add Surrogate-Capabilities or append to existing header. FragmentEvent e = (FragmentEvent) event; Header h = e.getHttpRequest().getFirstHeader(H_SURROGATE_CAPABILITIES); StringBuilder archCapabilities = new StringBuilder(Parameters.SMALL_BUFFER_SIZE); if (h != null && !isEmpty(h.getValue())) { archCapabilities.append(defaultString(h.getValue())); archCapabilities.append(", "); } String currentCapabilitiesHeader = null; if (h != null) { currentCapabilitiesHeader = h.getValue(); } String uniqueId = getUniqueToken(currentCapabilitiesHeader); e.getHttpRequest().setHeader(H_X_SURROGATE_ID, uniqueId); archCapabilities.append(uniqueId); archCapabilities.append(this.esigateToken); e.getHttpRequest().setHeader(H_SURROGATE_CAPABILITIES, archCapabilities.toString()); } else if (EventManager.EVENT_PROXY_PRE.equals(id)) { ProxyEvent e = (ProxyEvent) event; // Do we have another surrogate in front of esigate if (e.getOriginalRequest().containsHeader(H_SURROGATE_CAPABILITIES)) { e.getOriginalRequest().setHeader(H_X_SURROGATE, "true"); } } else if (EventManager.EVENT_PROXY_POST.equals(id)) { // Remove Surrogate Control content ProxyEvent e = (ProxyEvent) event; if (e.getResponse() != null) { processSurrogateControlContent(e.getResponse(), e.getOriginalRequest().containsHeader(H_X_SURROGATE)); removeVarySurrogateCapabilities(e.getResponse()); } else if (e.getErrorPage() != null) { processSurrogateControlContent(e.getErrorPage().getHttpResponse(), e.getOriginalRequest() .containsHeader(H_X_SURROGATE)); removeVarySurrogateCapabilities(e.getErrorPage().getHttpResponse()); } } return true; } private void removeVarySurrogateCapabilities(HttpResponse response) { // Remove Vary: Surrogate-Capabilities Header[] varyHeaders = response.getHeaders("Vary"); if (varyHeaders != null) { for (Header h : varyHeaders) { if (H_SURROGATE_CAPABILITIES.equals(h.getValue())) { response.removeHeader(h); break; } } } } /** * <ul> * <li>Inject H_X_ENABLED_CAPABILITIES into response.</li> * <li>Consume capabilities. Does not support targeting yet.</li> * <li>Update caching directives.</li> * </ul> * * @param event * Incoming fetch event. */ private void onPostFetch(Event event) { // Update caching policies FetchEvent e = (FetchEvent) event; String ourSurrogateId = e.getHttpRequest().getFirstHeader(H_X_SURROGATE_ID).getValue(); SurrogateCapabilitiesHeader surrogateCapabilitiesHeader = SurrogateCapabilitiesHeader.fromHeaderValue(e.getHttpRequest().getFirstHeader(H_SURROGATE_CAPABILITIES) .getValue()); if (!e.getHttpResponse().containsHeader(H_SURROGATE_CONTROL) && surrogateCapabilitiesHeader.getSurrogates().size() > 1) { // Ensure another proxy can process the request LinkedHashMap<String, List<String>> targetCapabilities = new LinkedHashMap<>(); initSurrogateMap(targetCapabilities, surrogateCapabilitiesHeader); for (String c : this.capabilities) { // Ignore Surrogate/1.0 if ("Surrogate/1.0".equals(c)) { continue; } String firstSurrogate = getFirstSurrogateFor(surrogateCapabilitiesHeader, c); // firstSurrogate cannot be null since we are the last surrogate. targetCapabilities.get(firstSurrogate).add(c); } fixSurrogateMap(targetCapabilities, ourSurrogateId); StringBuilder sb = new StringBuilder(); boolean firstDevice = true; for (String device : targetCapabilities.keySet()) { if (targetCapabilities.get(device).size() == 0) { continue; } if (!firstDevice) { sb.append(", "); } else { firstDevice = false; } sb.append("content=\""); boolean firstCap = true; for (String cap : targetCapabilities.get(device)) { if (!firstCap) { sb.append(" "); } else { firstCap = false; } sb.append(cap); } sb.append("\";"); sb.append(device); } e.getHttpResponse().addHeader(H_SURROGATE_CONTROL, sb.toString()); } if (!e.getHttpResponse().containsHeader(H_SURROGATE_CONTROL)) { return; } // If there is a Surrogate-Control header, add a Vary header to ensure content is not reuse when using a // different set of Surrogates e.getHttpResponse().addHeader("Vary", H_SURROGATE_CAPABILITIES); List<String> enabledCapabilities = new ArrayList<>(); List<String> remainingCapabilities = new ArrayList<>(); List<String> newSurrogateControlL = new ArrayList<>(); List<String> newCacheContent = new ArrayList<>(); String controlHeader = e.getHttpResponse().getFirstHeader(H_SURROGATE_CONTROL).getValue(); String[] control = split(controlHeader, ","); for (String directiveAndTarget : control) { String directive = strip(directiveAndTarget); // Is directive targeted int targetIndex = directive.lastIndexOf(';'); String target = null; if (targetIndex > 0) { target = directive.substring(targetIndex + 1); directive = directive.substring(0, targetIndex); } if (target != null && !target.equals(ourSurrogateId)) { // If directive is not targeted to current instance. newSurrogateControlL.add(strip(directiveAndTarget)); } else if (directive.startsWith("content=\"")) { // Handle content String[] content = split(directive.substring("content=\"".length(), directive.length() - 1), " "); for (String contentCap : content) { contentCap = strip(contentCap); if (contains(this.capabilities, contentCap)) { enabledCapabilities.add(contentCap); } else { remainingCapabilities.add(contentCap); } } if (remainingCapabilities.size() > 0) { newSurrogateControlL.add("content=\"" + join(remainingCapabilities, " ") + "\""); } } else if (directive.startsWith("max-age=")) { String[] maxAge = split(directive, "+"); newCacheContent.add(maxAge[0]); // Freshness extension if (maxAge.length > 1) { newCacheContent.add("stale-while-revalidate=" + maxAge[1]); newCacheContent.add("stale-if-error=" + maxAge[1]); } newSurrogateControlL.add(directive); } else if (directive.startsWith("no-store")) { newSurrogateControlL.add(directive); newCacheContent.add(directive); } else { newSurrogateControlL.add(directive); } } e.getHttpResponse().setHeader(H_X_ENABLED_CAPABILITIES, join(enabledCapabilities, " ")); e.getHttpResponse().setHeader(H_X_NEXT_SURROGATE_CONTROL, join(newSurrogateControlL, ", ")); // If cache control must be updated. if (newCacheContent.size() > 0) { MoveResponseHeader.moveHeader(e.getHttpResponse(), "Cache-Control", H_X_ORIGINAL_CACHE_CONTROL); e.getHttpResponse().setHeader("Cache-Control", join(newCacheContent, ", ")); } } /** * The current implementation of ESI cannot execute rules partially. For instance if ESI-Inline is requested, ESI, * ESI-Inline, X-ESI-Fragment are executed. * * <p> * This method handles this specific case : if one requested capability enables the Esi extension in this instance, * all other capabilities are moved to this instance. This prevents broken behavior. * * * @see Esi * @see org.esigate.extension.Esi * * @param targetCapabilities * @param currentSurrogate * the current surrogate id. */ private void fixSurrogateMap(LinkedHashMap<String, List<String>> targetCapabilities, String currentSurrogate) { boolean esiEnabledInEsigate = false; // Check if Esigate will perform ESI. for (String c : Esi.CAPABILITIES) { if (targetCapabilities.get(currentSurrogate).contains(c)) { esiEnabledInEsigate = true; break; } } if (esiEnabledInEsigate) { // Ensure all Esi capabilities are executed by our instance. for (String c : Esi.CAPABILITIES) { for (String device : targetCapabilities.keySet()) { if (device.equals(currentSurrogate)) { if (!targetCapabilities.get(device).contains(c)) { targetCapabilities.get(device).add(c); } } else { targetCapabilities.get(device).remove(c); } } } } } /** * Populate the Map with all current devices, with empty capabilities. * * @param targetCapabilities * @param surrogateCapabilitiesHeader */ private void initSurrogateMap(Map<String, List<String>> targetCapabilities, SurrogateCapabilitiesHeader surrogateCapabilitiesHeader) { for (SurrogateCapabilities sc : surrogateCapabilitiesHeader.getSurrogates()) { targetCapabilities.put(sc.getDeviceToken(), new ArrayList<String>()); } } /** * Returns the first surrogate which supports the requested capability. * * @param surrogateCapabilitiesHeader * @param capability * @return a Surrogate or null if the capability is not found. */ private String getFirstSurrogateFor(SurrogateCapabilitiesHeader surrogateCapabilitiesHeader, String capability) { for (SurrogateCapabilities surrogate : surrogateCapabilitiesHeader.getSurrogates()) { for (Capability sc : surrogate.getCapabilities()) { if (capability.equals(sc.toString())) { return surrogate.getDeviceToken(); } } } return null; } /** * Remove Surrogate-Control header or replace by its new value. * * @param response * backend HTTP response. * @param keepHeader * should the Surrogate-Control header be forwarded to the client. */ private static void processSurrogateControlContent(HttpResponse response, boolean keepHeader) { if (!response.containsHeader(H_SURROGATE_CONTROL)) { return; } if (!keepHeader) { response.removeHeaders(H_SURROGATE_CONTROL); return; } MoveResponseHeader.moveHeader(response, H_X_NEXT_SURROGATE_CONTROL, H_SURROGATE_CONTROL); } }