/* * Copyright 2014, The Sporting Exchange Limited * * 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. */ package com.betfair.cougar.transport.jetty; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.betfair.cougar.CougarVersion; import com.betfair.cougar.api.security.IdentityTokenResolver; import com.betfair.cougar.core.api.RequestTimer; import com.betfair.cougar.core.api.ServiceVersion; import com.betfair.cougar.core.api.exception.CougarException; import com.betfair.cougar.core.api.exception.CougarValidationException; import com.betfair.cougar.core.api.exception.ServerFaultCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.betfair.cougar.transport.api.TransportCommandProcessor; import com.betfair.cougar.transport.api.protocol.http.HttpCommand; import com.betfair.cougar.transport.api.protocol.http.ResponseCodeMapper; import com.betfair.cougar.util.HeaderUtils; import org.eclipse.jetty.continuation.Continuation; import org.eclipse.jetty.continuation.ContinuationListener; import org.eclipse.jetty.continuation.ContinuationSupport; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; public class JettyHandler extends AbstractHandler { private final static Logger LOGGER = LoggerFactory.getLogger(JettyHandler.class); private final TransportCommandProcessor<HttpCommand> commandProcessor; private final long MILLI=1000; private String protocolBindingRoot; private IdentityTokenResolverLookup identityTokenResolverLookup; private int timeoutInSeconds; private boolean suppressCommasInAccessLog; private static final String VERSION_HEADER = "Cougar 2 - "+CougarVersion.getVersion(); public JettyHandler(final TransportCommandProcessor<HttpCommand> commandProcessor, boolean suppressCommasInAccessLog) { this(commandProcessor, null, null, suppressCommasInAccessLog); } public JettyHandler(final TransportCommandProcessor<HttpCommand> commandProcessor, final JettyHandlerSpecification spec, final IdentityTokenResolverLookup identityTokenResolverLookup, boolean suppressCommasInAccessLog) { super(); this.commandProcessor = commandProcessor; if (spec != null) { protocolBindingRoot = spec.getProtocolBindingUriPrefix(); } else { protocolBindingRoot = ""; } this.identityTokenResolverLookup = identityTokenResolverLookup; this.suppressCommasInAccessLog = suppressCommasInAccessLog; } @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setHeader("Server", VERSION_HEADER); try { IdentityTokenResolver itr = null; if (identityTokenResolverLookup != null) { itr = identityTokenResolverLookup.lookupIdentityTokenResolver(baseRequest.getRequestURI()); } JettyTransportCommand command = new JettyTransportCommand(request, response, itr); if (!command.getContinuation().isExpired()) { LOGGER.debug("Message Received at Jetty Handler for path {}", target); commandProcessor.process(command); } else { LOGGER.debug("Message Timeout at Jetty Handler for path {}", target); response.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT); } } catch (CougarException ce) { LOGGER.warn("Cougar Exception thrown processing request", ce); response.sendError(ResponseCodeMapper.getHttpResponseCode(ce.getResponseCode())); } catch (Exception e) { LOGGER.error("Unexpected Exception thrown processing request", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } finally { // The request has been handled, whether or not the handling was a suspension. baseRequest.setHandled(true); } } public void setTimeoutInSeconds(int timeoutInSeconds) { this.timeoutInSeconds = timeoutInSeconds; } protected class JettyTransportCommand implements HttpCommand, ContinuationListener { private static final String CERTIFICATE_ATTRIBUTE_NAME = "javax.servlet.request.X509Certificate"; private String fullPath; private String operationPath; private final HttpServletRequest request; private final HttpServletResponse response; private final IdentityTokenResolver identityTokenResolver; private final Continuation continuation; private AtomicReference<CommandStatus> status; private RequestTimer timer = new RequestTimer(); public JettyTransportCommand(final HttpServletRequest request, final HttpServletResponse response) { this(request, response, null); } public JettyTransportCommand(final HttpServletRequest request, final HttpServletResponse response, IdentityTokenResolver identityTokenResolver) { status = new AtomicReference<CommandStatus>(CommandStatus.InProgress); this.request = request; this.response = response; this.identityTokenResolver = identityTokenResolver; HeaderUtils.setNoCache(response); continuation = ContinuationSupport.getContinuation(request); if (continuation.isInitial()) { continuation.setTimeout(MILLI * timeoutInSeconds ); continuation.suspend(response); continuation.addContinuationListener(this); } buildPathInfo(request); } /** * @see HttpCommand */ public Continuation getContinuation() { return continuation; } /** * @see HttpCommand */ @Override public HttpServletRequest getRequest() { return request; } /** * @see HttpCommand */ @Override public HttpServletResponse getResponse() { return response; } @Override public IdentityTokenResolver<?, ?, ?> getIdentityTokenResolver() { return identityTokenResolver; } /** * @see HttpCommand */ @Override public void onComplete() { if (status.compareAndSet(CommandStatus.InProgress, CommandStatus.Complete)) { continuation.complete(); } } /** * @see HttpCommand */ @Override public CommandStatus getStatus() { return status.get(); } /** * @see ContinuationListener */ @Override public void onComplete(Continuation continuation) { } /** * @see ContinuationListener */ @Override public void onTimeout(Continuation continuation) { status.set(CommandStatus.TimedOut); } @Override public RequestTimer getTimer() { return timer; } /** * @see HttpCommand */ @Override public String getFullPath() { String ret = fullPath; // a comma can really mess with us since we use it as a delimiter in our logging.. if (ret.contains(",")) { if (suppressCommasInAccessLog) { ret = ret.replace(",",""); } else { ret = ret.replace(",","\\,"); } } return fullPath; } /** * @see HttpCommand */ @Override public String getOperationPath() { return operationPath; } /** * getContextPath and getContextPath are defined in terms of servlets which we bypass completely. * we may need to amend this implementation * * @param request * @return */ private final void buildPathInfo(final HttpServletRequest request) { fullPath = request.getContextPath() + (request.getPathInfo() == null ? "" : request.getPathInfo()); // Strip the binding protocolBindingRoot off the front. if (protocolBindingRoot != null && protocolBindingRoot.length() > 0) { operationPath = fullPath.substring(protocolBindingRoot.length()); } else { operationPath = fullPath; } } } public static interface IdentityTokenResolverLookup { IdentityTokenResolver lookupIdentityTokenResolver(String uri); } public static class SingletonIdentityTokenResolverLookup implements IdentityTokenResolverLookup { private IdentityTokenResolver identityTokenResolver; public SingletonIdentityTokenResolverLookup(IdentityTokenResolver identityTokenResolver) { this.identityTokenResolver = identityTokenResolver; } @Override public IdentityTokenResolver lookupIdentityTokenResolver(String uri) { return identityTokenResolver; } } public static class GeneralHttpIdentityTokenResolverLookup implements IdentityTokenResolverLookup { private Pattern regex; private Map<String, IdentityTokenResolver> serviceVersionToIdentityTokenResolverMap = new HashMap<String, IdentityTokenResolver>(); public GeneralHttpIdentityTokenResolverLookup(String serviceContextRoot, JettyHandlerSpecification spec) { regex = Pattern.compile(serviceContextRoot + "/(v\\d+).*", Pattern.CASE_INSENSITIVE); for (Map.Entry<ServiceVersion, IdentityTokenResolver> entry : spec.getVersionToIdentityTokenResolverMap().entrySet()) { serviceVersionToIdentityTokenResolverMap.put("v" + entry.getKey().getMajor(), entry.getValue()); } } public String extractVersion(String uri) { Matcher m = regex.matcher(uri); if (m.matches()) { return m.group(1).toLowerCase(); } throw new CougarValidationException(ServerFaultCode.NoSuchService, "Uri [" + uri + "] did not contain a version"); } @Override public IdentityTokenResolver lookupIdentityTokenResolver(String uri) { String version = extractVersion(uri); return serviceVersionToIdentityTokenResolverMap.containsKey(version) ? serviceVersionToIdentityTokenResolverMap.get(version) : null; } } }