All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.webpieces.webserver.impl.RequestStreamWriter Maven / Gradle / Ivy

There is a newer version: 2.1.1
Show newest version
package org.webpieces.webserver.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.DataWrapper;
import org.webpieces.data.api.DataWrapperGenerator;
import org.webpieces.data.api.DataWrapperGeneratorFactory;
import org.webpieces.frontend2.api.ResponseStream;
import org.webpieces.router.api.exceptions.BadCookieException;
import org.webpieces.webserver.impl.body.BodyParser;
import org.webpieces.webserver.impl.body.BodyParsers;

import com.webpieces.hpack.api.HpackParserFactory;
import com.webpieces.hpack.api.dto.Http2Headers;
import com.webpieces.hpack.api.dto.Http2Request;
import com.webpieces.hpack.api.subparsers.AcceptType;
import com.webpieces.hpack.api.subparsers.HeaderPriorityParser;
import com.webpieces.http2engine.api.StreamWriter;
import com.webpieces.http2parser.api.dto.DataFrame;
import com.webpieces.http2parser.api.dto.lib.Http2Header;
import com.webpieces.http2parser.api.dto.lib.Http2HeaderName;
import com.webpieces.http2parser.api.dto.lib.StreamMsg;

public class RequestStreamWriter implements StreamWriter {

	private static final Logger log = LoggerFactory.getLogger(RequestStreamWriter.class);
	private static final HeaderPriorityParser headerParser = HpackParserFactory.createHeaderParser();
	private static final DataWrapperGenerator dataGen = DataWrapperGeneratorFactory.createDataWrapperGenerator();

	private static Set headersSupported = new HashSet<>();

	static {
		//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(Http2HeaderName.METHOD);
		headersSupported.add(Http2HeaderName.PATH);
		headersSupported.add(Http2HeaderName.AUTHORITY);
		headersSupported.add(Http2HeaderName.SCHEME);

		headersSupported.add(Http2HeaderName.DATE);
		headersSupported.add(Http2HeaderName.CONNECTION);
		headersSupported.add(Http2HeaderName.USER_AGENT);
		headersSupported.add(Http2HeaderName.CONTENT_LENGTH);
		headersSupported.add(Http2HeaderName.CONTENT_TYPE);
		headersSupported.add(Http2HeaderName.ACCEPT_ENCODING);
		headersSupported.add(Http2HeaderName.ACCEPT_LANGUAGE);
		headersSupported.add(Http2HeaderName.ACCEPT);
		headersSupported.add(Http2HeaderName.COOKIE);
		headersSupported.add(Http2HeaderName.REFERER);
		headersSupported.add(Http2HeaderName.ORIGIN);
		headersSupported.add(Http2HeaderName.CACHE_CONTROL);
		headersSupported.add(Http2HeaderName.PRAGMA);
		headersSupported.add(Http2HeaderName.X_REQUESTED_WITH);
		headersSupported.add(Http2HeaderName.X_FORWARDED_PROTO);
		
		//we don't do redirects or anything like that yet...
		headersSupported.add(Http2HeaderName.UPGRADE_INSECURE_REQUESTS);
	}

	private RequestHelpFacade facade;
	private ResponseStream stream;
	private Http2Request requestHeaders;

	private CompletableFuture outstandingRequest;
	private DataWrapper data = dataGen.emptyWrapper();
	private boolean cancelled;

	public RequestStreamWriter(RequestHelpFacade facade, ResponseStream stream, Http2Request headers) {
		this.facade = facade;
		this.stream = stream;
		this.requestHeaders = headers;
	}
	
	@Override
	public CompletableFuture processPiece(StreamMsg frame) {
		if(cancelled)
			return CompletableFuture.completedFuture(null);
		else if(frame instanceof DataFrame) {
			DataFrame dataFrame = (DataFrame) frame;
			data = dataGen.chainDataWrappers(data, dataFrame.getData());
			if(frame.isEndOfStream())
				outstandingRequest = handleCompleteRequest();
			return CompletableFuture.completedFuture(null);
		} else if(frame instanceof Http2Headers) {
			if(frame.isEndOfStream())
				outstandingRequest = handleCompleteRequest();			
			return CompletableFuture.completedFuture(null);
		}
		
		throw new IllegalStateException("frame not expected="+frame);
	}

	CompletableFuture handleCompleteRequest() {
		for(Http2Header h : requestHeaders.getHeaders()) {
			if (!headersSupported.contains(h.getKnownName()))
				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 = requestHeaders;
		
		fillInHttpsValue(routerRequest);
		
		routerRequest.isBackendRequest = stream.getSocket().isBackendSocket();

		String domain = requestHeaders.getAuthority();
		if(domain == null) {
			throw new IllegalArgumentException("Must contain Host(http1.1) or :authority(http2) header");
		}

		int port = 80;
        if(routerRequest.isHttps)
                port = 443;
        //if there is a firewall this port is wrong....and the above or below is right
		//int port = socketInfo.getLocalBoundAddress().getPort();
		
		int index2 = domain.indexOf(":");
		//host header may have port in it format is user@domain:port where user and port are optional
		//TODO(dhiller): find when user is used and test implement
		if(index2 >= 0) {
			port = Integer.parseInt(domain.substring(index2+1));
			domain = domain.substring(0, index2);
		}
		
		String methodString = requestHeaders.getMethodString();
		HttpMethod method = HttpMethod.lookup(methodString);
		if(method == null)
			throw new UnsupportedOperationException("method not supported="+methodString);

		parseCookies(requestHeaders, routerRequest);
		parseAcceptLang(requestHeaders, routerRequest);
		parseAccept(requestHeaders, routerRequest);
		routerRequest.encodings = headerParser.parseAcceptEncoding(requestHeaders);

		String referHeader = requestHeaders.getSingleHeaderValue(Http2HeaderName.REFERER);
		if(referHeader != null)
			routerRequest.referrer = referHeader;

		String xRequestedWithHeader = requestHeaders.getSingleHeaderValue(Http2HeaderName.X_REQUESTED_WITH);
		if("XMLHttpRequest".equals(xRequestedWithHeader))
			routerRequest.isAjaxRequest = true;
		
		String fullPath = requestHeaders.getPath();
		if(fullPath == null)
			throw new IllegalArgumentException(":path header(http2) or path in request line(http1.1) is required");
		
		parseBody(requestHeaders, routerRequest);
		routerRequest.method = method;
		routerRequest.domain = domain;
		routerRequest.port = port;
		int index = fullPath.indexOf("?");
		if(index > 0) {
			routerRequest.relativePath = fullPath.substring(0, index);
			String postfix = fullPath.substring(index+1);
			facade.urlEncodeParse(postfix, routerRequest);
		} 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");

		if(log.isDebugEnabled())
			log.debug("received request="+requestHeaders+" routerRequest="+routerRequest);

		ProxyResponse streamer = facade.createProxyResponse();
		try {
			streamer.init(routerRequest, requestHeaders, stream, facade.getMaxBodySize());

			return facade.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());
			return CompletableFuture.completedFuture(null);
		}
	}

	private void fillInHttpsValue(RouterRequest routerRequest) {
		//There are two ways to terminate SSL, x-forwarded-proto header from firewall OR you can configure your
		//firewall to point to the https port(AND turn that https port so it is only http) so you have TWO http
		//ports open, one will always be https and the other http.  
		String header = requestHeaders.getSingleHeaderValue(Http2HeaderName.X_FORWARDED_PROTO);
		if("https".equals(header))
			routerRequest.isHttps = true;
		else if("http".equals(header))
			routerRequest.isHttps = false;
		else
			routerRequest.isHttps = stream.getSocket().isForServingHttpsPages();
	}
	
	private void parseAccept(Http2Headers req, RouterRequest routerRequest) {
		List types = headerParser.parseAcceptFromRequest(req);
		List 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(Http2Headers req, RouterRequest routerRequest) {
		List headerItems = headerParser.parseAcceptLangFromRequest(req);
		
		//tack on DefaultLocale if not there..
		if(!headerItems.contains(facade.getConfig().getDefaultLocale()))
			headerItems.add(facade.getConfig().getDefaultLocale());
		
		routerRequest.preferredLocales = headerItems;
	}

	private void parseCookies(Http2Headers req, RouterRequest routerRequest) {
		//http://stackoverflow.com/questions/16305814/are-multiple-cookie-headers-allowed-in-an-http-request
		Map cookies = headerParser.parseCookiesFromRequest(req);
		routerRequest.cookies = copy(cookies);
	}



	private Map copy(Map cookies) {
		Map map = new HashMap<>();
		for(Entry 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(Http2Headers req, RouterRequest routerRequest) {
		String lengthHeader = req.getSingleHeaderValue(Http2HeaderName.CONTENT_LENGTH);
		String typeHeader = req.getSingleHeaderValue(Http2HeaderName.CONTENT_TYPE);

		routerRequest.body = data;

		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);
		} 
		
		if(typeHeader != null) {
			routerRequest.contentTypeHeaderValue = typeHeader;
		}
		
		parseBodyFromContentType(routerRequest);
	}

	/**
	 * This has to be above LoginFilter so LoginFilter can flash the multiPartParams so edits exist through
	 * a login!!  This moves body to the muliPartParams Map which LoginFilter uses
	 */
	private void parseBodyFromContentType(RouterRequest req) {
		if(req.contentLengthHeaderValue == null)
			return;
		else if(req.contentLengthHeaderValue == 0)
			return;
		
		if(req.contentTypeHeaderValue == null) {
			log.info("Incoming content length was specified, but no contentType was(We will not parse the body).  req="+req+" httpReq="+req.orginalRequest);
			return;
		}
		
		BodyParsers requestBodyParsers = facade.getBodyParsers();
		
		BodyParser parser = requestBodyParsers.lookup(req.contentTypeHeaderValue);
		if(parser == null) {
			log.error("Incoming content length was specified but content type was not 'application/x-www-form-urlencoded'(We will not parse body).  req="+req+" httpReq="+req.orginalRequest);
			return;
		}

		DataWrapper body = req.body;
		parser.parse(body, req);
	}
	
	public void setOutstandingRequest(CompletableFuture future) {
		this.outstandingRequest = future;
	}

	public void cancelOutstandingRequest() {
		cancelled = true;
		if(outstandingRequest != null)
			outstandingRequest.cancel(true);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy