/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.util; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.common.Version; import org.orbeon.oxf.controller.PageFlowControllerProcessor; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.processor.RegexpMatcher; import org.orbeon.oxf.properties.Properties; import org.orbeon.oxf.servlet.OrbeonXFormsFilter; import org.orbeon.oxf.xforms.XFormsProperties; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.regex.Pattern; /** * Utility class to rewrite URLs. */ public class URLRewriterUtils { // Versioned resources configuration public static final String RESOURCES_VERSIONED_PROPERTY = "oxf.resources.versioned"; public static final boolean RESOURCES_VERSIONED_DEFAULT = false; public static final boolean WSRP_ENCODE_RESOURCES_DEFAULT = false; private static final String REWRITING_PLATFORM_PATHS_PROPERTY = "oxf.url-rewriting.platform-paths"; private static final String REWRITING_APP_PATHS_PROPERTY = "oxf.url-rewriting.app-paths"; private static final String REWRITING_APP_PREFIX_PROPERTY = "oxf.url-rewriting.app-prefix"; private static final String REWRITING_CONTEXT_PROPERTY_PREFIX = "oxf.url-rewriting."; private static final String REWRITING_CONTEXT_PROPERTY_SUFFIX = ".context"; private static final String REWRITING_WSRP_ENCODE_RESOURCES_PROPERTY = "oxf.url-rewriting.wsrp.encode-resources"; public static final String RESOURCES_VERSION_NUMBER_PROPERTY = "oxf.resources.version-number"; public static final String REWRITING_SERVICE_BASE_URI_PROPERTY = "oxf.url-rewriting.service.base-uri"; public static final String REWRITING_SERVICE_BASE_URI_DEFAULT = ""; public static final List<PathMatcher> EMPTY_PATH_MATCHER_LIST = Collections.emptyList(); private static final PathMatcher MATCH_ALL_PATH_MATCHER; public static final List<URLRewriterUtils.PathMatcher> MATCH_ALL_PATH_MATCHERS; static { MATCH_ALL_PATH_MATCHER = new URLRewriterUtils.PathMatcher("/.*", null, true); MATCH_ALL_PATH_MATCHERS = Collections.singletonList(URLRewriterUtils.MATCH_ALL_PATH_MATCHER); } /** * Rewrite a URL based on the request URL, a URL, and a rewriting mode. * * @param request incoming request * @param urlString URL to rewrite * @param rewriteMode rewrite mode (see ExternalContext.Response) * @return rewritten URL */ public static String rewriteURL(ExternalContext.Request request, String urlString, int rewriteMode) { return rewriteURL(request.getScheme(), request.getServerName(), request.getServerPort(), request.getClientContextPath(urlString), request.getRequestPath(), urlString, rewriteMode); } /** * Rewrite a service URL. The URL is rewritten against a base URL which is: * * - specified externally or * - the incoming request if not specified externally * * @param request incoming request * @param urlString URL to rewrite * @param rewriteMode rewrite mode * @return rewritten URL */ public static String rewriteServiceURL(ExternalContext.Request request, String urlString, int rewriteMode) { // NOTE: We used to assert here that the mode required an absolute URL, but as of 2016-03-17 one caller // wants a path. // Case where a protocol is specified: the URL is left untouched if (NetUtils.urlHasProtocol(urlString)) return urlString; final String baseURIProperty = getServiceBaseURI(); if (org.apache.commons.lang3.StringUtils.isBlank(baseURIProperty)) { // Property not specified, use request to build base URI return rewriteURL(request.getScheme(), request.getServerName(), request.getServerPort(), request.getClientContextPath(urlString), request.getRequestPath(), urlString, rewriteMode); } else { // Property specified try { final URI baseURI = new URI(StringUtils.trimAllToEmpty(baseURIProperty)); // NOTE: Force absolute URL to be returned in this case anyway return rewriteURL(baseURI.getScheme() != null ? baseURI.getScheme() : request.getScheme(), baseURI.getHost() != null ? baseURI.getHost() : request.getServerName(), baseURI.getHost() != null ? baseURI.getPort() : request.getServerPort(), baseURI.getPath(), "", urlString, rewriteMode); } catch (URISyntaxException e) { throw new OXFException("Incorrect base URI property specified: " + baseURIProperty); } } } /** * Return a context path as seen by the client. This might be the current request's context path, or the forwarding * servlet's context path. The returned path might be different for Orbeon resources vs. application resources. * * @param request current request. * @param isPlatformPath whether the URL is a platform path * @return context path */ public static String getClientContextPath(ExternalContext.Request request, boolean isPlatformPath) { final Map<String, Object> attributes = request.getAttributesMap(); // NOTE: We don't check on javax.servlet.include.context_path, because that attribute behaves very differently: // in the case of includes, it represents properties of the included servlet, not the values of the including // servlet. final String sourceContextPath = (String) attributes.get("javax.servlet.forward.context_path"); if (sourceContextPath != null && isSeparateDeployment(request)) { // This is the case of forwarding in separate deployment if (isPlatformPath) { // Orbeon resources are forwarded // E.g. /foobar/orbeon return sourceContextPath + request.getContextPath(); } else { // Application resources are loaded from the original context // E.g. /foobar return sourceContextPath; } } else { // We were not forwarded to or we were forwarded but we are not in separate deployment return request.getContextPath(); } } public static boolean isSeparateDeployment(ExternalContext.Request request) { final Map<String, Object> attributes = request.getAttributesMap(); return "separate".equals(attributes.get(OrbeonXFormsFilter.RendererDeploymentAttributeName())); } public static boolean isForwarded(ExternalContext.Request request) { // NOTE: We don't check on javax.servlet.include.context_path, because that attribute behaves very differently: // in the case of includes, it represents properties of the included servlet, not the values of the including // servlet. final String sourceContextPath = (String) request.getAttributesMap().get("javax.servlet.forward.context_path"); return sourceContextPath != null; } /** * Rewrite a URL based on a base URL, a URL, and a rewriting mode. * * @param scheme base URL scheme * @param host base URL host * @param port base URL port * @param contextPath base URL context path * @param requestPath base URL request path * @param urlString URL to rewrite (accept human-readable URI) * @param rewriteMode rewrite mode (see ExternalContext.Response) * @return rewritten URL */ private static String rewriteURL(String scheme, String host, int port, String contextPath, String requestPath, String urlString, int rewriteMode) { // Accept human-readable URI urlString = NetUtils.encodeHRRI(urlString, true); // Case where a protocol is specified: the URL is left untouched (except for human-readable processing) if (NetUtils.urlHasProtocol(urlString)) return urlString; try { final String baseURLString; { String _baseURLString; // Prepend absolute base if needed if ((rewriteMode & ExternalContext.Response.REWRITE_MODE_ABSOLUTE) != 0) { _baseURLString = scheme + "://" + host + ((port == 80 || port == -1) ? "" : ":" + port); } else { _baseURLString = ""; } // Append context path if needed if ((rewriteMode & ExternalContext.Response.REWRITE_MODE_ABSOLUTE_PATH_NO_CONTEXT) == 0) _baseURLString = _baseURLString + contextPath; baseURLString = _baseURLString; } // Return absolute path URI with query string and fragment identifier if needed if (urlString.startsWith("?")) { // This is a special case that appears to be implemented // in Web browsers as a convenience. Users may use it. return baseURLString + requestPath + urlString; } else if ((rewriteMode & ExternalContext.Response.REWRITE_MODE_ABSOLUTE_PATH_OR_RELATIVE) != 0 && !urlString.startsWith("/") && !"".equals(urlString)) { // Don't change the URL if it is a relative path and we don't force absolute return urlString; } else { // Regular case, parse the URL final URI baseURIWithPath = new URI("http", "example.org", requestPath, null); final URI resolvedURI = baseURIWithPath.resolve(urlString).normalize();// normalize to remove "..", etc. // Append path, query and fragment final String query = resolvedURI.getRawQuery(); final String fragment = resolvedURI.getRawFragment(); final String tempResult = resolvedURI.getRawPath() + ((query != null) ? "?" + query : "") + ((fragment != null) ? "#" + fragment : ""); // Prepend base return baseURLString + tempResult; } } catch (Exception e) { throw new OXFException(e); } } /** * Rewrite a resource URL, possibly with version information, based on the incoming request as well as a list of * path matchers. * * @param request incoming request * @param urlString URL to rewrite * @param pathMatchers List of PathMatcher * @param rewriteMode rewrite mode * @return rewritten URL */ public static String rewriteResourceURL(ExternalContext.Request request, String urlString, List<URLRewriterUtils.PathMatcher> pathMatchers, int rewriteMode) { if (pathMatchers != null && pathMatchers.size() > 0) { // We need to match the URL against the matcher // 1. Rewrite to absolute path URI without context final String absoluteURINoContext = rewriteURL(request, urlString, ExternalContext.Response.REWRITE_MODE_ABSOLUTE_PATH_NO_CONTEXT); if (NetUtils.urlHasProtocol(absoluteURINoContext)) return absoluteURINoContext; // will be an absolute path // Obtain just the path final String absolutePathNoContext; { final URI absoluteURINoContextURI; try { absoluteURINoContextURI = new URI(absoluteURINoContext); } catch (URISyntaxException e) { throw new OXFException(e); } absolutePathNoContext = absoluteURINoContextURI.getPath(); } if (absolutePathNoContext.startsWith("/xforms-server/")) { // Special URL must not be rewritten as resource // TODO: when is this hit? return rewriteURL(request, urlString, rewriteMode); } // 2. Determine if URL is a platform or application URL based on reserved paths final boolean isPlatformURL = isPlatformPath(absolutePathNoContext); // TODO: get version only once for a run final String applicationVersion = getApplicationResourceVersion(); if (!isPlatformURL && (applicationVersion == null || isSeparateDeployment(request))) { // There is no application version OR we are in separate deployment so do usual rewrite return rewriteURL(request, urlString, rewriteMode); } // 3. Iterate through matchers and see if we get a match if (isVersionedURL(absolutePathNoContext, pathMatchers)) { // 4. Found a match, perform additional rewrite at the beginning final String version = isPlatformURL ? URLRewriterUtils.getOrbeonVersionForClient() : applicationVersion; // Call full method so that we can get the proper client context path return rewriteURL(request.getScheme(), request.getServerName(), request.getServerPort(), request.getClientContextPath(urlString), request.getRequestPath(), "/" + version + absoluteURINoContext, rewriteMode); } // No match found, perform regular rewrite return rewriteURL(request, urlString, rewriteMode); } else { // No Page Flow context, perform regular rewrite return rewriteURL(request, urlString, rewriteMode); } } /** * Check if the given path is a platform path (as opposed to a user application path). * * @param absolutePathNoContext path to check * @return true iif path is a platform path */ public static boolean isPlatformPath(String absolutePathNoContext) { final String regexp = Properties.instance().getPropertySet().getString(REWRITING_PLATFORM_PATHS_PROPERTY, null); // TODO: do not compile the regexp every time return regexp != null && RegexpMatcher.jMatchResult(Pattern.compile(regexp), absolutePathNoContext).matches(); } /** * Check if the given path is an application path, assuming it is not already a platform path. * * @param absolutePathNoContext path to check * @return true iif path is a platform path */ public static boolean isNonPlatformPathAppPath(String absolutePathNoContext) { final String regexp = Properties.instance().getPropertySet().getString(REWRITING_APP_PATHS_PROPERTY, null); // TODO: do not compile the regexp every time return regexp != null && RegexpMatcher.jMatchResult(Pattern.compile(regexp), absolutePathNoContext).matches(); } /** * Decode an absolute path with no context, depending on whether there is an app version or not. * * @param absolutePathNoContext path * @param isVersioned whether the resource is versioned or not * @return decoded path, or initial path if no decoding needed */ public static String decodeResourceURI(String absolutePathNoContext, boolean isVersioned) { if (isVersioned) { // Versioned case final boolean hasApplicationVersion = URLRewriterUtils.getApplicationResourceVersion() != null; if (hasApplicationVersion) { // Remove version on any path return prependAppPathIfNeeded(removeVersionPrefix(absolutePathNoContext)); } else { // Try to remove version then test for platform path final String pathWithVersionRemoved = removeVersionPrefix(absolutePathNoContext); if (isPlatformPath(pathWithVersionRemoved)) { // This was a versioned platform path return pathWithVersionRemoved; } else { // Not a versioned platform path // Don't remove version return prependAppPathIfNeeded(absolutePathNoContext); } } } else { // Non-versioned case return prependAppPathIfNeeded(absolutePathNoContext); } } private static String prependAppPathIfNeeded(String path) { if (isPlatformPath(path) || isNonPlatformPathAppPath(path)) { // Path doesn't need adjustment return path; } else { // Adjust to make an app path return Properties.instance().getPropertySet().getString(REWRITING_APP_PREFIX_PROPERTY, "") + path; } } private static String removeVersionPrefix(String absolutePathNoContext) { if (absolutePathNoContext.length() == 0) { return absolutePathNoContext; } else { final int slashIndex = absolutePathNoContext.indexOf('/', 1); return (slashIndex != -1) ? absolutePathNoContext.substring(slashIndex) : absolutePathNoContext; } } public static boolean isResourcesVersioned() { final boolean requested = Properties.instance().getPropertySet().getBoolean(RESOURCES_VERSIONED_PROPERTY, RESOURCES_VERSIONED_DEFAULT); return Version.instance().isPEFeatureEnabled(requested, RESOURCES_VERSIONED_PROPERTY); } public static String getRewritingContext(String rewritingStrategy, String defaultContext) { return Properties.instance().getPropertySet().getString(REWRITING_CONTEXT_PROPERTY_PREFIX + rewritingStrategy + REWRITING_CONTEXT_PROPERTY_SUFFIX, defaultContext); } public static boolean isWSRPEncodeResources() { return Properties.instance().getPropertySet().getBoolean(REWRITING_WSRP_ENCODE_RESOURCES_PROPERTY, WSRP_ENCODE_RESOURCES_DEFAULT); } public static String getServiceBaseURI() { return Properties.instance().getPropertySet().getStringOrURIAsString(REWRITING_SERVICE_BASE_URI_PROPERTY, REWRITING_SERVICE_BASE_URI_DEFAULT, false); } public static String getApplicationResourceVersion() { final String propertyString = Properties.instance().getPropertySet().getString(RESOURCES_VERSION_NUMBER_PROPERTY); return org.apache.commons.lang3.StringUtils.isBlank(propertyString) ? null : StringUtils.trimAllToEmpty(propertyString); } // Return the version string either in clear or encoded with HMAC depending on configuration public static String getOrbeonVersionForClient() { final boolean isEncodeVersion = XFormsProperties.isEncodeVersion(); return isEncodeVersion ? getHmacVersion() : Version.VersionNumber(); } private static String getHmacVersion() { return SecureUtils.hmacString(Version.VersionNumber(), "hex"); } public static List<URLRewriterUtils.PathMatcher> getMatchAllPathMatcher() { if (isResourcesVersioned()) { return MATCH_ALL_PATH_MATCHERS; } else { return null; } } public static class PathMatcher { public final String regexp; public final String mimeType; public final boolean versioned; public final Pattern pattern; /** * Construct from parameters. * * @param regexp regexp pattern to match * @param mimeType mediatype * @param versioned the resource is versioned */ public PathMatcher(String regexp, String mimeType, boolean versioned) { this.regexp = regexp; this.mimeType = mimeType; this.versioned = versioned; this.pattern = Pattern.compile(regexp); } } public static boolean isVersionedURL(String absolutePathNoContext, List<URLRewriterUtils.PathMatcher> pathMatchers) { for (final URLRewriterUtils.PathMatcher pathMatcher : pathMatchers) { if (RegexpMatcher.jMatchResult(pathMatcher.pattern, absolutePathNoContext).matches()) return true; } return false; } public static List<URLRewriterUtils.PathMatcher> getPathMatchers() { final List<URLRewriterUtils.PathMatcher> pathMatchers = (List<URLRewriterUtils.PathMatcher>) PipelineContext.get().getAttribute(PageFlowControllerProcessor.PathMatchers()); return (pathMatchers != null) ? pathMatchers : URLRewriterUtils.EMPTY_PATH_MATCHER_LIST; } // Get path matchers from the pipeline context public static Callable<List<PathMatcher>> getPathMatchersCallable() { return new Callable<List<URLRewriterUtils.PathMatcher>>() { public List<URLRewriterUtils.PathMatcher> call() throws Exception { return URLRewriterUtils.getPathMatchers(); } }; } /* * Copyright 2000-2005 The Apache Software Foundation * * 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. */ public static String globToRegexp(char[] pattern) { int ch; StringBuffer buffer; buffer = new StringBuffer(2 * pattern.length); boolean inCharSet = false; boolean questionMatchesZero = false; boolean starCannotMatchNull = false; for (ch = 0; ch < pattern.length; ch++) { switch (pattern[ch]) { case '*': if (inCharSet) buffer.append('*'); else { if (starCannotMatchNull) buffer.append(".+"); else buffer.append(".*"); } break; case '?': if (inCharSet) buffer.append('?'); else { if (questionMatchesZero) buffer.append(".?"); else buffer.append('.'); } break; case '[': inCharSet = true; buffer.append(pattern[ch]); if (ch + 1 < pattern.length) { switch (pattern[ch + 1]) { case '!': case '^': buffer.append('^'); ++ch; continue; case ']': buffer.append(']'); ++ch; continue; } } break; case ']': inCharSet = false; buffer.append(pattern[ch]); break; case '\\': buffer.append('\\'); if (ch == pattern.length - 1) { buffer.append('\\'); } else if (__isGlobMetaCharacter(pattern[ch + 1])) buffer.append(pattern[++ch]); else buffer.append('\\'); break; default: if (!inCharSet && __isPerl5MetaCharacter(pattern[ch])) buffer.append('\\'); buffer.append(pattern[ch]); break; } } return buffer.toString(); } private static boolean __isPerl5MetaCharacter(char ch) { return ("'*?+[]()|^$.{}\\".indexOf(ch) >= 0); } private static boolean __isGlobMetaCharacter(char ch) { return ("*?[]".indexOf(ch) >= 0); } }