/*
* JBoss, Home of Professional Open Source.
* Copyright 2010, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 software 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.osgi.httpservice;
import static org.jboss.as.web.WebLogger.WEB_LOGGER;
import java.io.File;
import java.io.IOException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Map;
import java.util.WeakHashMap;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.Host;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.ContextConfig;
import org.apache.tomcat.util.http.mapper.Mapper;
import org.jboss.as.osgi.OSGiMessages;
import org.jboss.as.osgi.httpservice.HttpServiceFactory.GlobalRegistry;
import org.jboss.as.osgi.httpservice.HttpServiceFactory.Registration;
import org.jboss.as.osgi.httpservice.HttpServiceFactory.Registration.Type;
import org.jboss.as.server.ServerEnvironment;
import org.jboss.as.web.WebMessages;
import org.jboss.as.web.WebServer;
import org.jboss.as.web.deployment.WebCtxLoader;
import org.osgi.framework.Bundle;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
/**
* An {@link HttpService} implementation
*
* @author Thomas.Diesler@jboss.com
* @author David Bosschaert
* @since 19-Jul-2012
*/
final class HttpServiceImpl implements HttpService {
private final GlobalRegistry registry;
private final ServerEnvironment serverEnvironment;
private final WebServer webServer;
private final Host virtualHost;
private final Bundle bundle;
// This map holds the shared ApplicationContexts to be used with the associated HttpContext.
// It is a WeakHashMap which means that the ApplicationContexts are remembered for as long
// as the HttpContext exists.
private final Map<HttpContext, ApplicationContext> contexts = new WeakHashMap<HttpContext, ApplicationContext>();
HttpServiceImpl(ServerEnvironment serverEnvironment, WebServer webServer, Host virtualHost, Bundle bundle) {
this.registry = GlobalRegistry.INSTANCE;
this.virtualHost = virtualHost;
this.webServer = webServer;
this.serverEnvironment = serverEnvironment;
this.bundle = bundle;
}
@Override
@SuppressWarnings("rawtypes")
public void registerServlet(String alias, Servlet servlet, Dictionary initparams, HttpContext httpContext) throws ServletException, NamespaceException {
validateAlias(alias, false);
validateServlet(servlet);
Wrapper wrapper = registerInternal(alias, servlet, initparams, httpContext, Type.SERVLET);
wrapper.allocate(); // Causes servlet.init() to be called, which must be done before we return
}
@Override
public void registerResources(String alias, String name, HttpContext httpContext) throws NamespaceException {
validateAlias(alias, false);
validateName(name);
if (httpContext == null)
httpContext = createDefaultHttpContext();
ResourceServlet servlet = new ResourceServlet(name, httpContext);
registerInternal(alias, servlet, null, null, Type.RESOURCE);
}
@SuppressWarnings("rawtypes")
private synchronized Wrapper registerInternal(String alias, Servlet servlet, Dictionary initparams, HttpContext httpContext, Type type) throws NamespaceException {
File storageDir = new File(serverEnvironment.getServerTempDir() + File.separator + alias + File.separator + "osgiservlet-root");
storageDir.mkdirs();
ShareableContext ctx;
ApplicationContext actx = null;
if (httpContext != null) {
actx = contexts.get(httpContext);
ctx = new ShareableContext(actx);
} else {
ctx = new ShareableContext(null);
httpContext = new DefaultHttpContext(bundle);
}
ctx.setDocBase(storageDir.getPath());
ctx.setPath(alias);
ctx.addLifecycleListener(new ContextConfig());
WebCtxLoader loader = new WebCtxLoader(servlet.getClass().getClassLoader());
loader.setContainer(virtualHost);
ctx.setLoader(loader);
ctx.addMimeMapping("html", "text/html");
ctx.addMimeMapping("jpg", "image/jpeg");
ctx.addMimeMapping("png", "image/png");
ctx.addMimeMapping("gif", "image/gif");
ctx.addMimeMapping("css", "text/css");
ctx.addMimeMapping("js", "text/javascript");
virtualHost.addChild(ctx);
WEB_LOGGER.registerWebapp(ctx.getName());
try {
ctx.create();
} catch (Exception ex) {
throw new NamespaceException(WebMessages.MESSAGES.createContextFailed(), ex);
}
try {
ctx.start();
} catch (LifecycleException ex) {
throw new NamespaceException(WebMessages.MESSAGES.startContextFailed(), ex);
}
String wrapperName = alias.substring(1);
Wrapper wrapper = ctx.createWrapper();
wrapper.setName(wrapperName);
wrapper.setServlet(new SecurityServletWrapper(servlet, httpContext));
wrapper.setServletClass(servlet.getClass().getName());
// Init parameters
if (initparams != null) {
Enumeration keys = initparams.keys();
while(keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String val = (String) initparams.get(key);
wrapper.addInitParameter(key, val);
}
}
registry.register(alias, bundle, ctx, servlet, type);
String pattern = "/*";
ctx.addChild(wrapper);
ctx.addServletMapping(pattern, wrapper.getName());
// Must be added to the main mapper as no dynamic servlets usually
Mapper mapper = webServer.getService().getMapper();
mapper.addWrapper(virtualHost.getName(), ctx.getPath(), pattern, wrapper, false);
if (httpContext != null && actx == null) {
// We have a new shared context, save it for later use
// The shared Servlet Context is put on a weak has map, which means that
// as soon as the last instance of a particular httpContext is gone, the
// shared context is garbage collected too.
contexts.put(httpContext, ctx.getApplicationContext());
}
return wrapper;
}
@Override
public void unregister(String alias) {
try {
validateAlias(alias, true);
} catch (NamespaceException e) {
WEB_LOGGER.errorf(e, "");
return;
}
Registration reg = registry.unregister(alias, bundle);
if (reg != null) {
unregisterInternal(reg);
}
}
@Override
public HttpContext createDefaultHttpContext() {
return new DefaultHttpContext(bundle);
}
void unregisterInternal(Registration reg) {
StandardContext context = reg.getContext();
try {
context.stop();
} catch (LifecycleException e) {
WEB_LOGGER.stopContextFailed(e);
}
try {
context.destroy();
} catch (Exception e) {
WEB_LOGGER.destroyContextFailed(e);
}
}
private void validateAlias(String alias, boolean exists) throws NamespaceException {
// An alias must begin with slash ('/') and must not end with slash ('/'), with the exception
// that an alias of the form "/" is used to denote the root alias
if (alias == null || !alias.startsWith("/"))
throw new IllegalArgumentException(OSGiMessages.MESSAGES.invalidServletAlias(alias));
if (alias.length() > 1 && alias.endsWith("/"))
throw new IllegalArgumentException(OSGiMessages.MESSAGES.invalidServletAlias(alias));
if (exists && !registry.exists(alias))
throw new IllegalArgumentException(OSGiMessages.MESSAGES.aliasMappingDoesNotExist(alias));
if (!exists && registry.exists(alias))
throw new NamespaceException(OSGiMessages.MESSAGES.aliasMappingAlreadyExists(alias));
}
private void validateName(String name) throws NamespaceException {
// The name parameter must also not end with slash ('/') with the exception
// that a name of the form "/" is used to denote the root of the bundle.
if (name == null || (name.length() > 1 && name.endsWith("/")))
throw new NamespaceException(OSGiMessages.MESSAGES.invalidResourceName(name));
}
private void validateServlet(Servlet servlet) throws ServletException {
// A single servlet instance can only be registered once.
if (registry.contains(servlet))
throw new ServletException(OSGiMessages.MESSAGES.servletAlreadyRegistered(servlet.getServletInfo()));
}
static class ShareableContext extends StandardContext {
ShareableContext(ApplicationContext existingContext) {
context = existingContext;
}
ApplicationContext getApplicationContext() {
if (context == null)
// initialize the servlet context
getServletContext();
return context; // will not be null at this point
}
}
/* This wrapper class takes care of handling the security through the HttpContext.
*/
static class SecurityServletWrapper implements Servlet {
private final HttpContext httpContext;
private final Servlet delegate;
SecurityServletWrapper(Servlet servlet, HttpContext ctx) {
if (servlet == null)
throw new NullPointerException();
delegate = servlet;
if (ctx == null)
throw new NullPointerException();
httpContext = ctx;
}
@Override
public void destroy() {
delegate.destroy();
}
@Override
public ServletConfig getServletConfig() {
return delegate.getServletConfig();
}
@Override
public String getServletInfo() {
return delegate.getServletInfo();
}
@Override
public void init(ServletConfig sc) throws ServletException {
delegate.init(sc);
}
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (!httpContext.handleSecurity(httpRequest, httpResponse))
return;
Object u = httpRequest.getAttribute(HttpContext.REMOTE_USER);
String remoteUser = u instanceof String ? (String) u : null;
Object a = httpRequest.getAttribute(HttpContext.AUTHENTICATION_TYPE);
String authType = a instanceof String ? (String) a : null;
if (remoteUser != null || authType != null) {
request = new SecurityRequestWrapper(remoteUser, authType, httpRequest);
}
}
delegate.service(request, response);
}
}
/* This wrapper class can provide the remote user and auth type information if provided through the HttpContext.
*/
static class SecurityRequestWrapper extends HttpServletRequestWrapper implements HttpServletRequest {
private final String remoteUser;
private final String authType;
SecurityRequestWrapper(String remoteUser, String authType, HttpServletRequest request) {
super(request);
this.remoteUser = remoteUser;
this.authType = authType;
}
@Override
public String getAuthType() {
return authType;
}
@Override
public String getRemoteUser() {
return remoteUser;
}
}
}