/*
* 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.Channel;
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.Http2Connection;
import io.netty.handler.codec.http2.Http2Error;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Stream;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.CaseInsensitiveHeaders;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.StreamResetException;
import io.vertx.core.impl.ContextImpl;
import io.vertx.core.net.NetSocket;
import io.vertx.core.spi.metrics.HttpClientMetrics;
import io.vertx.core.spi.metrics.NetworkMetrics;
import java.util.Map;
import static io.vertx.core.http.HttpHeaders.DEFLATE_GZIP;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
class Http2ClientConnection extends Http2ConnectionBase implements HttpClientConnection {
final Http2Pool http2Pool;
final HttpClientMetrics metrics;
final Object queueMetric;
int streamCount;
public Http2ClientConnection(Http2Pool http2Pool,
Object queueMetric,
ContextImpl context,
Channel channel,
VertxHttp2ConnectionHandler connHandler,
HttpClientMetrics metrics) {
super(channel, context, connHandler);
this.http2Pool = http2Pool;
this.metrics = metrics;
this.queueMetric = queueMetric;
}
@Override
public HttpClientMetrics metrics() {
return metrics;
}
@Override
void onGoAwaySent(int lastStreamId, long errorCode, ByteBuf debugData) {
http2Pool.discard(Http2ClientConnection.this);
}
@Override
void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData) {
super.onGoAwayReceived(lastStreamId, errorCode, debugData);
http2Pool.discard(Http2ClientConnection.this);
}
@Override
void onStreamClosed(Http2Stream nettyStream) {
super.onStreamClosed(nettyStream);
http2Pool.recycle(Http2ClientConnection.this);
}
synchronized HttpClientStream createStream() throws Http2Exception {
Http2Connection conn = handler.connection();
Http2Stream stream = conn.local().createStream(conn.local().incrementAndGetNextStreamId(), false);
boolean writable = handler.encoder().flowController().isWritable(stream);
Http2ClientStream clientStream = new Http2ClientStream(this, stream, writable);
streams.put(clientStream.stream.id(), clientStream);
return clientStream;
}
@Override
public synchronized void handleClosed() {
http2Pool.discard(this);
super.handleClosed();
}
@Override
public boolean isValid() {
Http2Connection conn = handler.connection();
return !isClosed() && !conn.goAwaySent() && !conn.goAwayReceived();
}
@Override
public synchronized void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endOfStream) throws Http2Exception {
Http2ClientStream stream = (Http2ClientStream) streams.get(streamId);
if (stream != null) {
context.executeFromIO(() -> {
stream.handleHeaders(headers, endOfStream);
});
}
}
@Override
public synchronized void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception {
Http2ClientStream stream = (Http2ClientStream) streams.get(streamId);
if (stream != null) {
Handler<HttpClientRequest> pushHandler = stream.pushHandler();
if (pushHandler != null) {
context.executeFromIO(() -> {
String rawMethod = headers.method().toString();
HttpMethod method = HttpUtils.toVertxMethod(rawMethod);
String uri = headers.path().toString();
String host = headers.authority() != null ? headers.authority().toString() : null;
MultiMap headersMap = new Http2HeadersAdaptor(headers);
Http2Stream promisedStream = handler.connection().stream(promisedStreamId);
int port = remoteAddress().port();
HttpClientRequestPushPromise pushReq = new HttpClientRequestPushPromise(this, promisedStream, http2Pool.client, isSsl(), method, rawMethod, uri, host, port, headersMap);
if (metrics.isEnabled()) {
pushReq.metric(metrics.responsePushed(queueMetric, metric(), localAddress(), remoteAddress(), pushReq));
}
streams.put(promisedStreamId, pushReq.getStream());
pushHandler.handle(pushReq);
});
return;
}
}
handler.writeReset(promisedStreamId, Http2Error.CANCEL.code());
}
static class Http2ClientStream extends VertxHttp2Stream<Http2ClientConnection> implements HttpClientStream {
private HttpClientRequestBase request;
private HttpClientResponseImpl response;
private boolean requestEnded;
private boolean responseEnded;
public Http2ClientStream(Http2ClientConnection conn, Http2Stream stream, boolean writable) throws Http2Exception {
this(conn, null, stream, writable);
}
public Http2ClientStream(Http2ClientConnection conn, HttpClientRequestBase request, Http2Stream stream, boolean writable) throws Http2Exception {
super(conn, stream, writable);
this.request = request;
}
@Override
public HttpVersion version() {
return HttpVersion.HTTP_2;
}
@Override
public int id() {
return super.id();
}
@Override
void handleEnd(MultiMap trailers) {
if (conn.metrics.isEnabled()) {
if (request.exceptionOccurred != null) {
conn.metrics.requestReset(request.metric());
} else {
conn.metrics.responseEnd(request.metric(), response);
}
}
responseEnded = true;
// Should use a shared immutable object for CaseInsensitiveHeaders ?
if (trailers == null) {
trailers = new CaseInsensitiveHeaders();
}
response.handleEnd(null, trailers);
}
@Override
void handleData(Buffer buf) {
response.handleChunk(buf);
}
@Override
void handleReset(long errorCode) {
if (responseEnded) {
return;
}
responseEnded = true;
if (conn.metrics.isEnabled()) {
conn.metrics.requestReset(request.metric());
}
handleException(new StreamResetException(errorCode));
}
@Override
void handleClose() {
if (!responseEnded) {
responseEnded = true;
if (conn.metrics.isEnabled()) {
conn.metrics.requestReset(request.metric());
}
handleException(new VertxException("Connection was closed")); // Put that in utility class
}
}
@Override
public void checkDrained() {
synchronized (conn) {
handleInterestedOpsChanged();
}
}
@Override
void handleInterestedOpsChanged() {
if (request instanceof HttpClientRequestImpl && !isNotWritable()) {
if (!isNotWritable()) {
((HttpClientRequestImpl) request).handleDrained();
}
}
}
@Override
void handleCustomFrame(int type, int flags, Buffer buff) {
response.handleUnknowFrame(new HttpFrameImpl(type, flags, buff));
}
void handleHeaders(Http2Headers headers, boolean end) {
if (response == null || response.statusCode() == 100) {
int status;
String statusMessage;
try {
status = Integer.parseInt(headers.status().toString());
statusMessage = HttpResponseStatus.valueOf(status).reasonPhrase();
} catch (Exception e) {
handleException(e);
writeReset(0x01 /* PROTOCOL_ERROR */);
return;
}
response = new HttpClientResponseImpl(
request,
HttpVersion.HTTP_2,
this,
status,
statusMessage,
new Http2HeadersAdaptor(headers)
);
if (conn.metrics.isEnabled()) {
conn.metrics.responseBegin(request.metric(), response);
}
request.handleResponse(response);
if (end) {
onEnd();
}
} else if (end) {
onEnd(new Http2HeadersAdaptor(headers));
}
}
void handleException(Throwable exception) {
if (!requestEnded || response == null) {
context.executeFromIO(() -> {
request.handleException(exception);
});
}
if (response != null) {
context.executeFromIO(() -> {
response.handleException(exception);
});
}
}
Handler<HttpClientRequest> pushHandler() {
return ((HttpClientRequestImpl) request).pushHandler();
}
@Override
public void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked) {
writeHeadWithContent(method, rawMethod, uri, headers, hostHeader, chunked, null, false);
}
@Override
public void writeHeadWithContent(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf content, boolean end) {
Http2Headers h = new DefaultHttp2Headers();
h.method(method != HttpMethod.OTHER ? method.name() : rawMethod);
if (method == HttpMethod.CONNECT) {
if (hostHeader == null) {
throw new IllegalArgumentException("Missing :authority / host header");
}
h.authority(hostHeader);
} else {
h.path(uri);
h.scheme("https");
if (hostHeader != null) {
h.authority(hostHeader);
}
}
if (headers != null && headers.size() > 0) {
for (Map.Entry<String, String> header : headers) {
h.add(Http2HeadersAdaptor.toLowerCase(header.getKey()), header.getValue());
}
}
if (conn.http2Pool.client.getOptions().isTryUseCompression() && h.get(HttpHeaderNames.ACCEPT_ENCODING) == null) {
h.set(HttpHeaderNames.ACCEPT_ENCODING, DEFLATE_GZIP);
}
if (conn.metrics.isEnabled()) {
request.metric(conn.metrics.requestBegin(conn.queueMetric, conn.metric(), conn.localAddress(), conn.remoteAddress(), request));
}
writeHeaders(h, end && content == null);
if (content != null) {
writeBuffer(content, end);
} else {
handlerContext.flush();
}
}
@Override
public void writeBuffer(ByteBuf buf, boolean end) {
if (buf == null && end) {
buf = Unpooled.EMPTY_BUFFER;
}
if (buf != null) {
writeData(buf, end);
}
if (end) {
handlerContext.flush();
}
}
@Override
public void writeFrame(int type, int flags, ByteBuf payload) {
super.writeFrame(type, flags, payload);
}
@Override
public Context getContext() {
return context;
}
@Override
public void doSetWriteQueueMaxSize(int size) {
}
@Override
public boolean isNotWritable() {
return super.isNotWritable();
}
@Override
public void beginRequest(HttpClientRequestImpl request) {
this.request = request;
}
@Override
public void endRequest() {
if (conn.metrics.isEnabled()) {
conn.metrics.requestEnd(request.metric());
}
requestEnded = true;
}
@Override
public void resetRequest(long code) {
if (!(requestEnded && responseEnded)) {
requestEnded = true;
responseEnded = true;
writeReset(code);
if (conn.metrics.isEnabled()) {
conn.metrics.requestReset(request.metric());
}
}
}
@Override
public void resetResponse(long code) {
resetRequest(code);
}
@Override
public HttpClientConnection connection() {
return conn;
}
@Override
public NetSocket createNetSocket() {
return conn.toNetSocket(this);
}
}
}