| /******************************************************************************* |
| * 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; |
| } |
| } |
| |
| } |