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

org.rapidoid.http.impl.lowlevel.LowLevelHttpIO Maven / Gradle / Ivy

The newest version!
/*-
 * #%L
 * rapidoid-http-fast
 * %%
 * Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

package org.rapidoid.http.impl.lowlevel;

import org.rapidoid.RapidoidThing;
import org.rapidoid.activity.RapidoidThreadLocals;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.buffer.Buf;
import org.rapidoid.commons.Dates;
import org.rapidoid.config.Conf;
import org.rapidoid.config.Config;
import org.rapidoid.ctx.Ctx;
import org.rapidoid.ctx.Ctxs;
import org.rapidoid.ctx.With;
import org.rapidoid.data.BufRange;
import org.rapidoid.data.JSON;
import org.rapidoid.http.*;
import org.rapidoid.http.customize.Customization;
import org.rapidoid.http.impl.MaybeReq;
import org.rapidoid.http.impl.ReqImpl;
import org.rapidoid.job.Jobs;
import org.rapidoid.log.GlobalCfg;
import org.rapidoid.log.Log;
import org.rapidoid.log.LogLevel;
import org.rapidoid.net.AsyncLogic;
import org.rapidoid.net.abstracts.Channel;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;
import org.rapidoid.writable.ReusableWritable;

import java.io.ByteArrayOutputStream;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import static org.rapidoid.util.Constants.CR_LF;


@Authors("Nikolche Mihajlovski")
@Since("5.3.0")
class LowLevelHttpIO extends RapidoidThing {

	private static final byte[] HTTP_200_OK = "HTTP/1.1 200 OK\r\n".getBytes();

	private static final byte[] HTTP_400_BAD_REQUEST = "HTTP/1.1 400 Bad Request\r\nContent-Length: 12\r\n\r\nBad Request!"
		.getBytes();

	private static final byte[] HEADER_SEP = ": ".getBytes();

	private static final byte[] CONN_KEEP_ALIVE = "Connection: keep-alive\r\n".getBytes();

	private static final byte[] CONN_CLOSE = "Connection: close\r\n".getBytes();

	private static final byte[] SERVER_HEADER;

	private static final byte[] CONTENT_LENGTH_IS = "Content-Length: ".getBytes();

	private static final byte[] CONTENT_LENGTH_UNKNOWN = "Content-Length: 0000000000".getBytes();

	private static final int CONTENT_LENGTHS_SIZE = 5000;

	private static final byte[] DATE_IS = "Date: ".getBytes();

	private static final byte[][] CONTENT_LENGTHS = new byte[CONTENT_LENGTHS_SIZE][];

	private static final boolean MANDATORY_HEADER_CONNECTION;
	private static final boolean MANDATORY_HEADER_DATE;
	private static final boolean MANDATORY_HEADER_SERVER;
	private static final boolean MANDATORY_HEADER_CONTENT_TYPE;

	private static final byte[] UNIFORM_DATE = "Sat, 10 Sep 2016 01:02:03 GMT".getBytes();

	private static final AtomicLong ASYNC_ID_GEN = new AtomicLong();

	static {
		for (int len = 0; len < CONTENT_LENGTHS.length; len++) {
			CONTENT_LENGTHS[len] = (new String(CONTENT_LENGTH_IS) + len + new String(CR_LF)).getBytes();
		}

		HttpResponseCodes.init();

		String serverName = Conf.HTTP.entry("serverName").or("Rapidoid");
		SERVER_HEADER = ("Server: " + serverName + "\r\n").getBytes();

		Config mandatoryHeaders = Conf.HTTP.sub("mandatoryHeaders");

		MANDATORY_HEADER_CONNECTION = mandatoryHeaders.entry("connection").or(true);
		MANDATORY_HEADER_DATE = mandatoryHeaders.entry("date").or(true);
		MANDATORY_HEADER_SERVER = mandatoryHeaders.entry("server").or(true);
		MANDATORY_HEADER_CONTENT_TYPE = mandatoryHeaders.entry("contentType").or(true);
	}

	LowLevelHttpIO() {
	}

	void removeTrailingSlash(Buf buf, BufRange range) {
		if (range.length > 1 && buf.get(range.last()) == '/') {
			range.length--;
		}
	}

	void startResponse(Resp resp, Channel channel, int code, boolean isKeepAlive, MediaType contentType) {
		channel.write(code == 200 ? HTTP_200_OK : HttpResponseCodes.get(code));

		addDefaultHeaders(channel, isKeepAlive, contentType);

		if (resp != null) {
			if (U.notEmpty(resp.headers())) {
				for (Map.Entry e : resp.headers().entrySet()) {
					addCustomHeader(channel, e.getKey().getBytes(), e.getValue().getBytes());
				}
			}

			if (U.notEmpty(resp.cookies())) {
				for (Map.Entry e : resp.cookies().entrySet()) {
					String cookie = e.getKey() + "=" + e.getValue();
					addCustomHeader(channel, HttpHeaders.SET_COOKIE.getBytes(), cookie.getBytes());
				}
			}
		}
	}

	private void addDefaultHeaders(Channel ctx, boolean isKeepAlive, MediaType contentType) {

		if (!isKeepAlive || MANDATORY_HEADER_CONNECTION) {
			ctx.write(isKeepAlive ? CONN_KEEP_ALIVE : CONN_CLOSE);
		}

		if (MANDATORY_HEADER_SERVER) {
			ctx.write(SERVER_HEADER);
		}

		if (MANDATORY_HEADER_DATE) {
			ctx.write(DATE_IS);

			if (!GlobalCfg.uniformOutput()) {
				ctx.write(Dates.getDateTimeBytes());
			} else {
				ctx.write(UNIFORM_DATE);
			}

			ctx.write(CR_LF);
		}

		if (MANDATORY_HEADER_CONTENT_TYPE) {
			ctx.write(contentType.asHttpHeader());
		}
	}

	void addCustomHeader(Channel ctx, byte[] name, byte[] value) {
		ctx.write(name);
		ctx.write(HEADER_SEP);
		ctx.write(value);
		ctx.write(CR_LF);
	}

	void writeResponse(final MaybeReq req, final Channel ctx, final boolean isKeepAlive,
	                   final int code, final MediaType contentType, final byte[] content) {

		startResponse(respOrNull(req), ctx, code, isKeepAlive, contentType);
		writeContentLengthAndBody(req, ctx, content);
	}

	void write200(MaybeReq req, Channel ctx, boolean isKeepAlive, MediaType contentTypeHeader, byte[] content) {
		writeResponse(req, ctx, isKeepAlive, 200, contentTypeHeader, content);
	}

	void error(final Req req, final Throwable error, LogLevel logLevel) {
		try {
			logError(req, error, logLevel);

			Resp resp = req.response().code(500).result(null);
			Object result = Customization.of(req).errorHandler().handleError(req, resp, error);

			HttpUtils.resultOf(req, result);

		} catch (Exception e) {
			Log.error("An error occurred inside the error handler!", e);
			HttpUtils.resultToResponse(req, HttpUtils.getErrorInfo(req.response(), e));
		}
	}

	private void logError(Req req, Throwable error, LogLevel logLevel) {

		if (error instanceof NotFound) return;

		if (Msc.isValidationError(error)) {
			if (Log.isDebugEnabled()) {
				Log.debug("Validation error when handling request: " + req);
				error.printStackTrace();
			}
			return;
		}

		if (error instanceof SecurityException) {
			Log.debug("Access denied for request: " + req, "client", req.clientIpAddress());
			return;
		}

		Log.log(null, logLevel, "Error occurred when handling request!", "error", error);
	}

	HttpStatus errorAndDone(final Req req, final Throwable error, final LogLevel logLevel) {

		req.revert();
		req.async();

		Runnable errorHandler = new Runnable() {
			@Override
			public void run() {
				error(req, error, logLevel);
				// the Req object will do the rendering
				req.done();
			}
		};

		Ctx ctx = Ctxs.get();

		if (ctx == null) {
			With.exchange(req).run(errorHandler);
		} else {
			Jobs.execute(errorHandler);
		}

		return HttpStatus.ASYNC;
	}

	void writeContentLengthAndBody(final MaybeReq req, final Channel ctx, final byte[] body) {
		writeContentLengthHeader(ctx, body.length);
		closeHeaders(req, ctx.output());
		ctx.write(body);
	}

	void writeContentLengthAndBody(final MaybeReq req, final Channel ctx, final ByteArrayOutputStream body) {
		writeContentLengthHeader(ctx, body.size());
		closeHeaders(req, ctx.output());
		ctx.output().append(body);
	}

	void writeContentLengthHeader(Channel ctx, long contentLength) {
		if (contentLength < CONTENT_LENGTHS_SIZE) {
			ctx.write(CONTENT_LENGTHS[(int) contentLength]);

		} else {
			ctx.write(CONTENT_LENGTH_IS);
			Buf out = ctx.output();
			out.putNumAsText(out.size(), contentLength, true);
			ctx.write(CR_LF);
		}
	}

	private Resp respOrNull(MaybeReq maybeReq) {
		ReqImpl req = (ReqImpl) maybeReq.getReqOrNull();

		if (req != null && req.hasResponseAttached()) {
			return req.response();
		} else {
			return null;
		}
	}

	void writeHttpResp(final MaybeReq req, final Channel ctx, final boolean isKeepAlive,
	                   int code, final MediaType contentType, final Object value) {

		Object result = value;

		Resp resp = respOrNull(req);

		if (resp != null) {

			if (resp.body() != null) {
				byte[] bytes = Msc.toBytes(resp.body());
				writeResponse(req, ctx, isKeepAlive, code, contentType, bytes);
				return;

			} else if (resp.result() != null) {
				result = resp.result();
			}
		}

		if (contentType == MediaType.JSON) {
			writeJsonResponse(req, resp, ctx, isKeepAlive, code, contentType, result);

		} else {
			byte[] bytes = Msc.toBytes(result);
			writeResponse(req, ctx, isKeepAlive, code, contentType, bytes);
		}
	}

	private void writeJsonResponse(MaybeReq req, Resp resp, Channel ctx, boolean isKeepAlive,
	                               int code, MediaType contentType, Object result) {

		startResponse(resp, ctx, code, isKeepAlive, contentType);

		RapidoidThreadLocals locals = Msc.locals();
		ReusableWritable out = locals.jsonRenderingStream();

		// FIXME headers

		JSON.stringify(result, out);

		writeContentLengthHeader(ctx, out.size());
		closeHeaders(req, ctx.output());

		ctx.write(out.array(), 0, out.size());
	}

	@SuppressWarnings("unused")
	private void writeOnBufferAsJson(MaybeReq req, Channel ctx, int code, boolean isKeepAlive, Object value) {
		startResponse(respOrNull(req), ctx, code, isKeepAlive, MediaType.JSON);

		Buf output = ctx.output();

		synchronized (output) {
			writeJsonBody(req, output.unwrap(), value);
		}
	}

	private void writeJsonBody(MaybeReq req, Buf out, Object value) {
		// Content-Length header
		out.append(CONTENT_LENGTH_UNKNOWN);
		int posConLen = out.size() - 1;
		out.append(CR_LF);

		closeHeaders(req, out);

		int posBefore = out.size();

		JSON.stringify(value, out.asOutputStream());

		int posAfter = out.size();
		int contentLength = posAfter - posBefore;

		out.putNumAsText(posConLen, contentLength, false);
	}

	void closeHeaders(MaybeReq req, Buf out) {
		// finishing the headers
		out.append(CR_LF);

		ReqImpl reqq = (ReqImpl) req.getReqOrNull();

		if (reqq != null) {
			U.must(reqq.channel().output() == out);
			reqq.onHeadersCompleted();
		}
	}

	void done(Req req) {
		ReqImpl reqq = (ReqImpl) req;
		reqq.doneProcessing();

		Channel channel = reqq.channel();
		channel.send();

		channel.closeIf(!reqq.isKeepAlive());
	}

	void resume(MaybeReq maybeReq, Channel channel, AsyncLogic logic) {
		Req req = maybeReq.getReqOrNull();

		if (req != null) {
			channel.resume(req.connectionId(), req.handle(), logic);
		} else {
			logic.resumeAsync();
		}
	}

	void writeBadRequest(Channel channel) {
		channel.write(HTTP_400_BAD_REQUEST);
		channel.close();
	}

	void respond(final MaybeReq maybeReq, final Channel channel, long connId, long handle,
	             final int code, final boolean isKeepAlive, final MediaType contentType, final RespBody body,
	             final Map headers, final Map cookies) {

		final ReqImpl req = (ReqImpl) maybeReq.getReqOrNull();

		if (handle < 0) {
			if (req != null) {
				handle = req.handle();
			} else {
				handle = channel.handle();
			}
		}

		if (connId < 0) {
			if (req != null) {
				connId = req.connectionId();
			} else {
				connId = channel.connId();
			}
		}

		final long id = ASYNC_ID_GEN.incrementAndGet();

		channel.resume(connId, handle, new AsyncLogic() {

			@Override
			public String toString() {
				return U.str(U.join(":", "#" + id, channel, code, body, isKeepAlive, contentType));
			}

			@Override
			public boolean resumeAsync() {

				boolean complete;

				startResponse(null, channel, code, isKeepAlive, contentType);

				if (U.notEmpty(headers)) {
					for (Map.Entry e : headers.entrySet()) {
						addCustomHeader(channel, e.getKey().getBytes(), e.getValue().getBytes());
					}
				}

				if (U.notEmpty(cookies)) {
					for (Map.Entry e : cookies.entrySet()) {
						String cookie = e.getKey() + "=" + e.getValue();
						addCustomHeader(channel, HttpHeaders.SET_COOKIE.getBytes(), cookie.getBytes());
					}
				}

				Buf output = channel.output();

				synchronized (channel) {
					if (body == null) {

						int posContentLengthValue = output.size() - 1;

						// finishing the headers
						closeHeaders(maybeReq, output);

						long posBeforeBody = output.size();

						if (req != null) {
							req.responded(posContentLengthValue, posBeforeBody, false);
						}

						complete = false;

					} else {

						writeContentLengthHeader(channel, body.length());
						closeHeaders(maybeReq, output);
						body.writeTo(channel);

						if (req != null) {
							req.completed(true);
							done(req);
						}

						complete = true;
					}
				}

				return complete;
			}
		});
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy