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

com.composum.sling.core.proxy.GenericProxyRequest Maven / Gradle / Ivy

package com.composum.sling.core.proxy;

import com.composum.sling.core.util.ValueEmbeddingReader;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.http.HttpService;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLFilter;
import org.xml.sax.XMLReader;

import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * a configurable proxy request service factory usable directly and also as a base for special proxy implementations
 */
@Component(service = GenericProxyService.class, scope = ServiceScope.PROTOTYPE)
@Designate(ocd = GenericProxyConfig.class, factory = true)
public class GenericProxyRequest implements GenericProxyService {

    private static final Logger LOG = LoggerFactory.getLogger(GenericProxyRequest.class);

    public static final Pattern XML_CONTENT_URL = Pattern.compile("^.*/[^/]+\\.(html|xml)(\\?.*)?$");
    public static final Pattern XML_CONTENT_TYPE = Pattern.compile("^text/(html|xml)(;.*)?$");

    protected GenericProxyConfig config;

    protected Pattern targetPattern;

    protected BundleContext bundleContext;

    @Activate
    @Modified
    protected void activate(final ComponentContext context, final GenericProxyConfig config) {
        this.bundleContext = context.getBundleContext();
        this.config = config;
        if (config.enabled()) {
            String rule = config.targetPattern();
            if (StringUtils.isNotBlank(rule)) {
                targetPattern = Pattern.compile(rule.startsWith("/") ? ("^" + rule) : rule);
            }
        }
    }

    @Override
    @NotNull
    public String getName() {
        return config.name();
    }


    /**
     * Handles the proxy request if appropriate (target pattern matches and access allowed)
     *
     * @param request   the proxy request
     * @param response  the response for the answer
     * @param targetUrl the url of the request which is addressing the target
     * @return 'true' if the request is supported by the service, allowed for the user and handle by the service
     */
    @Override
    public boolean doProxy(@NotNull final SlingHttpServletRequest request,
                           @NotNull final SlingHttpServletResponse response,
                           @NotNull final String targetUrl)
            throws IOException {
        if (config.enabled()) {
            Matcher matcher = targetPattern.matcher(targetUrl);
            if (matcher.find()) {
                try {
                    boolean allowed = false;
                    String referencePath = config.referencePath();
                    if (StringUtils.isNotBlank(referencePath)) {
                        ResourceResolver resolver = request.getResourceResolver();
                        if (referencePath.startsWith("/")) {
                            allowed = resolver.getResource(referencePath) != null;
                        } else {
                            for (String root : resolver.getSearchPath()) {
                                allowed = resolver.getResource(root + referencePath) != null;
                                if (allowed) {
                                    break;
                                }
                            }
                        }
                    }
                    if (allowed) {
                        doRequest(request, response, targetUrl, matcher);
                    } else {
                        return false;
                    }
                } catch (Exception ex) {
                    LOG.error(ex.getMessage(), ex);
                    response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                }
                return true; // if this service was the right one signal the proxy handling even if an erros has occured
            }
        }
        return false;
    }

    /**
     * Send the request to the proxies target and sends the reveived answer ans response
     *
     * @param request   the request to the proxy servlet
     * @param response  the response of the rquest to the proxy servlet
     * @param targetRef the URL derived from the request to the proxy servlet
     * @param matcher   the prepared matcher used to determine this proxy service implementation as the right one
     */
    protected void doRequest(@NotNull final SlingHttpServletRequest request,
                             @NotNull final SlingHttpServletResponse response,
                             @NotNull final String targetRef,
                             @NotNull final Matcher matcher)
            throws Exception {
        String targetUrl = getTargetUrl(request, targetRef, matcher);
        if (StringUtils.isNotBlank(targetUrl)) {
            CloseableHttpClient client = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(targetUrl);
            httpGet.addHeader("Cookie", request.getHeader("Cookie"));
            LOG.info("proxy request '{}'", httpGet.getRequestLine());
            try (CloseableHttpResponse targetResponse = client.execute(httpGet)) {
                final HttpEntity entity = targetResponse.getEntity();
                if (entity != null) {
                    doResponse(request, response, targetUrl, entity);
                } else {
                    LOG.warn("response is NULL ({})", targetUrl);
                }
            }
        } else {
            LOG.info("no target URL: NOP ({})", targetRef);
        }
    }

    /**
     * Prepare, filter and deliver the content entity reveived from the target.
     *
     * @param request   the request to the proxy servlet
     * @param response  the response object to send the answer
     * @param targetUrl the URL used to request the entity
     * @param entity    the content received from the target
     */
    protected void doResponse(@NotNull final SlingHttpServletRequest request,
                              @NotNull final SlingHttpServletResponse response,
                              @NotNull final String targetUrl,
                              @NotNull final HttpEntity entity)
            throws IOException {
        try (InputStream inputStream = entity.getContent()) {
            String contentType = getContentType(targetUrl, entity);
            if (StringUtils.isNotBlank(contentType)) {
                response.setContentType(contentType);
            }
            if (contentType != null && XML_CONTENT_TYPE.matcher(contentType).matches()) {
                SAXTransformerFactory stf = null;
                XMLFilter xmlFilter = null;
                String[] xsltChainPaths = config.XSLT_chain_paths();
                if (xsltChainPaths.length > 0) {
                    // build XML filter for XSLT transformation
                    stf = (SAXTransformerFactory) TransformerFactory.newInstance();
                    xmlFilter = getXsltFilter(stf, request.getResourceResolver(), xsltChainPaths);
                }
                if (xmlFilter != null) {
                    // do XSLT transformation (probably pre-filtered by the reader)...
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("XSLT transformation ({})...", xmlFilter);
                    }
                    try (Reader entityReader = getContentReader(targetUrl, inputStream)) {
                        Transformer transformer = stf.newTransformer();
                        SAXSource transformSource = new SAXSource(xmlFilter, new InputSource(entityReader));
                        transformer.transform(transformSource, new StreamResult(response.getWriter()));
                    } catch (Exception ex) {
                        LOG.error(ex.getMessage(), ex);
                    }
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("pumping HTML/XML content...");
                    }
                    // stream entity response (probably filtered by the reader)
                    try (Reader entityReader = getContentReader(targetUrl, inputStream)) {
                        IOUtils.copy(entityReader, response.getWriter());
                    } catch (Exception ex) {
                        LOG.error(ex.getMessage(), ex);
                    }
                }
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("pumping non XML content...");
                }
                // stream entity response (probably filtered by the reader)
                response.setContentLength((int) entity.getContentLength());
                try (Reader entityReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
                    IOUtils.copy(entityReader, response.getWriter());
                }
            }
        }
    }

    /**
     * @return the type of the requested content determined from the entity or the requested URL
     */
    @Nullable
    protected String getContentType(@NotNull final String targetUrl, @NotNull final HttpEntity entity) {
        Header type = entity.getContentType();
        if (type != null) {
            Header encoding = entity.getContentEncoding();
            return encoding != null ? type.getValue() + ";charset=" + encoding.getValue() : type.getValue();
        } else {
            Matcher matcher = XML_CONTENT_URL.matcher(targetUrl);
            return matcher.matches() ? "text/" + matcher.group(0) + ";charset=utf-8" : null;
        }
    }

    /**
     * the factory method for the reader to prepare and filter the content received from the target
     *
     * @param targetUrl     the URL used to request the entity
     * @param entityContent the received content as stream
     * @return the reader to use to receive the content
     */
    @NotNull
    protected Reader getContentReader(@NotNull final String targetUrl,
                                      @NotNull final InputStream entityContent) {
        String[] toRename = config.tags_to_rename();
        String[] toStrip = config.tags_to_strip();
        String[] toDrop = config.tags_to_drop();
        Reader reader = toRename.length > 0 || toStrip.length > 0 || toDrop.length > 0
                ? new GenericProxyReader(entityContent, toRename, toStrip, toDrop)
                : new InputStreamReader(entityContent, StandardCharsets.UTF_8);
        if (LOG.isDebugEnabled()) {
            LOG.debug("using reader '{}' ({})", reader, targetUrl);
        }
        return reader;
    }

    /**
     * Builds the URL for the target request using the URI built by the ProxyServlet and the matcher of that URI.
     *
     * @param request   the original request received by the ProxyServlet
     * @param targetRef the target URI derived from the original request (suffix + query string)
     * @param matcher   the URI pattern matcher (gives access to the groups declared by the pattern)
     * @return the URL for the HTTP request to the target
     */
    @Nullable
    protected String getTargetUrl(@NotNull final SlingHttpServletRequest request,
                                  @NotNull final String targetRef, @NotNull final Matcher matcher) {
        String targetUrl = config.targetUrl();
        if (StringUtils.isNotBlank(targetUrl)) {
            // if a targetURL is configured use the configured pattern to build the final URL based on the requested
            // URI; the configured targetUrl can contain value placeholders ${0},${1},... to embed groups of the matcher
            Map properties = new HashMap<>();
            addHttpValues(properties);
            properties.put("ctx", request.getContextPath());
            properties.put("url", targetRef);
            for (int i = 0; i < matcher.groupCount(); i++) { // add all available groups as properties
                properties.put(Integer.toString(i), matcher.group(i));
            }
            ValueEmbeddingReader reader = new ValueEmbeddingReader(new StringReader(targetUrl), properties);
            try {   // replace the palceholders of the target URL and embed the referenced properties...
                targetUrl = IOUtils.toString(reader);
            } catch (IOException ex) {
                LOG.error(ex.toString());
                targetUrl = null;
            }
        } else {
            targetUrl = targetRef.startsWith("/") // complete a path and prepend host and port
                    ? (request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + targetRef)
                    : targetRef;
        }
        return targetUrl;
    }

    protected void addHttpValues(@NotNull final Map values) {
        ServiceReference service = bundleContext.getServiceReference(HttpService.class);
        String endpoint = Arrays.asList((String[]) service.getProperty("osgi.http.endpoint")).get(0);
        if (endpoint.endsWith("/")) {
            endpoint = endpoint.substring(0, endpoint.length() - 1);
        }
        values.put("http.endpoint", endpoint);
        values.put("http.port", Integer.parseInt(service.getProperty("org.osgi.service.http.port").toString()));
    }

    //
    // XSLT transformation
    //

    /**
     * @return a chain of XML filters initialized with the XSLT resources resolved from the given path list
     */
    @Nullable
    protected XMLFilter getXsltFilter(@NotNull final SAXTransformerFactory stf,
                                      @NotNull final ResourceResolver resolver,
                                      @NotNull final String[] xsltChainPaths) {
        XMLFilter xmlFilter = null;
        try {
            for (String xsltPath : xsltChainPaths) {
                XMLFilter next = getXmlFilter(stf, resolver.getResource(xsltPath));
                if (next != null) {
                    if (xmlFilter == null) {
                        SAXParserFactory spf = SAXParserFactory.newInstance();
                        spf.setNamespaceAware(true);
                        spf.setValidating(false);
                        SAXParser parser = spf.newSAXParser();
                        XMLReader reader = parser.getXMLReader();
                        next.setParent(reader);
                    } else {
                        next.setParent(xmlFilter);
                    }
                    xmlFilter = next;
                }
            }
        } catch (ParserConfigurationException | SAXException ex) {
            LOG.error(ex.getMessage(), ex);
        }
        return xmlFilter;
    }

    public static XMLFilter getXmlFilter(@NotNull final SAXTransformerFactory stf,
                                         @Nullable final Resource xsltResource) {
        XMLFilter filter = null;
        InputStream inputStream = getFileContent(xsltResource);
        if (inputStream != null) {
            try {
                filter = stf.newXMLFilter(new StreamSource(new InputStreamReader(inputStream, StandardCharsets.UTF_8)));
            } catch (TransformerConfigurationException ex) {
                LOG.error(ex.getMessage(), ex);
            }
        }
        return filter;
    }

    @Nullable
    public static InputStream getFileContent(@Nullable Resource resource) {
        InputStream inputStream = null;
        if ((resource = getFileResource(resource)) != null) {
            ValueMap values = resource.getValueMap();
            inputStream = values.get(JcrConstants.JCR_DATA, InputStream.class);
        }
        return inputStream;
    }

    @Nullable
    public static Resource getFileResource(@Nullable final Resource resource) {
        return resource != null && resource.isResourceType(JcrConstants.NT_FILE)
                ? resource.getChild(JcrConstants.JCR_CONTENT) : resource;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy