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

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

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

import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webpieces.ctx.api.RouterRequest;
import org.webpieces.data.api.BufferPool;
import org.webpieces.frontend2.api.ResponseStream;
import org.webpieces.nio.api.exceptions.NioClosedChannelException;
import org.webpieces.router.api.ResponseStreamer;
import org.webpieces.router.api.exceptions.IllegalReturnValueException;
import org.webpieces.router.api.exceptions.WebSocketClosedException;
import org.webpieces.router.impl.compression.Compression;
import org.webpieces.router.impl.compression.CompressionLookup;
import org.webpieces.router.impl.dto.RedirectResponse;
import org.webpieces.router.impl.dto.RenderContentResponse;
import org.webpieces.router.impl.dto.RenderResponse;
import org.webpieces.router.impl.dto.RenderStaticResponse;
import org.webpieces.router.impl.dto.View;
import org.webpieces.templating.api.TemplateService;
import org.webpieces.templating.api.TemplateUtil;
import org.webpieces.templating.impl.tags.BootstrapModalTag;
import org.webpieces.util.filters.ExceptionUtil;
import org.webpieces.webserver.impl.ResponseCreator.ResponseEncodingTuple;

import com.webpieces.hpack.api.dto.Http2Request;
import com.webpieces.hpack.api.dto.Http2Response;
import com.webpieces.http2engine.api.StreamWriter;
import com.webpieces.http2parser.api.dto.DataFrame;
import com.webpieces.http2parser.api.dto.StatusCode;
import com.webpieces.http2parser.api.dto.lib.Http2Header;
import com.webpieces.http2parser.api.dto.lib.Http2HeaderName;

import groovy.lang.MissingPropertyException;

//MUST NOT BE @Singleton!!! since this is created per request
public class ProxyResponse implements ResponseStreamer {

	private static final Logger log = LoggerFactory.getLogger(ProxyResponse.class);
	
	private final TemplateService templatingService;
	private final StaticFileReader reader;
	private final CompressionLookup compressionLookup;
	private final ResponseCreator responseCreator;
	private final ChannelCloser channelCloser;
	private final BufferPool pool;
	
	private ResponseOverrideSender stream;
	//private HttpRequest request;
	private RouterRequest routerRequest;
	private Http2Request request;

	private int maxBodySize;
	private Object responseSent = null;

	@Inject
	public ProxyResponse(
		TemplateService templatingService, 
		StaticFileReader reader,
		CompressionLookup compressionLookup, 
		ResponseCreator responseCreator, 
		ChannelCloser channelCloser,
		BufferPool pool
	) {
		super();
		this.templatingService = templatingService;
		this.reader = reader;
		this.compressionLookup = compressionLookup;
		this.responseCreator = responseCreator;
		this.channelCloser = channelCloser;
		this.pool = pool;
	}

	public void init(RouterRequest req, Http2Request requestHeaders, ResponseStream responseSender, int maxBodySize) {
		this.routerRequest = req;
		this.request = requestHeaders;
		this.maxBodySize = maxBodySize;
		this.stream = new ResponseOverrideSender(responseSender);
	}

	public void sendRedirectAndClearCookie(RouterRequest req, String badCookieName) {
		RedirectResponse httpResponse = new RedirectResponse(false, req.isHttps, req.domain, req.port, req.relativePath);
		Http2Response response = createRedirect(httpResponse);
		
		responseCreator.addDeleteCookie(response, badCookieName);
		
		log.info("sending REDIRECT(due to bad cookie) response responseSender="+ stream);
		sendStreamHeaderResponse(response);

		channelCloser.closeIfNeeded(request, stream);
	}
	
	@Override
	public CompletableFuture sendRedirect(RedirectResponse httpResponse) {
		if(log.isDebugEnabled())
			log.debug("Sending redirect response. req="+request);
		Http2Response response = createRedirect(httpResponse);
		
		log.info("sending REDIRECT response responseSender="+ stream);
		return sendStreamHeaderResponse(response).thenApply(w -> {
			channelCloser.closeIfNeeded(request, stream);
			return null;
		});
	}

	private Http2Response createRedirect(RedirectResponse httpResponse) {
		Http2Response response = new Http2Response();

		if(httpResponse.isAjaxRedirect) {
			response.addHeader(new Http2Header(Http2HeaderName.STATUS, BootstrapModalTag.AJAX_REDIRECT_CODE+""));
			response.addHeader(new Http2Header("reason", "Ajax Redirect"));
		} else {
			response.addHeader(new Http2Header(Http2HeaderName.STATUS, StatusCode.HTTP_303_SEEOTHER.getCodeString()));
			response.addHeader(new Http2Header("reason", StatusCode.HTTP_303_SEEOTHER.getReason()));
		}
		
		String url = httpResponse.redirectToPath;
		if(url.startsWith("http")) {
			//do nothing
		} else if(httpResponse.domain != null && httpResponse.isHttps != null) {
			String prefix = "http://";
			if(httpResponse.isHttps)
				prefix = "https://";

			String portPostfix = "";
			if(httpResponse.port != 443 && httpResponse.port != 80)
				portPostfix = ":"+httpResponse.port;
				
			url = prefix + httpResponse.domain + portPostfix + httpResponse.redirectToPath;
		} else if(httpResponse.domain != null) {
			throw new IllegalReturnValueException("Controller is returning a domain without returning isHttps=true or"
					+ " isHttps=false so we can form the entire redirect.  Either drop the domain or set isHttps");
		} else if(httpResponse.isHttps != null) {
			throw new IllegalReturnValueException("Controller is returning isHttps="+httpResponse.isHttps+" but there is"
					+ "no domain set so we can't form the full redirect.  Either drop setting isHttps or set the domain");
		}
		
		Http2Header location = new Http2Header(Http2HeaderName.LOCATION, url);
		response.addHeader(location );
		
		responseCreator.addCommonHeaders(request, response, false, true);

		//Firefox requires a content length of 0 on redirect(chrome doesn't)!!!...
		response.addHeader(new Http2Header(Http2HeaderName.CONTENT_LENGTH, 0+""));
		return response;
	}

	@Override
	public CompletableFuture sendRenderHtml(RenderResponse resp) {
		if(log.isInfoEnabled())
		log.info("Sending render html response. req="+request+" controller="
						+resp.view.getControllerName()+"."+resp.view.getMethodName());
		View view = resp.view;
		String packageStr = view.getPackageName();
		//For this type of View, the template is the name of the method..
		String templateClassName = view.getRelativeOrAbsolutePath();
		int lastIndexOf = templateClassName.lastIndexOf(".");
		String extension = null;
		if(lastIndexOf > 0) {
			extension = templateClassName.substring(lastIndexOf+1);
		}
		
		String templatePath = templateClassName;
		if(!templatePath.startsWith("/")) {
			//relative path so need to form absolute path...
			if(lastIndexOf > 0) {
				templateClassName = templateClassName.substring(0, lastIndexOf);
			}
			templatePath = getTemplatePath(packageStr, templateClassName, extension);
		}
		
		//TODO: stream this out with chunked response instead??....
		StringWriter out = new StringWriter();
		
		try {
			templatingService.loadAndRunTemplate(templatePath, out, resp.pageArgs);
		} catch(MissingPropertyException e) {
			Set keys = resp.pageArgs.keySet();
			throw new ControllerPageArgsException("Controller.method="+view.getControllerName()+"."+view.getMethodName()+" did\nnot"
					+ " return enough arguments for the template ="+templatePath+".  specifically, the method\nreturned these"
					+ " arguments="+keys+"  There is a chance in your html you forgot the '' around a variable name\n"
							+ "such as #{set 'key'}# but you put #{set key}# which is 'usually' not the correct way\n"
							+ "The missing properties are as follows....\n"+e.getMessage(), e);
		}
		
		String content = out.toString();
		
		StatusCode statusCode;
		switch(resp.routeType) {
		case HTML:
			statusCode = StatusCode.HTTP_200_OK;
			break;
		case NOT_FOUND:
			statusCode = StatusCode.HTTP_404_NOTFOUND;
			break;
		case INTERNAL_SERVER_ERROR:
			statusCode = StatusCode.HTTP_500_INTERNAL_SVR_ERROR;
			break;
		default:
			throw new IllegalStateException("did add case for state="+resp.routeType);
		}
		
		//NOTE: These are ALL String templates, so default the mimeType to text/plain
		//The real mime type is looked up based on extension so htm or html results in text/html
		if(extension == null) {
			extension = "txt";
		}

		String finalExt = extension;
		
		return ExceptionUtil.wrapException(
				 () -> createResponseAndSend(statusCode, content, finalExt, "text/plain"), 
				 (t) -> convert(t));
	}
	
	private Throwable convert(Throwable t) {
		if(t instanceof NioClosedChannelException)
			//router does not know about the nio layer but it knows about WebSocketClosedException
			//so throw this as a flag to it that it doesn't need to keep trying error pages
			return new WebSocketClosedException("Socket is already closed", t);
		else
			return t;
	}
	
	
	@Override
	public CompletableFuture sendRenderStatic(RenderStaticResponse renderStatic) {
		if(log.isDebugEnabled())
			log.debug("Sending render static html response. req="+request);
		RequestInfo requestInfo = new RequestInfo(routerRequest, request, pool, stream);
		return ExceptionUtil.wrapException(
			() -> reader.sendRenderStatic(requestInfo, renderStatic), 
			(t) -> convert(t)
		);
	}
	
	private String getTemplatePath(String packageStr, String templateClassName, String extension) {
		String className = templateClassName;
		if(!"".equals(packageStr))
			className = packageStr+"."+className;
		if(!"".equals(extension))
			className = className+"_"+extension;
		
		return TemplateUtil.convertTemplateClassToPath(className);
	}

	@Override
	public CompletableFuture sendRenderContent(RenderContentResponse resp) {
		ResponseEncodingTuple tuple = responseCreator.createContentResponse(request, resp.getStatusCode(), resp.getReason(), resp.getMimeType());
		return maybeCompressAndSend(null, tuple, resp.getPayload()); 
	}
	
	private CompletableFuture createResponseAndSend(StatusCode statusCode, String content, String extension, String defaultMime) {
		if(content == null)
			throw new IllegalArgumentException("content cannot be null");
		
		ResponseEncodingTuple tuple = responseCreator.createResponse(request, statusCode, extension, defaultMime, true);
		
		if(log.isDebugEnabled())
			log.debug("content about to be sent back="+content);
		
		Charset encoding = tuple.mimeType.htmlResponsePayloadEncoding;
		byte[] bytes = content.getBytes(encoding);
		
		return maybeCompressAndSend(extension, tuple, bytes);
	}

	private CompletableFuture maybeCompressAndSend(String extension, ResponseEncodingTuple tuple, byte[] bytes) {
		Compression compression = compressionLookup.createCompressionStream(routerRequest.encodings, extension, tuple.mimeType);
		
		Http2Response resp = tuple.response;

		if(bytes.length == 0) {
			resp.setEndOfStream(true);
			return sendStreamHeaderResponse(resp).thenApply(w -> null);
		}

		return sendChunkedResponse(resp, bytes, compression);
	}

	private CompletableFuture sendStreamHeaderResponse(Http2Response response) {
		if(responseSent != null)
			throw new IllegalStateException("You already sent a response.  "
					+ "do not call Actions.redirect or Actions.render more than once.  previous response="
					+responseSent+" 2nd response="+response);
		responseSent = response;
		return stream.sendResponse(response);
	}
	
	private CompletableFuture sendChunkedResponse(Http2Response resp, byte[] bytes, final Compression compression) {		
				
		boolean compressed = false;
		Compression usingCompression;
		if(compression == null) {
			usingCompression = new NoCompression();
		} else {
			usingCompression = compression;
			compressed = true;
			resp.addHeader(new Http2Header(Http2HeaderName.CONTENT_ENCODING, usingCompression.getCompressionType()));
		}

		log.info("sending RENDERHTML response. size="+bytes.length+" code="+resp+" for domain="+routerRequest.domain+" path"+routerRequest.relativePath+" responseSender="+ stream);

		boolean isCompressed = compressed;

		// Send the headers and get the responseid.
		return sendStreamHeaderResponse(resp).thenCompose(writer -> {

			List frames = possiblyCompress(bytes, usingCompression, isCompressed);
			
			CompletableFuture future = CompletableFuture.completedFuture(null);
			
			
			for(int i = 0; i < frames.size(); i++) {
				DataFrame f = frames.get(i);
				if(i == frames.size()-1)
					f.setEndOfStream(true);
				
				future = future.thenCompose(v -> {
					return writer.processPiece(f);
				});
			}
			
			return future;
			
		}).thenApply(w -> null);
	}

	private List possiblyCompress(byte[] bytes, Compression usingCompression, boolean isCompressed) {
		ChunkedStream chunkedStream = new ChunkedStream(maxBodySize, isCompressed);

		try(OutputStream chainStream = usingCompression.createCompressionStream(chunkedStream)) {
			//IF wrapped in compression above(ie. not NoCompression), sending the WHOLE byte[] in comes out in
			//pieces that get sent out as it is being compressed
			//and http chunks are sent under the covers(in ChunkedStream)
			chainStream.write(bytes);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		if(!chunkedStream.isClosed())
			throw new IllegalStateException("ChunkedStream should have been closed");
		
		List frames = chunkedStream.getFrames();
		return frames;
	}

	@Override
	public CompletableFuture failureRenderingInternalServerErrorPage(Throwable e) {
		if(log.isDebugEnabled())
			log.debug("Sending failure html response. req="+request);

		//TODO: IF instance of HttpException with a KnownStatusCode, we should actually send that status code
		//TODO: we should actually just render our own internalServerError.html page with styling and we could do that.
		
		//This is a final failure so we send a webpieces page next (in the future, we should just use a customer static html file if set)
		//This is only if the webapp 500 html page fails as many times it is a template and they could have another bug in that template.
		String html = "This website had a bug, "
				+ "then when rendering the page explaining the bug, well, they hit another bug.  "
				+ "The webpieces platform saved them from sending back an ugly stack trace.  Contact website owner "
				+ "with a screen shot of this page";
		
		return createResponseAndSend(StatusCode.HTTP_500_INTERNAL_SVR_ERROR, html, "html", "text/html");
	}

//	public void sendFailure(HttpException exc) {
//		log.debug(() -> "Sending failure response. req="+request);
//
//		createResponseAndSend(exc.getStatusCode(), "Something went wrong(are you hacking the system?)", "txt", "text/plain");
//	}

}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy