All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.acs.commons.redirects.filter.RedirectFilter Maven / Gradle / Ivy

There is a newer version: 6.6.0
Show newest version
/*
 * #%L
 * ACS AEM Commons Bundle
 * %%
 * Copyright (C) 2016 Adobe
 * %%
 * 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.
 * #L%
 */
package com.adobe.acs.commons.redirects.filter;

import com.adobe.acs.commons.redirects.LocationHeaderAdjuster;
import com.adobe.acs.commons.redirects.models.RedirectMatch;
import com.adobe.acs.commons.redirects.models.RedirectRule;
import com.adobe.granite.jmx.annotation.AnnotatedStandardMBean;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.wcm.api.WCMMode;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.engine.EngineConstants;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.NotCompliantMBeanException;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Hashtable;
import java.util.Dictionary;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.apache.sling.engine.EngineConstants.SLING_FILTER_SCOPE;
import static org.osgi.framework.Constants.SERVICE_DESCRIPTION;
import static org.osgi.framework.Constants.SERVICE_RANKING;
import static org.osgi.framework.Constants.SERVICE_ID;

/**
 * A request filter that implements support for virtual redirects.
 */
@Component(service = {Filter.class, RedirectFilterMBean.class, EventHandler.class}, property = {
        SERVICE_DESCRIPTION + "=A request filter implementing support for virtual redirects",
        SLING_FILTER_SCOPE + "=" + EngineConstants.FILTER_SCOPE_REQUEST,
        SERVICE_RANKING + ":Integer=10000",
        "jmx.objectname=" + "com.adobe.acs.commons:type=Redirect Manager",
        EventConstants.EVENT_TOPIC + "=" + ReplicationAction.EVENT_TOPIC

})
@Designate(ocd = RedirectFilter.Configuration.class)
public class RedirectFilter extends AnnotatedStandardMBean
        implements Filter, EventHandler, ResourceChangeListener, RedirectFilterMBean {

    public static final String DEFAULT_STORAGE_PATH = "/conf/acs-commons/redirects";
    public static final String ACS_REDIRECTS_RESOURCE_TYPE = "acs-commons/components/utilities/manage-redirects";
    public static final String REDIRECT_RULE_RESOURCE_TYPE = ACS_REDIRECTS_RESOURCE_TYPE + "/redirect-row";


    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    private static final String SERVICE_NAME = "redirect-manager";

    @ObjectClassDefinition(name = "ACS Commons Redirect Filter")
    public @interface Configuration {
        @AttributeDefinition(name = "Enable Redirect Filter", description = "Indicates whether the redirect filter is enabled or not.", type = AttributeType.BOOLEAN)
        boolean enabled() default true;

        @AttributeDefinition(name = "Rewrite Location Header", description = "Apply Sling Resource Mappings (/etc/map) to Location header. "
                + "Use if Location header should rewritten using ResourceResolver#map", type = AttributeType.BOOLEAN)
        boolean mapUrls() default true;

        @AttributeDefinition(name = "Request Extensions", description = "List of extensions for which redirection is allowed", type = AttributeType.STRING)
        String[] extensions() default {};

        @AttributeDefinition(name = "Request Paths", description = "List of paths for which redirection is allowed", type = AttributeType.STRING)
        String[] paths() default {"/content"};

        @AttributeDefinition(name = "Preserve Query String", description = "Preserve query string in redirects", type = AttributeType.BOOLEAN)
        boolean preserveQueryString() default true;

        @AttributeDefinition(name = "Storage Path", description = "The path in the repository to store redirect configurations", type = AttributeType.STRING)
        String storagePath() default DEFAULT_STORAGE_PATH;

        @AttributeDefinition(name = "Additional Response Headers", description = "Optional response headers in the name:value format to apply on delivery,"
                + " e.g. Cache-Control: max-age=3600", type = AttributeType.STRING)
        String[] additionalHeaders() default {};
    }

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Reference(
            cardinality = ReferenceCardinality.OPTIONAL,
            policy = ReferencePolicy.DYNAMIC,
            policyOption = ReferencePolicyOption.GREEDY
    )
    private LocationHeaderAdjuster urlAdjuster;

    private ServiceRegistration listenerRegistration;
    private boolean enabled;
    private boolean mapUrls;
    private boolean preserveQueryString;
    private List
onDeliveryHeaders; private Collection methods = Arrays.asList("GET", "HEAD"); private Collection exts; private Collection paths; private String storagePath; private Map pathRules; private Map patternRules; private ExecutorService executor; public RedirectFilter() throws NotCompliantMBeanException { super(RedirectFilterMBean.class); } @Override public void init(FilterConfig filterConfig) throws ServletException { // no op } @Activate @Modified protected final void activate(Configuration config, BundleContext context) { enabled = config.enabled(); Dictionary properties = new Hashtable<>(); properties.put(ResourceChangeListener.PATHS, config.storagePath()); listenerRegistration = context.registerService(ResourceChangeListener.class, this, properties); log.debug("Registered {}:{}", SERVICE_ID, listenerRegistration.getReference().getProperty(SERVICE_ID)); if (enabled) { exts = config.extensions() == null ? Collections.emptySet() : Arrays.stream(config.extensions()).filter(ext -> !ext.isEmpty()).collect(Collectors.toSet()); paths = config.paths() == null ? Collections.emptySet() : Arrays.stream(config.paths()).filter(path -> !path.isEmpty()).collect(Collectors.toSet()); mapUrls = config.mapUrls(); storagePath = config.storagePath(); onDeliveryHeaders = new ArrayList<>(); for(String kv : config.additionalHeaders()){ int idx = kv.indexOf(':'); if(idx == -1 || idx > kv.length() - 1) { log.error("invalid on-delivery header: {}", kv); continue; } String name = kv.substring(0, idx).trim(); String value = kv.substring(idx + 1).trim(); onDeliveryHeaders.add(new BasicHeader(name, value)); } preserveQueryString = config.preserveQueryString(); log.debug("exts: {}, paths: {}, rewriteUrls: {}, storagePath: {}", exts, paths, mapUrls, storagePath); executor = Executors.newSingleThreadExecutor(); refreshCache(); } } @Modified protected void modify(BundleContext context, Configuration config) { deactivate(); activate(config, context); } @Deactivate public void deactivate() { executor.shutdown(); if (listenerRegistration != null) { log.debug("unregistering ... "); listenerRegistration.unregister(); listenerRegistration = null; } } @Override public void handleEvent(Event event) { String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH); if (path.startsWith(getStoragePath())) { log.debug(event.toString()); // loading redirect configurations can be expensive and needs to run // asynchronously, // outside of the Sling event processing chain executor.submit(() -> refreshCache()); } } @Override public void onChange(List changes) { boolean changed = changes.stream().anyMatch(e -> e.getPath().startsWith(getStoragePath())); if(changed) { log.debug(changes.toString()); executor.submit(() -> refreshCache()); } } @Override public void refreshCache() { Map pathMatchingRules = new HashMap<>(); Map patternMatchingRules = new LinkedHashMap<>(); long t0 = System.currentTimeMillis(); try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver( Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_NAME))) { Resource storageResource = resolver.getResource(getStoragePath()); if (storageResource != null) { Collection rules = getRules(storageResource); for (RedirectRule rule : rules) { if (rule.getRegex() != null) { patternMatchingRules.put(rule.getRegex(), rule); } else { pathMatchingRules.put(rule.getSource(), rule); } } } } catch (LoginException e) { log.error("Failed to get resolver for {}", SERVICE_NAME, e); } synchronized (this) { this.pathRules = pathMatchingRules; this.patternRules = patternMatchingRules; } log.debug("{} rules loaded in {} ms", pathMatchingRules.size() + patternMatchingRules.size(), System.currentTimeMillis() - t0); } Map getPathRules() { return pathRules; } Map getPatternRules() { return patternRules; } /** * Read redirect configurations from the repository, i.e. * /conf/acs-commons/redirects --> Collection * * @param resource the parent resource containing redirect configurations * @return a list of redirect configurations . Can be empty if no redirects are * configured. */ public static Collection getRules(Resource resource) { Collection rules = new ArrayList<>(); for (Resource res : resource.getChildren()) { if(res.isResourceType(REDIRECT_RULE_RESOURCE_TYPE)){ rules.add(new RedirectRule(res.getValueMap())); } } return rules; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof SlingHttpServletRequest) || !(response instanceof SlingHttpServletResponse)) { chain.doFilter(request, response); return; } SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request; SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response; if (isEnabled() && doesRequestMatch(slingRequest) && handleRedirect(slingRequest, slingResponse)) { return; } chain.doFilter(request, response); } public boolean handleRedirect(SlingHttpServletRequest slingRequest, SlingHttpServletResponse slingResponse) { long t0 = System.currentTimeMillis(); boolean redirected = false; RedirectMatch match = match(slingRequest); if (match != null) { RedirectRule redirectRule = match.getRule(); ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime untilDateTime = redirectRule.getUntilDateTime(); if (untilDateTime != null && untilDateTime.isBefore(now)) { log.info("redirect rule matched, but expired: {}", redirectRule.getUntilDate()); } else { RequestPathInfo pathInfo = slingRequest.getRequestPathInfo(); String resourcePath = pathInfo.getResourcePath(); log.info("matched {} to {} in {} ms", resourcePath, redirectRule.toString(), System.currentTimeMillis() - t0); String location = redirectRule.evaluate(match.getMatcher()); if (StringUtils.startsWith(location, "/") && !StringUtils.startsWith(location, "//")) { String ext = pathInfo.getExtension(); if (ext != null && !location.endsWith(ext)) { location += "." + ext; } if (mapUrls()) { location = mapUrl(location, slingRequest.getResourceResolver()); } if(preserveQueryString) { String queryString = slingRequest.getQueryString(); if (queryString != null) { int idx = location.indexOf('?'); if (idx == -1) { idx = location.indexOf('#'); } if (idx != -1) { location = location.substring(0, idx); } location += "?" + queryString; } } if(urlAdjuster != null){ location = urlAdjuster.adjust(slingRequest, location); } } log.info("Redirecting {} to {}, statusCode: {}", resourcePath, location, redirectRule.getStatusCode()); slingResponse.setHeader("Location", location); for(Header header : onDeliveryHeaders){ slingResponse.addHeader(header.getName(), header.getValue()); } slingResponse.setStatus(redirectRule.getStatusCode()); redirected = true; } } return redirected; } String mapUrl(String url, ResourceResolver resourceResolver) { return resourceResolver.map(url); } @Override public void destroy() { // no op } protected boolean mapUrls() { return mapUrls; } /** * @return whether redirection is enabled */ protected boolean isEnabled() { return enabled; } public String getStoragePath() { return storagePath; } protected Collection getExtensions() { return exts; } protected Collection getPaths() { return paths; } protected Collection getMethods() { return methods; } protected List
getOnDeliveryHeaders() { return onDeliveryHeaders; } /** * Check whether redirection for the given request is allowed. *
    *
  1. On author redirects are disabled in EDIT, PREVIEW and DESIGN WCM Modes. * To test on author you need to disable WCM mode and append &wcmmode=disabled * to the query string
  2. *
  3. Redirects are supported only for GET and HEAD methods
  4. * This can be changed in the OSGi configuration *
  5. If configured, redirects are allowed only for the specified extensions, * e.g. only *.html requests will be redirected. Same path with .json extension * will not be redirected. This feature is disabled by default.
  6. *
* * @param request the request to check * @return whether redirection for the given is allowed */ private boolean doesRequestMatch(SlingHttpServletRequest request) { WCMMode wcmMode = WCMMode.fromRequest(request); if (wcmMode != null && wcmMode != WCMMode.DISABLED) { log.trace("Request in author mode: {}, no redirection.", wcmMode); return false; } String method = request.getMethod(); if (!getMethods().contains(method)) { log.trace("Request method [{}] does not match any of {}.", method, methods); return false; } String ext = request.getRequestPathInfo().getExtension(); if (ext != null && !getExtensions().isEmpty() && !getExtensions().contains(ext)) { log.trace("Request extension [{}] does not match any of {}.", ext, exts); return false; } String resourcePath = request.getRequestPathInfo().getResourcePath(); boolean matches = getPaths().isEmpty() || getPaths().stream().anyMatch(p -> resourcePath.startsWith(p + "/")); if (!matches) { log.trace("Request path [{}] not within any of {}.", resourcePath, paths); return false; } return true; } /** * @return resource path without extension */ private static String getResourcePath(RequestPathInfo pathInfo) { String resourcePath = pathInfo.getResourcePath(); int sep = resourcePath.indexOf('.'); if (sep != -1 && !resourcePath.startsWith("/content/dam/")) { // strip off extension if present resourcePath = resourcePath.substring(0, sep); } return resourcePath; } /** * Match a path to a redirect configuration. *

* If multiple rules match then the exact rule by path takes precedence over * pattern matches, for example, if two rules match then the exact match by path * will be used: * * @param requestPath path to match * @return redirect match or null */ RedirectMatch match(String requestPath) { RedirectMatch match = null; RedirectRule rule = getPathRules().get(requestPath); if (rule != null) { match = new RedirectMatch(rule, null); } else { for (Map.Entry entry : getPatternRules().entrySet()) { Matcher m = entry.getKey().matcher(requestPath); if (m.matches()) { match = new RedirectMatch(entry.getValue(), m); break; } } } return match; } /** * Match a path to a redirect configuration. *

* This method performs two tries: first for the request path, e.g. * /content/we.retail/en/page. If the first try didn't match then rewrite the url * ( /content/we.retail/en/page -> /en/page ) and try it. *

* * @param slingRequest the request to match * @return redirect match or null */ RedirectMatch match(SlingHttpServletRequest slingRequest) { String resourcePath = getResourcePath(slingRequest.getRequestPathInfo()); RedirectMatch rule = match(resourcePath); if (rule == null) { rule = match(mapUrl(resourcePath, slingRequest.getResourceResolver())); } return rule; } /** * Display cache contents in the MBean * * @return the redirect configurations in a tabular format for the MBean */ @Override public TabularData getRedirectConfigurations() throws OpenDataException { String sourceUrl = "Source Url"; String targetUrl = "Target Url"; String statusCode = "Status Code"; String redirectRules = "Redirect Rules"; CompositeType cacheEntryType = new CompositeType(redirectRules, redirectRules, new String[]{sourceUrl, targetUrl, statusCode}, new String[]{sourceUrl, targetUrl, statusCode}, new OpenType[]{SimpleType.STRING, SimpleType.STRING, SimpleType.INTEGER}); TabularDataSupport tabularData = new TabularDataSupport( new TabularType(redirectRules, redirectRules, cacheEntryType, new String[]{sourceUrl})); Collection rules = new ArrayList<>(); Map pathMatchingRules = getPathRules(); if (pathMatchingRules != null) { rules.addAll(pathMatchingRules.values()); } Map patternMatchingRules = getPatternRules(); if (patternMatchingRules != null) { rules.addAll(patternMatchingRules.values()); } for (RedirectRule rule : rules) { Map row = new LinkedHashMap<>(); row.put(sourceUrl, rule.getSource()); row.put(targetUrl, rule.getTarget()); row.put(statusCode, rule.getStatusCode()); tabularData.put(new CompositeDataSupport(cacheEntryType, row)); } return tabularData; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy