/* * 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.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Stream; import io.vertx.core.AsyncResult; 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.HttpServerOptions; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.StreamResetException; import io.vertx.core.impl.ContextImpl; import io.vertx.core.spi.metrics.HttpServerMetrics; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayDeque; /** * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> */ public class Http2ServerConnection extends Http2ConnectionBase { private final HttpServerOptions options; private final String serverOrigin; private final Handler<HttpServerRequest> requestHandler; private final HttpServerMetrics metrics; private Long maxConcurrentStreams; private int concurrentStreams; private final ArrayDeque<Push> pendingPushes = new ArrayDeque<>(8); Http2ServerConnection( Channel channel, ContextImpl context, String serverOrigin, VertxHttp2ConnectionHandler connHandler, HttpServerOptions options, Handler<HttpServerRequest> requestHandler, HttpServerMetrics metrics) { super(channel, context, connHandler); this.options = options; this.serverOrigin = serverOrigin; this.requestHandler = requestHandler; this.metrics = metrics; } public HttpServerMetrics metrics() { return metrics; } private static boolean isMalformedRequest(Http2Headers headers) { if (headers.method() == null) { return true; } String method = headers.method().toString(); if (method.equals("CONNECT")) { if (headers.scheme() != null || headers.path() != null || headers.authority() == null) { return true; } } else { if (headers.method() == null || headers.scheme() == null || headers.path() == null) { return true; } } if (headers.authority() != null) { URI uri; try { uri = new URI(null, headers.authority().toString(), null, null, null); } catch (URISyntaxException e) { return true; } if (uri.getRawUserInfo() != null) { return true; } } return false; } @Override public synchronized void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endOfStream) { VertxHttp2Stream stream = streams.get(streamId); if (stream == null) { if (isMalformedRequest(headers)) { handler.writeReset(streamId, Http2Error.PROTOCOL_ERROR.code()); return; } String contentEncoding = options.isCompressionSupported() ? HttpUtils.determineContentEncoding(headers) : null; Http2Stream s = handler.connection().stream(streamId); boolean writable = handler.encoder().flowController().isWritable(s); Http2ServerRequestImpl req = new Http2ServerRequestImpl(this, s, metrics, serverOrigin, headers, contentEncoding, writable); stream = req; CharSequence value = headers.get(HttpHeaderNames.EXPECT); if (options.isHandle100ContinueAutomatically() && ((value != null && HttpHeaderValues.CONTINUE.equals(value)) || headers.contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE))) { req.response().writeContinue(); } streams.put(streamId, req); context.executeFromIO(() -> { Http2ServerResponseImpl resp = req.response(); resp.beginRequest(); requestHandler.handle(req); boolean hasPush = resp.endRequest(); if (hasPush) { ctx.flush(); } }); } else { // Http server request trailer - not implemented yet (in api) } if (endOfStream) { context.executeFromIO(stream::onEnd); } } @Override public synchronized void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) { Long v = settings.maxConcurrentStreams(); if (v != null) { maxConcurrentStreams = v; } super.onSettingsRead(ctx, settings); } synchronized void sendPush(int streamId, String host, HttpMethod method, MultiMap headers, String path, Handler<AsyncResult<HttpServerResponse>> completionHandler) { Http2Headers headers_ = new DefaultHttp2Headers(); if (method == HttpMethod.OTHER) { throw new IllegalArgumentException("Cannot push HttpMethod.OTHER"); } else { headers_.method(method.name()); } headers_.path(path); headers_.scheme(isSsl() ? "https" : "http"); if (host != null) { headers_.authority(host); } if (headers != null) { headers.forEach(header -> headers_.add(header.getKey(), header.getValue())); } handler.writePushPromise(streamId, headers_, new Handler<AsyncResult<Integer>>() { @Override public void handle(AsyncResult<Integer> ar) { if (ar.succeeded()) { synchronized (Http2ServerConnection.this) { int promisedStreamId = ar.result(); String contentEncoding = HttpUtils.determineContentEncoding(headers_); Http2Stream promisedStream = handler.connection().stream(promisedStreamId); boolean writable = handler.encoder().flowController().isWritable(promisedStream); Push push = new Push(promisedStream, contentEncoding, method, path, writable, completionHandler); streams.put(promisedStreamId, push); if (maxConcurrentStreams == null || concurrentStreams < maxConcurrentStreams) { concurrentStreams++; context.executeFromIO(push::complete); } else { pendingPushes.add(push); } } } else { context.executeFromIO(() -> { completionHandler.handle(Future.failedFuture(ar.cause())); }); } } }); } protected void updateSettings(Http2Settings settingsUpdate, Handler<AsyncResult<Void>> completionHandler) { settingsUpdate.remove(Http2CodecUtil.SETTINGS_ENABLE_PUSH); super.updateSettings(settingsUpdate, completionHandler); } private class Push extends VertxHttp2Stream<Http2ServerConnection> { private final HttpMethod method; private final String uri; private final String contentEncoding; private Http2ServerResponseImpl response; private final Future<HttpServerResponse> completionHandler; public Push(Http2Stream stream, String contentEncoding, HttpMethod method, String uri, boolean writable, Handler<AsyncResult<HttpServerResponse>> completionHandler) { super(Http2ServerConnection.this, stream, writable); this.method = method; this.uri = uri; this.contentEncoding = contentEncoding; this.completionHandler = Future.<HttpServerResponse>future().setHandler(completionHandler); } @Override void handleEnd(MultiMap trailers) { } @Override void handleData(Buffer buf) { } @Override void handleInterestedOpsChanged() { if (response != null) { response.writabilityChanged(); } } @Override void handleReset(long errorCode) { if (response != null) { response.callReset(errorCode); } else { completionHandler.fail(new StreamResetException(errorCode)); } } @Override void handleException(Throwable cause) { if (response != null) { response.handleError(cause); } } @Override void handleClose() { if (pendingPushes.remove(this)) { completionHandler.fail("Push reset by client"); } else { concurrentStreams--; while ((maxConcurrentStreams == null || concurrentStreams < maxConcurrentStreams) && pendingPushes.size() > 0) { Push push = pendingPushes.pop(); concurrentStreams++; context.runOnContext(v -> { push.complete(); }); } response.handleClose(); } } void complete() { synchronized (Http2ServerConnection.this) { response = new Http2ServerResponseImpl(Http2ServerConnection.this, this, method, uri, true, contentEncoding); completionHandler.complete(response); } } } }