/*
* Copyright 2013, 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 com.betfair.cougar.CougarVersion;
import com.betfair.cougar.api.ExecutionContext;
import com.betfair.cougar.api.ResponseCode;
import com.betfair.cougar.api.security.IdentityTokenResolver;
import com.betfair.cougar.core.api.RequestTimer;
import com.betfair.cougar.logging.CougarLogger;
import com.betfair.cougar.logging.CougarLoggingUtils;
import com.betfair.cougar.transport.api.RequestLogger;
import com.betfair.cougar.transport.api.protocol.http.ExecutionContextFactory;
import com.betfair.cougar.transport.api.protocol.http.GeoLocationDeserializer;
import com.betfair.cougar.transport.api.protocol.http.HttpCommand;
import com.betfair.cougar.util.ServletResponseFileStreamer;
import com.betfair.cougar.util.geolocation.GeoIPLocator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.regex.Pattern;
@ManagedResource
public class StaticContentServiceHandler extends ContextHandler {
private static final CougarLogger logger = CougarLoggingUtils.getLogger(StaticContentServiceHandler.class);
private static final String VERSION_HEADER = "Cougar 2 - "+CougarVersion.getVersion();
private static final String INFER_CONTENT_TYPE = "INFER";
private static final String DEFAULT_CONTENT_TYPE = "text/html";
private final Pattern IS_STATIC_CONTENT_PATH;
private final String contextPath;
private final String contentType;
private final MediaType mediaType;
private final AtomicLong numOK = new AtomicLong();
private final AtomicLong numErrors = new AtomicLong();
private final AtomicLong num404s = new AtomicLong();
private final AtomicLong ioErrorsEncountered = new AtomicLong();
private final String uuidHeader;
private final GeoLocationDeserializer deserializer;
private final GeoIPLocator geoIPLocator;
private int unknownCipherKeyLength;
// if true then removes, otherwise escapes them with a backslash
private boolean suppressCommasInAccessLog = true;
private RequestLogger requestLogger;
private static final String[][] CACHE_CONTROL_HEADER = {{"Cache-Control", "private, max-age=2592000"}};
public StaticContentServiceHandler(
String contextPath,
String staticContentRegex,
String contentType,
String uuidHeader,
GeoLocationDeserializer deserializer,
GeoIPLocator geoIPLocator,
RequestLogger requestLogger,
boolean suppressCommasInAccessLog) {
this.contextPath = contextPath;
this.contentType = contentType;
this.uuidHeader = uuidHeader;
this.deserializer = deserializer;
this.geoIPLocator = geoIPLocator;
this.requestLogger = requestLogger;
mediaType = contentType.equals(INFER_CONTENT_TYPE) ? null : MediaType.valueOf(contentType);
IS_STATIC_CONTENT_PATH = Pattern.compile(staticContentRegex, Pattern.CASE_INSENSITIVE);
}
@Override
public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
logger.log(Level.FINE, "Static content stream handler for context path %s invoked for request path %s", getContextPath(), target);
baseRequest.setHandled(true);
final RequestTimer timer = new RequestTimer();
response.setHeader("Server", VERSION_HEADER);
long bytesWritten = 0;
ResponseCode responseCode = ResponseCode.Ok;
if (IS_STATIC_CONTENT_PATH.matcher(target).matches()) {
try {
InputStream rawStream = getClass().getResourceAsStream(target);
if (rawStream != null) {
logger.log(Level.FINE, "Static content stream found for path %s", target);
bytesWritten = ServletResponseFileStreamer.getInstance().streamFileToResponse(rawStream, response,
HttpServletResponse.SC_OK, getContentType(contentType, target), CACHE_CONTROL_HEADER );
numOK.incrementAndGet();
}
else {
logger.log(Level.FINE, "Static content stream not found for path %s", target);
responseCode = ResponseCode.NotFound;
bytesWritten = ServletResponseFileStreamer.getInstance().stream404ToResponse(response);
num404s.incrementAndGet();
}
} catch (IOException e) {
// Assume that the the output pipe is broken. Broken network connections are not
// really exceptional and should not be reported by dumping the stack trace.
// Instead a summary debug level log message with some relevant info
ioErrorsEncountered.incrementAndGet();
logger.log(Level.FINEST, "Failed to marshall static data to the output channel.", e);
} catch (Exception e) {
logger.log(Level.SEVERE, "Unexpected Exception thrown processing WSDL request", e);
responseCode = ResponseCode.InternalError;
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
numErrors.incrementAndGet();
}
} else {
logger.log(Level.FINE, "Static content stream did not match regex for path %s", target);
responseCode = ResponseCode.NotFound;
bytesWritten = ServletResponseFileStreamer.getInstance().stream404ToResponse(response);
num404s.incrementAndGet();
}
logAccess(request, response, bytesWritten, responseCode, timer);
}
private String getContentType(String contentType, String target) {
if (contentType.equals(INFER_CONTENT_TYPE)) {
return null; // let the browser do the work...
} else {
return contentType;
}
}
private void logAccess(HttpServletRequest request, HttpServletResponse response, long bytesWritten, ResponseCode responseCode, RequestTimer timer) {
HttpCommand cmd = getHttpCommand(request, response, timer);
int keyLength = 0;
if (request.getScheme().equals("https")) {
keyLength = SSLRequestUtils.getTransportSecurityStrengthFactor(request, unknownCipherKeyLength);
}
ExecutionContext ctx = ExecutionContextFactory.resolveExecutionContext(cmd, null, uuidHeader, deserializer, geoIPLocator, null, keyLength, false, new Date());
requestLogger.logAccess(cmd, ctx, 0, bytesWritten, null, mediaType, responseCode);
}
private HttpCommand getHttpCommand(final HttpServletRequest request, final HttpServletResponse response, RequestTimer timer) {
return new StaticHttpCommand(request, response, timer, suppressCommasInAccessLog);
}
@ManagedAttribute
@Override
public String getContextPath() {
return contextPath;
}
@ManagedAttribute
public long getNumOKRequests() {
return numOK.longValue();
}
@ManagedAttribute
public long getNumErrors() {
return numErrors.longValue();
}
@ManagedAttribute
public long getNumNotFound() {
return num404s.longValue();
}
@ManagedAttribute
public String getContentType() {
return contentType;
}
private static class StaticHttpCommand implements HttpCommand {
private final HttpServletRequest request;
private final HttpServletResponse response;
private final RequestTimer timer;
private final boolean suppressCommasInAccessLog;
private StaticHttpCommand(HttpServletRequest request, HttpServletResponse response, RequestTimer timer, boolean suppressCommasInAccessLog) {
this.request = request;
this.response = response;
this.timer = timer;
this.suppressCommasInAccessLog = suppressCommasInAccessLog;
timer.requestComplete();
}
@Override
public HttpServletRequest getRequest() {
return request;
}
@Override
public HttpServletResponse getResponse() {
return response;
}
@Override
public IdentityTokenResolver<?, ?, ?> getIdentityTokenResolver() {
return null;
}
@Override
public String getFullPath() {
String pathInfo = (request.getPathInfo() == null ? "" : request.getPathInfo());
// a comma can really mess with us since we use it as a delimiter in our logging..
if (pathInfo.contains(",")) {
if (suppressCommasInAccessLog) {
pathInfo = pathInfo.replace(",","");
}
else {
pathInfo = pathInfo.replace(",","\\,");
}
}
return request.getContextPath() + pathInfo;
}
@Override
public String getOperationPath() {
return null; // No operation here
}
@Override
public X509Certificate[] getClientX509CertificateChain() {
return new X509Certificate[0]; // Not interested in this.
}
@Override
public void onComplete() {
// Errr, nope.
}
@Override
public CommandStatus getStatus() {
return CommandStatus.InProcess;
}
@Override
public RequestTimer getTimer() {
return timer;
}
}
public void setUnknownCipherKeyLength(int unknownCipherKeyLength) {
this.unknownCipherKeyLength = unknownCipherKeyLength;
}
@ManagedAttribute
public int getUnknownCipherKeyLength() {
return unknownCipherKeyLength;
}
@ManagedAttribute
public boolean isSuppressCommasInAccessLog() {
return suppressCommasInAccessLog;
}
@ManagedAttribute
public boolean isEscapeCommasInAccessLog() {
return !suppressCommasInAccessLog;
}
}