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

org.webpieces.frontend2.impl.Http11StreamImpl Maven / Gradle / Ivy

package org.webpieces.frontend2.impl;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webpieces.data.api.DataWrapper;
import org.webpieces.frontend2.api.FrontendSocket;
import org.webpieces.frontend2.api.HttpStream;
import org.webpieces.frontend2.api.ResponseStream;
import org.webpieces.http2translations.api.Http2ToHttp11;
import org.webpieces.httpparser.api.HttpParser;
import org.webpieces.httpparser.api.common.Header;
import org.webpieces.httpparser.api.common.KnownHeaderName;
import org.webpieces.httpparser.api.dto.HttpChunk;
import org.webpieces.httpparser.api.dto.HttpData;
import org.webpieces.httpparser.api.dto.HttpLastChunk;
import org.webpieces.httpparser.api.dto.HttpPayload;
import org.webpieces.httpparser.api.dto.HttpRequest;
import org.webpieces.httpparser.api.dto.HttpResponse;
import org.webpieces.util.locking.PermitQueue;

import com.webpieces.http2.api.dto.highlevel.Http2Request;
import com.webpieces.http2.api.dto.highlevel.Http2Response;
import com.webpieces.http2.api.dto.lowlevel.CancelReason;
import com.webpieces.http2.api.dto.lowlevel.DataFrame;
import com.webpieces.http2.api.dto.lowlevel.Http2Method;
import com.webpieces.http2.api.dto.lowlevel.lib.Http2Header;
import com.webpieces.http2.api.dto.lowlevel.lib.Http2HeaderName;
import com.webpieces.http2.api.dto.lowlevel.lib.Http2Msg;
import com.webpieces.http2.api.dto.lowlevel.lib.StreamMsg;
import com.webpieces.http2.api.streaming.PushStreamHandle;
import com.webpieces.http2.api.streaming.StreamRef;
import com.webpieces.http2.api.streaming.StreamWriter;

public class Http11StreamImpl implements ResponseStream {
	private static final Logger log = LoggerFactory.getLogger(Http11StreamImpl.class);
	//private static final DataWrapperGenerator dataGen = DataWrapperGeneratorFactory.createDataWrapperGenerator();

	private FrontendSocketImpl socket;
	private HttpParser http11Parser;
	private AtomicReference endingFrame = new AtomicReference<>();
	private Map session = new HashMap();

	private HttpStream streamHandle;

	private int streamId;

	private PermitQueue permitQueue;

	private boolean sentFullRequest;

	private Http2Request http2Request;

	private HttpRequest http1Req;

	private boolean isForConnectRequeest;
	private boolean hasRespondedToConnect;
	private StreamRef streamRef;

	public Http11StreamImpl(
			int streamId, 
			FrontendSocketImpl socket, 
			HttpParser http11Parser, 
			PermitQueue permitQueue, 
			HttpRequest http1Req, 
			Http2Request headers
	) {
		this.streamId = streamId;
		this.socket = socket;
		this.http11Parser = http11Parser;
		this.permitQueue = permitQueue;
		this.http1Req = http1Req;
		this.http2Request = headers;
		if(headers.getKnownMethod() == Http2Method.CONNECT)
			isForConnectRequeest = true;
	}

	@Override
	public CompletableFuture process(Http2Response headers) {
		closeCheck(headers);
		HttpResponse response = Http2ToHttp11.translateResponse(headers);
		
		if(http2Request.getKnownMethod() == Http2Method.CONNECT) {
			//In this case, it is an upgrade to a bi-directional stream
			//connect has no content length BUT we are basically creating a 'stream' here of
			//bytes so we don't care about parsing anymore(ie. SSL or http)..
			return write(response).thenApply(c -> new Http11ChunkedWriter(http1Req, http2Request));
		} else if(headers.isEndOfStream()) {
			validateHeader(response);
			remove(headers);
			return write(response).thenApply(w -> {
				permitQueue.releasePermit();
				return new NoWritesWriter();
			});
		} else if(contentLengthGreaterThanZero(headers)) {
			return write(response).thenApply(w -> new ContentLengthResponseWriter(headers));
		}
		
		return write(response).thenApply(c -> new Http11ChunkedWriter(http1Req, http2Request));
	}

	private void closeCheck(Http2Msg msg) {
		if(endingFrame.get() != null)
			throw new IllegalArgumentException("You already sent a frame with endOfStream=true so cannot send more data."
					+ "  You already sent\n"+endingFrame.get()+"\n\nAnd NOW, you are trying to send=\n"+msg);
	}

	private void validateHeader(HttpResponse response) {
		Header contentLenHeader = response.getHeaderLookupStruct().getHeader(KnownHeaderName.CONTENT_LENGTH);
		if(contentLenHeader == null)
			throw new IllegalArgumentException("Content Length header required and missing and should be set to zero");
		else if(contentLenHeader.getValue() == null)
			throw new IllegalArgumentException("Content Length header found but it's value is null");
		
		int len = Integer.parseInt(contentLenHeader.getValue());
		if(len != 0)
			throw new IllegalArgumentException("Content Length header found but it's value is 0 while response.isEndOfStream is true.  this is contradictory");
	}

	private boolean contentLengthGreaterThanZero(Http2Response headers) {
		Http2Header contentLenHeader = headers.getHeaderLookupStruct().getHeader(Http2HeaderName.CONTENT_LENGTH);
		if(contentLenHeader != null) {
			int len = Integer.parseInt(contentLenHeader.getValue());
			if(len > 0) //for redirect firefox content len is 0
				return true;
			else if(len == 0) {
				if(!headers.isEndOfStream())
					throw new IllegalStateException("Content-Length=0 but response.isEndOfStream==false");
			}
		}
		return false;
	}

	private class NoWritesWriter implements StreamWriter {
		@Override
		public CompletableFuture processPiece(StreamMsg data) {
			CompletableFuture future = new CompletableFuture<>();
			future.completeExceptionally(new IllegalStateException("You already sent a response with endStream==true"));
			return future;
		}

	}
	
	private class ContentLengthResponseWriter implements StreamWriter {
		private int len;
		private int totalWritten;
		
		public ContentLengthResponseWriter(Http2Response response) {
			Http2Header contentLenHeader = response.getHeaderLookupStruct().getHeader(Http2HeaderName.CONTENT_LENGTH);
			this.len = Integer.parseInt(contentLenHeader.getValue());
		}
		
		@Override
		public CompletableFuture processPiece(StreamMsg data) {
			closeCheck(data);
			if(!(data instanceof DataFrame))
				throw new UnsupportedOperationException("not supported in http1.1="+data);
			
			DataFrame frame = (DataFrame) data;
			
			totalWritten += frame.getData().getReadableSize();
			if(totalWritten > len)
				throw new IllegalArgumentException("You wrote more than the content length header="+len+" written size="+totalWritten);
			else if(frame.isEndOfStream() && totalWritten != len)
				throw new IllegalArgumentException("You did not write enough data.  written="+totalWritten+" content length header="+len);
			
			if(frame.isEndOfStream()) {
				log.info(socket+" done sending response2");
				remove(data);
			}

			HttpData httpData = new HttpData(frame.getData(), frame.isEndOfStream());
			return write(httpData).thenApply(c -> {
				if(frame.isEndOfStream())
					permitQueue.releasePermit();
				return null;
			});
		}
	}
	
	private class Http11ChunkedWriter implements StreamWriter {

		private HttpRequest http1Req2;
		private Http2Request headers2;

		public Http11ChunkedWriter(HttpRequest http1Req, Http2Request headers) {
			http1Req2 = http1Req;
			headers2 = headers;
		}

		@Override
		public String toString() {
			return "Http1ChunkedWriter["+headers2.getSingleHeaderValue(Http2HeaderName.PATH)+"]["+socket+"]";
		}
		
		@Override
		public CompletableFuture processPiece(StreamMsg data) {
			closeCheck(data);
			if(!(data instanceof DataFrame))
				throw new UnsupportedOperationException("not supported in http1.1="+data);
			DataFrame frame = (DataFrame) data;

			
			//MULTIPLE scenarios here.
			//1. Someone sends StreamMsg with isEndOfStream=true  WITH data -> send HttpChunk AND HttpLastChunk
			//2. Someone sends StreamMsg with isEndOfStream=true  WITH NO data -> send HttpLastChunk
			//3. Someone sends StreamMsg with isEndOfStream=false WITH data -> send HttpChunk
			//4. Someone sends StreamMsg with isEndOfStream=false WITH NO data -> exception...make clients fix their bugs
			if(!frame.isEndOfStream()) {
				if(frame.getData().getReadableSize() == 0)
					throw new IllegalArgumentException("DataFrame must contain data if isEndOfStream is false");
				return write(new HttpChunk(frame.getData()));
			}

			CompletableFuture future = CompletableFuture.completedFuture(null);
			if(frame.getData().getReadableSize() > 0)
				future = write(new HttpChunk(frame.getData()));
			
			remove(data);	

			if(log.isDebugEnabled())
				log.debug(socket+" done sending response");
			future = future.thenCompose(w -> {
				return write(new HttpLastChunk());
			}).thenApply(v -> {
				permitQueue.releasePermit();
				return null;
			});
			
			return future;
		}
	}

	private void remove(Http2Msg data) {
		Http11StreamImpl current = socket.getCurrentStream();
		if(endingFrame.get() != null)
			throw new IllegalStateException("You had already sent a frame with endOfStream "
					+ "set and can't send more.  ending frame was="+endingFrame+" but you just sent="+data);
		else if(current != this)
			throw new IllegalStateException("Due to http1.1 spec, YOU MUST return "
					+ "responses in order and this is not the current response that needs responding to");

		endingFrame.set(data);
		socket.setCurrentStream(null);
		
	}
	
	private CompletableFuture write(HttpPayload payload) {
		if(hasRespondedToConnect) {
			HttpChunk chunk = payload.getHttpChunk();
			DataWrapper body = chunk.getBodyNonNull();
			byte[] createByteArray = body.createByteArray();
			ByteBuffer buffer = ByteBuffer.wrap(createByteArray);
			return socket.getChannel().write(buffer);
		}
		
		
		ByteBuffer buf = http11Parser.marshalToByteBuffer(socket.getHttp11MarshalState(), payload);
		if(isForConnectRequeest) {
			hasRespondedToConnect = true;
		}
		return socket.getChannel().write(buf);
	}
	
	@Override
	public PushStreamHandle openPushStream() {
		throw new UnsupportedOperationException("not supported for http1.1 requests");
	}

	@Override
	public CompletableFuture cancel(CancelReason reason) {
		return socket.getChannel().close();
	}

	@Override
	public FrontendSocket getSocket() {
		return socket;
	}

	@Override
	public Map getSession() {
		return session;
	}

	public void setStreamHandle(HttpStream streamHandle2) {
		this.streamHandle = streamHandle2;
	}

	public HttpStream getStreamHandle() {
		return streamHandle;
	}

	public int getStreamId() {
		return streamId;
	}

	public void setSentFullRequest(boolean sent) {
		this.sentFullRequest = sent;
	}

	public boolean isForConnectRequeest() {
		return isForConnectRequeest;
	}

	public void setStreamRef(StreamRef streamRef) {
		this.streamRef = streamRef;
	}

	public StreamRef getStreamRef() {
		return streamRef;
	}
	
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy