// Copyright (C) 2003-2009 by Object Mentor, Inc. All rights reserved. // Released under the terms of the CPL Common Public License version 1.0. package fitnesse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; import java.util.GregorianCalendar; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import fitnesse.components.LogData; import fitnesse.http.HttpException; import fitnesse.http.Request; import fitnesse.http.Response; import fitnesse.http.ResponseSender; import fitnesse.http.SimpleResponse; import fitnesse.responders.ErrorResponder; import org.apache.commons.lang.StringUtils; public class FitNesseExpediter implements ResponseSender, Runnable { private static final Logger LOG = Logger.getLogger(FitNesseExpediter.class.getName()); private final Socket socket; private final InputStream input; private final OutputStream output; private final FitNesseContext context; private final ExecutorService executorService; private final long requestParsingTimeLimit; private Request request; private Response response; public FitNesseExpediter(Socket socket, FitNesseContext context, ExecutorService executorService) throws IOException { this(socket, context, executorService, 10000); } public FitNesseExpediter(Socket socket, FitNesseContext context, ExecutorService executorService, long requestParsingTimeLimit) throws IOException { this.context = context; this.socket = socket; this.executorService = executorService; input = socket.getInputStream(); output = socket.getOutputStream(); this.requestParsingTimeLimit = requestParsingTimeLimit; } @Override public void run() { try { // Storing them in instance fields, since we need info for logging when the connection is closed. request = makeRequest(); response = makeResponse(request); sendResponse(response); } catch (SocketException se) { // can be thrown by makeResponse or sendResponse. } catch (Throwable e) { // NOSONAR // This catch is intentional, since it's the last point where we can catch exceptions that occur in this thread. LOG.log(Level.WARNING, "Unexpected exception", e); } } @Override public void send(byte[] bytes) throws IOException { output.write(bytes); output.flush(); } @Override public void close() { log(socket, request, response); if (!socket.isClosed()) { try { socket.close(); } catch (IOException e) { LOG.log(Level.WARNING, "Error while closing socket", e); } } } private Request makeRequest() { Request request = new Request(input); request.setContextRoot(context.contextRoot); return request; } private void sendResponse(Response response) throws IOException { response.sendTo(this); } private Response makeResponse(final Request request) throws Exception { Response response; try { executorService.submit(new Callable<Request>() { @Override public Request call() throws Exception { request.parse(); return request; } }).get(requestParsingTimeLimit, TimeUnit.MILLISECONDS); if (request.hasBeenParsed()) { if (context.contextRoot.equals(request.getRequestUri() + "/")) { response = new SimpleResponse(); response.redirect(context.contextRoot, ""); } else { response = createGoodResponse(request); } } else { response = reportError(request, 400, "The request could not be parsed."); } } catch (SocketException se) { throw se; } catch (TimeoutException e) { String message = "The client request has been unproductive for too long. It has timed out and will no longer be processed."; LOG.log(Level.FINE, message, e); response = reportError(request, 408, message); } catch (HttpException e) { LOG.log(Level.FINE, "An error occured while fulfilling user request", e); response = reportError(request, 400, e.getMessage()); } catch (Exception e) { LOG.log(Level.WARNING, "An error occured while fulfilling user request", e); response = reportError(request, e); } // Add those as default headers? response.addHeader("Server", "FitNesse-" + context.version); response.addHeader("Connection", "close"); return response; } public Response createGoodResponse(Request request) throws Exception { if (StringUtils.isBlank(request.getResource()) && StringUtils.isBlank(request.getQueryString())) request.setResource("FrontPage"); Responder responder = context.responderFactory.makeResponder(request); responder = context.authenticator.authenticate(context, request, responder); return responder.makeResponse(context, request); } private Response reportError(Request request, int status, String message) throws Exception { return new ErrorResponder(message, status).makeResponse(context, request); } private Response reportError(Request request, Exception e) throws Exception { return new ErrorResponder(e).makeResponse(context, request); } public static LogData makeLogData(Socket socket, Request request, Response response) { return new LogData( ((InetSocketAddress) socket.getRemoteSocketAddress()).getAddress().getHostAddress(), new GregorianCalendar(), request.getRequestLine(), response.getStatus(), response.getContentSize(), request.getAuthorizationUsername()); } public void log(Socket s, Request request, Response response) { if (context.logger != null) context.logger.log(makeLogData(s, request, response)); } }