package org.jooby.internal.netty;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import org.jooby.internal.netty.NettyPipeline.Http2OrHttpHandler;
import org.jooby.internal.netty.NettyPipeline.Http2PrefaceOrHttpHandler;
import org.jooby.spi.HttpHandler;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.SourceCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.EventExecutorGroup;
@RunWith(PowerMockRunner.class)
@PrepareForTest({NettyPipeline.class, SslContext.class, HttpServerCodec.class,
IdleStateHandler.class, InboundHttp2ToHttpAdapterBuilder.class,
HttpToHttp2ConnectionHandlerBuilder.class })
public class NettyPipelineTest {
private Block pipeline = unit -> {
SocketChannel channel = unit.get(SocketChannel.class);
expect(channel.pipeline()).andReturn(unit.get(ChannelPipeline.class));
};
private Block ctxpipeline = unit -> {
ChannelHandlerContext channel = unit.get(ChannelHandlerContext.class);
expect(channel.pipeline()).andReturn(unit.get(ChannelPipeline.class));
};
private Block ssl = unit -> {
ByteBufAllocator bufalloc = unit.mock(ByteBufAllocator.class);
SocketChannel channel = unit.get(SocketChannel.class);
expect(channel.alloc()).andReturn(bufalloc);
SslHandler handler = unit.mock(SslHandler.class);
SslContext sslContext = unit.get(SslContext.class);
expect(sslContext.newHandler(bufalloc)).andReturn(handler);
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast("ssl", handler)).andReturn(pipeline);
};
private Block sslContext = unit -> {
SslContext sslcontext = unit.powerMock(SslContext.class);
unit.registerMock(SslContext.class, sslcontext);
};
private Block http2OrHttp = unit -> {
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(eq("h1.1/h2"), unit.capture(Http2OrHttpHandler.class)))
.andReturn(pipeline);
};
@Test
public void https1_1() throws Exception {
Config conf = conf(false, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(sslContext)
.expect(pipeline)
.expect(ssl)
.expect(http2OrHttp)
.expect(ctxpipeline)
.expect(http1Codec())
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, unit.get(SslContext.class))
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
.next();
handler.configurePipeline(unit.get(ChannelHandlerContext.class), "http/1.1");
});
}
@Test
public void http1_1() throws Exception {
Config conf = conf(false, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(pipeline)
.expect(http1Codec())
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, null)
.initChannel(unit.get(SocketChannel.class));
});
}
@Test
public void h2cDirect() throws Exception {
Config conf = conf(true, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(pipeline)
.expect(unit -> {
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
.andReturn(pipeline);
})
.expect(ctxpipeline)
.expect(ctxpipeline)
.expect(h2(456, (u, h2) -> {
ChannelPipeline p = u.get(ChannelPipeline.class);
expect(p.addAfter(null, "h2", h2)).andReturn(p);
ChannelHandlerContext ctx = u.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("h2");
expect(p.context(h2)).andReturn(ctx);
expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
}))
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, null)
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
.iterator()
.next();
handler.decode(unit.get(ChannelHandlerContext.class),
Unpooled.wrappedBuffer("PRI HTTP".getBytes(StandardCharsets.UTF_8)), null);
});
}
@Test
public void h2prefaceIgnored() throws Exception {
Config conf = conf(true, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(pipeline)
.expect(unit -> {
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
.andReturn(pipeline);
})
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, null)
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
.iterator()
.next();
handler.decode(unit.get(ChannelHandlerContext.class),
Unpooled.wrappedBuffer("123".getBytes(StandardCharsets.UTF_8)), null);
});
}
@Test
public void httpToh2c() throws Exception {
Config conf = conf(true, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(pipeline)
.expect(unit -> {
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
.andReturn(pipeline);
})
.expect(ctxpipeline)
.expect(ctxpipeline)
.expect(http1Codec((p, h) -> {
expect(p.addAfter(null, "codec", h)).andReturn(p);
}))
.expect(unit -> {
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("codec");
ChannelPipeline p = unit.get(ChannelPipeline.class);
expect(p.context(unit.get(HttpServerCodec.class))).andReturn(ctx);
})
.expect(unit -> {
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("h2upgrade");
ChannelPipeline p = unit.get(ChannelPipeline.class);
expect(p.addAfter(eq("codec"), eq("h2upgrade"),
unit.capture(HttpServerUpgradeHandler.class)))
.andReturn(p);
expect(p.context(isA(HttpServerUpgradeHandler.class))).andReturn(ctx);
expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
})
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, null)
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
.iterator()
.next();
handler.decode(unit.get(ChannelHandlerContext.class),
Unpooled.wrappedBuffer("GET HTTP".getBytes(StandardCharsets.UTF_8)), null);
});
}
@Test
public void httpToh2cIgnoreUpgrade() throws Exception {
Config conf = conf(true, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(pipeline)
.expect(unit -> {
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
.andReturn(pipeline);
})
.expect(ctxpipeline)
.expect(ctxpipeline)
.expect(http1Codec((p, h) -> {
expect(p.addAfter(null, "codec", h)).andReturn(p);
}))
.expect(unit -> {
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("codec");
ChannelPipeline p = unit.get(ChannelPipeline.class);
expect(p.context(unit.get(HttpServerCodec.class))).andReturn(ctx);
})
.expect(unit -> {
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("h2upgrade");
HttpServerCodec http1Codec = unit.get(HttpServerCodec.class);
HttpServerUpgradeHandler h = unit.constructor(HttpServerUpgradeHandler.class)
.args(SourceCodec.class, UpgradeCodecFactory.class, int.class)
.build(eq(http1Codec), unit.capture(UpgradeCodecFactory.class), eq(456));
ChannelPipeline p = unit.get(ChannelPipeline.class);
expect(p.addAfter("codec", "h2upgrade", h)).andReturn(p);
expect(p.context(isA(HttpServerUpgradeHandler.class))).andReturn(ctx);
expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
})
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, null)
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
.iterator()
.next();
handler.decode(unit.get(ChannelHandlerContext.class),
Unpooled.wrappedBuffer("GET HTTP".getBytes(StandardCharsets.UTF_8)), null);
}, unit -> {
UpgradeCodecFactory u = unit.captured(UpgradeCodecFactory.class).iterator().next();
assertEquals(null, u.newUpgradeCodec("http/1.1"));
});
}
@Test
public void httpToh2cSuccessUpgrade() throws Exception {
Config conf = conf(true, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(pipeline)
.expect(unit -> {
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
.andReturn(pipeline);
})
.expect(ctxpipeline)
.expect(ctxpipeline)
.expect(http1Codec((p, h) -> {
expect(p.addAfter(null, "codec", h)).andReturn(p);
}))
.expect(unit -> {
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("codec");
ChannelPipeline p = unit.get(ChannelPipeline.class);
expect(p.context(unit.get(HttpServerCodec.class))).andReturn(ctx);
})
.expect(unit -> {
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
expect(ctx.name()).andReturn("h2upgrade");
HttpServerCodec http1Codec = unit.get(HttpServerCodec.class);
HttpServerUpgradeHandler h = unit.constructor(HttpServerUpgradeHandler.class)
.args(SourceCodec.class, UpgradeCodecFactory.class, int.class)
.build(eq(http1Codec), unit.capture(UpgradeCodecFactory.class), eq(456));
ChannelPipeline p = unit.get(ChannelPipeline.class);
expect(p.addAfter("codec", "h2upgrade", h)).andReturn(p);
expect(p.context(isA(HttpServerUpgradeHandler.class))).andReturn(ctx);
expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
})
.expect(idle(567))
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, null)
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
.iterator()
.next();
handler.decode(unit.get(ChannelHandlerContext.class),
Unpooled.wrappedBuffer("GET HTTP".getBytes(StandardCharsets.UTF_8)), null);
}, unit -> {
UpgradeCodecFactory u = unit.captured(UpgradeCodecFactory.class).iterator().next();
assertTrue(u.newUpgradeCodec("h2c") instanceof Http2ServerUpgradeCodec);
});
}
private Block http1Codec() {
return http1Codec((p, h) -> {
expect(p.addLast("codec", h)).andReturn(p);
});
}
private Block http1Codec(final BiConsumer<ChannelPipeline, HttpServerCodec> callback) {
return unit -> {
HttpServerCodec codec = unit.constructor(HttpServerCodec.class)
.build(123, 234, 345, false);
unit.registerMock(HttpServerCodec.class, codec);
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
callback.accept(pipeline, codec);
};
}
@Test
public void h2() throws Exception {
Config conf = conf(true, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(sslContext)
.expect(pipeline)
.expect(ssl)
.expect(http2OrHttp)
.expect(ctxpipeline)
.expect(h2(456))
.expect(idle(567))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, unit.get(SslContext.class))
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
.next();
handler.configurePipeline(unit.get(ChannelHandlerContext.class), "h2");
});
}
@Test
public void https1_1_noTimeout() throws Exception {
Config conf = conf(false, 123, 234, 345, 456, -1);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(sslContext)
.expect(pipeline)
.expect(ssl)
.expect(http2OrHttp)
.expect(ctxpipeline)
.expect(http1Codec())
.expect(aggregator(456))
.expect(jooby(conf))
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, unit.get(SslContext.class))
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
.next();
handler.configurePipeline(unit.get(ChannelHandlerContext.class), "http/1.1");
});
}
@Test
public void unknownProtocol() throws Exception {
Config conf = conf(false, 123, 234, 345, 456, 567L);
new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
ChannelPipeline.class, ChannelHandlerContext.class)
.expect(sslContext)
.expect(pipeline)
.expect(ssl)
.expect(http2OrHttp)
.run(unit -> {
new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
conf, unit.get(SslContext.class))
.initChannel(unit.get(SocketChannel.class));
}, unit -> {
Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
.next();
try {
handler.configurePipeline(unit.get(ChannelHandlerContext.class), "h2");
fail();
} catch (IllegalStateException x) {
assertEquals("Unknown protocol: h2", x.getMessage());
}
});
}
private Block jooby(final Config conf) {
return unit -> {
NettyHandler handler = unit.constructor(NettyHandler.class)
.build(unit.get(HttpHandler.class), conf);
unit.registerMock(NettyHandler.class, handler);
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast(unit.get(EventExecutorGroup.class), "jooby", handler))
.andReturn(pipeline);
};
}
private Block aggregator(final int len) {
return unit -> {
HttpObjectAggregator aggregator = unit.constructor(HttpObjectAggregator.class)
.build(len);
unit.registerMock(HttpObjectAggregator.class, aggregator);
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast("aggregator", aggregator)).andReturn(pipeline);
};
}
private Block h2(final int l) {
return h2(l, (u, h2) -> {
ChannelPipeline pipeline = u.get(ChannelPipeline.class);
expect(pipeline.addLast("h2", h2)).andReturn(pipeline);
});
}
private Block h2(final int l,
final BiConsumer<MockUnit, HttpToHttp2ConnectionHandler> handler) {
return unit -> {
DefaultHttp2Connection connection = unit.constructor(DefaultHttp2Connection.class)
.build(true);
InboundHttp2ToHttpAdapterBuilder builder = unit
.constructor(InboundHttp2ToHttpAdapterBuilder.class)
.build(connection);
InboundHttp2ToHttpAdapter adapter = unit.mock(InboundHttp2ToHttpAdapter.class);
Http2FrameLogger logger = unit.constructor(Http2FrameLogger.class)
.build(LogLevel.DEBUG);
expect(builder.propagateSettings(false)).andReturn(builder);
expect(builder.validateHttpHeaders(false)).andReturn(builder);
expect(builder.maxContentLength(l)).andReturn(builder);
expect(builder.build()).andReturn(adapter);
HttpToHttp2ConnectionHandler h2 = unit.mock(HttpToHttp2ConnectionHandler.class);
HttpToHttp2ConnectionHandlerBuilder h2builder = unit
.constructor(HttpToHttp2ConnectionHandlerBuilder.class)
.build();
expect(h2builder.frameListener(adapter)).andReturn(h2builder);
expect(h2builder.frameLogger(logger)).andReturn(h2builder);
expect(h2builder.connection(connection)).andReturn(h2builder);
expect(h2builder.build()).andReturn(h2);
handler.accept(unit, h2);
};
}
private Block idle(final long timeout) {
return unit -> {
IdleStateHandler idle = unit.constructor(IdleStateHandler.class)
.build(0L, 0L, timeout, TimeUnit.MILLISECONDS);
unit.registerMock(IdleStateHandler.class, idle);
ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
expect(pipeline.addLast("timeout", idle)).andReturn(pipeline);
};
}
private Config conf(final boolean http2, final int i, final int j, final int k, final int l,
final long m) {
return ConfigFactory.empty()
.withValue("netty.http.MaxInitialLineLength", ConfigValueFactory.fromAnyRef(i))
.withValue("netty.http.MaxHeaderSize", ConfigValueFactory.fromAnyRef(j))
.withValue("netty.http.MaxChunkSize", ConfigValueFactory.fromAnyRef(k))
.withValue("netty.http.MaxContentLength", ConfigValueFactory.fromAnyRef(l))
.withValue("netty.http.IdleTimeout", ConfigValueFactory.fromAnyRef(m))
.withValue("server.http2.enabled", ConfigValueFactory.fromAnyRef(http2));
}
}