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

org.ops4j.pax.web.service.internal.HttpContextProcessing Maven / Gradle / Ivy

Go to download

Pax Web is a OSGi Http Service based on Jetty 6. Detailed information to be found at http://wiki.ops4j.org/confluence/x/AYAz.

There is a newer version: 9.0.18
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.ops4j.pax.web.service.internal;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.servlet.annotation.ServletSecurity;

import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.util.descriptor.web.WebXml;
import org.apache.tomcat.util.descriptor.web.WebXmlParser;
import org.ops4j.pax.web.service.PaxWebConstants;
import org.ops4j.pax.web.service.WebContainer;
import org.ops4j.pax.web.service.internal.views.ProcessingWebContainerView;
import org.ops4j.pax.web.service.spi.model.OsgiContextModel;
import org.ops4j.pax.web.service.spi.model.elements.LoginConfigModel;
import org.ops4j.pax.web.service.spi.model.elements.SecurityConstraintModel;
import org.ops4j.pax.web.service.spi.task.Batch;
import org.ops4j.pax.web.service.spi.task.ContextParamsChange;
import org.ops4j.pax.web.service.spi.task.ContextStartChange;
import org.ops4j.pax.web.service.spi.task.ContextStopChange;
import org.ops4j.pax.web.service.spi.task.OpCode;
import org.ops4j.pax.web.service.spi.task.SecurityConfigChange;
import org.ops4j.pax.web.service.spi.util.NamedThreadFactory;
import org.ops4j.pax.web.service.spi.util.Utils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
import org.osgi.service.cm.ManagedServiceFactory;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

Tracks CM factory configurations which, in declarative way, are used to provide additional configuration * for given bundle's instance of {@link org.osgi.service.http.HttpService}/{@link WebContainer}.

*/ public class HttpContextProcessing implements ManagedServiceFactory { public static final Logger LOG = LoggerFactory.getLogger(HttpContextProcessing.class); public static final String PID = PaxWebConstants.PID + ".context"; private static final String KEY_CONTEXT_ID = "context.id"; private static final String KEY_BUNDLE_SN = "bundle.symbolicName"; private static final String KEY_WEB_FRAGMENT = "context.webFragment"; private static final String KEY_WHITEBOARD = "whiteboard"; private static final String PREFIX_CONTEXT_PARAM = "context.param."; private static final String PREFIX_LOGIN_CONFIG = "login.config."; private static final String PREFIX_SECURITY = "security."; private static final String PREFIX_SECURITY_ROLE = "security.roles"; private final ExecutorService configExecutor = new ThreadPoolExecutor(0, 1, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new NamedThreadFactory("paxweb-context")); private final BundleContext serviceContext; private final ConcurrentMap httpContextTrackers = new ConcurrentHashMap<>(); public HttpContextProcessing(BundleContext serviceContext) { this.serviceContext = serviceContext; } @Override public String getName() { return PID; } @Override public void updated(String pid, Dictionary properties) { LOG.info("Updated configuration for pid={}", pid); // we're in the context of ConfigurationAdmin UpdateThread, but we should not hold it too long configExecutor.submit(new ConfigurationChangeTask(pid, properties)); } @Override public void deleted(String pid) { LOG.info("Deleted configuration for pid={}", pid); configExecutor.submit(new ConfigurationChangeTask(pid, null)); } public void destroy() { configExecutor.shutdown(); } /** * Synchronous task (single instance running at given time) that's handling an update to context processing * configuration. */ private class ConfigurationChangeTask implements Runnable { private final String pid; private final Dictionary properties; ConfigurationChangeTask(String pid, Dictionary properties) { this.pid = pid; this.properties = properties; } @Override public void run() { try { LOG.debug("Processing {} PID {}", pid, properties == null ? "removal" : "change"); HttpContextTracker p = httpContextTrackers.computeIfAbsent(pid, HttpContextTracker::new); p.reconfigure(properties); } catch (Exception e) { LOG.error(e.getMessage(), e); } } } /** *

A class that operates on {@link org.osgi.service.http.HttpService}/{@link WebContainer} scoped * for given, configured {@link Bundle}.

*

We don't have to operate on wirings/revisions - that's not required for * {@link org.osgi.framework.Constants#SCOPE_BUNDLE}.

*/ private class HttpContextTracker implements BundleTrackerCustomizer { // real PID from factory PID holding given processor's configuration private final String pid; private Map properties; // declared symbolic name of bundle to track private String symbolicName; // tracker for given symbolic name private BundleTracker bundleTracker; // tracked bundle + version private Bundle bundle; private Version version; private String contextId; private boolean whiteboard; // transient values for current configuration private final Map contextParams = new LinkedHashMap<>(); // private LoginConfigModel loginConfiguration; // private final Set securityRoles = new LinkedHashSet<>(); // private final List securityMappings = new LinkedList<>(); HttpContextTracker(String pid) { this.pid = pid; } /** * Sets new/changed/removed configuration in given processor - method to be run in "paxweb-context" * single threaded executor. * @param props */ public void reconfigure(Dictionary props) { if (props == null) { // we should stop tracking the configured bundle and its HttpService/WebContainer if (bundleTracker != null) { bundleTracker.close(); } // ... and get rid of processor for given PID HttpContextTracker tracker = httpContextTrackers.remove(pid); if (tracker != null && symbolicName != null) { // tracker should be 'this', but let's be paranoid configExecutor.execute(tracker::cleanupContext); } properties = null; } else { // assuming we wouldn't be called with unchanged properties (that's ConfigurationManager job // to notify ManagedServices only when configuration has changed), we just have to clean up // previous configuration and apply the new one // even if updated configuration misses bundle selector, we have to cleanup existing customization if (bundleTracker != null) { bundleTracker.close(); // we have to schedule cleanup first - it'll be executed before bundleTracker.addingBundle() // will schedule processing configExecutor.execute(this::cleanupContext); } if (props.get(KEY_BUNDLE_SN) == null) { LOG.warn("Incorrect {} configuration - missing {} selector", pid, KEY_BUNDLE_SN); return; } // non-null symbolicName is kind of indicator that the configuration is correct properties = Utils.toMap(props); symbolicName = properties.get(KEY_BUNDLE_SN).trim(); contextId = properties.get(KEY_CONTEXT_ID); if (contextId == null) { contextId = PaxWebConstants.DEFAULT_CONTEXT_NAME; } whiteboard = "true".equalsIgnoreCase(properties.get(KEY_WHITEBOARD)); if (bundleTracker == null) { bundleTracker = new BundleTracker<>(serviceContext, Bundle.ACTIVE, this); // we don't have to clean up anything } // we'll be tracking new (possibly) symbolic name, processContext() will be scheduled from // BundleTracker.addingBundle() bundleTracker.open(); } } /** * This method is executed in the context of "paxweb-context" executor - it actually uses * configured bundle's {@link WebContainer} to register additional web items */ private void processContext() { OsgiContextModel osgiContextModel = getProcessedOsgiContextModel(); if (osgiContextModel == null) { return; } LOG.info("Customizing {}", osgiContextModel); ProcessingWebContainerView view = getProcessingView(); if (view == null) { return; } Batch batch = new Batch("Processing context \"" + contextId + "\" for bundle " + bundle.getSymbolicName()); // we always have to restart the context batch.getOperations().add(new ContextStopChange(OpCode.MODIFY, osgiContextModel)); // we could set the properties directly, but we want to do it in elegant way - using a batch // surrounded by restart of the target context. // Properties file (configuration admin config) can encode parts of security-related stuff from web.xml // and also (as a bonus) context parameters. // This "HTTP context processing" was designed as limited subset of the information content from // full web[-fragment].xml // // we handle: // 0) meta information to select the context (no web[-fragment].xml equivalent) // context.id = // bundle.symbolicName = // // 1) context parameters // // token // string // // encoded as: // context.param.param-name = param-value // // 2) security roles: // // token // // encoded as: // security.roles = role1, role2, ... // // 3) login configuration: // // token // token // // /token // /token // // // encoded as: // login.config.authMethod = auth-method // login.config.realmName = realm-name // login.config.formLoginPage = form-login-page // login.config.formErrorPage = form-error-page // // 4) security constraints (multiple) // // token // // token // string // // token // token // // // token // // // CONFIDENTIAL // // // encoded as: // security.[constraint.name].url(s) = url-pattern1, url-pattern2, ... // security.[constraint.name].methods = GET, POST, ... // security.[constraint.name].methodOmissions = GET, POST, ... // security.[constraint.name].roles = role1, role2, ... // security.[constraint.name].transportGuarantee = CONFIDENTIAL|INTEGRAL|NONE // // however if there's "context.webFragment" property, we assume it's web-fragment.xml location // that we can parse and extract relevant information from it String webFragmentLocation = properties.get(KEY_WEB_FRAGMENT); if (webFragmentLocation != null && !"".equals(webFragmentLocation.trim())) { File f = new File(webFragmentLocation); if (!f.isFile()) { LOG.warn("Web Fragment location {} is not accessible. Skipping.", webFragmentLocation); } else { LOG.info("Processing Web Fragment {}", webFragmentLocation); WebXml fragment = new WebXml(); fragment.setDistributable(true); fragment.setOverridable(true); fragment.setAlwaysAddWelcomeFiles(false); fragment.setReplaceWelcomeFiles(true); try { fragment.setURL(f.toURI().toURL()); WebXmlParser parser = new WebXmlParser(false, false, true); parser.setClassLoader(WebXmlParser.class.getClassLoader()); parser.parseWebXml(fragment.getURL(), fragment, true); // context params contextParams.putAll(fragment.getContextParams()); // security roles securityRoles.addAll(fragment.getSecurityRoles()); // login config from web-fragment.xml LoginConfig loginConfig = fragment.getLoginConfig(); if (loginConfig != null) { loginConfiguration = new LoginConfigModel(); LOG.info("Registering login configuration in WebContainer for bundle \"" + symbolicName + "\": method={}, realm={}", loginConfig.getAuthMethod(), loginConfig.getRealmName()); loginConfiguration.setAuthMethod(loginConfig.getAuthMethod()); loginConfiguration.setRealmName(loginConfig.getRealmName()); loginConfiguration.setFormLoginPage(loginConfig.getLoginPage()); loginConfiguration.setFormErrorPage(loginConfig.getErrorPage()); } // security constraints for (SecurityConstraint sc : fragment.getSecurityConstraints()) { SecurityConstraintModel constraint = new SecurityConstraintModel(); // (no at this level) constraint.setName(sc.getDisplayName()); // elements for (SecurityCollection wrc : sc.findCollections()) { SecurityConstraintModel.WebResourceCollection collection = new SecurityConstraintModel.WebResourceCollection(); collection.setName(wrc.getName()); collection.getMethods().addAll(Arrays.asList(wrc.findMethods())); collection.getOmittedMethods().addAll(Arrays.asList(wrc.findOmittedMethods())); collection.getPatterns().addAll(Arrays.asList(wrc.findPatterns())); constraint.getWebResourceCollections().add(collection); } // elements constraint.setAuthRolesSet(sc.getAuthConstraint()); constraint.getAuthRoles().addAll(Arrays.asList(sc.findAuthRoles())); // in case the roles were missing and used in , we have to add them here securityRoles.addAll(constraint.getAuthRoles()); // if (sc.getUserConstraint() != null && !"".equals(sc.getUserConstraint().trim())) { if (ServletSecurity.TransportGuarantee.NONE.toString().equals(sc.getUserConstraint())) { constraint.setTransportGuarantee(ServletSecurity.TransportGuarantee.NONE); } else { constraint.setTransportGuarantee(ServletSecurity.TransportGuarantee.CONFIDENTIAL); } } securityMappings.add(constraint); } } catch (IOException e) { LOG.warn("Failure parsing default {}: {}", webFragmentLocation, e.getMessage(), e); } } } // context params from Configuration Admin override those from web-fragment.xml contextParams.putAll(collectContextParams(properties)); if (!contextParams.isEmpty()) { LOG.info("Setting context parameters in {}", osgiContextModel); batch.getOperations().add(new ContextParamsChange(OpCode.ADD, osgiContextModel, contextParams)); } // login config from Configuration Admin - may override the config from web-fragment.xml LoginConfigModel cmLoginConfig = collectLoginConfiguration(properties); if (cmLoginConfig != null) { if (loginConfiguration == null) { loginConfiguration = new LoginConfigModel(); LOG.info("Registering login configuration in {}: method={}, realm={}", osgiContextModel, cmLoginConfig.getAuthMethod(), cmLoginConfig.getRealmName()); } else { LOG.info("Overriding login configuration in {}: method={}, realm={}", osgiContextModel, cmLoginConfig.getAuthMethod(), cmLoginConfig.getRealmName()); } loginConfiguration.setAuthMethod(cmLoginConfig.getAuthMethod()); loginConfiguration.setRealmName(cmLoginConfig.getRealmName()); loginConfiguration.setFormLoginPage(cmLoginConfig.getFormLoginPage()); loginConfiguration.setFormErrorPage(cmLoginConfig.getFormErrorPage()); } // finally security mappings from CM have higher priority than the ones from web-fragment.xml List cmSecurityMappings = collectSecurityMappings(properties); cmSecurityMappings.addAll(securityMappings); securityMappings.clear(); securityMappings.addAll(cmSecurityMappings); if (!securityMappings.isEmpty()) { LOG.info("Registering security mappings in {}", osgiContextModel); } // login config and security constraints batch.getOperations().add(new SecurityConfigChange(OpCode.ADD, osgiContextModel, loginConfiguration, securityMappings, new ArrayList<>(securityRoles))); // start after reconfiguration batch.getOperations().add(new ContextStartChange(OpCode.MODIFY, osgiContextModel)); // apply the changes by sending the batch view.sendBatch(batch); } /** * Brings given {@link WebContainer} to initial state - to be called within "paxweb-context" executor */ public void cleanupContext() { LOG.info("{}: Restoring WebContainer for bundle {}/{}", this, symbolicName, version); OsgiContextModel osgiContextModel = getProcessedOsgiContextModel(); if (osgiContextModel == null) { return; } ProcessingWebContainerView view = getProcessingView(); if (view == null) { return; } Batch batch = new Batch("Processing context \"" + contextId + "\" for bundle " + bundle.getSymbolicName()); // we always have to restart the context batch.getOperations().add(new ContextStopChange(OpCode.MODIFY, osgiContextModel)); // context params batch.getOperations().add(new ContextParamsChange(OpCode.DELETE, osgiContextModel, contextParams)); // login config and security constraints - yes - login config be changed to null (even if there was something // configured before HTTP context processing) batch.getOperations().add(new SecurityConfigChange(OpCode.DELETE, osgiContextModel, loginConfiguration, securityMappings, new ArrayList<>(securityRoles))); // start after reconfiguration batch.getOperations().add(new ContextStartChange(OpCode.MODIFY, osgiContextModel)); // apply the changes by sending the batch view.sendBatch(batch); } private LoginConfigModel collectLoginConfiguration(Map properties) { if (properties != null) { if (properties.get(PREFIX_LOGIN_CONFIG + "authMethod") != null) { // all other properties are optional LoginConfigModel lc = new LoginConfigModel(); lc.setAuthMethod(properties.get(PREFIX_LOGIN_CONFIG + "authMethod")); lc.setRealmName(properties.get(PREFIX_LOGIN_CONFIG + "realmName")); if (lc.getRealmName() == null) { lc.setRealmName("default"); } lc.setFormLoginPage(properties.get(PREFIX_LOGIN_CONFIG + "formLoginPage")); lc.setFormErrorPage(properties.get(PREFIX_LOGIN_CONFIG + "formErrorPage")); return lc; } } return null; } private Map collectContextParams(Map properties) { Map result = new HashMap<>(); if (properties != null) { for (String k : properties.keySet()) { if (k != null && k.startsWith(PREFIX_CONTEXT_PARAM)) { String v = properties.get(k); String paramName = k.substring(PREFIX_CONTEXT_PARAM.length()); result.put(paramName, v); } } } return result; } private List collectSecurityMappings(Map properties) { List result = new LinkedList<>(); if (properties != null) { final Map temp = new LinkedHashMap<>(); for (String k : properties.keySet()) { if (k != null && k.startsWith(PREFIX_SECURITY)) { String paramName = k.substring(PREFIX_SECURITY.length()); if (paramName.contains(".")) { String constraintName = paramName.substring(0, paramName.lastIndexOf('.')); SecurityConstraintModel model = temp.computeIfAbsent(constraintName, n -> { SecurityConstraintModel scm = new SecurityConstraintModel(); scm.setName(constraintName); SecurityConstraintModel.WebResourceCollection collection = new SecurityConstraintModel.WebResourceCollection(); collection.setName(constraintName); // same as / // only one element per in ConfigAdmin scm.getWebResourceCollections().add(collection); // no way to decide between "set no roles" (deny all) and "no roles set" (allow all) // so only "deny all behavior" scm.setAuthRolesSet(true); return scm; }); SecurityConstraintModel.WebResourceCollection collection = model.getWebResourceCollections().get(0); // encoded as: // security.[constraint.name].url(s) = url-pattern1, url-pattern2, ... // security.[constraint.name].method(s) = GET, POST, ... // security.[constraint.name].methodOmissions = GET, POST, ... // security.[constraint.name].roles = role1, role2, ... // security.[constraint.name].transportGuarantee = CONFIDENTIAL|INTEGRAL|NONE String v = properties.get(k); if (paramName.endsWith(".url")) { collection.getPatterns().add(v.trim()); } else if (paramName.endsWith(".urls")) { collection.getPatterns().addAll(Arrays.asList(v.split("\\s*,\\s*"))); } else if (paramName.endsWith(".method")) { collection.getMethods().add(v.trim()); } else if (paramName.endsWith(".methods")) { collection.getMethods().addAll(Arrays.asList(v.split("\\s*,\\s*"))); } else if (paramName.endsWith(".methodOmissions")) { collection.getOmittedMethods().addAll(Arrays.asList(v.split("\\s*,\\s*"))); } else if (paramName.endsWith(".roles")) { model.getAuthRoles().addAll(Arrays.asList(v.split("\\s*,\\s*"))); } else if (paramName.endsWith(".transportGuarantee")) { if (ServletSecurity.TransportGuarantee.CONFIDENTIAL.toString().equalsIgnoreCase(v.trim()) || "INTEGRAL".equalsIgnoreCase(v.trim())) { model.setTransportGuarantee(ServletSecurity.TransportGuarantee.CONFIDENTIAL); } else { model.setTransportGuarantee(ServletSecurity.TransportGuarantee.NONE); } } } } } result.addAll(temp.values()); } return result; } private OsgiContextModel getProcessedOsgiContextModel() { ProcessingWebContainerView view = getProcessingView(); if (view == null) { return null; } OsgiContextModel osgiContextModel = whiteboard ? view.getContextModel(null, contextId) : view.getContextModel(bundle, contextId); if (osgiContextModel == null) { if (whiteboard) { LOG.warn("Can't find whiteboard OsgiContextModel with name \"{}\"", contextId); } else { LOG.warn("Can't find OsgiContextModel with name \"{}\" for bundle {}", contextId, bundle); } return null; } return osgiContextModel; } private ProcessingWebContainerView getProcessingView() { WebContainer wc = getWebContainer(); if (wc == null) { return null; } return wc.adapt(ProcessingWebContainerView.class); } @Override public Object addingBundle(Bundle bundle, BundleEvent event) { if (symbolicName.equals(bundle.getSymbolicName())) { // we have a bundle! this.bundle = bundle; this.version = bundle.getVersion(); LOG.info("Found bundle \"" + symbolicName + "\", scheduling customization of its WebContainer"); configExecutor.execute(this::processContext); } // we don't actually customize the bundle, so let's not return anything return null; } @Override public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { // bundle has changed, but we don't need to do anything - our customization of // bundle-scoped WebContainer is still valid } @Override public void removedBundle(Bundle bundle, BundleEvent event, Object object) { this.bundle = null; this.version = null; // no need to reconfigure the context, because everything will be cleaned up anyway } /** * Gets a {@link WebContainer} service for tracked bundle * @return */ private WebContainer getWebContainer() { Bundle currentBundle = bundle; if (currentBundle == null) { LOG.debug("Bundle context for {} bundle is no longer valid", symbolicName); return null; } BundleContext context = currentBundle.getBundleContext(); if (context == null) { LOG.debug("Bundle context for {} bundle is no longer valid", symbolicName); return null; } try { ServiceReference sr = context.getServiceReference(WebContainer.class); if (sr == null) { LOG.warn("Can't obtain service reference for WebContainer for bundle {}", symbolicName); return null; } // We know it's bundle-scoped, so no need for careful instance counting (via ServiceObjects) WebContainer wc = context.getService(sr); if (wc == null) { LOG.warn("Can't obtain WebContainer service for bundle {}", symbolicName); return null; } return wc; } catch (IllegalStateException e) { LOG.debug("Bundle context for {} bundle is no longer valid", symbolicName); return null; } } @Override public String toString() { return "HTTP Context Processor {" + "bundle=" + bundle + '}'; } } }