private static HttpResponse addEtag( HttpRequest nettyRequest, HttpResponse httpResponse, File file) { if (Play.mode == Play.Mode.DEV) { httpResponse.setHeader(CACHE_CONTROL, "no-cache"); } else { String maxAge = Play.configuration.getProperty("http.cacheControl", "3600"); if (maxAge.equals("0")) { httpResponse.setHeader(CACHE_CONTROL, "no-cache"); } else { httpResponse.setHeader(CACHE_CONTROL, "max-age=" + maxAge); } } boolean useEtag = Play.configuration.getProperty("http.useETag", "true").equals("true"); long last = file.lastModified(); final String etag = "\"" + last + "-" + file.hashCode() + "\""; if (!isModified(etag, last, nettyRequest)) { if (nettyRequest.getMethod().equals(HttpMethod.GET)) { httpResponse.setStatus(HttpResponseStatus.NOT_MODIFIED); } if (useEtag) { httpResponse.setHeader(ETAG, etag); } } else { httpResponse.setHeader(LAST_MODIFIED, Utils.getHttpDateFormatter().format(new Date(last))); if (useEtag) { httpResponse.setHeader(ETAG, etag); } } return httpResponse; }
protected static void addToRequest(HttpRequest nettyRequest, Request request) { for (String key : nettyRequest.getHeaderNames()) { Http.Header hd = new Http.Header(); hd.name = key.toLowerCase(); hd.values = new ArrayList<String>(); for (String next : nettyRequest.getHeaders(key)) { hd.values.add(next); } request.headers.put(hd.name, hd); } String value = nettyRequest.getHeader(COOKIE); if (value != null) { Set<Cookie> cookies = new CookieDecoder().decode(value); if (cookies != null) { for (Cookie cookie : cookies) { Http.Cookie playCookie = new Http.Cookie(); playCookie.name = cookie.getName(); playCookie.path = cookie.getPath(); playCookie.domain = cookie.getDomain(); playCookie.secure = cookie.isSecure(); playCookie.value = cookie.getValue(); playCookie.httpOnly = cookie.isHttpOnly(); request.cookies.put(playCookie.name, playCookie); } } } }
public static boolean isModified(String etag, long last, HttpRequest nettyRequest) { if (nettyRequest.containsHeader(IF_NONE_MATCH)) { final String browserEtag = nettyRequest.getHeader(IF_NONE_MATCH); if (browserEtag.equals(etag)) { return false; } return true; } if (nettyRequest.containsHeader(IF_MODIFIED_SINCE)) { final String ifModifiedSince = nettyRequest.getHeader(IF_MODIFIED_SINCE); if (!StringUtils.isEmpty(ifModifiedSince)) { try { Date browserDate = Utils.getHttpDateFormatter().parse(ifModifiedSince); if (browserDate.getTime() >= last) { return false; } } catch (ParseException ex) { Logger.warn("Can't parse HTTP date", ex); } return true; } } return true; }
protected static void writeResponse( ChannelHandlerContext ctx, Response response, HttpResponse nettyResponse, HttpRequest nettyRequest) { Logger.trace("writeResponse: begin"); byte[] content = null; final boolean keepAlive = isKeepAlive(nettyRequest); if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) { content = new byte[0]; } else { content = response.out.toByteArray(); } ChannelBuffer buf = ChannelBuffers.copiedBuffer(content); nettyResponse.setContent(buf); if (keepAlive) { // Add 'Content-Length' header only for a keep-alive connection. Logger.trace("writeResponse: content length [" + response.out.size() + "]"); setContentLength(nettyResponse, response.out.size()); } ChannelFuture f = ctx.getChannel().write(nettyResponse); // Decide whether to close the connection or not. if (!keepAlive) { // Close the connection when the whole content is written out. f.addListener(ChannelFutureListener.CLOSE); } Logger.trace("writeResponse: end"); }
void saveExceededSizeError(HttpRequest nettyRequest, Request request, Response response) { String warning = nettyRequest.getHeader(HttpHeaders.Names.WARNING); String length = nettyRequest.getHeader(HttpHeaders.Names.CONTENT_LENGTH); if (warning != null) { Logger.trace("saveExceededSizeError: begin"); try { StringBuilder error = new StringBuilder(); error.append("\u0000"); // Cannot put warning which is play.netty.content.length.exceeded // as Key as it will result error when printing error error.append("play.netty.maxContentLength"); error.append(":"); String size = null; try { size = JavaExtensions.formatSize(Long.parseLong(length)); } catch (Exception e) { size = length + " bytes"; } error.append(Messages.get(warning, size)); error.append("\u0001"); error.append(size); error.append("\u0000"); if (request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS") != null && request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS").value != null) { error.append(request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS").value); } String errorData = URLEncoder.encode(error.toString(), "utf-8"); Http.Cookie c = new Http.Cookie(); c.value = errorData; c.name = Scope.COOKIE_PREFIX + "_ERRORS"; request.cookies.put(Scope.COOKIE_PREFIX + "_ERRORS", c); Logger.trace("saveExceededSizeError: end"); } catch (Exception e) { throw new UnexpectedException("Error serialization problem", e); } } }
@Override public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) throws Exception { Logger.trace("messageReceived: begin"); final Object msg = e.getMessage(); // Http request if (msg instanceof HttpRequest) { final HttpRequest nettyRequest = (HttpRequest) msg; // Websocket upgrade if (HttpHeaders.Values.UPGRADE.equalsIgnoreCase(nettyRequest.getHeader(CONNECTION)) && HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase( nettyRequest.getHeader(HttpHeaders.Names.UPGRADE))) { websocketHandshake(ctx, nettyRequest, e); return; } // Plain old HttpRequest try { final Request request = parseRequest(ctx, nettyRequest); final Response response = new Response(); Http.Response.current.set(response); // Buffered in memory output response.out = new ByteArrayOutputStream(); // Direct output (will be set later) response.direct = null; // Streamed output (using response.writeChunk) response.onWriteChunk( new Action<Object>() { public void invoke(Object result) { writeChunk(request, response, ctx, nettyRequest, result); } }); // Raw invocation boolean raw = false; for (PlayPlugin plugin : Play.plugins) { if (plugin.rawInvocation(request, response)) { raw = true; break; } } if (raw) { copyResponse(ctx, request, response, nettyRequest); } else { // Deleguate to Play framework Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e)); } } catch (Exception ex) { serve500(ex, ctx, nettyRequest); } } // Websocket frame if (msg instanceof WebSocketFrame) { WebSocketFrame frame = (WebSocketFrame) msg; websocketFrameReceived(ctx, frame); } Logger.trace("messageReceived: end"); }
public void serveStatic( RenderStatic renderStatic, ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest, MessageEvent e) { Logger.trace("serveStatic: begin"); HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status)); if (exposePlayServer) { nettyResponse.setHeader(SERVER, signature); } try { VirtualFile file = Play.getVirtualFile(renderStatic.file); if (file != null && file.exists() && file.isDirectory()) { file = file.child("index.html"); if (file != null) { renderStatic.file = file.relativePath(); } } if ((file == null || !file.exists())) { serve404( new NotFound("The file " + renderStatic.file + " does not exist"), ctx, request, nettyRequest); } else { boolean raw = false; for (PlayPlugin plugin : Play.plugins) { if (plugin.serveStatic(file, Request.current(), Response.current())) { raw = true; break; } } if (raw) { copyResponse(ctx, request, response, nettyRequest); } else { final File localFile = file.getRealFile(); final boolean keepAlive = isKeepAlive(nettyRequest); nettyResponse = addEtag(nettyRequest, nettyResponse, localFile); if (nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { Channel ch = e.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); if (!keepAlive) { // Write the content. writeFuture.addListener(ChannelFutureListener.CLOSE); } } else { final RandomAccessFile raf = new RandomAccessFile(localFile, "r"); try { long fileLength = raf.length(); Logger.trace("keep alive " + keepAlive); Logger.trace( "content type " + (MimeTypes.getContentType(localFile.getName(), "text/plain"))); if (keepAlive && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { // Add 'Content-Length' header only for a keep-alive connection. Logger.trace("file length " + fileLength); setContentLength(nettyResponse, fileLength); } nettyResponse.setHeader( CONTENT_TYPE, (MimeTypes.getContentType(localFile.getName(), "text/plain"))); Channel ch = e.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); // Write the content. if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) { writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192)); } else { raf.close(); } if (!keepAlive) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } catch (Throwable exx) { try { raf.close(); } catch (Throwable ex) { /* Left empty */ } try { ctx.getChannel().close(); } catch (Throwable ex) { /* Left empty */ } } } } } } catch (Throwable ez) { Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url); try { HttpResponse errorResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR); ChannelBuffer buf = ChannelBuffers.copiedBuffer("Internal Error (check logs)".getBytes("utf-8")); errorResponse.setContent(buf); ChannelFuture future = ctx.getChannel().write(errorResponse); future.addListener(ChannelFutureListener.CLOSE); } catch (Exception ex) { Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url); } } Logger.trace("serveStatic: end"); }
public Request parseRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest) throws Exception { Logger.trace("parseRequest: begin"); Logger.trace("parseRequest: URI = " + nettyRequest.getUri()); int index = nettyRequest.getUri().indexOf("?"); String querystring = ""; String uri = nettyRequest.getUri(); // Remove domain and port from URI if it's present. if (uri.startsWith("http://") || uri.startsWith("https://")) { // Begins searching / after 9th character (last / of https://) uri = uri.substring(uri.indexOf("/", 9)); } String path = URLDecoder.decode(uri, "UTF-8"); if (index != -1) { path = URLDecoder.decode(uri.substring(0, index), "UTF-8"); querystring = uri.substring(index + 1); } final Request request = new Request(); request.remoteAddress = getRemoteIPAddress(ctx); request.method = nettyRequest.getMethod().getName(); request.path = path; request.querystring = querystring; final String contentType = nettyRequest.getHeader(CONTENT_TYPE); if (contentType != null) { request.contentType = contentType.split(";")[0].trim().toLowerCase(); } else { request.contentType = "text/html"; } if (nettyRequest.getHeader("X-HTTP-Method-Override") != null) { request.method = nettyRequest.getHeader("X-HTTP-Method-Override").intern(); } ChannelBuffer b = nettyRequest.getContent(); if (b instanceof FileChannelBuffer) { FileChannelBuffer buffer = (FileChannelBuffer) b; // An error occurred Integer max = Integer.valueOf(Play.configuration.getProperty("play.netty.maxContentLength", "-1")); request.body = buffer.getInputStream(); if (!(max == -1 || request.body.available() < max)) { request.body = new ByteArrayInputStream(new byte[0]); } } else { ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.copy(new ChannelBufferInputStream(b), out); byte[] n = out.toByteArray(); request.body = new ByteArrayInputStream(n); } request.url = uri; request.host = nettyRequest.getHeader(HOST); request.isLoopback = ((InetSocketAddress) ctx.getChannel().getRemoteAddress()).getAddress().isLoopbackAddress() && request.host.matches("^127\\.0\\.0\\.1:?[0-9]*$"); if (request.host == null) { request.host = ""; request.port = 80; request.domain = ""; } else { if (request.host.contains(":")) { final String[] host = request.host.split(":"); request.port = Integer.parseInt(host[1]); request.domain = host[0]; } else { request.port = 80; request.domain = request.host; } } if (Play.configuration.containsKey("XForwardedSupport") && nettyRequest.getHeader("X-Forwarded-For") != null) { if (!Arrays.asList( Play.configuration.getProperty("XForwardedSupport", "127.0.0.1").split(",")) .contains(request.remoteAddress)) { throw new RuntimeException( "This proxy request is not authorized: " + request.remoteAddress); } else { request.secure = ("https".equals(Play.configuration.get("XForwardedProto")) || "https".equals(nettyRequest.getHeader("X-Forwarded-Proto")) || "on".equals(nettyRequest.getHeader("X-Forwarded-Ssl"))); if (Play.configuration.containsKey("XForwardedHost")) { request.host = (String) Play.configuration.get("XForwardedHost"); } else if (nettyRequest.getHeader("X-Forwarded-Host") != null) { request.host = nettyRequest.getHeader("X-Forwarded-Host"); } if (nettyRequest.getHeader("X-Forwarded-For") != null) { request.remoteAddress = nettyRequest.getHeader("X-Forwarded-For"); } } } addToRequest(nettyRequest, request); request.resolveFormat(); request._init(); Logger.trace("parseRequest: end"); return request; }
public void copyResponse( ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest) throws Exception { Logger.trace("copyResponse: begin"); // response.out.flush(); // Decide whether to close the connection or not. HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status)); if (exposePlayServer) { nettyResponse.setHeader(SERVER, signature); } if (response.contentType != null) { nettyResponse.setHeader( CONTENT_TYPE, response.contentType + (response.contentType.startsWith("text/") && !response.contentType.contains("charset") ? "; charset=utf-8" : "")); } else { nettyResponse.setHeader(CONTENT_TYPE, "text/plain; charset=utf-8"); } addToResponse(response, nettyResponse); final Object obj = response.direct; File file = null; ChunkedInput stream = null; InputStream is = null; if (obj instanceof File) { file = (File) obj; } else if (obj instanceof InputStream) { is = (InputStream) obj; } else if (obj instanceof ChunkedInput) { stream = (ChunkedInput) obj; } final boolean keepAlive = isKeepAlive(nettyRequest); if (file != null && file.isFile()) { try { nettyResponse = addEtag(nettyRequest, nettyResponse, file); if (nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { Channel ch = ctx.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); if (!keepAlive) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } else { nettyResponse.setHeader( CONTENT_TYPE, MimeTypes.getContentType(file.getName(), "text/plain")); final RandomAccessFile raf = new RandomAccessFile(file, "r"); try { long fileLength = raf.length(); if (keepAlive) { // Add 'Content-Length' header only for a keep-alive connection. Logger.trace("file length is [" + fileLength + "]"); setContentLength(nettyResponse, fileLength); } Channel ch = ctx.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); // Write the content. // If it is not a HEAD if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) { writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192)); } else { raf.close(); } if (!keepAlive) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } catch (Throwable exx) { try { raf.close(); } catch (Throwable ex) { /* Left empty */ } try { ctx.getChannel().close(); } catch (Throwable ex) { /* Left empty */ } } } } catch (Exception e) { throw e; } } else if (is != null) { ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); if (!nettyRequest.getMethod().equals(HttpMethod.HEAD) && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { writeFuture = ctx.getChannel().write(new ChunkedStream(is)); } else { is.close(); } if (!keepAlive) { writeFuture.addListener(ChannelFutureListener.CLOSE); } } else if (stream != null) { ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); if (!nettyRequest.getMethod().equals(HttpMethod.HEAD) && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { writeFuture = ctx.getChannel().write(stream); } else { stream.close(); } if (!keepAlive) { writeFuture.addListener(ChannelFutureListener.CLOSE); } } else { writeResponse(ctx, response, nettyResponse, nettyRequest); } Logger.trace("copyResponse: end"); }
private void websocketHandshake(final ChannelHandlerContext ctx, HttpRequest req, MessageEvent e) throws Exception { // Create the WebSocket handshake response. HttpResponse res = new DefaultHttpResponse( HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake")); res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET); res.addHeader(CONNECTION, HttpHeaders.Values.UPGRADE); // Fill in the headers and contents depending on handshake method. if (req.containsHeader(SEC_WEBSOCKET_KEY1) && req.containsHeader(SEC_WEBSOCKET_KEY2)) { // New handshake method with a challenge: res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN)); res.addHeader( SEC_WEBSOCKET_LOCATION, "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri()); String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL); if (protocol != null) { res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol); } // Calculate the answer of the challenge. String key1 = req.getHeader(SEC_WEBSOCKET_KEY1); String key2 = req.getHeader(SEC_WEBSOCKET_KEY2); int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length()); int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length()); long c = req.getContent().readLong(); ChannelBuffer input = ChannelBuffers.buffer(16); input.writeInt(a); input.writeInt(b); input.writeLong(c); try { ChannelBuffer output = ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array())); res.setContent(output); } catch (NoSuchAlgorithmException ex) { throw new UnexpectedException(ex); } } else { // Old handshake method with no challenge: res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN)); res.addHeader( WEBSOCKET_LOCATION, "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri()); String protocol = req.getHeader(WEBSOCKET_PROTOCOL); if (protocol != null) { res.addHeader(WEBSOCKET_PROTOCOL, protocol); } } // Keep the original request Http.Request request = parseRequest(ctx, req); // Route the websocket request request.method = "WS"; Map<String, String> route = Router.route(request.method, request.path); if (!route.containsKey("action")) { // No route found to handle this websocket connection ctx.getChannel() .write(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)); return; } // Upgrade the connection and send the handshake response. ChannelPipeline p = ctx.getChannel().getPipeline(); p.remove("aggregator"); p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder()); // Connect ctx.getChannel().write(res); p.replace("encoder", "wsencoder", new WebSocketFrameEncoder()); req.setMethod(new HttpMethod("WEBSOCKET")); // Inbound Http.Inbound inbound = new Http.Inbound() { @Override public boolean isOpen() { return ctx.getChannel().isOpen(); } }; channels.put(ctx, inbound); // Outbound Http.Outbound outbound = new Http.Outbound() { final List<ChannelFuture> writeFutures = Collections.synchronizedList(new ArrayList<ChannelFuture>()); Promise<Void> closeTask; synchronized void writeAndClose(ChannelFuture writeFuture) { if (!writeFuture.isDone()) { writeFutures.add(writeFuture); writeFuture.addListener( new ChannelFutureListener() { public void operationComplete(ChannelFuture cf) throws Exception { writeFutures.remove(cf); futureClose(); } }); } } void futureClose() { if (closeTask != null && writeFutures.isEmpty()) { closeTask.invoke(null); } } @Override public void send(String data) { if (!isOpen()) { throw new IllegalStateException("The outbound channel is closed"); } writeAndClose(ctx.getChannel().write(new DefaultWebSocketFrame(data))); } @Override public void send(byte opcode, byte[] data, int offset, int length) { if (!isOpen()) { throw new IllegalStateException("The outbound channel is closed"); } writeAndClose( ctx.getChannel() .write(new DefaultWebSocketFrame(opcode, wrappedBuffer(data, offset, length)))); } @Override public synchronized boolean isOpen() { return ctx.getChannel().isOpen() && closeTask == null; } @Override public synchronized void close() { closeTask = new Promise<Void>(); closeTask.onRedeem( new Action<Promise<Void>>() { public void invoke(Promise<Void> completed) { writeFutures.clear(); ctx.getChannel().disconnect(); closeTask = null; } }); futureClose(); } }; Invoker.invoke(new WebSocketInvocation(route, request, inbound, outbound, ctx, e)); }