/* * Copyright (c) 2011-2013 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.vertx.core.http.impl; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2Headers; import io.vertx.codegen.annotations.Nullable; import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.StreamResetException; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; /** * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> */ public class Http2ServerResponseImpl implements HttpServerResponse { private static final Logger log = LoggerFactory.getLogger(Http2ServerResponseImpl.class); private final VertxHttp2Stream stream; private final ChannelHandlerContext ctx; private final Http2ServerConnection conn; private final boolean push; private final Object metric; private final String host; private Http2Headers headers = new DefaultHttp2Headers(); private Http2HeadersAdaptor headersMap; private Http2Headers trailers; private Http2HeadersAdaptor trailedMap; private boolean chunked; private boolean headWritten; private boolean ended; private int statusCode = 200; private String statusMessage; // Not really used but we keep the message for the getStatusMessage() private Handler<Void> drainHandler; private Handler<Throwable> exceptionHandler; private Handler<Void> headersEndHandler; private Handler<Void> bodyEndHandler; private Handler<Void> closeHandler; private Handler<Void> endHandler; private long bytesWritten; private int numPush; private boolean inHandler; public Http2ServerResponseImpl(Http2ServerConnection conn, VertxHttp2Stream stream, Object metric, boolean push, String contentEncoding, String host) { this.metric = metric; this.stream = stream; this.ctx = conn.handlerContext; this.conn = conn; this.push = push; this.host = host; if (contentEncoding != null) { putHeader(HttpHeaderNames.CONTENT_ENCODING, contentEncoding); } } public Http2ServerResponseImpl( Http2ServerConnection conn, VertxHttp2Stream stream, HttpMethod method, String path, boolean push, String contentEncoding) { this.stream = stream; this.ctx = conn.handlerContext; this.conn = conn; this.push = push; this.host = null; if (contentEncoding != null) { putHeader(HttpHeaderNames.CONTENT_ENCODING, contentEncoding); } this.metric = conn.metrics().responsePushed(conn.metric(), method, path, this); } synchronized void beginRequest() { inHandler = true; } synchronized boolean endRequest() { inHandler = false; return numPush > 0; } void callReset(long code) { handleEnded(true); handleError(new StreamResetException(code)); } void handleError(Throwable cause) { if (exceptionHandler != null) { exceptionHandler.handle(cause); } } void handleClose() { handleEnded(true); } private void checkHeadWritten() { if (headWritten) { throw new IllegalStateException("Header already sent"); } } @Override public HttpServerResponse exceptionHandler(Handler<Throwable> handler) { synchronized (conn) { checkEnded(); exceptionHandler = handler; return this; } } @Override public int getStatusCode() { synchronized (conn) { return statusCode; } } @Override public HttpServerResponse setStatusCode(int statusCode) { if (statusCode < 0) { throw new IllegalArgumentException("code: " + statusCode + " (expected: 0+)"); } synchronized (conn) { checkHeadWritten(); this.statusCode = statusCode; return this; } } @Override public String getStatusMessage() { synchronized (conn) { if (statusMessage == null) { return HttpResponseStatus.valueOf(statusCode).reasonPhrase(); } return statusMessage; } } @Override public HttpServerResponse setStatusMessage(String statusMessage) { synchronized (conn) { checkHeadWritten(); this.statusMessage = statusMessage; return this; } } @Override public HttpServerResponse setChunked(boolean chunked) { synchronized (conn) { checkHeadWritten(); this.chunked = true; return this; } } @Override public boolean isChunked() { synchronized (conn) { return chunked; } } @Override public MultiMap headers() { synchronized (conn) { if (headersMap == null) { headersMap = new Http2HeadersAdaptor(headers); } return headersMap; } } @Override public HttpServerResponse putHeader(String name, String value) { synchronized (conn) { checkHeadWritten(); headers().set(name, value); return this; } } @Override public HttpServerResponse putHeader(CharSequence name, CharSequence value) { synchronized (conn) { checkHeadWritten(); headers().set(name, value); return this; } } @Override public HttpServerResponse putHeader(String name, Iterable<String> values) { synchronized (conn) { checkHeadWritten(); headers().set(name, values); return this; } } @Override public HttpServerResponse putHeader(CharSequence name, Iterable<CharSequence> values) { synchronized (conn) { checkHeadWritten(); headers().set(name, values); return this; } } @Override public MultiMap trailers() { synchronized (conn) { if (trailedMap == null) { trailedMap = new Http2HeadersAdaptor(trailers = new DefaultHttp2Headers()); } return trailedMap; } } @Override public HttpServerResponse putTrailer(String name, String value) { synchronized (conn) { checkEnded(); trailers().set(name, value); return this; } } @Override public HttpServerResponse putTrailer(CharSequence name, CharSequence value) { synchronized (conn) { checkEnded(); trailers().set(name, value); return this; } } @Override public HttpServerResponse putTrailer(String name, Iterable<String> values) { synchronized (conn) { checkEnded(); trailers().set(name, values); return this; } } @Override public HttpServerResponse putTrailer(CharSequence name, Iterable<CharSequence> value) { synchronized (conn) { checkEnded(); trailers().set(name, value); return this; } } @Override public HttpServerResponse closeHandler(Handler<Void> handler) { synchronized (conn) { checkEnded(); closeHandler = handler; return this; } } @Override public HttpServerResponse endHandler(@Nullable Handler<Void> handler) { synchronized (conn) { checkEnded(); endHandler = handler; return this; } } @Override public HttpServerResponse writeContinue() { synchronized (conn) { checkHeadWritten(); stream.writeHeaders(new DefaultHttp2Headers().status("100"), false); ctx.flush(); return this; } } @Override public HttpServerResponse write(Buffer chunk) { ByteBuf buf = chunk.getByteBuf(); return write(buf); } @Override public HttpServerResponse write(String chunk, String enc) { return write(Buffer.buffer(chunk, enc).getByteBuf()); } @Override public HttpServerResponse write(String chunk) { return write(Buffer.buffer(chunk).getByteBuf()); } private Http2ServerResponseImpl write(ByteBuf chunk) { write(chunk, false); return this; } @Override public void end(String chunk) { end(Buffer.buffer(chunk)); } @Override public void end(String chunk, String enc) { end(Buffer.buffer(chunk, enc)); } @Override public void end(Buffer chunk) { end(chunk.getByteBuf()); } @Override public void end() { end((ByteBuf) null); } void toNetSocket() { checkEnded(); checkSendHeaders(false); handleEnded(false); } private void end(ByteBuf chunk) { synchronized (conn) { if (chunk != null && !headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { headers().set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(chunk.readableBytes())); } write(chunk, true); } } private boolean checkSendHeaders(boolean end) { if (!headWritten) { if (headersEndHandler != null) { headersEndHandler.handle(null); } headWritten = true; headers.status(Integer.toString(statusCode)); stream.writeHeaders(headers, end); if (end) { ctx.flush(); } return true; } else { return false; } } void write(ByteBuf chunk, boolean end) { synchronized (conn) { checkEnded(); if (end) { handleEnded(false); } boolean hasBody = chunk != null; boolean sent = checkSendHeaders(end && !hasBody && trailers == null); if (hasBody || (!sent && end)) { if (chunk == null) { chunk = Unpooled.EMPTY_BUFFER; } int len = chunk.readableBytes(); stream.writeData(chunk, end && trailers == null); bytesWritten += len; } if (end && trailers != null) { stream.writeHeaders(trailers, true); } if (end && bodyEndHandler != null) { bodyEndHandler.handle(null); } } } @Override public HttpServerResponse writeCustomFrame(int type, int flags, Buffer payload) { synchronized (conn) { checkEnded(); checkSendHeaders(false); stream.writeFrame(type, flags, payload.getByteBuf()); ctx.flush(); return this; } } private void checkEnded() { if (ended) { throw new IllegalStateException("Response has already been written"); } } private void handleEnded(boolean failed) { if (!ended) { ended = true; if (metric != null) { // Null in case of push response : handle this case if (failed) { conn.metrics().requestReset(metric); } else { conn.reportBytesWritten(bytesWritten); conn.metrics().responseEnd(metric, this); } } if (endHandler != null) { conn.getContext().runOnContext(endHandler); } if (closeHandler != null) { conn.getContext().runOnContext(closeHandler); } } } void writabilityChanged() { if (!ended && !writeQueueFull() && drainHandler != null) { drainHandler.handle(null); } } @Override public boolean writeQueueFull() { synchronized (conn) { checkEnded(); return stream.isNotWritable(); } } @Override public HttpServerResponse setWriteQueueMaxSize(int maxSize) { synchronized (conn) { checkEnded(); // It does not seem to be possible to configure this at the moment } return this; } @Override public HttpServerResponse drainHandler(Handler<Void> handler) { synchronized (conn) { checkEnded(); drainHandler = handler; return this; } } @Override public HttpServerResponse sendFile(String filename, long offset, long length) { return sendFile(filename, offset, length, null); } @Override public HttpServerResponse sendFile(String filename, long offset, long length, Handler<AsyncResult<Void>> resultHandler) { synchronized (conn) { checkEnded(); Context resultCtx = resultHandler != null ? stream.vertx.getOrCreateContext() : null; File file = stream.vertx.resolveFile(filename); if (!file.exists()) { if (resultHandler != null) { resultCtx.runOnContext((v) -> resultHandler.handle(Future.failedFuture(new FileNotFoundException()))); } else { log.error("File not found: " + filename); } return this; } RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (IOException e) { if (resultHandler != null) { resultCtx.runOnContext((v) -> resultHandler.handle(Future.failedFuture(e))); } else { log.error("Failed to send file", e); } return this; } long contentLength = Math.min(length, file.length() - offset); if (headers.get(HttpHeaderNames.CONTENT_LENGTH) == null) { putHeader(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(contentLength)); } if (headers.get(HttpHeaderNames.CONTENT_TYPE) == null) { String contentType = MimeMapping.getMimeTypeForFilename(filename); if (contentType != null) { putHeader(HttpHeaderNames.CONTENT_TYPE, contentType); } } checkSendHeaders(false); FileStreamChannel fileChannel = new FileStreamChannel(ar -> { if (ar.succeeded()) { bytesWritten += ar.result(); end(); } if (resultHandler != null) { resultCtx.runOnContext(v -> { resultHandler.handle(Future.succeededFuture()); }); } }, stream, offset, contentLength); drainHandler(fileChannel.drainHandler); ctx.channel().eventLoop().register(fileChannel); fileChannel.pipeline().fireUserEventTriggered(raf); } return this; } @Override public void close() { conn.close(); } @Override public boolean ended() { synchronized (conn) { return ended; } } @Override public boolean closed() { return conn.isClosed(); } @Override public boolean headWritten() { synchronized (conn) { return headWritten; } } @Override public HttpServerResponse headersEndHandler(@Nullable Handler<Void> handler) { synchronized (conn) { headersEndHandler = handler; return this; } } @Override public HttpServerResponse bodyEndHandler(@Nullable Handler<Void> handler) { synchronized (conn) { bodyEndHandler = handler; return this; } } @Override public long bytesWritten() { synchronized (conn) { return bytesWritten; } } @Override public int streamId() { return stream.id(); } @Override public void reset(long code) { synchronized (conn) { checkEnded(); handleEnded(true); stream.writeReset(code); ctx.flush(); } } @Override public HttpServerResponse push(HttpMethod method, String host, String path, Handler<AsyncResult<HttpServerResponse>> handler) { return push(method, host, path, null, handler); } @Override public HttpServerResponse push(HttpMethod method, String path, MultiMap headers, Handler<AsyncResult<HttpServerResponse>> handler) { return push(method, null, path, headers, handler); } @Override public HttpServerResponse push(HttpMethod method, String host, String path, MultiMap headers, Handler<AsyncResult<HttpServerResponse>> handler) { synchronized (conn) { if (push) { throw new IllegalStateException("A push response cannot promise another push"); } checkEnded(); conn.sendPush(stream.id(), host, method, headers, path, handler); if (!inHandler) { ctx.flush(); } numPush++; return this; } } @Override public HttpServerResponse push(HttpMethod method, String path, Handler<AsyncResult<HttpServerResponse>> handler) { return push(method, host, path, handler); } }