package org.webpieces.webserver.impl; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; import javax.inject.Provider; import org.webpieces.ctx.api.AcceptMediaType; import org.webpieces.ctx.api.HttpMethod; import org.webpieces.ctx.api.RouterCookie; import org.webpieces.ctx.api.RouterRequest; import org.webpieces.data.api.BufferPool; import org.webpieces.data.api.DataWrapper; import org.webpieces.data.api.DataWrapperGenerator; import org.webpieces.data.api.DataWrapperGeneratorFactory; 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.HttpParserFactory; import org.webpieces.httpparser.api.common.Header; import org.webpieces.httpparser.api.common.KnownHeaderName; import org.webpieces.httpparser.api.dto.Headers; import org.webpieces.httpparser.api.dto.HttpRequest; import org.webpieces.httpparser.api.dto.HttpRequestLine; import org.webpieces.httpparser.api.dto.UrlInfo; import org.webpieces.httpparser.api.subparsers.AcceptType; import org.webpieces.httpparser.api.subparsers.HeaderPriorityParser; import org.webpieces.router.api.RouterService; import org.webpieces.router.api.exceptions.BadCookieException; import org.webpieces.util.logging.Logger; import org.webpieces.util.logging.LoggerFactory; import org.webpieces.util.urlparse.UrlEncodedParser; import org.webpieces.webserver.api.WebServerConfig; import com.webpieces.http2parser.api.dto.lib.Http2Header; public class RequestReceiver implements RequestListener { private static final Logger log = LoggerFactory.getLogger(RequestReceiver.class); private static final HeaderPriorityParser headerParser = HttpParserFactory.createHeaderParser(); private static final DataWrapperGenerator dataGen = DataWrapperGeneratorFactory.createDataWrapperGenerator(); @Inject private RouterService routingService; @Inject private WebServerConfig config; @Inject private UrlEncodedParser urlEncodedParser; @Inject private BufferPool bufferPool; //I don't use javax.inject.Provider much as reflection creation is a tad slower but screw it......(it's fast enough)..AND //it keeps the code a bit more simple. We could fix this later @Inject private Provider<ProxyResponse> responseProvider; private Set<String> headersSupported = new HashSet<>(); private class RequestCollectingData { public HttpRequest req; public List<CompletableFuture<Void>> futures; public RequestCollectingData(HttpRequest req) { this.req = req; this.futures = new LinkedList<>(); } } private ConcurrentHashMap<RequestId, RequestCollectingData> requestsStillCollecting = new ConcurrentHashMap<>(); public RequestReceiver() { //We keep this list in place to log out what we have not implemented yet. This allows us to see if //we missed anything on the request side. headersSupported.add(KnownHeaderName.HOST.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.DATE.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.CONNECTION.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.USER_AGENT.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.CONTENT_LENGTH.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.CONTENT_TYPE.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.ACCEPT_ENCODING.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.ACCEPT_LANGUAGE.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.ACCEPT.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.COOKIE.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.REFERER.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.ORIGIN.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.CACHE_CONTROL.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.PRAGMA.getHeaderName().toLowerCase()); headersSupported.add(KnownHeaderName.X_REQUESTED_WITH.getHeaderName().toLowerCase()); //we don't do redirects or anything like that yet... headersSupported.add(KnownHeaderName.UPGRADE_INSECURE_REQUESTS.getHeaderName().toLowerCase()); } private void completeRequest(RequestId id, ResponseSender sender) { // Now we're done, remove it so that another request with the same id can come in RequestCollectingData collector = requestsStillCollecting.get(id); requestsStillCollecting.remove(id); handleCompleteRequest(collector.req, id, sender); // Complete all the futures because we're done dealing with this data. for(CompletableFuture<Void> fut: collector.futures) { fut.complete(null); } } @Override public CompletableFuture<Void> incomingData(DataWrapper data, RequestId id, boolean isComplete, ResponseSender sender) { RequestCollectingData collector = requestsStillCollecting.get(id); CompletableFuture<Void> future = new CompletableFuture<>(); collector.req.setBody(dataGen.chainDataWrappers(collector.req.getBodyNonNull(), data)); collector.futures.add(future); if(isComplete) { completeRequest(id, sender); } return future; } // We don't actually support any trailer headers, so we ignore them. @Override public void incomingTrailer(List<Http2Header> headers, RequestId id, boolean isComplete, ResponseSender sender) { if(isComplete) { completeRequest(id, sender); } } private void handleCompleteRequest(HttpRequest req, RequestId requestId, ResponseSender responseSender) { for(Header h : req.getHeaders()) { if (!headersSupported.contains(h.getName().toLowerCase())) log.error("This webserver has not thought about supporting header=" + h.getName() + " quite yet. value=" + h.getValue() + " Please let us know and we can quickly add support"); } RouterRequest routerRequest = new RouterRequest(); routerRequest.orginalRequest = req; routerRequest.isHttps = req.isHttps(); int port = 80; if(routerRequest.isHttps) port = 443; Header header = req.getHeaderLookupStruct().getHeader(KnownHeaderName.HOST); if(header == null) { throw new IllegalArgumentException("Must contain Host header"); } String domain = header.getValue(); int index2 = domain.indexOf(":"); //host header may have port in it if(index2 >= 0) { port = Integer.parseInt(domain.substring(index2+1)); domain = domain.substring(0, index2); } HttpRequestLine requestLine = req.getRequestLine(); UrlInfo uriInfo = requestLine.getUri().getUriBreakdown(); HttpMethod method = HttpMethod.lookup(requestLine.getMethod().getMethodAsString()); if(method == null) throw new UnsupportedOperationException("method not supported="+requestLine.getMethod().getMethodAsString()); parseCookies(req, routerRequest); parseAcceptLang(req, routerRequest); parseAccept(req, routerRequest); routerRequest.encodings = headerParser.parseAcceptEncoding(req); Header referHeader = req.getHeaderLookupStruct().getHeader(KnownHeaderName.REFERER); if(referHeader != null) routerRequest.referrer = referHeader.getValue().trim(); Header xRequestedWithHeader = req.getHeaderLookupStruct().getHeader(KnownHeaderName.X_REQUESTED_WITH); if(xRequestedWithHeader != null && "XMLHttpRequest".equals(xRequestedWithHeader.getValue().trim())) routerRequest.isAjaxRequest = true; parseBody(req, routerRequest); routerRequest.method = method; routerRequest.domain = domain; routerRequest.port = port; String fullPath = uriInfo.getFullPath(); int index = fullPath.indexOf("?"); if(index > 0) { routerRequest.relativePath = fullPath.substring(0, index); String postfix = fullPath.substring(index+1); urlEncodedParser.parse(postfix, (k, v) -> addToMap(k,v,routerRequest.queryParams)); } else { routerRequest.queryParams = new HashMap<>(); routerRequest.relativePath = fullPath; } //http1.1 so no... routerRequest.isSendAheadNextResponses = false; if(routerRequest.relativePath.contains("?")) throw new UnsupportedOperationException("not supported yet"); ProxyResponse streamer = responseProvider.get(); try { streamer.init(routerRequest, responseSender, bufferPool, requestId); routingService.incomingCompleteRequest(routerRequest, streamer); } catch (BadCookieException e) { log.warn("This occurs if secret key changed, or you booted another webapp with different key on same port or someone modified the cookie", e); streamer.sendRedirectAndClearCookie(routerRequest, e.getCookieName()); } } @Override public void incomingRequest(HttpRequest req, RequestId requestId, boolean isComplete, ResponseSender responseSender) { //log.info("request received on channel="+channel); if(isComplete) { handleCompleteRequest(req, requestId, responseSender); } else { requestsStillCollecting.put(requestId, new RequestCollectingData(req)); } } private void parseAccept(HttpRequest req, RouterRequest routerRequest) { List<AcceptType> types = headerParser.parseAcceptFromRequest(req); List<AcceptMediaType> acceptedTypes = new ArrayList<>(); for(AcceptType t : types) { if(t.isMatchesAllTypes()) acceptedTypes.add(new AcceptMediaType()); else if(t.isMatchesAllSubtypes()) acceptedTypes.add(new AcceptMediaType(t.getMainType())); else acceptedTypes.add(new AcceptMediaType(t.getMainType(), t.getSubType())); } routerRequest.acceptedTypes = acceptedTypes; } private void parseAcceptLang(HttpRequest req, RouterRequest routerRequest) { List<Locale> headerItems = headerParser.parseAcceptLangFromRequest(req); //tack on DefaultLocale if not there.. if(!headerItems.contains(config.getDefaultLocale())) headerItems.add(config.getDefaultLocale()); routerRequest.preferredLocales = headerItems; } private void parseCookies(HttpRequest req, RouterRequest routerRequest) { //http://stackoverflow.com/questions/16305814/are-multiple-cookie-headers-allowed-in-an-http-request Map<String, String> cookies = headerParser.parseCookiesFromRequest(req); routerRequest.cookies = copy(cookies); } private Void addToMap(String k, String v, Map<String, List<String>> queryParams) { List<String> list = queryParams.get(k); if(list == null) { list = new ArrayList<>(); queryParams.put(k, list); } list.add(v); return null; } private Map<String, RouterCookie> copy(Map<String, String> cookies) { Map<String, RouterCookie> map = new HashMap<>(); for(Entry<String, String> entry : cookies.entrySet()) { RouterCookie c = copy(entry.getKey(), entry.getValue()); map.put(c.name, c); } return map; } private RouterCookie copy(String name, String val) { RouterCookie rCookie = new RouterCookie(); rCookie.name = name; rCookie.value = val; return rCookie; } private void parseBody(HttpRequest req, RouterRequest routerRequest) { Headers headers = req.getHeaderLookupStruct(); Header lengthHeader = headers.getHeader(KnownHeaderName.CONTENT_LENGTH); Header typeHeader = headers.getHeader(KnownHeaderName.CONTENT_TYPE); routerRequest.body = req.getBodyNonNull(); if(lengthHeader != null) { //Integer.parseInt(lengthHeader.getValue()); should not fail as it would have failed earlier in the parser when //reading in the body routerRequest.contentLengthHeaderValue = Integer.parseInt(lengthHeader.getValue()); } if(typeHeader != null) { routerRequest.contentTypeHeaderValue = typeHeader.getValue(); } } @Override public void incomingError(HttpException exc, HttpSocket httpSocket) { //If status is a 4xx, send it back to the client with just raw information //If status is a 5xx, send it into the routingService to be displayed back to the user //in the case of ssl timeout, we can't send anything since ssl engine/connection is not established so we can't encrypt if(httpSocket.getUnderlyingChannel().isSslChannel() && exc instanceof HttpClientException) { return; } if(!(exc instanceof HttpClientException)) log.error("Need to clean this up and render good 500 page for real bugs. thread="+Thread.currentThread().getName(), exc); ProxyResponse proxyResp = responseProvider.get(); HttpRequest req = new HttpRequest(); RouterRequest routerReq = new RouterRequest(); routerReq.orginalRequest = req; proxyResp.init(routerReq, ((HttpServerSocket) httpSocket).getResponseSender(), bufferPool, new RequestId(0)); proxyResp.sendFailure(exc); } @Override public void clientOpenChannel(HttpSocket httpSocket) { log.info("browser client open channel " + httpSocket); } @Override public void channelClosed(HttpSocket httpSocket, boolean browserClosed) { log.info("channel closed" + httpSocket+" browser closed="+browserClosed); } @Override public void applyWriteBackPressure(ResponseSender sender) { } @Override public void releaseBackPressure(ResponseSender sender) { } }