package org.webpieces.frontend.impl;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.webpieces.data.api.DataWrapper;
import org.webpieces.frontend.api.FrontendConfig;
import org.webpieces.frontend.api.HttpServerSocket;
import org.webpieces.httpcommon.api.HttpSocket;
import org.webpieces.httpcommon.api.RequestId;
import org.webpieces.httpcommon.api.RequestListener;
import org.webpieces.httpcommon.api.ResponseSender;
import org.webpieces.httpcommon.api.exceptions.HttpClientException;
import org.webpieces.httpcommon.api.exceptions.HttpException;
import org.webpieces.httpparser.api.dto.HttpRequest;
import org.webpieces.httpparser.api.dto.KnownStatusCode;
import org.webpieces.nio.api.channels.Channel;
import org.webpieces.nio.api.channels.ChannelSession;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.util.threading.SafeRunnable;
import com.webpieces.http2parser.api.dto.lib.Http2Header;
class TimedRequestListener implements RequestListener {
private static final Logger log = LoggerFactory.getLogger(TimedRequestListener.class);
private ScheduledExecutorService timer;
private RequestListener listener;
private FrontendConfig config;
private Map<HttpSocket, ScheduledFuture<?>> socketToTimeout = new Hashtable<>();
TimedRequestListener(ScheduledExecutorService timer, RequestListener listener, FrontendConfig config) {
this.timer = timer;
this.listener = listener;
this.config = config;
}
private HttpServerSocket getHttpServerSocketForChannel(Channel channel) {
ChannelSession session = channel.getSession();
return (HttpServerSocket) session.get("webpieces.httpServerSocket");
}
@Override
public void incomingRequest(HttpRequest req, RequestId id, boolean isComplete, ResponseSender responseSender) {
releaseTimeout(getHttpServerSocketForChannel(responseSender.getUnderlyingChannel()));
listener.incomingRequest(req, id, isComplete, responseSender);
}
private void releaseTimeout(HttpSocket httpSocket) {
ScheduledFuture<?> scheduledFuture = socketToTimeout.remove(httpSocket);
if(scheduledFuture != null) {
scheduledFuture.cancel(false);
}
}
@Override
public CompletableFuture<Void> incomingData(DataWrapper data, RequestId id, boolean isComplete, ResponseSender sender) {
return listener.incomingData(data, id, isComplete, sender);
}
@Override
public void incomingTrailer(List<Http2Header> headers, RequestId id, boolean isComplete, ResponseSender sender) {
listener.incomingTrailer(headers, id, isComplete, sender);
}
@Override
public void clientOpenChannel(HttpSocket HttpSocket) {
listener.clientOpenChannel(HttpSocket);
}
@Override
public void incomingError(HttpException exc, HttpSocket httpSocket) {
listener.incomingError(exc, httpSocket);
//safety measure preventing leak on quick connect/close clients
releaseTimeout(httpSocket);
log.info("closing socket="+httpSocket+" due to response code="+exc.getStatusCode());
((HttpServerSocket) httpSocket).getResponseSender().close();
listener.channelClosed(httpSocket, false);
}
void openedConnection(HttpServerSocket httpServerSocket, boolean isReadyForWrites) {
log.info("opened connection from " + httpServerSocket + " isReadyForWrites=" + isReadyForWrites);
if(!httpServerSocket.getUnderlyingChannel().isSslChannel()) {
scheduleTimeout(httpServerSocket);
clientOpenChannel(httpServerSocket);
} else if(isReadyForWrites) {
//if ready for writes, the tcpChannel is encrypted and fully open
clientOpenChannel(httpServerSocket);
} else { //if not ready for writes, the socket is open but encryption handshake is not been done yet
scheduleTimeout(httpServerSocket);
}
}
private void scheduleTimeout(HttpSocket HttpSocket) {
if(timer == null || config.maxConnectToRequestTimeoutMs == null)
return;
ScheduledFuture<?> future = timer.schedule(new TimeoutOnRequest(HttpSocket), config.maxConnectToRequestTimeoutMs, TimeUnit.MILLISECONDS);
//lifecycle of the entry in the Map is until the TimeoutOnRequest runs OR
//until incomingRequest is invoked as we have a request OR
//client closes the socket before sending http request and before the timeout
socketToTimeout.put(HttpSocket, future);
}
private class TimeoutOnRequest extends SafeRunnable {
private HttpSocket httpSocket;
TimeoutOnRequest(HttpSocket httpSocket) {
this.httpSocket = httpSocket;
}
@Override
public void runImpl() {
socketToTimeout.remove(httpSocket);
log.info("timing out a client that did not send a request in time="+config.maxConnectToRequestTimeoutMs+"ms so we are closing that client's socket. httpSocket="+ httpSocket);
HttpClientException exc = new HttpClientException("timing out a client who did not send a request in time", KnownStatusCode.HTTP_408_REQUEST_TIMEOUT);
incomingError(exc, httpSocket);
}
}
@Override
public void channelClosed(HttpSocket httpSocket, boolean browserClosed) {
releaseTimeout(httpSocket);
listener.channelClosed(httpSocket, browserClosed);
}
@Override
public void applyWriteBackPressure(ResponseSender responseSender) {
listener.applyWriteBackPressure(responseSender);
}
@Override
public void releaseBackPressure(ResponseSender responseSender) {
listener.releaseBackPressure(responseSender);
}
}