Java tutorial
/** * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2016 Chiori Greene a.k.a. Chiori-chan <me@chiorichan.com> * All Right Reserved. */ package com.chiorichan.http; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelProgressiveFuture; import io.netty.channel.ChannelProgressiveFutureListener; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.stream.ChunkedStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import org.apache.commons.lang3.exception.ExceptionUtils; import com.chiorichan.AppConfig; import com.chiorichan.event.EventBus; import com.chiorichan.event.http.ErrorEvent; import com.chiorichan.event.http.HttpExceptionEvent; import com.chiorichan.factory.ScriptingContext; import com.chiorichan.lang.HttpError; import com.chiorichan.logger.experimental.LogEvent; import com.chiorichan.net.NetworkManager; import com.chiorichan.session.Session; import com.chiorichan.session.SessionException; import com.chiorichan.util.Versioning; import com.google.common.base.Charsets; import com.google.common.collect.Maps; /** * Wraps the Netty HttpResponse to provide easy methods for manipulating the result of each request */ public class HttpResponseWrapper { Charset encoding = Charsets.UTF_8; final Map<String, String> headers = Maps.newHashMap(); ApacheHandler htaccess = null; String httpContentType = "text/html"; HttpResponseStatus httpStatus = HttpResponseStatus.OK; final LogEvent log; ByteBuf output = Unpooled.buffer(); final Map<String, String> annotations = Maps.newHashMap(); final HttpRequestWrapper request; HttpResponseStage stage = HttpResponseStage.READING; protected HttpResponseWrapper(HttpRequestWrapper request, LogEvent log) { this.request = request; this.log = log; } public void close() { request.getChannel().close(); stage = HttpResponseStage.CLOSED; } public void finishMultipart() throws IOException { if (stage == HttpResponseStage.CLOSED) throw new IllegalStateException( "You can't access closeMultipart unless you start MULTIPART with sendMultipart."); stage = HttpResponseStage.CLOSED; // Write the end marker ChannelFuture lastContentFuture = request.getChannel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); // Decide whether to close the connection or not. // if ( !isKeepAlive( request ) ) { // Close the connection when the whole content is written out. lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } public String getAnnotation(String key) { return annotations.get(key); } public int getHttpCode() { return httpStatus.code(); } public String getHttpMsg() { return HttpCode.msg(httpStatus.code()); } public ByteBuf getOutput() { return output; } public byte[] getOutputBytes() { byte[] bytes = new byte[output.writerIndex()]; int inx = output.readerIndex(); output.readerIndex(0); output.readBytes(bytes); output.readerIndex(inx); return bytes; } /** * * @return HttpResponseStage */ public HttpResponseStage getStage() { return stage; } public boolean isCommitted() { return stage == HttpResponseStage.CLOSED || stage == HttpResponseStage.WRITTEN; } @Deprecated public void print(byte[] bytes) throws IOException { write(bytes); } /** * Prints a single string of text to the buffered output * * @param var * string of text. * @throws IOException * if there was a problem with the output buffer. */ public void print(String var) throws IOException { if (var != null && !var.isEmpty()) write(var.getBytes(encoding)); } /** * Prints a single string of text with a line return to the buffered output * * @param var * string of text. * @throws IOException * if there was a problem with the output buffer. */ public void println(String var) throws IOException { if (var != null && !var.isEmpty()) write((var + "\n").getBytes(encoding)); } public void resetBuffer() { output = Unpooled.buffer(); } public void sendError(Exception e) throws IOException { if (e instanceof HttpError) sendError(((HttpError) e).getHttpCode(), ((HttpError) e).getReason(), ((HttpError) e).getMessage()); else sendError(500, e.getMessage()); } public void sendError(HttpResponseStatus status) throws IOException { sendError(status, null, null); } public void sendError(HttpResponseStatus status, String httpMsg) throws IOException { sendError(status, httpMsg, null); } public void sendError(HttpResponseStatus status, String httpMsg, String msg) throws IOException { if (stage == HttpResponseStage.CLOSED) throw new IllegalStateException( "You can't access sendError method within this HttpResponse because the connection has been closed."); if (httpMsg == null) httpMsg = status.reasonPhrase().toString(); // NetworkManager.getLogger().info( ConsoleColor.RED + "HttpError{httpCode=" + status.code() + ",httpMsg=" + httpMsg + ",subdomain=" + request.getSubDomain() + ",domain=" + request.getDomain() + ",uri=" + request.getUri() + // ",remoteIp=" + request.getIpAddr() + "}" ); if (msg == null || msg.length() > 100) log.log(Level.SEVERE, "%s {code=%s}", httpMsg, status.code()); else log.log(Level.SEVERE, "%s {code=%s,reason=%s}", httpMsg, status.code(), msg); resetBuffer(); // Trigger an internal Error Event to notify plugins of a possible problem. ErrorEvent event = new ErrorEvent(request, status.code(), httpMsg); EventBus.instance().callEvent(event); // TODO Make these error pages a bit more creative and/or informational to developers. if (event.getErrorHtml() != null && !event.getErrorHtml().isEmpty()) { print(event.getErrorHtml()); sendResponse(); } else { boolean printHtml = true; if (htaccess != null && htaccess.getErrorDocument(status.code()) != null) { String resp = htaccess.getErrorDocument(status.code()).getResponse(); if (resp.startsWith("/")) { sendRedirect(request.getBaseUrl() + resp); printHtml = false; } else if (resp.startsWith("http")) { sendRedirect(resp); printHtml = false; } else httpMsg = resp; } if (printHtml) { println("<html><head><title>" + status.code() + " - " + httpMsg + "</title></head><body>"); println("<h1>" + status.code() + " - " + httpMsg + "</h1>"); if (msg != null && !msg.isEmpty()) println("<p>" + msg + "</p>"); println("<hr>"); println("<small>Running <a href=\"https://github.com/ChioriGreene/ChioriWebServer\">" + Versioning.getProduct() + "</a> Version " + Versioning.getVersion() + " (Build #" + Versioning.getBuildNumber() + ")<br />" + Versioning.getCopyright() + "</small>"); println("</body></html>"); sendResponse(); } } } public void sendError(int httpCode) throws IOException { sendError(httpCode, null); } public void sendError(int httpCode, String httpMsg) throws IOException { sendError(httpCode, httpMsg, null); } public void sendError(int status, String httpMsg, String msg) throws IOException { if (status < 1) status = 500; sendError(HttpResponseStatus.valueOf(status), httpMsg, msg); } public void sendException(Throwable cause) throws IOException { if (stage == HttpResponseStage.CLOSED) throw new IllegalStateException( "You can't access sendException method within this HttpResponse because the connection has been closed."); if (cause instanceof HttpError) { sendError((HttpError) cause); return; } HttpExceptionEvent event = new HttpExceptionEvent(request, cause, AppConfig.get().getBoolean("server.developmentMode")); EventBus.instance().callEvent(event); int httpCode = event.getHttpCode(); if (httpCode < 1) httpCode = 500; httpStatus = HttpResponseStatus.valueOf(httpCode); // NetworkManager.getLogger().info( ConsoleColor.RED + "HttpError{httpCode=" + httpCode + ",httpMsg=" + HttpCode.msg( httpCode ) + ",domain=" + request.getSubDomain() + "." + request.getDomain() + ",uri=" + request.getUri() + // ",remoteIp=" + request.getIpAddr() + "}" ); if (Versioning.isDevelopment()) { if (event.getErrorHtml() != null) { log.log(Level.SEVERE, "%s {code=500}", HttpCode.msg(500)); resetBuffer(); print(event.getErrorHtml()); sendResponse(); } else { String stackTrace = ExceptionUtils.getStackTrace(cause); if (request.getEvalFactory() != null) for (Entry<String, ScriptingContext> e : request.getEvalFactory().stack() .getScriptTraceHistory().entrySet()) stackTrace = stackTrace.replace(e.getKey(), e.getValue().filename()); sendError(httpStatus, null, "<pre>" + stackTrace + "</pre>"); } } else { StringBuilder sb = new StringBuilder(); sb.append( "<p>The server encountered an exception and unforchantly the server is not in development mode, so no debug information is available.</p>\n"); sb.append( "<p>If you are the server owner or developer, you can turn development on by changing 'server.developmentMode' to true in the config file.</p>\n"); sendError(500, null, sb.toString()); } } /** * Sends the client to the site login page found in configuration and also sends a please login message along with it. */ public void sendLoginPage() { sendLoginPage("You must be logged in to view this page"); } /** * Sends the client to the site login page * * @param msg * The message to pass to the login page */ public void sendLoginPage(String msg) { sendLoginPage(msg, null); } /** * Sends the client to the site login page * * @param msg * The message to pass to the login page * @param level * The severity level of this login page redirect */ public void sendLoginPage(String msg, String level) { sendLoginPage(msg, level, null); } /** * Sends the client to the site login page * * @param msg * The message to pass to the login page * @param level * The severity level of this login page redirect * @param target * The target to redirect to once we receive a successful login */ public void sendLoginPage(String msg, String level, String target) { Nonce nonce = request.getSession().getNonce(); nonce.mapValues("msg", msg); nonce.mapValues("level", level == null || level.length() == 0 ? "danger" : level); nonce.mapValues("target", target == null || target.length() == 0 ? request.getFullUrl() : target); String loginForm = request.getLocation().getLoginForm(); if (!loginForm.toLowerCase().startsWith("http")) loginForm = (request.isSecure() ? "https://" : "http://") + loginForm; sendRedirect(String.format("%s?%s=%s", loginForm, nonce.key(), nonce.value())); } public void sendMultipart(byte[] bytesToWrite) throws IOException { if (request.method() == HttpMethod.HEAD) throw new IllegalStateException("You can't start MULTIPART mode on a HEAD Request."); if (stage != HttpResponseStage.MULTIPART) { stage = HttpResponseStage.MULTIPART; HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); HttpHeaders h = response.headers(); try { request.getSession().save(); } catch (SessionException e) { e.printStackTrace(); } for (HttpCookie c : request.getCookies()) if (c.needsUpdating()) h.add("Set-Cookie", c.toHeaderValue()); if (h.get("Server") == null) h.add("Server", Versioning.getProduct() + " Version " + Versioning.getVersion()); h.add("Access-Control-Allow-Origin", request.getLocation().getConfig().getString("site.web-allowed-origin", "*")); h.add("Connection", "close"); h.add("Cache-Control", "no-cache"); h.add("Cache-Control", "private"); h.add("Pragma", "no-cache"); h.set("Content-Type", "multipart/x-mixed-replace; boundary=--cwsframe"); // if ( isKeepAlive( request ) ) { // response.headers().set( CONNECTION, HttpHeaders.Values.KEEP_ALIVE ); } request.getChannel().write(response); } else { StringBuilder sb = new StringBuilder(); sb.append("--cwsframe\r\n"); sb.append("Content-Type: " + httpContentType + "\r\n"); sb.append("Content-Length: " + bytesToWrite.length + "\r\n\r\n"); ByteArrayOutputStream ba = new ByteArrayOutputStream(); ba.write(sb.toString().getBytes(encoding)); ba.write(bytesToWrite); ba.flush(); ChannelFuture sendFuture = request.getChannel().write( new ChunkedStream(new ByteArrayInputStream(ba.toByteArray())), request.getChannel().newProgressivePromise()); ba.close(); sendFuture.addListener(new ChannelProgressiveFutureListener() { @Override public void operationComplete(ChannelProgressiveFuture future) throws Exception { NetworkManager.getLogger().info("Transfer complete."); } @Override public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { if (total < 0) NetworkManager.getLogger().info("Transfer progress: " + progress); else NetworkManager.getLogger().info("Transfer progress: " + progress + " / " + total); } }); } } /** * Send the client to a specified page with http code 302 automatically. * * @param target * The destination URL. Can either be relative or absolute. */ public void sendRedirect(String target) { sendRedirect(target, 302); } /** * Sends the client to a specified page with specified http code but with the option to not automatically go. * * @param target * The destination url. Can be relative or absolute. * @param httpStatus * What http code to use. */ public void sendRedirect(String target, int httpStatus) { sendRedirect(target, httpStatus, null); } public void sendRedirect(String target, int httpStatus, Map<String, String> nonceValues) { // NetworkManager.getLogger().info( ConsoleColor.DARK_GRAY + "Sending page redirect to `" + target + "` using httpCode `" + httpStatus + " - " + HttpCode.msg( httpStatus ) + "`" ); log.log(Level.INFO, "Redirect {uri=%s,httpCode=%s,status=%s}", target, httpStatus, HttpCode.msg(httpStatus)); if (stage == HttpResponseStage.CLOSED) throw new IllegalStateException( "You can't access sendRedirect method within this HttpResponse because the connection has been closed."); if (nonceValues != null && nonceValues.size() > 0) { target += (target.contains("?") ? "&" : "?") + request.getSession().getNonce().query(); request.getSession().nonce().mapValues(nonceValues); } if (!isCommitted()) { setStatus(httpStatus); setHeader("Location", target); } else try { sendError(301, "The requested URL has been relocated to '" + target + "'"); } catch (IOException e) { e.printStackTrace(); } try { sendResponse(); } catch (IOException e) { e.printStackTrace(); } } public void sendRedirect(String target, Map<String, String> nonceValues) { sendRedirect(target, 302, nonceValues); } public void sendRedirectRepost(String target) { sendRedirect(target, request.getHttpVersion() == HttpVersion.HTTP_1_0 ? 302 : 307); } /** * Sends the data to the client. Internal Use. * * @throws IOException * if there was a problem sending the data, like the connection was unexpectedly closed. */ public void sendResponse() throws IOException { if (stage == HttpResponseStage.CLOSED || stage == HttpResponseStage.WRITTEN) return; FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpStatus, output); HttpHeaders h = response.headers(); if (request.hasSession()) { Session session = request.getSession(); /** * Initiate the Session Persistence Method. * This is usually done with a cookie but we should make a param optional */ session.processSessionCookie(request.getDomain()); for (HttpCookie c : session.getCookies().values()) if (c.needsUpdating()) h.add("Set-Cookie", c.toHeaderValue()); if (session.getSessionCookie().needsUpdating()) h.add("Set-Cookie", session.getSessionCookie().toHeaderValue()); } if (h.get("Server") == null) h.add("Server", Versioning.getProduct() + " Version " + Versioning.getVersion()); // This might be a temporary measure - TODO Properly set the charset for each request. h.set("Content-Type", httpContentType + "; charset=" + encoding.name()); h.add("Access-Control-Allow-Origin", request.getLocation().getConfig().getString("site.web-allowed-origin", "*")); for (Entry<String, String> header : headers.entrySet()) h.add(header.getKey(), header.getValue()); // Expires: Wed, 08 Apr 2015 02:32:24 GMT // DateTimeFormatter formatter = DateTimeFormat.forPattern( "EE, dd-MMM-yyyy HH:mm:ss zz" ); // h.set( HttpHeaders.Names.EXPIRES, formatter.print( DateTime.now( DateTimeZone.UTC ).plusDays( 1 ) ) ); // h.set( HttpHeaders.Names.CACHE_CONTROL, "public, max-age=86400" ); h.setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); stage = HttpResponseStage.WRITTEN; request.getChannel().writeAndFlush(response); } public void setAnnotation(String key, String val) { annotations.put(key, val); } public void setApacheParser(ApacheHandler htaccess) { this.htaccess = htaccess; } /** * Sets the ContentType header. * * @param type * e.g., text/html or application/xml */ public void setContentType(String type) { if (type == null || type.isEmpty()) type = "text/html"; httpContentType = type; } public void setEncoding(Charset encoding) { this.encoding = encoding; } public void setEncoding(String encoding) { this.encoding = Charset.forName(encoding); } public void setHeader(String key, String val) { headers.put(key, val); } public void setStatus(HttpResponseStatus httpStatus) { if (stage == HttpResponseStage.CLOSED) throw new IllegalStateException( "You can't access setStatus method within this HttpResponse because the connection has been closed."); this.httpStatus = httpStatus; } public void setStatus(int status) { setStatus(HttpResponseStatus.valueOf(status)); } /** * Redirects the current page load to a secure HTTPS connection */ public boolean switchToSecure() { if (!NetworkManager.isHttpsRunning()) { log.log(Level.SEVERE, "We were going to attempt to switch to a secure HTTPS connection and aborted due to the HTTPS server not running."); return false; } if (request.isSecure()) return true; sendRedirectRepost(request.getFullUrl(true) + request.getQuery()); return true; } /** * Redirects the current page load to an unsecure HTTP connection */ public boolean switchToUnsecure() { if (!NetworkManager.isHttpRunning()) { log.log(Level.SEVERE, "We were going to attempt to switch to an unsecure HTTP connection and aborted due to the HTTP server not running."); return false; } if (!request.isSecure()) return true; sendRedirectRepost(request.getFullUrl(false) + request.getQuery()); return true; } /** * Writes a byte array to the buffered output. * * @param bytes * byte array to print * @throws IOException * if there was a problem with the output buffer. */ public void write(byte[] bytes) throws IOException { if (stage != HttpResponseStage.MULTIPART) stage = HttpResponseStage.WRITTING; output.writeBytes(bytes); } /** * Writes a ByteBuf to the buffered output * * @param buf * byte buffer to print * @throws IOException * if there was a problem with the output buffer. */ public void write(ByteBuf buf) throws IOException { if (stage != HttpResponseStage.MULTIPART) stage = HttpResponseStage.WRITTING; output.writeBytes(buf.retain()); } }