Skip to content

Commit

Permalink
Raise SessionProtocolNegotiationException if failed to upgrade when s…
Browse files Browse the repository at this point in the history
…essionProtocol is set explicitly
  • Loading branch information
trustin committed Feb 4, 2016
1 parent dd37972 commit e8293fe
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 66 deletions.
141 changes: 103 additions & 38 deletions src/main/java/com/linecorp/armeria/client/HttpConfigurator.java
Expand Up @@ -20,8 +20,6 @@

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.EnumSet;
import java.util.Set;

import javax.net.ssl.SSLException;

Expand All @@ -32,6 +30,7 @@
import com.linecorp.armeria.common.http.AbstractHttpToHttp2ConnectionHandler;
import com.linecorp.armeria.common.http.Http1ClientCodec;
import com.linecorp.armeria.common.http.Http1ClientUpgradeHandler;
import com.linecorp.armeria.common.util.Exceptions;

import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
Expand Down Expand Up @@ -82,24 +81,45 @@ class HttpConfigurator extends ChannelInitializer<Channel> {

private static final Logger logger = LoggerFactory.getLogger(HttpConfigurator.class);

private static final Set<SessionProtocol> http2preferredProtocols = EnumSet.of(SessionProtocol.H2,
SessionProtocol.H2C,
SessionProtocol.HTTP,
SessionProtocol.HTTPS);
private enum HttpPreference {
HTTP1_REQUIRED,
HTTP2_PREFERRED,
HTTP2_REQUIRED
}

private final SslContext sslCtx;
private final boolean isHttp2Preferred;
private final HttpPreference httpPreference;
private final RemoteInvokerOptions options;

HttpConfigurator(SessionProtocol sessionProtocol, RemoteInvokerOptions options) {
isHttp2Preferred = http2preferredProtocols.contains(sessionProtocol);
switch (sessionProtocol) {
case HTTP:
case HTTPS:
httpPreference = HttpPreference.HTTP2_PREFERRED;
break;
case H1:
case H1C:
httpPreference = HttpPreference.HTTP1_REQUIRED;
break;
case H2:
case H2C:
httpPreference = HttpPreference.HTTP2_REQUIRED;
break;
default:
// Should never reach here.
throw new Error();
}

this.options = requireNonNull(options, "options");

if (sessionProtocol.isTls()) {
try {
SslContextBuilder builder = SslContextBuilder.forClient();
options.trustManagerFactory().ifPresent(builder::trustManager);

if (isHttp2Preferred) {
if (httpPreference == HttpPreference.HTTP2_REQUIRED ||
httpPreference == HttpPreference.HTTP2_PREFERRED) {

builder.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
Expand Down Expand Up @@ -137,37 +157,54 @@ private void configureAsHttps(Channel ch) {
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof SslHandshakeCompletionEvent) {
SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt;
final SessionProtocol protocol;
if (event.isSuccess()) {
if (isHttp2Protocol(sslHandler)) {
pipeline.addLast(newHttp2ConnectionHandler());
protocol = SessionProtocol.H2;
} else {
pipeline.addLast(newHttp1Codec());
protocol = SessionProtocol.H1;
}
finishConfiguration(pipeline, protocol);
if (!(evt instanceof SslHandshakeCompletionEvent)) {
ctx.fireUserEventTriggered(evt);
return;
}

final SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt;
if (!handshakeEvent.isSuccess()) {
// The connection will be closed automatically by SslHandler.
return;
}

final SessionProtocol protocol;
if (isHttp2Protocol(sslHandler)) {
if (httpPreference == HttpPreference.HTTP1_REQUIRED) {
failWithUnexpectedProtocol(ctx, SessionProtocol.H1, SessionProtocol.H2);
return;
}
pipeline.remove(this);

pipeline.addLast(newHttp2ConnectionHandler());
protocol = SessionProtocol.H2;
} else {
if (httpPreference == HttpPreference.HTTP2_REQUIRED) {
failWithUnexpectedProtocol(ctx, SessionProtocol.H2, SessionProtocol.H1);
return;
}

pipeline.addLast(newHttp1Codec());
protocol = SessionProtocol.H1;
}
ctx.fireUserEventTriggered(evt);
finishConfiguration(pipeline, protocol);
pipeline.remove(this);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warn("{} Unexpected exception:", ctx.channel(), cause);
Exceptions.logIfUnexpected(logger, ctx.channel(), cause);
ctx.close();
}
});
}

// refer https://http2.github.io/http2-spec/#discover-http
private void configureAsHttp(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
final ChannelPipeline pipeline = ch.pipeline();

if (httpPreference == HttpPreference.HTTP2_REQUIRED ||
httpPreference == HttpPreference.HTTP2_PREFERRED) {

if (isHttp2Preferred) {
Http1ClientCodec http1Codec = newHttp1Codec();
Http2ClientUpgradeCodec http2ClientUpgradeCodec =
new Http2ClientUpgradeCodec(newHttp2ConnectionHandler());
Expand All @@ -180,20 +217,37 @@ private void configureAsHttp(Channel ch) {
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof UpgradeEvent) {
switch ((UpgradeEvent) evt) {
case UPGRADE_SUCCESSFUL:
finishConfiguration(pipeline, SessionProtocol.H2C);
pipeline.remove(this);
break;
case UPGRADE_REJECTED:
// FIXME(trustin): Handle critical status codes such as 400.
finishConfiguration(pipeline, SessionProtocol.H1C);
pipeline.remove(this);
break;
if (!(evt instanceof UpgradeEvent)) {
ctx.fireUserEventTriggered(evt);
return;
}

final UpgradeEvent upgradeEvent = (UpgradeEvent) evt;
if (upgradeEvent == UpgradeEvent.UPGRADE_ISSUED) {
// Uninterested in this event
return;
}

switch (upgradeEvent) {
case UPGRADE_SUCCESSFUL:
finishConfiguration(pipeline, SessionProtocol.H2C);
break;
case UPGRADE_REJECTED:
if (httpPreference == HttpPreference.HTTP2_REQUIRED) {
failWithUnexpectedProtocol(ctx, SessionProtocol.H2C, SessionProtocol.H1C);
return;
}

// FIXME(trustin): Handle critical status codes such as 400.
finishConfiguration(pipeline, SessionProtocol.H1C);
break;
default:
// Should never reach here.
throw new Error();
}
ctx.fireUserEventTriggered(evt);

pipeline.remove(this);
logger.warn("{}", pipeline);
}
});
pipeline.addLast(new UpgradeRequestHandler());
Expand Down Expand Up @@ -236,6 +290,17 @@ void finishConfiguration(ChannelPipeline pipeline, SessionProtocol protocol) {
pipeline.channel().eventLoop().execute(() -> pipeline.fireUserEventTriggered(protocol));
}

void failWithUnexpectedProtocol(ChannelHandlerContext ctx,
SessionProtocol expected, SessionProtocol actual) {

final ChannelPipeline pipeline = ctx.pipeline();
pipeline.channel().eventLoop().execute(
() -> pipeline.fireUserEventTriggered(
new SessionProtocolNegotiationException(expected, actual)));
ctx.close();
}


boolean isHttp2Protocol(SslHandler sslHandler) {
return ApplicationProtocolNames.HTTP_2.equals(sslHandler.applicationProtocol());
}
Expand Down
Expand Up @@ -135,7 +135,16 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
timeoutFuture.cancel(false);
result.trySuccess(ctx.channel());
ctx.pipeline().remove(this);
return;
}

if (evt instanceof SessionProtocolNegotiationException) {
timeoutFuture.cancel(false);
result.tryFailure((SessionProtocolNegotiationException) evt);
ctx.close();
return;
}

ctx.fireUserEventTriggered(evt);
}
});
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java
Expand Up @@ -35,6 +35,8 @@
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
Expand Down Expand Up @@ -117,9 +119,22 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
|| serializationFormat == SerializationFormat.NONE) {
iCtx.resolvePromise(resultPromise, response.retain());
} else {
iCtx.rejectPromise(
resultPromise,
new InvalidResponseException("HTTP Response code: " + response.status()));
final DecoderResult decoderResult = response.decoderResult();
final Throwable cause;

if (decoderResult.isSuccess()) {
cause = new InvalidResponseException("HTTP Response code: " + response.status());
} else {
final Throwable decoderCause = decoderResult.cause();
if (decoderCause instanceof DecoderException) {
cause = decoderCause;
} else {
cause = new DecoderException("protocol violation: " + decoderCause,
decoderCause);
}
}

iCtx.rejectPromise(resultPromise, cause);
}
} finally {
ReferenceCountUtil.release(msg);
Expand Down
@@ -0,0 +1,72 @@
/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.client;

import static java.util.Objects.requireNonNull;

import java.util.Optional;

import com.linecorp.armeria.common.SessionProtocol;

/**
* An exception triggered when failed to negotiate the desired {@link SessionProtocol} with a server.
*/
public final class SessionProtocolNegotiationException extends RuntimeException {

private static final long serialVersionUID = 5788454584691399858L;

private final SessionProtocol expected;
private final SessionProtocol actual;

/**
* Creates a new instance with the specified expected {@link SessionProtocol}.
*/
public SessionProtocolNegotiationException(SessionProtocol expected) {
super("expected: " + requireNonNull(expected, "expected"));
this.expected = expected;
actual = null;
}

/**
* Creates a new instance with the specified expected and actual {@link SessionProtocol}s.
*/
public SessionProtocolNegotiationException(SessionProtocol expected, SessionProtocol actual) {
super("expected: " + requireNonNull(expected, "expected") +
", actual: " + requireNonNull(actual, "actual"));
this.expected = expected;
this.actual = actual;
}

/**
* Returns the expected {@link SessionProtocol}.
*/
public SessionProtocol expected() {
return expected;
}

/**
* Returns the actual {@link SessionProtocol}.
*/
public Optional<SessionProtocol> actual() {
return Optional.ofNullable(actual);
}

@Override
public Throwable fillInStackTrace() {
return this;
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/linecorp/armeria/common/util/Exceptions.java
Expand Up @@ -16,6 +16,8 @@

package com.linecorp.armeria.common.util;

import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.regex.Pattern;
Expand All @@ -27,6 +29,7 @@
import io.netty.channel.Channel;
import io.netty.channel.ChannelException;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.util.internal.EmptyArrays;

/**
* Provides the methods that are useful for handling exceptions.
Expand All @@ -50,6 +53,17 @@ public static void logIfUnexpected(Logger logger, Channel ch, Throwable cause) {
logger.warn("{} Unexpected exception:", ch, cause);
}

/**
* Logs the specified exception if it is {@linkplain #isExpected(Throwable)} unexpected}.
*/
public static void logIfUnexpected(Logger logger, Channel ch, String debugData, Throwable cause) {
if (!logger.isWarnEnabled() || !isExpected(cause)) {
return;
}

logger.warn("{} Unexpected exception: {}", ch, debugData, cause);
}

/**
* Returns {@code true} if the specified exception is expected to occur in well-known circumstances.
* <ul>
Expand Down Expand Up @@ -85,5 +99,14 @@ public static boolean isExpected(Throwable cause) {
return false;
}

/**
* Empties the stack trace of the specified {@code exception}.
*/
public static <T extends Throwable> T clearTrace(T exception) {
requireNonNull(exception, "exception");
exception.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
return exception;
}

private Exceptions() {}
}

0 comments on commit e8293fe

Please sign in to comment.