blob: a7f751f94abccbb0f6545473bc5f13836a6e999e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.rt.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.security.AccessController;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.security.auth.Subject;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.scout.commons.LocaleThreadLocal;
import org.eclipse.scout.commons.exception.ProcessingException;
import org.eclipse.scout.commons.logger.IScoutLogger;
import org.eclipse.scout.commons.logger.ScoutLogManager;
import org.eclipse.scout.commons.osgi.BundleInspector;
import org.eclipse.scout.commons.serialization.SerializationUtility;
import org.eclipse.scout.http.servletfilter.HttpServletEx;
import org.eclipse.scout.http.servletfilter.helper.HttpAuthJaasFilter;
import org.eclipse.scout.rt.server.admin.html.AdminSession;
import org.eclipse.scout.rt.server.internal.Activator;
import org.eclipse.scout.rt.server.services.common.session.IServerSessionRegistryService;
import org.eclipse.scout.rt.shared.servicetunnel.DefaultServiceTunnelContentHandler;
import org.eclipse.scout.rt.shared.servicetunnel.IServiceTunnelContentHandler;
import org.eclipse.scout.rt.shared.servicetunnel.ServiceTunnelRequest;
import org.eclipse.scout.rt.shared.servicetunnel.ServiceTunnelResponse;
import org.eclipse.scout.rt.shared.ui.UserAgent;
import org.eclipse.scout.service.SERVICES;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
/**
* Use this servlet to dispatch scout gui service requests using {@link ServiceTunnelRequest},
* {@link ServiceTunnelResponse} and any {@link IServiceTunnelContentHandler} implementation.
* <p>
* Override the methods
* {@link DefaultTransactionDelegate#validateInput(org.eclipse.scout.rt.shared.validate.IValidationStrategy, Object, java.lang.reflect.Method, Object[])
* DefaultTransactionDelegate#validateInput} and
* {@link DefaultTransactionDelegate#validateOutput(org.eclipse.scout.rt.shared.validate.IValidationStrategy, Object, java.lang.reflect.Method, Object, Object[])
* DefaultTransactionDelegate#validateOutput} to do central input/output validation.
* <p>
* By default there is a jaas convenience filter {@link HttpAuthJaasFilter} on /process and a {@link SoapWsseJaasFilter}
* on /ajax with priority 1000
* <p>
* When using RAP (rich ajax platform) as the ui web app then there must be a {@link WebSessionIdPrincipal} in the
* subject, in order to map those requests to virtual sessions instead of (the unique) http session.
*/
public class ServiceTunnelServlet extends HttpServletEx {
public static final String HTTP_DEBUG_PARAM = "org.eclipse.scout.rt.server.http.debug";
private static final long serialVersionUID = 1L;
private static final IScoutLogger LOG = ScoutLogManager.getLogger(ServiceTunnelServlet.class);
private transient IServiceTunnelContentHandler m_contentHandler;
private transient Bundle[] m_orderedBundleList;
private Object m_orderedBundleListLock = new Boolean(true);
private volatile boolean m_isAjaxSessionTimeoutInitialized = false;
private VirtualSessionCache m_ajaxSessionCache = new VirtualSessionCache();
private Object m_msgEncoderLock = new Boolean(true);
private Class<? extends IServerSession> m_serverSessionClass;
private Version m_requestMinVersion;
private boolean m_debug;
public ServiceTunnelServlet() {
String text = Activator.getDefault().getBundle().getBundleContext().getProperty(HTTP_DEBUG_PARAM);
if (text != null && text.equalsIgnoreCase("true")) {
m_debug = true;
}
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
m_requestMinVersion = initRequestMinVersion(config);
}
@SuppressWarnings("unchecked")
protected void lazyInit(HttpServletRequest req, HttpServletResponse res) throws ServletException {
if (m_serverSessionClass == null) {
m_serverSessionClass = locateServerSessionClass(req, res);
}
if (m_serverSessionClass == null) {
String qname = getServletConfig().getInitParameter("session");
if (qname != null) {
int i = qname.lastIndexOf('.');
try {
m_serverSessionClass = (Class<? extends IServerSession>) Platform.getBundle(qname.substring(0, i)).loadClass(qname);
}
catch (ClassNotFoundException e) {
throw new ServletException("Loading class " + qname, e);
}
}
}
if (m_serverSessionClass == null) {
// find bundle that defines this servlet
try {
Bundle bundle = findServletContributor(req.getServletPath());
if (bundle != null) {
m_serverSessionClass = (Class<? extends IServerSession>) bundle.loadClass(bundle.getSymbolicName() + ".ServerSession");
}
}
catch (Throwable t) {
// nop
}
}
if (m_serverSessionClass == null) {
throw new ServletException("Expected init-param \"session\"");
}
}
protected Class<? extends IServerSession> locateServerSessionClass(HttpServletRequest req, HttpServletResponse res) {
return null;
}
/**
* <p>
* Reads the minimum version a request must have.
* </p>
* <p>
* The version has to be defined as init parameter in the servlet configuration. <br/>
* This can be done by adding a new init-param at the {@link DefaultHttpProxyHandlerServlet} on the extension point
* org.eclipse.equinox.http.registry.servlets and setting its name to min-version and its value to the desired version
* (like 1.2.3).
* </p>
* <p>
* If there is no min-version defined it uses the Bundle-Version of the bundle which contains the running product.
* </p>
*
* @param config
* @return
*/
protected Version initRequestMinVersion(ServletConfig config) {
Version version = null;
String v = config.getInitParameter("min-version");
if (v != null) {
Version tmp = Version.parseVersion(v);
version = new Version(tmp.getMajor(), tmp.getMinor(), tmp.getMicro());
}
else if (Platform.getProduct() != null) {
v = (String) Platform.getProduct().getDefiningBundle().getHeaders().get("Bundle-Version");
Version tmp = Version.parseVersion(v);
version = new Version(tmp.getMajor(), tmp.getMinor(), tmp.getMicro());
}
return version;
}
/**
* create the (reusable) content handler (soap, xml, binary) for marshalling scout/osgi remote service calls
* <p>
* This method is part of the protected api and can be overridden.
*/
protected IServiceTunnelContentHandler createContentHandler(Class<? extends IServerSession> sessionClass) {
DefaultServiceTunnelContentHandler e = new DefaultServiceTunnelContentHandler();
e.initialize(getOrderedBundleList(), sessionClass.getClassLoader());
return e;
}
private IServiceTunnelContentHandler getServiceTunnelContentHandler() {
synchronized (m_msgEncoderLock) {
if (m_contentHandler == null) {
m_contentHandler = createContentHandler(m_serverSessionClass);
}
}
return m_contentHandler;
}
protected Bundle[] getOrderedBundleList() {
synchronized (m_orderedBundleListLock) {
if (m_orderedBundleList == null) {
String[] bundleOrderPrefixes = SerializationUtility.getBundleOrderPrefixes();
m_orderedBundleList = BundleInspector.getOrderedBundleList(bundleOrderPrefixes);
}
}
return m_orderedBundleList;
}
protected IServerSession lookupScoutServerSessionOnHttpSession(HttpServletRequest req, HttpServletResponse res, Subject subject, UserAgent userAgent) throws ProcessingException, ServletException {
//external request: apply locking, this is the session initialization phase
synchronized (req.getSession()) {
IServerSession serverSession = (IServerSession) req.getSession().getAttribute(IServerSession.class.getName());
if (serverSession == null) {
serverSession = SERVICES.getService(IServerSessionRegistryService.class).newServerSession(m_serverSessionClass, subject, userAgent);
req.getSession().setAttribute(IServerSession.class.getName(), serverSession);
}
return serverSession;
}
}
private IServerSession lookupScoutServerSessionOnVirtualSession(HttpServletRequest req, HttpServletResponse res, String ajaxSessionId, Subject subject, UserAgent userAgent) throws ProcessingException, ServletException {
initializeAjaxSessionTimeout(req);
IServerSession serverSession = m_ajaxSessionCache.get(ajaxSessionId);
if (serverSession == null) {
synchronized (m_ajaxSessionCache) {
serverSession = m_ajaxSessionCache.get(ajaxSessionId);
if (serverSession == null) { // double checking
return createAndCacheNewServerSession(ajaxSessionId, subject, userAgent, req, res);
}
}
}
m_ajaxSessionCache.touch(ajaxSessionId);
return serverSession;
}
/**
* Initialize Ajax Session timeout only once from the HTTP session
*/
protected void initializeAjaxSessionTimeout(HttpServletRequest req) {
if (!m_isAjaxSessionTimeoutInitialized) {
final long defaultSessionTimeout = 1800L;
final long millisecondsInSeconds = 1000L;
m_ajaxSessionCache.setSessionTimeoutMillis(Math.max(millisecondsInSeconds * defaultSessionTimeout, millisecondsInSeconds * req.getSession().getMaxInactiveInterval()));
m_isAjaxSessionTimeoutInitialized = true;
}
}
private IServerSession createAndCacheNewServerSession(String ajaxSessionId, Subject subject, UserAgent userAgent, HttpServletRequest req, HttpServletResponse res) throws ProcessingException {
IServerSession serverSession = SERVICES.getService(IServerSessionRegistryService.class).newServerSession(m_serverSessionClass, subject, userAgent);
m_ajaxSessionCache.put(ajaxSessionId, serverSession);
return serverSession;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
Subject subject = Subject.getSubject(AccessController.getContext());
if (subject == null) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
//invoke
Map<Class, Object> backup = ThreadContext.backup();
try {
lazyInit(req, res);
//
//legacy, deprecated, do not use servlet request/response in scout code
ThreadContext.putHttpServletRequest(req);
ThreadContext.putHttpServletResponse(res);
//
UserAgent userAgent = UserAgent.createDefault();
IServerSession serverSession = lookupScoutServerSessionOnHttpSession(req, res, subject, userAgent);
//
ServerJob job = new AdminServiceJob(serverSession, subject, req, res);
job.runNow(new NullProgressMonitor());
job.throwOnError();
}
catch (ProcessingException e) {
throw new ServletException(e);
}
finally {
ThreadContext.restore(backup);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
Subject subject = Subject.getSubject(AccessController.getContext());
if (subject == null) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
try {
lazyInit(req, res);
Map<Class, Object> backup = ThreadContext.backup();
Locale oldLocale = LocaleThreadLocal.get();
try {
ThreadContext.putHttpServletRequest(req);
ThreadContext.putHttpServletResponse(res);
//read request
ServiceTunnelRequest serviceRequest = deserializeInput(req.getInputStream());
LocaleThreadLocal.set(serviceRequest.getLocale());
//virtual or http session?
IServerSession serverSession;
String virtualSessionId = serviceRequest.getVirtualSessionId();
UserAgent userAgent = UserAgent.createByIdentifier(serviceRequest.getUserAgent());
if (virtualSessionId != null) {
serverSession = lookupScoutServerSessionOnVirtualSession(req, res, virtualSessionId, subject, userAgent);
}
else {
serverSession = lookupScoutServerSessionOnHttpSession(req, res, subject, userAgent);
}
//invoke
AtomicReference<ServiceTunnelResponse> serviceResponseHolder = new AtomicReference<ServiceTunnelResponse>();
ServerJob job = createServiceTunnelServerJob(serverSession, serviceRequest, serviceResponseHolder, subject);
job.setTransactionSequence(serviceRequest.getRequestSequence());
job.runNow(new NullProgressMonitor());
job.throwOnError();
serializeOutput(res, serviceResponseHolder.get());
}
finally {
ThreadContext.restore(backup);
LocaleThreadLocal.set(oldLocale);
}
}
catch (Throwable t) {
//ignore disconnect errors
// we don't want to throw an exception, if the client closed the connection
Throwable cause = t;
while (cause != null) {
if (cause instanceof SocketException) {
return;
}
else if (cause.getClass().getSimpleName().equalsIgnoreCase("EofException")) {
return;
}
else if (cause instanceof InterruptedIOException) {
return;
}
// next
cause = cause.getCause();
}
LOG.error("Session=" + req.getSession().getId() + ", Client=" + req.getRemoteUser() + "@" + req.getRemoteAddr() + "/" + req.getRemoteHost(), t);
res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
protected ServiceTunnelRequest deserializeInput(InputStream in) throws Exception {
ServiceTunnelRequest req = getServiceTunnelContentHandler().readRequest(in);
return req;
}
protected void serializeOutput(HttpServletResponse httpResponse, ServiceTunnelResponse res) throws Exception {
// security: do not send back error stack trace
if (res.getException() != null) {
res.getException().setStackTrace(new StackTraceElement[0]);
}
//
httpResponse.setDateHeader("Expires", -1);
httpResponse.setHeader("Cache-Control", "no-cache");
httpResponse.setHeader("pragma", "no-cache");
httpResponse.setContentType("text/xml");
getServiceTunnelContentHandler().writeResponse(httpResponse.getOutputStream(), res);
}
private Bundle findServletContributor(String alias) throws CoreException {
BundleContext context = Activator.getDefault().getBundle().getBundleContext();
ServiceReference ref = context.getServiceReference(IExtensionRegistry.class.getName());
Bundle bundle = null;
if (ref != null) {
IExtensionRegistry reg = (IExtensionRegistry) context.getService(ref);
if (reg != null) {
IExtensionPoint xpServlet = reg.getExtensionPoint("org.eclipse.equinox.http.registry.servlets");
if (xpServlet != null) {
for (IExtension xServlet : xpServlet.getExtensions()) {
for (IConfigurationElement cServlet : xServlet.getConfigurationElements()) {
if (cServlet.getName().equals("servlet")) {
if (this.getClass().getName().equals(cServlet.getAttribute("class"))) {
// half match, go on looping
bundle = Platform.getBundle(xServlet.getContributor().getName());
if (alias.equals(cServlet.getAttribute("alias"))) {
// full match, return
return bundle;
}
}
}
}
}
}
}
}
return bundle;
}
/**
* Create the {@link ServerJob} that runs the request as a single atomic transaction
*/
protected ServerJob createServiceTunnelServerJob(IServerSession serverSession, ServiceTunnelRequest serviceRequest, AtomicReference<ServiceTunnelResponse> serviceResponseHolder, Subject subject) {
return new RemoteServiceJob(serverSession, serviceRequest, serviceResponseHolder, subject);
}
/**
* runnable content of the {@link ServerJob}, thzis is the atomic transaction
* <p>
* This method is part of the protected api and can be overridden.
*/
protected ServiceTunnelResponse runServerJobTransaction(ServiceTunnelRequest req) throws Exception {
return runServerJobTransactionWithDelegate(req, getOrderedBundleList(), m_requestMinVersion, m_debug);
}
protected ServiceTunnelResponse runServerJobTransactionWithDelegate(ServiceTunnelRequest req, Bundle[] loaderBundles, Version requestMinVersion, boolean debug) throws Exception {
return new DefaultTransactionDelegate(loaderBundles, requestMinVersion, debug).invoke(req);
}
private class RemoteServiceJob extends ServerJob {
private final ServiceTunnelRequest m_serviceRequest;
private final AtomicReference<ServiceTunnelResponse> m_serviceResponseHolder;
public RemoteServiceJob(IServerSession serverSession, ServiceTunnelRequest serviceRequest, AtomicReference<ServiceTunnelResponse> serviceResponseHolder, Subject subject) {
super("RemoteServiceCall", serverSession, subject);
m_serviceRequest = serviceRequest;
m_serviceResponseHolder = serviceResponseHolder;
}
public ServiceTunnelRequest getServiceRequest() {
return m_serviceRequest;
}
public AtomicReference<ServiceTunnelResponse> getServiceResponseHolder() {
return m_serviceResponseHolder;
}
@Override
protected IStatus runTransaction(IProgressMonitor monitor) throws Exception {
ServiceTunnelResponse serviceRes = runServerJobTransaction(getServiceRequest());
getServiceResponseHolder().set(serviceRes);
return Status.OK_STATUS;
}
}
private class AdminServiceJob extends ServerJob {
protected HttpServletRequest m_request;
protected HttpServletResponse m_response;
public AdminServiceJob(IServerSession serverSession, Subject subject, HttpServletRequest request, HttpServletResponse response) {
super("AdminServiceCall", serverSession, subject);
m_request = request;
m_response = response;
}
@Override
protected IStatus runTransaction(IProgressMonitor monitor) throws Exception {
// get session
HttpSession session = m_request.getSession();
String key = AdminSession.class.getName();
AdminSession as = (AdminSession) session.getAttribute(key);
if (as == null) {
as = new AdminSession();
session.setAttribute(key, as);
}
as.serviceRequest(m_request, m_response);
return Status.OK_STATUS;
}
}
}