diff --git a/build.gradle b/build.gradle
index e80dee0..75e2208 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,8 +65,6 @@ jar {
test {
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
- include 'org/xbib/netty/http/client/test/Http2FrameAdapterTest*'
- include 'org/xbib/netty/http/client/test/InboundHttp2ToHttpAdapterTest*'
testLogging {
showStandardStreams = false
exceptionFormat = 'full'
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 978e78a..0e96606 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d42d371..4dee9e0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,4 +1,4 @@
-#Mon Apr 17 15:12:33 CEST 2017
+#Tue May 02 21:00:09 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4453cce
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/main/java/org/xbib/netty/http/client/Http2EventHandler.java b/src/main/java/org/xbib/netty/http/client/Http2EventHandler.java
new file mode 100644
index 0000000..7377d13
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/Http2EventHandler.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.http.FullHttpMessage;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.Http2CodecUtil;
+import io.netty.handler.codec.http2.Http2Connection;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import io.netty.handler.codec.http2.Http2Error;
+import io.netty.handler.codec.http2.Http2EventAdapter;
+import io.netty.handler.codec.http2.Http2Exception;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2LocalFlowController;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.handler.codec.http2.Http2Stream;
+import io.netty.handler.codec.http2.HttpConversionUtil;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
+import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
+import static io.netty.handler.codec.http2.Http2Exception.connectionError;
+
+/**
+ * A HTTP/2 event adapter for a client.
+ * This event adapter expects {@link Http2Settings} are sent from the server before the
+ * {@link HttpRequest} is submitted by sending a header frame, and, if a body exists, a
+ * data frame.
+ * The push promises of a server response are acknowledged and the headers of a push promise
+ * are stored in the {@link HttpRequestContext} for being received later.
+ */
+public class Http2EventHandler extends Http2EventAdapter {
+
+ private static final Logger logger = Logger.getLogger(Http2EventHandler.class.getName());
+
+ private final Http2Connection connection;
+
+ private final Http2Connection.PropertyKey messageKey;
+
+ private final int maxContentLength;
+
+ private final boolean validateHttpHeaders;
+
+ /**
+ * Constructor for {@link Http2EventHandler}.
+ * @param connection the HTTP/2 connection
+ * @param maxContentLength the maximum content length
+ * @param validateHeaders true if headers should be validated
+ */
+ Http2EventHandler(Http2Connection connection, int maxContentLength, boolean validateHeaders) {
+ this.connection = connection;
+ this.maxContentLength = maxContentLength;
+ this.validateHttpHeaders = validateHeaders;
+ this.messageKey = connection.newKey();
+ }
+
+ /**
+ * Handles an inbound {@code SETTINGS} frame.
+ * After frame is received, the reuqets is sent.
+ *
+ * @param ctx the context from the handler where the frame was read.
+ * @param settings the settings received from the remote endpoint.
+ */
+ @Override
+ public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
+ throws Http2Exception {
+ logger.log(Level.FINEST, () -> "settings received " + settings);
+ Channel channel = ctx.channel();
+ final HttpRequestContext httpRequestContext =
+ channel.attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
+ final HttpRequest httpRequest = httpRequestContext.getHttpRequest();
+ ChannelPromise channelPromise = channel.newPromise();
+ Http2Headers headers = toHttp2Headers(httpRequestContext);
+ logger.log(Level.FINEST, () -> "write request " + httpRequest + " headers = " + headers);
+ boolean hasBody = httpRequestContext.getHttpRequest() instanceof FullHttpRequest;
+ Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class);
+ Integer streamId = httpRequestContext.getStreamId().get();
+ ChannelFuture channelFuture = handler.encoder().writeHeaders(ctx, streamId,
+ headers, 0, !hasBody, channelPromise);
+ httpRequestContext.putStreamID(streamId, channelFuture, channelPromise);
+ if (hasBody) {
+ FullHttpRequest fullHttpRequest = (FullHttpRequest) httpRequestContext.getHttpRequest();
+ ChannelPromise contentChannelPromise = channel.newPromise();
+ streamId = httpRequestContext.getStreamId().get();
+ ChannelFuture contentChannelFuture = handler.encoder().writeData(ctx, streamId,
+ fullHttpRequest.content(), 0, true, contentChannelPromise);
+ httpRequestContext.putStreamID(streamId, contentChannelFuture, contentChannelPromise);
+ channel.flush();
+ }
+ httpRequestContext.getSettingsPromise().setSuccess();
+ }
+
+ /**
+ * Handles an inbound {@code HEADERS} frame.
+ *
+ * Only one of the following methods will be called for each {@code HEADERS} frame sequence.
+ * One will be called when the {@code END_HEADERS} flag has been received.
+ *
+ * - {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
+ * - {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
+ * - {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
+ *
+ *
+ * To say it another way; the {@link Http2Headers} will contain all of the headers
+ * for the current message exchange step (additional queuing is not necessary).
+ *
+ * @param ctx the context from the handler where the frame was read.
+ * @param streamId the subject stream for the frame.
+ * @param headers the received headers.
+ * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and
+ * 256 (inclusive).
+ * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint
+ * for this stream.
+ */
+ @Override
+ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
+ boolean endOfStream) throws Http2Exception {
+ logger.log(Level.FINEST, () -> "headers received " + headers);
+ Http2Stream stream = connection.stream(streamId);
+ FullHttpMessage msg = beginHeader(ctx, stream, headers, true, true);
+ if (msg != null) {
+ endHeader(ctx, stream, msg, endOfStream);
+ }
+ }
+
+ /**
+ * Handles an inbound {@code HEADERS} frame with priority information specified.
+ * Only called if {@code END_HEADERS} encountered.
+ *
+ * Only one of the following methods will be called for each {@code HEADERS} frame sequence.
+ * One will be called when the {@code END_HEADERS} flag has been received.
+ *
+ * - {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
+ * - {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
+ * - {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
+ *
+ *
+ * To say it another way; the {@link Http2Headers} will contain all of the headers
+ * for the current message exchange step (additional queuing is not necessary).
+ *
+ * @param ctx the context from the handler where the frame was read.
+ * @param streamId the subject stream for the frame.
+ * @param headers the received headers.
+ * @param streamDependency the stream on which this stream depends, or 0 if dependent on the
+ * connection.
+ * @param weight the new weight for the stream.
+ * @param exclusive whether or not the stream should be the exclusive dependent of its parent.
+ * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and
+ * 256 (inclusive).
+ * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint
+ * for this stream.
+ */
+ @Override
+ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
+ short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
+ logger.log(Level.FINEST, () -> "headers received " + headers);
+ Http2Stream stream = connection.stream(streamId);
+ FullHttpMessage msg = beginHeader(ctx, stream, headers, true, true);
+ if (msg != null) {
+ if (streamDependency != Http2CodecUtil.CONNECTION_STREAM_ID) {
+ msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(),
+ streamDependency);
+ }
+ msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), weight);
+ endHeader(ctx, stream, msg, endOfStream);
+ }
+ }
+
+ /**
+ * Handles an inbound {@code DATA} frame.
+ *
+ * @param ctx the context from the handler where the frame was read.
+ * @param streamId the subject stream for the frame.
+ * @param data payload buffer for the frame. This buffer will be released by the codec.
+ * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and
+ * 256 (inclusive).
+ * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint for this stream.
+ * @return the number of bytes that have been processed by the application. The returned bytes are used by the
+ * inbound flow controller to determine the appropriate time to expand the inbound flow control window (i.e. send
+ * {@code WINDOW_UPDATE}). Returning a value equal to the length of {@code data} + {@code padding} will effectively
+ * opt-out of application-level flow control for this frame. Returning a value less than the length of {@code data}
+ * + {@code padding} will defer the returning of the processed bytes, which the application must later return via
+ * {@link Http2LocalFlowController#consumeBytes(Http2Stream, int)}. The returned value must
+ * be >= {@code 0} and <= {@code data.readableBytes()} + {@code padding}.
+ */
+ @Override
+ public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
+ throws Http2Exception {
+ logger.log(Level.FINEST, () -> "data received " + data);
+ Http2Stream stream = connection.stream(streamId);
+ FullHttpMessage msg = getMessage(stream);
+ if (msg == null) {
+ throw connectionError(PROTOCOL_ERROR, "data frame received for unknown stream id %d", streamId);
+ }
+ ByteBuf content = msg.content();
+ final int dataReadableBytes = data.readableBytes();
+ if (content.readableBytes() > maxContentLength - dataReadableBytes) {
+ throw connectionError(INTERNAL_ERROR,
+ "content length exceeded maximum of %d for stream id %d", maxContentLength, streamId);
+ }
+ content.writeBytes(data, data.readerIndex(), dataReadableBytes);
+ if (endOfStream) {
+ fireChannelRead(ctx, msg, false, stream);
+ }
+ return dataReadableBytes + padding;
+ }
+
+ /**
+ * Handles an inbound {@code RST_STREAM} frame. Deletes push stream id if present.
+ *
+ * @param ctx the context from the handler where the frame was read.
+ * @param streamId the stream that is terminating.
+ * @param errorCode the error code identifying the type of failure.
+ */
+ @Override
+ public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
+ logger.log(Level.FINEST, () -> "rst stream received: error code = " + errorCode);
+ Http2Stream stream = connection.stream(streamId);
+ FullHttpMessage msg = getMessage(stream);
+ if (msg != null) {
+ removeMessage(stream, true);
+ }
+ final HttpRequestContext httpRequestContext =
+ ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
+ httpRequestContext.getPushMap().remove(streamId);
+ }
+
+ /**
+ * Handles an inbound {@code PUSH_PROMISE} frame. Only called if {@code END_HEADERS} encountered.
+ *
+ * Promised requests MUST be authoritative, cacheable, and safe.
+ * See [RFC http2], Section 8.2.
+ *
+ * Only one of the following methods will be called for each {@code HEADERS} frame sequence.
+ * One will be called when the {@code END_HEADERS} flag has been received.
+ *
+ * - {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
+ * - {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
+ * - {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
+ *
+ *
+ * To say it another way; the {@link Http2Headers} will contain all of the headers
+ * for the current message exchange step (additional queuing is not necessary).
+ *
+ * @param ctx the context from the handler where the frame was read.
+ * @param streamId the stream the frame was sent on.
+ * @param promisedStreamId the ID of the promised stream.
+ * @param headers the received headers.
+ * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and
+ * 256 (inclusive).
+ */
+ @Override
+ public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
+ Http2Headers headers, int padding) throws Http2Exception {
+ logger.log(Level.FINEST, () -> "push promise received: streamId " + streamId +
+ " promised stream ID = " + promisedStreamId + " headers =" + headers);
+ Http2Stream promisedStream = connection.stream(promisedStreamId);
+ FullHttpMessage msg = beginHeader(ctx, promisedStream, headers, false, false);
+ if (msg != null) {
+ msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId);
+ msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(),
+ Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT);
+ endHeader(ctx, promisedStream, msg, false);
+ }
+ Channel channel = ctx.channel();
+ final HttpRequestContext httpRequestContext =
+ channel.attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
+ httpRequestContext.receiveStreamID(promisedStreamId, headers, channel.newPromise());
+ }
+
+ /**
+ * Notifies the listener that the given stream has now been removed from the connection and
+ * will no longer be returned via {@link Http2Connection#stream(int)}. The connection may
+ * maintain inactive streams for some time before removing them.
+ *
+ * If a {@link RuntimeException} is thrown it will be logged and not propagated.
+ * Throwing from this method is not supported and is considered a programming error.
+ */
+ @Override
+ public void onStreamRemoved(Http2Stream stream) {
+ logger.log(Level.FINEST, () -> "stream removed " + stream);
+ removeMessage(stream, true);
+ }
+
+ /**
+ * Create a new {@link FullHttpMessage} based upon the current connection parameters.
+ *
+ * @param stream The stream to create a message for
+ * @param headers The headers associated with {@code stream}
+ * @param validateHttpHeaders
+ *
+ * - {@code true} to validate HTTP headers in the http-codec
+ * - {@code false} not to validate HTTP headers in the http-codec
+ *
+ * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message
+ * @throws Http2Exception if message can not be created
+ */
+ private FullHttpMessage newMessage(Http2Stream stream, Http2Headers headers, boolean validateHttpHeaders,
+ ByteBufAllocator alloc) throws Http2Exception {
+ if (headers.status() != null) {
+ return HttpConversionUtil.toHttpResponse(stream.id(), headers, alloc, validateHttpHeaders);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the {@link FullHttpMessage} associated with {@code stream}.
+ * @param stream The stream to get the associated state from
+ * @return The {@link FullHttpMessage} associated with {@code stream}.
+ */
+ private FullHttpMessage getMessage(Http2Stream stream) {
+ return (FullHttpMessage) stream.getProperty(messageKey);
+ }
+
+ /**
+ * Make {@code message} be the state associated with {@code stream}.
+ * @param stream The stream which {@code message} is associated with.
+ * @param message The message which contains the HTTP semantics.
+ */
+ private void putMessage(Http2Stream stream, FullHttpMessage message) {
+ FullHttpMessage previous = stream.setProperty(messageKey, message);
+ if (previous != message && previous != null) {
+ previous.release();
+ }
+ }
+ /**
+ * The stream is out of scope for the HTTP message flow and will no longer be tracked
+ * @param stream The stream to remove associated state with
+ * @param release {@code true} to call release on the value if it is present. {@code false} to not call release.
+ */
+ private void removeMessage(Http2Stream stream, boolean release) {
+ FullHttpMessage msg = stream.removeProperty(messageKey);
+ if (release && msg != null) {
+ msg.release();
+ }
+ }
+
+ /**
+ * Set final headers and fire a channel read event
+ *
+ * @param ctx The context to fire the event on
+ * @param msg The message to send
+ * @param release {@code true} to call release on the value if it is present. {@code false} to not call release.
+ * @param stream the stream of the message which is being fired
+ */
+ private void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, boolean release,
+ Http2Stream stream) {
+ removeMessage(stream, release);
+ HttpUtil.setContentLength(msg, msg.content().readableBytes());
+ ctx.fireChannelRead(msg);
+ }
+
+ private FullHttpMessage beginHeader(ChannelHandlerContext ctx, Http2Stream stream, Http2Headers headers,
+ boolean allowAppend, boolean appendToTrailer) throws Http2Exception {
+ FullHttpMessage msg = getMessage(stream);
+ if (msg == null) {
+ msg = newMessage(stream, headers, validateHttpHeaders, ctx.alloc());
+ } else {
+ if (allowAppend) {
+ HttpConversionUtil.addHttp2ToHttpHeaders(stream.id(), headers, msg, appendToTrailer);
+ } else {
+ throw new Http2Exception(Http2Error.PROTOCOL_ERROR, "stream already exists");
+ }
+ }
+ return msg;
+ }
+
+ private void endHeader(ChannelHandlerContext ctx, Http2Stream stream, FullHttpMessage msg,
+ boolean endOfStream) {
+ if (endOfStream) {
+ fireChannelRead(ctx, msg, getMessage(stream) != msg, stream);
+ } else {
+ putMessage(stream, msg);
+ }
+ }
+
+ private static Http2Headers toHttp2Headers(HttpRequestContext httpRequestContext) {
+ HttpRequest httpRequest = httpRequestContext.getHttpRequest();
+ Http2Headers headers = new DefaultHttp2Headers()
+ .method(httpRequest.method().asciiName())
+ .path(httpRequest.uri())
+ .scheme(httpRequestContext.getURI().getScheme())
+ .authority(httpRequestContext.getURI().getHost());
+ HttpConversionUtil.toHttp2Headers(httpRequest.headers(), headers);
+ return headers;
+ }
+}
diff --git a/src/main/java/org/xbib/netty/http/client/Http2Handler.java b/src/main/java/org/xbib/netty/http/client/Http2Handler.java
deleted file mode 100644
index 769d705..0000000
--- a/src/main/java/org/xbib/netty/http/client/Http2Handler.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright 2017 Jörg Prante
- *
- * Jörg Prante 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 org.xbib.netty.http.client;
-
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelPromise;
-import io.netty.channel.SimpleChannelInboundHandler;
-import io.netty.channel.pool.ChannelPool;
-import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http2.HttpConversionUtil;
-import io.netty.util.internal.PlatformDependent;
-
-import java.util.AbstractMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Netty channel handler for HTTP/2 responses.
- */
-@ChannelHandler.Sharable
-public class Http2Handler extends SimpleChannelInboundHandler {
-
- private static final Logger logger = Logger.getLogger(Http2Handler.class.getName());
-
- private final Map> streamidPromiseMap;
-
- private final HttpClient httpClient;
-
- Http2Handler(HttpClient httpClient) {
- this.streamidPromiseMap = PlatformDependent.newConcurrentHashMap();
- this.httpClient = httpClient;
- }
-
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception {
- logger.log(Level.FINE, () -> httpResponse.getClass().getName());
- Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
- if (streamId == null) {
- logger.log(Level.WARNING, () -> "stream ID missing");
- return;
- }
- final HttpRequestContext httpRequestContext =
- ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
- Entry entry = streamidPromiseMap.get(streamId);
- if (entry != null) {
- HttpResponseListener httpResponseListener =
- ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
- if (httpResponseListener != null) {
- httpResponseListener.onResponse(httpResponse);
- }
- entry.getValue().setSuccess();
- if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
- return;
- }
- logger.log(Level.FINE, () -> "success");
- httpRequestContext.success("response arrived");
- final ChannelPool channelPool =
- ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
- channelPool.release(ctx.channel());
- } else {
- logger.log(Level.WARNING, () -> "stream id not found in promises: " + streamId);
- }
- }
-
- @Override
- public void channelInactive(ChannelHandlerContext ctx) throws Exception {
- logger.log(Level.FINE, ctx::toString);
- final ChannelPool channelPool =
- ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
- channelPool.release(ctx.channel());
- }
-
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
- ExceptionListener exceptionListener =
- ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
- logger.log(Level.FINE, () -> "exception caught");
- if (exceptionListener != null) {
- exceptionListener.onException(cause);
- }
- final HttpRequestContext httpRequestContext =
- ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
- httpRequestContext.fail(cause.getMessage());
- final ChannelPool channelPool =
- ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
- channelPool.release(ctx.channel());
- }
-
- void put(int streamId, ChannelFuture channelFuture, ChannelPromise promise) {
- logger.log(Level.FINE, "put stream ID " + streamId);
- streamidPromiseMap.put(streamId, new AbstractMap.SimpleEntry<>(channelFuture, promise));
- }
-
- void awaitResponses(HttpRequestContext httpRequestContext, ExceptionListener exceptionListener) {
- int timeout = httpRequestContext.getTimeout();
- Iterator>> iterator = streamidPromiseMap.entrySet().iterator();
- while (iterator.hasNext()) {
- Entry> entry = iterator.next();
- ChannelFuture channelFuture = entry.getValue().getKey();
- if (!channelFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
- IllegalStateException illegalStateException =
- new IllegalStateException("time out while waiting to write for stream id " + entry.getKey());
- if (exceptionListener != null) {
- exceptionListener.onException(illegalStateException);
- httpRequestContext.fail(illegalStateException.getMessage());
- final ChannelPool channelPool =
- channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
- channelPool.release(channelFuture.channel());
- }
- throw illegalStateException;
- }
- if (!channelFuture.isSuccess()) {
- throw new RuntimeException(channelFuture.cause());
- }
- ChannelPromise promise = entry.getValue().getValue();
- if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
- IllegalStateException illegalStateException =
- new IllegalStateException("time out while waiting for response on stream id " + entry.getKey());
- if (exceptionListener != null) {
- exceptionListener.onException(illegalStateException);
- httpRequestContext.fail(illegalStateException.getMessage());
- final ChannelPool channelPool =
- channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
- channelPool.release(channelFuture.channel());
- }
- throw illegalStateException;
- }
- if (!promise.isSuccess()) {
- RuntimeException runtimeException = new RuntimeException(promise.cause());
- if (exceptionListener != null) {
- exceptionListener.onException(runtimeException);
- httpRequestContext.fail(runtimeException.getMessage());
- final ChannelPool channelPool =
- channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
- channelPool.release(channelFuture.channel());
- }
- throw runtimeException;
- }
- iterator.remove();
- }
- }
-}
diff --git a/src/main/java/org/xbib/netty/http/client/Http2ResponseHandler.java b/src/main/java/org/xbib/netty/http/client/Http2ResponseHandler.java
new file mode 100644
index 0000000..476812e
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/Http2ResponseHandler.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.pool.ChannelPool;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
+import io.netty.handler.codec.http.cookie.Cookie;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.HttpConversionUtil;
+import org.xbib.netty.http.client.listener.CookieListener;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.listener.HttpHeadersListener;
+import org.xbib.netty.http.client.listener.HttpPushListener;
+import org.xbib.netty.http.client.listener.HttpResponseListener;
+
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Netty channel handler for HTTP/2 responses.
+ */
+@ChannelHandler.Sharable
+public class Http2ResponseHandler extends SimpleChannelInboundHandler {
+
+ private static final Logger logger = Logger.getLogger(Http2ResponseHandler.class.getName());
+
+ private final HttpClient httpClient;
+
+ Http2ResponseHandler(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception {
+ logger.log(Level.FINE, () -> httpResponse.getClass().getName());
+ Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
+ if (streamId == null) {
+ logger.log(Level.WARNING, () -> "stream ID missing in headers");
+ return;
+ }
+ final HttpRequestContext httpRequestContext =
+ ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
+ HttpHeaders httpHeaders = httpResponse.headers();
+ HttpHeadersListener httpHeadersListener =
+ ctx.channel().attr(HttpClientChannelContext.HEADER_LISTENER_ATTRIBUTE_KEY).get();
+ if (httpHeadersListener != null) {
+ logger.log(Level.FINE, () -> "firing onHeaders");
+ httpHeadersListener.onHeaders(httpHeaders);
+ }
+ CookieListener cookieListener =
+ ctx.channel().attr(HttpClientChannelContext.COOKIE_LISTENER_ATTRIBUTE_KEY).get();
+ for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) {
+ Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
+ httpRequestContext.addCookie(cookie);
+ if (cookieListener != null) {
+ logger.log(Level.FINE, () -> "firing onCookie");
+ cookieListener.onCookie(cookie);
+ }
+ }
+ Entry pushEntry = httpRequestContext.getPushMap().get(streamId);
+ if (pushEntry != null) {
+ final HttpPushListener httpPushListener =
+ ctx.channel().attr(HttpClientChannelContext.PUSH_LISTENER_ATTRIBUTE_KEY).get();
+ if (httpPushListener != null) {
+ httpPushListener.onPushReceived(pushEntry.getKey(), httpResponse);
+ }
+ if (!pushEntry.getValue().isSuccess()) {
+ pushEntry.getValue().setSuccess();
+ }
+ httpRequestContext.getPushMap().remove(streamId);
+ if (httpRequestContext.isFinished()) {
+ httpRequestContext.success("response finished");
+ }
+ return;
+ }
+ Entry promiseEntry = httpRequestContext.getStreamIdPromiseMap().get(streamId);
+ if (promiseEntry != null) {
+ final HttpResponseListener httpResponseListener =
+ ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
+ if (httpResponseListener != null) {
+ httpResponseListener.onResponse(httpResponse);
+ }
+ if (!promiseEntry.getValue().isSuccess()) {
+ promiseEntry.getValue().setSuccess();
+ }
+ if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
+ return;
+ }
+ httpRequestContext.getStreamIdPromiseMap().remove(streamId);
+ if (httpRequestContext.isFinished()) {
+ httpRequestContext.success("response finished");
+ }
+ }
+ }
+
+ /**
+ * The only method to release a HTTP/2 channel back to the pool is to wait for inactivity.
+ * @param ctx the channel handler context
+ * @throws Exception if the channel could not be released back to the pool
+ */
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ logger.log(Level.FINE, ctx::toString);
+ final ChannelPool channelPool =
+ ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
+ channelPool.release(ctx.channel());
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ logger.log(Level.FINE, () -> "exception caught: " + cause);
+ ExceptionListener exceptionListener =
+ ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
+ if (exceptionListener != null) {
+ exceptionListener.onException(cause);
+ }
+ final HttpRequestContext httpRequestContext =
+ ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
+ httpRequestContext.fail(cause.getMessage());
+ final ChannelPool channelPool =
+ ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
+ channelPool.release(ctx.channel());
+ }
+}
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClient.java b/src/main/java/org/xbib/netty/http/client/HttpClient.java
index 3049ed9..925d38a 100755
--- a/src/main/java/org/xbib/netty/http/client/HttpClient.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClient.java
@@ -18,6 +18,7 @@ package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
@@ -32,15 +33,27 @@ import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
+import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
+import org.xbib.netty.http.client.listener.CookieListener;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.listener.HttpPushListener;
+import org.xbib.netty.http.client.listener.HttpHeadersListener;
+import org.xbib.netty.http.client.listener.HttpResponseListener;
+import org.xbib.netty.http.client.util.InetAddressKey;
+import org.xbib.netty.http.client.util.NetworkUtils;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
-import java.net.URL;
+import java.net.URI;
import java.net.URLDecoder;
+import java.util.Collection;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -51,6 +64,8 @@ public final class HttpClient implements Closeable {
private static final Logger logger = Logger.getLogger(HttpClient.class.getName());
+ private static final AtomicInteger streamId = new AtomicInteger(3);
+
private final ByteBufAllocator byteBufAllocator;
private final EventLoopGroup eventLoopGroup;
@@ -58,7 +73,7 @@ public final class HttpClient implements Closeable {
private final HttpClientChannelPoolMap poolMap;
/**
- * Create a new HTTP client.
+ * Create a new HTTP client. Use {@link #builder()} to build HTTP client instance.
*/
HttpClient(ByteBufAllocator byteBufAllocator,
EventLoopGroup eventLoopGroup,
@@ -68,6 +83,9 @@ public final class HttpClient implements Closeable {
this.byteBufAllocator = byteBufAllocator;
this.eventLoopGroup = eventLoopGroup;
this.poolMap = new HttpClientChannelPoolMap(this, httpClientChannelContext, bootstrap, maxConnections);
+ NetworkUtils.extendSystemProperties();
+ logger.log(Level.FINE, () -> "local host name = " + NetworkUtils.getLocalHostName("localhost"));
+ logger.log(Level.FINE, NetworkUtils::displayNetworkInterfaces);
}
/**
@@ -80,7 +98,7 @@ public final class HttpClient implements Closeable {
}
public HttpClientRequestBuilder prepareRequest(HttpMethod method) {
- return new HttpClientRequestBuilder(this, method, byteBufAllocator);
+ return new HttpClientRequestBuilder(this, method, byteBufAllocator, streamId.getAndAdd(2));
}
/**
@@ -172,11 +190,18 @@ public final class HttpClient implements Closeable {
logger.log(Level.FINE, () -> "closed");
}
- void dispatch(HttpRequestContext httpRequestContext, HttpResponseListener httpResponseListener,
- ExceptionListener exceptionListener) {
- final URL url = httpRequestContext.getURL();
+ void dispatch(final HttpRequestContext httpRequestContext) {
+ final URI uri = httpRequestContext.getURI();
final HttpRequest httpRequest = httpRequestContext.getHttpRequest();
- logger.log(Level.FINE, () -> "trying URL " + url);
+ if (!httpRequestContext.getCookies().isEmpty()) {
+ logger.log(Level.FINE, () -> "configured cookies: " + httpRequestContext.getCookies());
+ Collection cookies = httpRequestContext.matchCookies();
+ if (!cookies.isEmpty()) {
+ logger.log(Level.FINE, () -> "updating cookie header with matched cookies: " + cookies);
+ httpRequest.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies));
+ }
+ }
+ logger.log(Level.FINE, () -> "trying URL " + uri);
if (httpRequestContext.isExpired()) {
httpRequestContext.fail("request expired");
}
@@ -185,27 +210,31 @@ public final class HttpClient implements Closeable {
return;
}
HttpVersion version = httpRequestContext.getHttpRequest().protocolVersion();
- InetAddressKey inetAddressKey = new InetAddressKey(url, version);
- // effectivly disable pool for HTTP/2
- if (version.majorVersion() == 2) {
- poolMap.remove(inetAddressKey);
- }
+ boolean secure = "https".equals(uri.getScheme());
+ InetAddressKey inetAddressKey = new InetAddressKey(uri.getHost(), uri.getPort(), version, secure);
final FixedChannelPool pool = poolMap.get(inetAddressKey);
logger.log(Level.FINE, () -> "connecting to " + inetAddressKey);
Future futureChannel = pool.acquire();
futureChannel.addListener((FutureListener) future -> {
+ final ExceptionListener exceptionListener = httpRequestContext.getExceptionListener();
if (future.isSuccess()) {
Channel channel = future.getNow();
+ // set settings promise before adding httpRequestContext as a channel attribute
+ ChannelPromise settingsPromise = channel.newPromise();
+ httpRequestContext.setSettingsPromise(settingsPromise);
channel.attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).set(pool);
channel.attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).set(httpRequestContext);
- if (httpResponseListener != null) {
- channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).set(httpResponseListener);
- }
- if (exceptionListener != null) {
- channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).set(exceptionListener);
- }
+ HttpResponseListener httpResponseListener = httpRequestContext.getHttpResponseListener();
+ channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).set(httpResponseListener);
+ HttpPushListener httpPushListener = httpRequestContext.getHttpPushListener();
+ channel.attr(HttpClientChannelContext.PUSH_LISTENER_ATTRIBUTE_KEY).set(httpPushListener);
+ HttpHeadersListener httpHeadersListener = httpRequestContext.getHttpHeadersListener();
+ channel.attr(HttpClientChannelContext.HEADER_LISTENER_ATTRIBUTE_KEY).set(httpHeadersListener);
+ CookieListener cookieListener = httpRequestContext.getCookieListener();
+ channel.attr(HttpClientChannelContext.COOKIE_LISTENER_ATTRIBUTE_KEY).set(cookieListener);
+ channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).set(exceptionListener);
if (httpRequestContext.isFailed()) {
- logger.log(Level.FINE, () -> "detected fail, close now");
+ logger.log(Level.FINE, () -> "detected fail, close channel");
future.cancel(true);
if (channel.isOpen()) {
channel.close();
@@ -227,22 +256,60 @@ public final class HttpClient implements Closeable {
}
});
} else if (httpRequest.protocolVersion().majorVersion() == 2) {
- HttpClientChannelInitializer.Http2SettingsHandler http2SettingsHandler =
- poolMap.getHttpClientChannelInitializer().getHttp2SettingsHandler();
- if (http2SettingsHandler != null) {
- logger.log(Level.FINE, "HTTP2: waiting for settings");
- http2SettingsHandler.awaitSettings(httpRequestContext, exceptionListener);
- }
- Http2Handler http2Handler = poolMap.getHttpClientChannelInitializer().getHttp2Handler();
- if (http2Handler != null) {
- logger.log(Level.FINE, () ->
- "HTTP2: trying to write, streamID=" + httpRequestContext.getStreamId() +
- " request: " + httpRequest.toString());
- ChannelPromise channelPromise = channel.newPromise();
- http2Handler.put(httpRequestContext.getStreamId(), channel.write(httpRequest), channelPromise);
- channel.flush();
- logger.log(Level.FINE, "HTTP2: waiting for responses");
- http2Handler.awaitResponses(httpRequestContext, exceptionListener);
+ logger.log(Level.FINE, () -> "waiting for HTTP/2 settings");
+ settingsPromise.await(httpRequestContext.getTimeout(), TimeUnit.MILLISECONDS);
+ logger.log(Level.FINE, () -> "waiting for HTTP/2 responses = " + httpRequestContext.getStreamIdPromiseMap().size());
+ int timeout = httpRequestContext.getTimeout();
+ for (Map.Entry> entry :
+ httpRequestContext.getStreamIdPromiseMap().entrySet()) {
+ ChannelFuture channelFuture = entry.getValue().getKey();
+ if (channelFuture != null) {
+ logger.log(Level.FINE, "waiting for channel, stream ID = " + entry.getKey());
+ if (!channelFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
+ IllegalStateException illegalStateException =
+ new IllegalStateException("time out while waiting to write for stream id " + entry.getKey());
+ if (exceptionListener != null) {
+ exceptionListener.onException(illegalStateException);
+ httpRequestContext.fail(illegalStateException.getMessage());
+ final ChannelPool channelPool =
+ channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
+ channelPool.release(channelFuture.channel());
+ }
+ throw illegalStateException;
+ }
+ if (!channelFuture.isSuccess()) {
+ throw new RuntimeException(channelFuture.cause());
+ }
+ }
+ ChannelPromise promise = entry.getValue().getValue();
+ logger.log(Level.FINE, "waiting for promise of stream ID = " + entry.getKey());
+ if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
+ IllegalStateException illegalStateException =
+ new IllegalStateException("time out while waiting for response on stream id " + entry.getKey());
+ if (exceptionListener != null) {
+ exceptionListener.onException(illegalStateException);
+ httpRequestContext.fail(illegalStateException.getMessage());
+ if (channelFuture != null) {
+ final ChannelPool channelPool =
+ channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
+ channelPool.release(channelFuture.channel());
+ }
+ }
+ throw illegalStateException;
+ }
+ if (!promise.isSuccess()) {
+ RuntimeException runtimeException = new RuntimeException(promise.cause());
+ if (exceptionListener != null) {
+ exceptionListener.onException(runtimeException);
+ httpRequestContext.fail(runtimeException.getMessage());
+ if (channelFuture != null) {
+ final ChannelPool channelPool =
+ channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
+ channelPool.release(channelFuture.channel());
+ }
+ }
+ throw runtimeException;
+ }
}
}
} else {
@@ -262,7 +329,7 @@ public final class HttpClient implements Closeable {
HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET :
httpRequestContext.getHttpRequest().method();
if (httpRequestContext.getRedirectCount().getAndIncrement() < httpRequestContext.getMaxRedirects()) {
- dispatchRedirect(channel, method, new URL(redirUrl), httpRequestContext);
+ dispatchRedirect(method, URI.create(redirUrl), httpRequestContext);
} else {
httpRequestContext.fail("too many redirections");
final ChannelPool channelPool =
@@ -295,7 +362,7 @@ public final class HttpClient implements Closeable {
return location;
} else {
logger.log(Level.FINE, "(relative->absolute) redirect to " + location);
- return makeAbsolute(httpRequestContext.getURL(), location);
+ return makeAbsolute(httpRequestContext.getURI(), location);
}
default:
break;
@@ -303,35 +370,31 @@ public final class HttpClient implements Closeable {
return null;
}
- private void dispatchRedirect(Channel channel, HttpMethod method, URL url,
+ private void dispatchRedirect(HttpMethod method, URI uri,
HttpRequestContext httpRequestContext) {
- final String uri = httpRequestContext.getHttpRequest().protocolVersion().majorVersion() == 2 ?
- url.toExternalForm() : makeRelative(url);
+ final String uriStr = httpRequestContext.getHttpRequest().protocolVersion().majorVersion() == 2 ?
+ uri.toASCIIString() : makeRelative(uri);
final HttpRequest httpRequest;
if (method.equals(httpRequestContext.getHttpRequest().method()) &&
httpRequestContext.getHttpRequest() instanceof DefaultFullHttpRequest) {
DefaultFullHttpRequest defaultFullHttpRequest = (DefaultFullHttpRequest) httpRequestContext.getHttpRequest();
FullHttpRequest fullHttpRequest = defaultFullHttpRequest.copy();
- fullHttpRequest.setUri(uri);
+ fullHttpRequest.setUri(uriStr);
httpRequest = fullHttpRequest;
} else {
- httpRequest = new DefaultHttpRequest(httpRequestContext.getHttpRequest().protocolVersion(), method, uri);
+ httpRequest = new DefaultHttpRequest(httpRequestContext.getHttpRequest().protocolVersion(), method, uriStr);
}
for (Map.Entry e : httpRequestContext.getHttpRequest().headers().entries()) {
httpRequest.headers().add(e.getKey(), e.getValue());
}
- httpRequest.headers().set(HttpHeaderNames.HOST, url.getHost());
- HttpRequestContext redirectContext = new HttpRequestContext(url, httpRequest,
+ httpRequest.headers().set(HttpHeaderNames.HOST, uri.getHost());
+ HttpRequestContext redirectContext = new HttpRequestContext(uri, httpRequest,
httpRequestContext);
- logger.log(Level.FINE, "dispatchRedirect url = " + url + " with new request " + httpRequest.toString());
- HttpResponseListener httpResponseListener =
- channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
- ExceptionListener exceptionListener =
- channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
- dispatch(redirectContext, httpResponseListener, exceptionListener);
+ logger.log(Level.FINE, "dispatchRedirect url = " + uri + " with new request " + httpRequest.toString());
+ dispatch(redirectContext);
}
- private String makeRelative(URL base) {
+ private String makeRelative(URI base) {
String uri = base.getPath();
if (base.getQuery() != null) {
uri = uri + "?" + base.getQuery();
@@ -339,7 +402,7 @@ public final class HttpClient implements Closeable {
return uri;
}
- private String makeAbsolute(URL base, String location) throws UnsupportedEncodingException {
+ private String makeAbsolute(URI base, String location) throws UnsupportedEncodingException {
String path = base.getPath() == null ? "/" : URLDecoder.decode(base.getPath(), "UTF-8");
if (location.startsWith("/")) {
path = location;
@@ -348,7 +411,7 @@ public final class HttpClient implements Closeable {
} else {
path += "/" + location;
}
- String scheme = base.getProtocol();
+ String scheme = base.getScheme();
StringBuilder sb = new StringBuilder(scheme).append("://").append(base.getHost());
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
if (defaultPort != -1 && base.getPort() != -1 && defaultPort != base.getPort()) {
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java
index eb45240..6bc4d33 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java
@@ -28,6 +28,7 @@ import io.netty.handler.proxy.Socks4ProxyHandler;
import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
+import org.xbib.netty.http.client.util.ClientAuthMode;
import java.io.InputStream;
import java.net.InetSocketAddress;
@@ -46,7 +47,7 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
private Bootstrap bootstrap;
- // let Netty decide, where default is Runtime.getRuntime().availableProcessors() * 2
+ // let Netty decide about thread number, default is Runtime.getRuntime().availableProcessors() * 2
private int threads = 0;
private boolean tcpNodelay = DEFAULT_TCP_NODELAY;
@@ -95,7 +96,7 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
private boolean useServerNameIdentification = DEFAULT_USE_SERVER_NAME_IDENTIFICATION;
- private SslClientAuthMode sslClientAuthMode = DEFAULT_SSL_CLIENT_AUTH_MODE;
+ private ClientAuthMode clientAuthMode = DEFAULT_SSL_CLIENT_AUTH_MODE;
private HttpProxyHandler httpProxyHandler;
@@ -252,8 +253,8 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
return this;
}
- public HttpClientBuilder setSslClientAuthMode(SslClientAuthMode sslClientAuthMode) {
- this.sslClientAuthMode = sslClientAuthMode;
+ public HttpClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) {
+ this.clientAuthMode = clientAuthMode;
return this;
}
@@ -319,7 +320,7 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
readTimeoutMillis, enableGzip, installHttp2Upgrade,
sslProvider, ciphers, cipherSuiteFilter, trustManagerFactory,
keyCertChainInputStream, keyInputStream, keyPassword,
- useServerNameIdentification, sslClientAuthMode,
+ useServerNameIdentification, clientAuthMode,
httpProxyHandler, socks4ProxyHandler, socks5ProxyHandler);
return new HttpClient(byteBufAllocator, eventLoopGroup, bootstrap, maxConnections, httpClientChannelContext);
}
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java
index c72d7c6..8d9d2ba 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java
@@ -22,6 +22,12 @@ import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
import io.netty.util.AttributeKey;
+import org.xbib.netty.http.client.listener.CookieListener;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.listener.HttpPushListener;
+import org.xbib.netty.http.client.listener.HttpHeadersListener;
+import org.xbib.netty.http.client.listener.HttpResponseListener;
+import org.xbib.netty.http.client.util.ClientAuthMode;
import java.io.InputStream;
import javax.net.ssl.TrustManagerFactory;
@@ -39,6 +45,15 @@ final class HttpClientChannelContext {
static final AttributeKey RESPONSE_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientResponseListener");
+ static final AttributeKey HEADER_LISTENER_ATTRIBUTE_KEY =
+ AttributeKey.valueOf("httpHeaderListener");
+
+ static final AttributeKey COOKIE_LISTENER_ATTRIBUTE_KEY =
+ AttributeKey.valueOf("cookieListener");
+
+ static final AttributeKey PUSH_LISTENER_ATTRIBUTE_KEY =
+ AttributeKey.valueOf("pushListener");
+
static final AttributeKey EXCEPTION_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientExceptionListener");
@@ -74,7 +89,7 @@ final class HttpClientChannelContext {
private final boolean useServerNameIdentification;
- private final SslClientAuthMode sslClientAuthMode;
+ private final ClientAuthMode clientAuthMode;
private final HttpProxyHandler httpProxyHandler;
@@ -98,7 +113,7 @@ final class HttpClientChannelContext {
InputStream keyInputStream,
String keyPassword,
boolean useServerNameIdentification,
- SslClientAuthMode sslClientAuthMode,
+ ClientAuthMode clientAuthMode,
HttpProxyHandler httpProxyHandler,
Socks4ProxyHandler socks4ProxyHandler,
Socks5ProxyHandler socks5ProxyHandler) {
@@ -118,7 +133,7 @@ final class HttpClientChannelContext {
this.keyInputStream = keyInputStream;
this.keyPassword = keyPassword;
this.useServerNameIdentification = useServerNameIdentification;
- this.sslClientAuthMode = sslClientAuthMode;
+ this.clientAuthMode = clientAuthMode;
this.httpProxyHandler = httpProxyHandler;
this.socks4ProxyHandler = socks4ProxyHandler;
this.socks5ProxyHandler = socks5ProxyHandler;
@@ -188,8 +203,8 @@ final class HttpClientChannelContext {
return useServerNameIdentification;
}
- SslClientAuthMode getSslClientAuthMode() {
- return sslClientAuthMode;
+ ClientAuthMode getClientAuthMode() {
+ return clientAuthMode;
}
HttpProxyHandler getHttpProxyHandler() {
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java
index 847a881..035cde8 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java
@@ -17,9 +17,12 @@ package org.xbib.netty.http.client;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.CipherSuiteFilter;
+import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import org.xbib.netty.http.client.util.InetAddressKey;
+import org.xbib.netty.http.client.util.ClientAuthMode;
import javax.net.ssl.TrustManagerFactory;
@@ -105,7 +108,7 @@ public interface HttpClientChannelContextDefaults {
/**
* Default SSL provider.
*/
- SslProvider DEFAULT_SSL_PROVIDER = SslProvider.OPENSSL;
+ SslProvider DEFAULT_SSL_PROVIDER = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
Iterable DEFAULT_CIPHERS = Http2SecurityUtil.CIPHERS;
@@ -118,5 +121,5 @@ public interface HttpClientChannelContextDefaults {
/**
* Default for SSL client authentication.
*/
- SslClientAuthMode DEFAULT_SSL_CLIENT_AUTH_MODE = SslClientAuthMode.NONE;
+ ClientAuthMode DEFAULT_SSL_CLIENT_AUTH_MODE = ClientAuthMode.NONE;
}
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java
index 5fcb76e..0b9d33b 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java
@@ -15,12 +15,12 @@
*/
package org.xbib.netty.http.client;
+import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
-import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.ChannelInputShutdownReadComplete;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
@@ -36,19 +36,19 @@ import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent;
import io.netty.handler.codec.http2.Http2FrameLogger;
-import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
-import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslCloseCompletionEvent;
-import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.util.InetAddressKey;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@@ -61,33 +61,41 @@ import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
/**
+ * Netty HTTP client channel initializer.
*/
class HttpClientChannelInitializer extends ChannelInitializer {
private static final Logger logger = Logger.getLogger(HttpClientChannelInitializer.class.getName());
- private static final Http2FrameLogger frameLogger =
- new Http2FrameLogger(LogLevel.TRACE, HttpClientChannelInitializer.class);
-
private final HttpClientChannelContext context;
- private final Http1Handler http1Handler;
+ private final HttpHandler httpHandler;
- private final Http2Handler http2Handler;
+ private final Http2ResponseHandler http2ResponseHandler;
private InetAddressKey key;
- private Http2SettingsHandler http2SettingsHandler;
-
- private UserEventLogger userEventLogger;
-
- HttpClientChannelInitializer(HttpClientChannelContext context, Http1Handler http1Handler,
- Http2Handler http2Handler) {
+ /**
+ * Constructor for a new {@link HttpClientChannelInitializer}.
+ * @param context the HTTP client channel context
+ * @param httpHandler the HTTP 1.x handler
+ * @param http2ResponseHandler the HTTP 2 handler
+ */
+ HttpClientChannelInitializer(HttpClientChannelContext context, HttpHandler httpHandler,
+ Http2ResponseHandler http2ResponseHandler) {
this.context = context;
- this.http1Handler = http1Handler;
- this.http2Handler = http2Handler;
+ this.httpHandler = httpHandler;
+ this.http2ResponseHandler = http2ResponseHandler;
}
+ /**
+ * Sets up a {@link InetAddressKey} for the channel initialization and initializes the channel.
+ * Using this method, the channel initializer can handle secure channels, the HTTP protocol version,
+ * and the host name for Server Name Identification (SNI).
+ * @param ch the channel
+ * @param key the key of the internet address
+ * @throws Exception if channel
+ */
void initChannel(SocketChannel ch, InetAddressKey key) throws Exception {
this.key = key;
initChannel(ch);
@@ -96,6 +104,9 @@ class HttpClientChannelInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
logger.log(Level.FINE, () -> "initChannel with key = " + key);
+ if (key == null) {
+ throw new IllegalStateException("no key set for channel initialization");
+ }
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new TrafficLoggingHandler());
if (context.getHttpProxyHandler() != null) {
@@ -108,22 +119,12 @@ class HttpClientChannelInitializer extends ChannelInitializer {
pipeline.addLast(context.getSocks5ProxyHandler());
}
pipeline.addLast(new ReadTimeoutHandler(context.getReadTimeoutMillis(), TimeUnit.MILLISECONDS));
- http2SettingsHandler = new Http2SettingsHandler(ch.newPromise());
- userEventLogger = new UserEventLogger();
if (context.getSslProvider() != null && key.isSecure()) {
configureEncrypted(ch);
} else {
configureClearText(ch);
}
- logger.log(Level.FINE, () -> "initChannel pipeline handler names = " + ch.pipeline().names());
- }
-
- Http2SettingsHandler getHttp2SettingsHandler() {
- return http2SettingsHandler;
- }
-
- Http2Handler getHttp2Handler() {
- return http2Handler;
+ logger.log(Level.FINE, () -> "initChannel complete, pipeline handler names = " + ch.pipeline().names());
}
private void configureClearText(SocketChannel ch) {
@@ -134,6 +135,7 @@ class HttpClientChannelInitializer extends ChannelInitializer {
configureHttp1Pipeline(pipeline);
} else if (key.getVersion().majorVersion() == 2) {
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
+ // using the upgrade handler means mixed HTTP 1 and HTTP 2 on the same connection
if (context.isInstallHttp2Upgrade()) {
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
Http2ClientUpgradeCodec upgradeCodec =
@@ -154,65 +156,47 @@ class HttpClientChannelInitializer extends ChannelInitializer {
private void configureEncrypted(SocketChannel ch) throws SSLException {
ChannelPipeline pipeline = ch.pipeline();
+ SslContextBuilder sslContextBuilder = SslContextBuilder.forClient()
+ .sslProvider(context.getSslProvider())
+ .keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword())
+ .ciphers(context.getCiphers(), context.getCipherSuiteFilter())
+ .trustManager(context.getTrustManagerFactory());
if (key.getVersion().majorVersion() == 2) {
- final SslContext http2SslContext = SslContextBuilder.forClient()
- .sslProvider(context.getSslProvider())
- .keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword())
- .ciphers(context.getCiphers(), context.getCipherSuiteFilter())
- .trustManager(context.getTrustManagerFactory())
- .applicationProtocolConfig(new ApplicationProtocolConfig(
- ApplicationProtocolConfig.Protocol.ALPN,
- ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
- ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
- ApplicationProtocolNames.HTTP_2,
- ApplicationProtocolNames.HTTP_1_1))
- .build();
- SslHandler sslHandler = http2SslContext.newHandler(ch.alloc());
- try {
- SSLEngine engine = sslHandler.engine();
- if (context.isUseServerNameIdentification()) {
- // execute DNS lookup and/or reverse lookup if IP for host name
- String fullQualifiedHostname = key.getInetSocketAddress().getHostName();
- SSLParameters params = engine.getSSLParameters();
- params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
- engine.setSSLParameters(params);
- }
- switch (context.getSslClientAuthMode()) {
- case NEED:
- engine.setNeedClientAuth(true);
- break;
- case WANT:
- engine.setWantClientAuth(true);
- break;
- default:
- break;
- }
- } finally {
- pipeline.addLast(sslHandler);
- }
- pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1));
- } else if (key.getVersion().majorVersion() == 1) {
- final SslContext hhtp1SslContext = SslContextBuilder.forClient()
- .sslProvider(context.getSslProvider())
- .keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword())
- .ciphers(context.getCiphers(), context.getCipherSuiteFilter())
- .trustManager(context.getTrustManagerFactory())
- .build();
- SslHandler sslHandler = hhtp1SslContext.newHandler(ch.alloc());
- switch (context.getSslClientAuthMode()) {
- case NEED:
- sslHandler.engine().setNeedClientAuth(true);
- break;
- case WANT:
- sslHandler.engine().setWantClientAuth(true);
- break;
- default:
- break;
+ sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig(
+ ApplicationProtocolConfig.Protocol.ALPN,
+ ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
+ ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
+ ApplicationProtocolNames.HTTP_2,
+ ApplicationProtocolNames.HTTP_1_1));
+ }
+ SslHandler sslHandler = sslContextBuilder.build().newHandler(ch.alloc());
+ SSLEngine engine = sslHandler.engine();
+ try {
+ if (context.isUseServerNameIdentification()) {
+ String fullQualifiedHostname = key.getInetSocketAddress().getHostName();
+ SSLParameters params = engine.getSSLParameters();
+ params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
+ engine.setSSLParameters(params);
}
+ } finally {
pipeline.addLast(sslHandler);
+ }
+ switch (context.getClientAuthMode()) {
+ case NEED:
+ engine.setNeedClientAuth(true);
+ break;
+ case WANT:
+ engine.setWantClientAuth(true);
+ break;
+ default:
+ break;
+ }
+ if (key.getVersion().majorVersion() == 1) {
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
pipeline.addLast(http1connectionHandler);
configureHttp1Pipeline(pipeline);
+ } else if (key.getVersion().majorVersion() == 2) {
+ pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1));
}
}
@@ -224,13 +208,12 @@ class HttpClientChannelInitializer extends ChannelInitializer {
new HttpObjectAggregator(context.getMaxContentLength(), false);
httpObjectAggregator.setMaxCumulationBufferComponents(context.getMaxCompositeBufferComponents());
pipeline.addLast(httpObjectAggregator);
- pipeline.addLast(http1Handler);
+ pipeline.addLast(httpHandler);
}
private void configureHttp2Pipeline(ChannelPipeline pipeline) {
- pipeline.addLast(http2SettingsHandler);
- pipeline.addLast(userEventLogger);
- pipeline.addLast(http2Handler);
+ pipeline.addLast(new UserEventLogger());
+ pipeline.addLast(http2ResponseHandler);
}
private HttpClientCodec createHttp1ConnectionHandler() {
@@ -241,13 +224,9 @@ class HttpClientChannelInitializer extends ChannelInitializer {
final Http2Connection http2Connection = new DefaultHttp2Connection(false);
return new HttpToHttp2ConnectionHandlerBuilder()
.connection(http2Connection)
- .frameLogger(frameLogger)
+ .frameLogger(new Http2FrameLogger(LogLevel.TRACE, HttpClientChannelInitializer.class))
.frameListener(new DelegatingDecompressorFrameListener(http2Connection,
- new InboundHttp2ToHttpAdapterBuilder(http2Connection)
- .maxContentLength(context.getMaxContentLength())
- .propagateSettings(true)
- .validateHttpHeaders(false)
- .build()))
+ new Http2EventHandler(http2Connection, context.getMaxContentLength(), false)))
.build();
}
@@ -263,81 +242,37 @@ class HttpClientChannelInitializer extends ChannelInitializer {
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
ctx.pipeline().addLast(http2connectionHandler);
configureHttp2Pipeline(ctx.pipeline());
- logger.log(Level.FINE, "negotiated HTTP/2: handler = " + ctx.pipeline().names());
+ logger.log(Level.FINE, () -> "negotiated HTTP/2: handler = " + ctx.pipeline().names());
return;
}
if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
ctx.pipeline().addLast(http1connectionHandler);
configureHttp1Pipeline(ctx.pipeline());
- logger.log(Level.FINE, "negotiated HTTP/1.1: handler = " + ctx.pipeline().names());
+ logger.log(Level.FINE, () -> "negotiated HTTP/1.1: handler = " + ctx.pipeline().names());
return;
}
+ // close and fail
ctx.close();
throw new IllegalStateException("unexpected protocol: " + protocol);
}
}
- class Http2SettingsHandler extends SimpleChannelInboundHandler {
-
- private final ChannelPromise promise;
-
- Http2SettingsHandler(ChannelPromise promise) {
- this.promise = promise;
- }
-
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
- promise.setSuccess();
- ctx.pipeline().remove(this);
- logger.log(Level.FINE, "settings handler removed, pipeline = " + ctx.pipeline().names());
- }
-
- /**
- * Forward channel exceptions to the exception listener.
- * @param ctx the channel handler context
- * @param cause the cause of the exception
- * @throws Exception if forwarding fails
- */
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
- ExceptionListener exceptionListener =
- ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
- logger.log(Level.FINE, () -> "exceptionCaught");
- if (exceptionListener != null) {
- exceptionListener.onException(cause);
- }
- final HttpRequestContext httpRequestContext =
- ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
- httpRequestContext.fail(cause.getMessage());
- }
-
- void awaitSettings(HttpRequestContext httpRequestContext, ExceptionListener exceptionListener) throws Exception {
- int timeout = httpRequestContext.getTimeout();
- if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
- IllegalStateException exception = new IllegalStateException("time out while waiting for HTTP/2 settings");
- if (exceptionListener != null) {
- exceptionListener.onException(exception);
- httpRequestContext.fail(exception.getMessage());
- }
- throw exception;
- }
- if (!promise.isSuccess()) {
- throw new RuntimeException(promise.cause());
- }
- }
- }
-
@Sharable
private class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
+ /**
+ * Send an upgrade request if channel becomes active.
+ * @param ctx the channel handler context
+ * @throws Exception if upgrade request sending fails
+ */
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
ctx.writeAndFlush(upgradeRequest);
super.channelActive(ctx);
ctx.pipeline().remove(this);
- logger.log(Level.FINE, "upgrade request handler removed, pipeline = " + ctx.pipeline().names());
+ logger.log(Level.FINE, () -> "upgrade request handler removed, pipeline = " + ctx.pipeline().names());
}
/**
@@ -348,9 +283,9 @@ class HttpClientChannelInitializer extends ChannelInitializer {
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ logger.log(Level.FINE, () -> "exceptionCaught " + cause.getMessage());
ExceptionListener exceptionListener =
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
- logger.log(Level.FINE, () -> "exceptionCaught");
if (exceptionListener != null) {
exceptionListener.onException(cause);
}
@@ -360,6 +295,9 @@ class HttpClientChannelInitializer extends ChannelInitializer {
}
}
+ /**
+ * A Netty handler that logs user events and find expetced ones.
+ */
@Sharable
private class UserEventLogger extends ChannelInboundHandlerAdapter {
@@ -369,11 +307,46 @@ class HttpClientChannelInitializer extends ChannelInitializer {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent ||
evt instanceof SslCloseCompletionEvent ||
evt instanceof ChannelInputShutdownReadComplete) {
- // Expected events
+ // log expected events
logger.log(Level.FINE, () -> "user event is expected: " + evt);
return;
}
super.userEventTriggered(ctx, evt);
}
}
+
+ /**
+ * A Netty handler that logs the I/O traffic of a connection.
+ */
+ @Sharable
+ private final class TrafficLoggingHandler extends LoggingHandler {
+
+ TrafficLoggingHandler() {
+ super("client", LogLevel.TRACE);
+ }
+
+ @Override
+ public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
+ ctx.fireChannelRegistered();
+ }
+
+ @Override
+ public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
+ ctx.fireChannelUnregistered();
+ }
+
+ @Override
+ public void flush(ChannelHandlerContext ctx) throws Exception {
+ ctx.flush();
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+ if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
+ ctx.write(msg, promise);
+ } else {
+ super.write(ctx, msg, promise);
+ }
+ }
+ }
}
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java
index c226706..1d4b90d 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java
@@ -18,6 +18,7 @@ package org.xbib.netty.http.client;
import io.netty.channel.Channel;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.socket.SocketChannel;
+import org.xbib.netty.http.client.util.InetAddressKey;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
@@ -45,7 +46,7 @@ public class HttpClientChannelPoolHandler implements ChannelPoolHandler {
@Override
public void channelCreated(Channel ch) throws Exception {
- logger.log(Level.INFO, () -> "channel created " + ch + " key:" + key);
+ logger.log(Level.FINE, () -> "channel created " + ch + " key:" + key);
channelInitializer.initChannel((SocketChannel) ch, key);
int n = active.incrementAndGet();
if (n > peak) {
@@ -55,12 +56,12 @@ public class HttpClientChannelPoolHandler implements ChannelPoolHandler {
@Override
public void channelAcquired(Channel ch) throws Exception {
- logger.log(Level.INFO, () -> "channel acquired from pool " + ch);
+ logger.log(Level.FINE, () -> "channel acquired from pool " + ch);
}
@Override
public void channelReleased(Channel ch) throws Exception {
- logger.log(Level.INFO, () -> "channel released to pool " + ch);
+ logger.log(Level.FINE, () -> "channel released to pool " + ch);
active.decrementAndGet();
}
diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java
index c8f07fa..3706be0 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java
@@ -18,6 +18,7 @@ package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.pool.AbstractChannelPoolMap;
import io.netty.channel.pool.FixedChannelPool;
+import org.xbib.netty.http.client.util.InetAddressKey;
/**
*
@@ -49,7 +50,7 @@ public class HttpClientChannelPoolMap extends AbstractChannelPoolMap removeHeaders;
+ private final Set cookies;
+
private final HttpMethod httpMethod;
private int timeout = DEFAULT_TIMEOUT_MILLIS;
@@ -60,9 +76,11 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
private int maxRedirects = DEFAULT_MAX_REDIRECT;
- private URL url;
+ private URI uri;
- private ByteBuf body;
+ private QueryStringEncoder queryStringEncoder;
+
+ private ByteBuf content;
private HttpRequest httpRequest;
@@ -72,6 +90,12 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
private ExceptionListener exceptionListener;
+ private HttpHeadersListener httpHeadersListener;
+
+ private CookieListener cookieListener;
+
+ private HttpPushListener httpPushListener;
+
/**
* Construct HTTP client request builder.
*
@@ -79,12 +103,15 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
* @param httpMethod HTTP method
* @param byteBufAllocator byte buf allocator
*/
- HttpClientRequestBuilder(HttpClient httpClient, HttpMethod httpMethod, ByteBufAllocator byteBufAllocator) {
+ HttpClientRequestBuilder(HttpClient httpClient, HttpMethod httpMethod,
+ ByteBufAllocator byteBufAllocator, int streamId) {
this.httpClient = httpClient;
this.httpMethod = httpMethod;
this.byteBufAllocator = byteBufAllocator;
+ this.streamId = new AtomicInteger(streamId);
this.headers = new DefaultHttpHeaders();
this.removeHeaders = new ArrayList<>();
+ this.cookies = new HashSet<>();
}
@Override
@@ -93,24 +120,19 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
return this;
}
- protected int getTimeout() {
- return timeout;
- }
-
@Override
public HttpRequestBuilder setURL(String url) {
- try {
- this.url = new URL(url);
- } catch (MalformedURLException e) {
- throw new UncheckedIOException(e);
+ this.uri = URI.create(url);
+ QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri, StandardCharsets.UTF_8);
+ this.queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path());
+ for (Map.Entry> entry : queryStringDecoder.parameters().entrySet()) {
+ for (String value : entry.getValue()) {
+ queryStringEncoder.addParam(entry.getKey(), value);
+ }
}
return this;
}
- protected URL getURL() {
- return url;
- }
-
@Override
public HttpRequestBuilder addHeader(String name, Object value) {
headers.add(name, value);
@@ -129,6 +151,20 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
return this;
}
+ @Override
+ public HttpRequestBuilder addParam(String name, String value) {
+ if (queryStringEncoder != null) {
+ queryStringEncoder.addParam(name, value);
+ }
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilder addCookie(Cookie cookie) {
+ cookies.add(cookie);
+ return this;
+ }
+
@Override
public HttpRequestBuilder contentType(String contentType) {
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
@@ -141,10 +177,6 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
return this;
}
- protected HttpVersion getVersion() {
- return httpVersion;
- }
-
@Override
public HttpRequestBuilder acceptGzip(boolean gzip) {
this.gzip = gzip;
@@ -157,20 +189,12 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
return this;
}
- protected boolean isFollowRedirect() {
- return followRedirect;
- }
-
@Override
public HttpRequestBuilder setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
- protected int getMaxRedirects() {
- return maxRedirects;
- }
-
@Override
public HttpRequestBuilder setUserAgent(String userAgent) {
this.userAgent = userAgent;
@@ -179,60 +203,90 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
@Override
public HttpRequestBuilder text(String text) throws IOException {
- setBody(text, HttpHeaderValues.TEXT_PLAIN);
+ content(text, HttpHeaderValues.TEXT_PLAIN);
return this;
}
@Override
public HttpRequestBuilder json(String json) throws IOException {
- setBody(json, HttpHeaderValues.APPLICATION_JSON);
+ content(json, HttpHeaderValues.APPLICATION_JSON);
return this;
}
@Override
public HttpRequestBuilder xml(String xml) throws IOException {
- setBody(xml, "application/xml");
+ content(xml, "application/xml");
return this;
}
@Override
- public HttpRequestBuilder setBody(CharSequence charSequence, String contentType) throws IOException {
- setBody(charSequence.toString().getBytes(CharsetUtil.UTF_8), AsciiString.of(contentType));
+ public HttpRequestBuilder content(CharSequence charSequence, String contentType) throws IOException {
+ content(charSequence.toString().getBytes(CharsetUtil.UTF_8), AsciiString.of(contentType));
return this;
}
@Override
- public HttpRequestBuilder setBody(byte[] buf, String contentType) throws IOException {
- setBody(buf, AsciiString.of(contentType));
+ public HttpRequestBuilder content(byte[] buf, String contentType) throws IOException {
+ content(buf, AsciiString.of(contentType));
return this;
}
@Override
- public HttpRequestBuilder setBody(ByteBuf body, String contentType) throws IOException {
- setBody(body, AsciiString.of(contentType));
+ public HttpRequestBuilder content(ByteBuf body, String contentType) throws IOException {
+ content(body, AsciiString.of(contentType));
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilder onHeaders(HttpHeadersListener httpHeadersListener) {
+ this.httpHeadersListener = httpHeadersListener;
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilder onCookie(CookieListener cookieListener) {
+ this.cookieListener = cookieListener;
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener) {
+ this.httpResponseListener = httpResponseListener;
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilder onException(ExceptionListener exceptionListener) {
+ this.exceptionListener = exceptionListener;
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilder onPushReceived(HttpPushListener httpPushListener) {
+ this.httpPushListener = httpPushListener;
return this;
}
@Override
public HttpRequest build() {
- if (url == null) {
+ if (uri == null) {
throw new IllegalStateException("URL not set");
}
- if (url.getHost() == null) {
- throw new IllegalStateException("URL host not set: " + url);
+ if (uri.getHost() == null) {
+ throw new IllegalStateException("URL host not set: " + uri);
}
DefaultHttpRequest httpRequest = createHttpRequest();
- String scheme = url.getProtocol();
- StringBuilder sb = new StringBuilder(url.getHost());
+ String scheme = uri.getScheme();
+ StringBuilder sb = new StringBuilder(uri.getHost());
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
- if (defaultPort != -1 && url.getPort() != -1 && defaultPort != url.getPort()) {
- sb.append(":").append(url.getPort());
+ if (defaultPort != -1 && uri.getPort() != -1 && defaultPort != uri.getPort()) {
+ sb.append(":").append(uri.getPort());
}
if (httpVersion.majorVersion() == 2) {
- // this is a hack, because we only use the "origin-form" in request URIs
httpRequest.headers().set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme);
}
- httpRequest.headers().add(HttpHeaderNames.HOST, sb.toString());
+ String host = sb.toString();
+ httpRequest.headers().add(HttpHeaderNames.HOST, host);
httpRequest.headers().add(HttpHeaderNames.DATE,
DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT"))));
if (userAgent != null) {
@@ -258,31 +312,57 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
return httpRequest;
}
- private DefaultHttpRequest createHttpRequest() {
- // Regarding request-target URI:
- // RFC https://tools.ietf.org/html/rfc7230#section-5.3.2
- // would allow url.toExternalForm as absolute-form,
- // but some servers do not support that. So, we create origin-form.
- // But for HTTP/2, we should create the absolute-form, otherwise
- // netty will throw "java.lang.IllegalArgumentException: :scheme must be specified."
- String requestTarget = toOriginForm(url);
- return body == null ?
- new DefaultHttpRequest(httpVersion, httpMethod, requestTarget) :
- new DefaultFullHttpRequest(httpVersion, httpMethod, requestTarget, body);
+ @Override
+ public HttpRequestContext execute() {
+ if (httpRequest == null) {
+ httpRequest = build();
+ }
+ if (httpResponseListener == null) {
+ httpResponseListener = httpRequestContext;
+ }
+ httpRequestContext = new HttpRequestContext(uri, httpRequest, streamId,
+ new AtomicBoolean(false),
+ new AtomicBoolean(false),
+ timeout, System.currentTimeMillis(),
+ followRedirect, maxRedirects, new AtomicInteger(0),
+ new CountDownLatch(1),
+ httpResponseListener,
+ exceptionListener,
+ httpHeadersListener,
+ cookieListener,
+ httpPushListener);
+ // copy cookie(s) to context, will be added later to headers in dispatch (because of auto-cookie setting while redirect)
+ if (!cookies.isEmpty()) {
+ for (Cookie cookie : cookies) {
+ httpRequestContext.addCookie(cookie);
+ }
+ }
+ httpClient.dispatch(httpRequestContext);
+ return httpRequestContext;
}
- private String toOriginForm(URL base) {
+ @Override
+ public CompletableFuture execute(Function supplier) {
+ final CompletableFuture completableFuture = new CompletableFuture<>();
+ onResponse(response -> completableFuture.complete(supplier.apply(response)));
+ onException(completableFuture::completeExceptionally);
+ execute();
+ return completableFuture;
+ }
+
+ private DefaultHttpRequest createHttpRequest() {
+ String requestTarget = toOriginForm();
+ logger.log(Level.FINE, () -> "origin form is " + requestTarget);
+ return content == null ?
+ new DefaultHttpRequest(httpVersion, httpMethod, requestTarget) :
+ new DefaultFullHttpRequest(httpVersion, httpMethod, requestTarget, content);
+ }
+
+ private String toOriginForm() {
StringBuilder sb = new StringBuilder();
- String path = base.getPath() != null && !base.getPath().isEmpty() ? base.getPath() : "/";
- String query = base.getQuery();
- String ref = base.getRef();
- if (path.charAt(0) != '/') {
- sb.append('/');
- }
- sb.append(path);
- if (query != null && !query.isEmpty()) {
- sb.append('?').append(query);
- }
+ String pathAndQuery = queryStringEncoder.toString();
+ sb.append(pathAndQuery.isEmpty() ? "/" : pathAndQuery);
+ String ref = uri.getFragment();
if (ref != null && !ref.isEmpty()) {
sb.append('#').append(ref);
}
@@ -293,60 +373,18 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
headers.add(name, value);
}
- private void setBody(CharSequence charSequence, AsciiString contentType) throws IOException {
- setBody(charSequence.toString().getBytes(CharsetUtil.UTF_8), contentType);
+ private void content(CharSequence charSequence, AsciiString contentType) throws IOException {
+ content(charSequence.toString().getBytes(CharsetUtil.UTF_8), contentType);
}
- private void setBody(byte[] buf, AsciiString contentType) throws IOException {
+ private void content(byte[] buf, AsciiString contentType) throws IOException {
ByteBuf buffer = byteBufAllocator.buffer(buf.length).writeBytes(buf);
- setBody(buffer, contentType);
+ content(buffer, contentType);
}
- private void setBody(ByteBuf body, AsciiString contentType) throws IOException {
- this.body = body;
+ private void content(ByteBuf body, AsciiString contentType) throws IOException {
+ this.content = body;
addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
}
-
- @Override
- public HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener) {
- this.httpResponseListener = httpResponseListener;
- return this;
- }
-
- @Override
- public HttpRequestBuilder onError(ExceptionListener exceptionListener) {
- this.exceptionListener = exceptionListener;
- return this;
- }
-
- @Override
- public HttpRequestContext execute() {
- if (httpRequest == null) {
- httpRequest = build();
- }
- if (httpRequestContext == null) {
- httpRequestContext = new HttpRequestContext(getURL(),
- httpRequest,
- new AtomicBoolean(false),
- new AtomicBoolean(false),
- getTimeout(), System.currentTimeMillis(),
- isFollowRedirect(), getMaxRedirects(), new AtomicInteger(0),
- new CountDownLatch(1), streamId.get());
- }
- if (httpResponseListener == null) {
- httpResponseListener = httpRequestContext;
- }
- httpClient.dispatch(httpRequestContext, httpResponseListener, exceptionListener);
- return httpRequestContext;
- }
-
- @Override
- public CompletableFuture execute(Function supplier) {
- final CompletableFuture completableFuture = new CompletableFuture<>();
- onResponse(response -> completableFuture.complete(supplier.apply(response)));
- onError(completableFuture::completeExceptionally);
- execute();
- return completableFuture;
- }
}
diff --git a/src/main/java/org/xbib/netty/http/client/Http1Handler.java b/src/main/java/org/xbib/netty/http/client/HttpHandler.java
similarity index 69%
rename from src/main/java/org/xbib/netty/http/client/Http1Handler.java
rename to src/main/java/org/xbib/netty/http/client/HttpHandler.java
index 7f2d84e..b33aedc 100755
--- a/src/main/java/org/xbib/netty/http/client/Http1Handler.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpHandler.java
@@ -20,21 +20,29 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.pool.ChannelPool;
import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
+import io.netty.handler.codec.http.cookie.Cookie;
+import org.xbib.netty.http.client.listener.CookieListener;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.listener.HttpHeadersListener;
+import org.xbib.netty.http.client.listener.HttpResponseListener;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
- * Netty channel handler for HTTP 1.1.
+ * HTTP 1.x Netty channel handler.
*/
@ChannelHandler.Sharable
-final class Http1Handler extends ChannelInboundHandlerAdapter {
+final class HttpHandler extends ChannelInboundHandlerAdapter {
- private static final Logger logger = Logger.getLogger(Http1Handler.class.getName());
+ private static final Logger logger = Logger.getLogger(HttpHandler.class.getName());
private final HttpClient httpClient;
- Http1Handler(HttpClient httpClient) {
+ HttpHandler(HttpClient httpClient) {
this.httpClient = httpClient;
}
@@ -52,16 +60,34 @@ final class Http1Handler extends ChannelInboundHandlerAdapter {
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
if (msg instanceof FullHttpResponse) {
FullHttpResponse httpResponse = (FullHttpResponse) msg;
+ HttpHeaders httpHeaders = httpResponse.headers();
+ HttpHeadersListener httpHeadersListener =
+ ctx.channel().attr(HttpClientChannelContext.HEADER_LISTENER_ATTRIBUTE_KEY).get();
+ if (httpHeadersListener != null) {
+ logger.log(Level.FINE, () -> "firing onHeaders");
+ httpHeadersListener.onHeaders(httpHeaders);
+ }
+ CookieListener cookieListener =
+ ctx.channel().attr(HttpClientChannelContext.COOKIE_LISTENER_ATTRIBUTE_KEY).get();
+ for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) {
+ Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
+ httpRequestContext.addCookie(cookie);
+ if (cookieListener != null) {
+ logger.log(Level.FINE, () -> "firing onCookie");
+ cookieListener.onCookie(cookie);
+ }
+ }
HttpResponseListener httpResponseListener =
ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
if (httpResponseListener != null) {
+ logger.log(Level.FINE, () -> "firing onResponse");
httpResponseListener.onResponse(httpResponse);
}
+ logger.log(Level.FINE, () -> "trying redirect");
if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
return;
}
- logger.log(Level.FINE, () -> "success");
- httpRequestContext.success("response arrived");
+ httpRequestContext.success("response finished");
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java
index 49ee16a..ed8071a 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java
@@ -18,6 +18,12 @@ package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.cookie.Cookie;
+import org.xbib.netty.http.client.listener.CookieListener;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.listener.HttpPushListener;
+import org.xbib.netty.http.client.listener.HttpHeadersListener;
+import org.xbib.netty.http.client.listener.HttpResponseListener;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
@@ -39,6 +45,10 @@ public interface HttpRequestBuilder {
HttpRequestBuilder removeHeader(String name);
+ HttpRequestBuilder addParam(String name, String value);
+
+ HttpRequestBuilder addCookie(Cookie cookie);
+
HttpRequestBuilder contentType(String contentType);
HttpRequestBuilder acceptGzip(boolean gzip);
@@ -49,7 +59,7 @@ public interface HttpRequestBuilder {
HttpRequestBuilder setUserAgent(String userAgent);
- HttpRequestBuilder setBody(CharSequence charSequence, String contentType) throws IOException;
+ HttpRequestBuilder content(CharSequence charSequence, String contentType) throws IOException;
HttpRequestBuilder text(String text) throws IOException;
@@ -57,14 +67,20 @@ public interface HttpRequestBuilder {
HttpRequestBuilder xml(String xmlText) throws IOException;
- HttpRequestBuilder setBody(byte[] buf, String contentType) throws IOException;
+ HttpRequestBuilder content(byte[] buf, String contentType) throws IOException;
- HttpRequestBuilder setBody(ByteBuf body, String contentType) throws IOException;
+ HttpRequestBuilder content(ByteBuf body, String contentType) throws IOException;
- HttpRequestBuilder onError(ExceptionListener exceptionListener);
+ HttpRequestBuilder onHeaders(HttpHeadersListener httpHeadersListener);
+
+ HttpRequestBuilder onCookie(CookieListener cookieListener);
HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener);
+ HttpRequestBuilder onException(ExceptionListener exceptionListener);
+
+ HttpRequestBuilder onPushReceived(HttpPushListener httpPushListener);
+
HttpRequest build();
HttpRequestContext execute();
diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java b/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java
index 4fc8716..6f2578d 100755
--- a/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java
+++ b/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java
@@ -15,16 +15,32 @@
*/
package org.xbib.netty.http.client;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.cookie.Cookie;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.util.internal.PlatformDependent;
+import org.xbib.netty.http.client.listener.CookieListener;
+import org.xbib.netty.http.client.listener.ExceptionListener;
+import org.xbib.netty.http.client.listener.HttpPushListener;
+import org.xbib.netty.http.client.listener.HttpHeadersListener;
+import org.xbib.netty.http.client.listener.HttpResponseListener;
+import org.xbib.netty.http.client.util.LimitedHashSet;
-import java.net.URL;
+import java.net.URI;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
/**
*
@@ -33,7 +49,7 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
private static final Logger logger = Logger.getLogger(HttpRequestContext.class.getName());
- private final URL url;
+ private final URI uri;
private final HttpRequest httpRequest;
@@ -53,19 +69,43 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
private final CountDownLatch latch;
- private final Integer streamId;
+ private final AtomicInteger streamId;
- private FullHttpResponse httpResponse;
+ private final HttpResponseListener httpResponseListener;
+
+ private final ExceptionListener exceptionListener;
+
+ private final HttpHeadersListener httpHeadersListener;
+
+ private final CookieListener cookieListener;
+
+ private final HttpPushListener httpPushListener;
+
+ private final Map> promiseMap;
+
+ private final Map> pushMap;
+
+ private ChannelPromise settingsPromise;
+
+ private Collection cookies;
+
+ private Map httpResponses;
private Long stopTime;
- HttpRequestContext(URL url, HttpRequest httpRequest,
+ HttpRequestContext(URI uri, HttpRequest httpRequest, AtomicInteger streamId,
AtomicBoolean succeeded, AtomicBoolean failed,
int timeout, Long startTime,
boolean followRedirect, int maxRedirects, AtomicInteger redirectCount,
- CountDownLatch latch, Integer streamId) {
- this.url = url;
+ CountDownLatch latch,
+ HttpResponseListener httpResponseListener,
+ ExceptionListener exceptionListener,
+ HttpHeadersListener httpHeadersListener,
+ CookieListener cookieListener,
+ HttpPushListener httpPushListener) {
+ this.uri = uri;
this.httpRequest = httpRequest;
+ this.streamId = streamId;
this.succeeded = succeeded;
this.failed = failed;
this.timeout = timeout;
@@ -74,12 +114,24 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
this.maxRedirects = maxRedirects;
this.redirectCount = redirectCount;
this.latch = latch;
- this.streamId = streamId;
+ this.httpResponseListener = httpResponseListener;
+ this.exceptionListener = exceptionListener;
+ this.httpHeadersListener = httpHeadersListener;
+ this.cookieListener = cookieListener;
+ this.httpPushListener = httpPushListener;
+ this.promiseMap = PlatformDependent.newConcurrentHashMap();
+ this.pushMap = PlatformDependent.newConcurrentHashMap();
+ this.cookies = new LimitedHashSet<>(10);
}
- HttpRequestContext(URL url, HttpRequest httpRequest, HttpRequestContext httpRequestContext) {
- this.url = url;
+ /**
+ * A follow-up request to a given context with same stream ID (redirect).
+ *
+ */
+ HttpRequestContext(URI uri, HttpRequest httpRequest, HttpRequestContext httpRequestContext) {
+ this.uri = uri;
this.httpRequest = httpRequest;
+ this.streamId = httpRequestContext.streamId;
this.succeeded = httpRequestContext.succeeded;
this.failed = httpRequestContext.failed;
this.failed.lazySet(false); // reset
@@ -89,17 +141,105 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
this.maxRedirects = httpRequestContext.maxRedirects;
this.redirectCount = httpRequestContext.redirectCount;
this.latch = httpRequestContext.latch;
- this.streamId = httpRequestContext.streamId;
+ this.httpResponseListener = httpRequestContext.httpResponseListener;
+ this.exceptionListener = httpRequestContext.exceptionListener;
+ this.httpHeadersListener = httpRequestContext.httpHeadersListener;
+ this.cookieListener = httpRequestContext.cookieListener;
+ this.httpPushListener = httpRequestContext.httpPushListener;
+ this.promiseMap = httpRequestContext.promiseMap;
+ this.pushMap = httpRequestContext.pushMap;
+ this.cookies = httpRequestContext.cookies;
}
- public URL getURL() {
- return url;
+ public URI getURI() {
+ return uri;
}
public HttpRequest getHttpRequest() {
return httpRequest;
}
+ public HttpResponseListener getHttpResponseListener() {
+ return httpResponseListener;
+ }
+
+ public ExceptionListener getExceptionListener() {
+ return exceptionListener;
+ }
+
+ public HttpHeadersListener getHttpHeadersListener() {
+ return httpHeadersListener;
+ }
+
+ public CookieListener getCookieListener() {
+ return cookieListener;
+ }
+
+ public HttpPushListener getHttpPushListener() {
+ return httpPushListener;
+ }
+
+ public void setSettingsPromise(ChannelPromise settingsPromise) {
+ this.settingsPromise = settingsPromise;
+ }
+
+ public ChannelPromise getSettingsPromise() {
+ return settingsPromise;
+ }
+
+ public Map> getStreamIdPromiseMap() {
+ return promiseMap;
+ }
+
+ public void putStreamID(Integer streamId, ChannelFuture channelFuture, ChannelPromise channelPromise) {
+ logger.log(Level.FINE, () -> "put stream ID " + streamId + " future = " + channelFuture);
+ promiseMap.put(streamId, new AbstractMap.SimpleEntry<>(channelFuture, channelPromise));
+ }
+
+ public Map> getPushMap() {
+ return pushMap;
+ }
+
+ public void receiveStreamID(Integer streamId, Http2Headers headers, ChannelPromise channelPromise) {
+ logger.log(Level.FINE, () -> "receive stream ID " + streamId + " " + headers);
+ pushMap.put(streamId, new AbstractMap.SimpleEntry<>(headers, channelPromise));
+ }
+
+ public boolean isFinished() {
+ return promiseMap.isEmpty() && pushMap.isEmpty();
+ }
+
+ public void addCookie(Cookie cookie) {
+ cookies.add(cookie);
+ }
+
+ public Collection getCookies() {
+ return cookies;
+ }
+
+ public List matchCookies() {
+ return cookies.stream()
+ .filter(this::matchCookie)
+ .collect(Collectors.toList());
+ }
+
+ private boolean matchCookie(Cookie cookie) {
+ boolean domainMatch = cookie.domain() == null || uri.getHost().endsWith(cookie.domain());
+ if (!domainMatch) {
+ return false;
+ }
+ boolean pathMatch = "/".equals(cookie.path()) || uri.getPath().startsWith(cookie.path());
+ if (!pathMatch) {
+ return false;
+ }
+ boolean secure = "https".equals(uri.getScheme());
+ boolean secureMatch = (secure && cookie.isSecure()) || (!secure && !cookie.isSecure());
+ if (!secureMatch) {
+ return false;
+ }
+ return true;
+ }
+
public int getTimeout() {
return timeout;
}
@@ -144,7 +284,7 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
return latch;
}
- public Integer getStreamId() {
+ public AtomicInteger getStreamId() {
return streamId;
}
@@ -162,12 +302,15 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
logger.log(Level.FINE, () -> "success because of " + reason);
if (succeeded.compareAndSet(false, true)) {
latch.countDown();
-
}
}
public void fail(String reason) {
logger.log(Level.FINE, () -> "failed because of " + reason);
+ IllegalStateException exception = new IllegalStateException(reason);
+ if (exceptionListener != null) {
+ exceptionListener.onException(exception);
+ }
if (failed.compareAndSet(false, true)) {
latch.countDown();
}
@@ -175,11 +318,10 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
@Override
public void onResponse(FullHttpResponse fullHttpResponse) {
- this.httpResponse = fullHttpResponse;
+ this.httpResponses.put(streamId.get(), fullHttpResponse);
}
- public FullHttpResponse getHttpResponse() {
- return httpResponse;
+ public Map getHttpResponses() {
+ return httpResponses;
}
-
}
diff --git a/src/main/java/org/xbib/netty/http/client/TrafficLoggingHandler.java b/src/main/java/org/xbib/netty/http/client/TrafficLoggingHandler.java
deleted file mode 100644
index 1a1940e..0000000
--- a/src/main/java/org/xbib/netty/http/client/TrafficLoggingHandler.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2017 Jörg Prante
- *
- * Jörg Prante 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 org.xbib.netty.http.client;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelPromise;
-import io.netty.handler.logging.LogLevel;
-import io.netty.handler.logging.LoggingHandler;
-
-/**
- * A Netty handler that logs the I/O traffic of a connection.
- */
-public final class TrafficLoggingHandler extends LoggingHandler {
-
- public TrafficLoggingHandler() {
- super("client", LogLevel.TRACE);
- }
-
- @Override
- public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
- ctx.fireChannelRegistered();
- }
-
- @Override
- public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
- ctx.fireChannelUnregistered();
- }
-
- @Override
- public void flush(ChannelHandlerContext ctx) throws Exception {
- ctx.flush();
- }
-
- @Override
- public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
- if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
- ctx.write(msg, promise);
- } else {
- super.write(ctx, msg, promise);
- }
- }
-}
diff --git a/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java b/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java
new file mode 100644
index 0000000..718efed
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java
@@ -0,0 +1,11 @@
+package org.xbib.netty.http.client.listener;
+
+import io.netty.handler.codec.http.cookie.Cookie;
+
+/**
+ */
+@FunctionalInterface
+public interface CookieListener {
+
+ void onCookie(Cookie cookie);
+}
diff --git a/src/main/java/org/xbib/netty/http/client/ExceptionListener.java b/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java
similarity index 94%
rename from src/main/java/org/xbib/netty/http/client/ExceptionListener.java
rename to src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java
index 4adc333..ee011dc 100644
--- a/src/main/java/org/xbib/netty/http/client/ExceptionListener.java
+++ b/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-package org.xbib.netty.http.client;
+package org.xbib.netty.http.client.listener;
/**
*/
diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java b/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java
new file mode 100644
index 0000000..91c0cd1
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client.listener;
+
+import io.netty.handler.codec.http.HttpHeaders;
+
+/**
+ */
+@FunctionalInterface
+public interface HttpHeadersListener {
+
+ void onHeaders(HttpHeaders httpHeaders);
+}
diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java b/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java
new file mode 100644
index 0000000..24a5e6e
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client.listener;
+
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http2.Http2Headers;
+
+/**
+ * This listener can forward HTTP push
+ *
+ */
+@FunctionalInterface
+public interface HttpPushListener {
+
+ void onPushReceived(Http2Headers headers, FullHttpResponse fullHttpResponse);
+}
diff --git a/src/main/java/org/xbib/netty/http/client/HttpResponseListener.java b/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java
similarity index 94%
rename from src/main/java/org/xbib/netty/http/client/HttpResponseListener.java
rename to src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java
index b462987..a71f3b6 100644
--- a/src/main/java/org/xbib/netty/http/client/HttpResponseListener.java
+++ b/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-package org.xbib.netty.http.client;
+package org.xbib.netty.http.client.listener;
import io.netty.handler.codec.http.FullHttpResponse;
diff --git a/src/main/java/org/xbib/netty/http/client/listener/package-info.java b/src/main/java/org/xbib/netty/http/client/listener/package-info.java
new file mode 100644
index 0000000..b1e55f4
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/listener/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Listeners for Netty HTTP client.
+ */
+package org.xbib.netty.http.client.listener;
diff --git a/src/main/java/org/xbib/netty/http/client/SslClientAuthMode.java b/src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java
similarity index 83%
rename from src/main/java/org/xbib/netty/http/client/SslClientAuthMode.java
rename to src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java
index 0de5849..008fc2d 100644
--- a/src/main/java/org/xbib/netty/http/client/SslClientAuthMode.java
+++ b/src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java
@@ -13,11 +13,11 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-package org.xbib.netty.http.client;
+package org.xbib.netty.http.client.util;
/**
- *
+ * Client authentication modes, useful for SSL channels.
*/
-public enum SslClientAuthMode {
+public enum ClientAuthMode {
NONE, WANT, NEED
}
diff --git a/src/main/java/org/xbib/netty/http/client/InetAddressKey.java b/src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java
similarity index 57%
rename from src/main/java/org/xbib/netty/http/client/InetAddressKey.java
rename to src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java
index acb98b1..07b59f3 100644
--- a/src/main/java/org/xbib/netty/http/client/InetAddressKey.java
+++ b/src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java
@@ -13,66 +13,64 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-package org.xbib.netty.http.client;
+package org.xbib.netty.http.client.util;
import io.netty.handler.codec.http.HttpVersion;
import java.net.InetSocketAddress;
-import java.net.URL;
/**
+ * A key for host, port, HTTP version, and secure transport mode of a channel for HTTP.
*/
public class InetAddressKey {
- private final InetSocketAddress inetSocketAddress;
+ private final String host;
+
+ private final int port;
private final HttpVersion version;
private final Boolean secure;
- InetAddressKey(URL url, HttpVersion version) {
- this.version = version;
- String protocol = url.getProtocol();
- this.secure = "https".equals(protocol);
- int port = url.getPort();
- if (port == -1) {
- port = "http".equals(protocol) ? 80 : (secure ? 443 : -1);
- }
- this.inetSocketAddress = new InetSocketAddress(url.getHost(), port);
- }
+ private InetSocketAddress inetSocketAddress;
- InetAddressKey(InetSocketAddress inetSocketAddress, HttpVersion version, boolean secure) {
- this.inetSocketAddress = inetSocketAddress;
+ public InetAddressKey(String host, int port, HttpVersion version, boolean secure) {
+ this.host = host;
+ this.port = port == -1 ? secure ? 443 : 80 : port;
this.version = version;
this.secure = secure;
}
- InetSocketAddress getInetSocketAddress() {
+ public InetSocketAddress getInetSocketAddress() {
+ if (inetSocketAddress == null) {
+ this.inetSocketAddress = new InetSocketAddress(host, port);
+ }
return inetSocketAddress;
}
- HttpVersion getVersion() {
+ public HttpVersion getVersion() {
return version;
}
- boolean isSecure() {
+ public boolean isSecure() {
return secure;
}
public String toString() {
- return inetSocketAddress + " (version:" + version + ",secure:" + secure + ")";
+ return host + ":" + port + " (version:" + version + ",secure:" + secure + ")";
}
@Override
public boolean equals(Object object) {
return object instanceof InetAddressKey &&
- inetSocketAddress.equals(((InetAddressKey) object).inetSocketAddress) &&
+ host.equals(((InetAddressKey) object).host) &&
+ port == ((InetAddressKey) object).port &&
version.equals(((InetAddressKey) object).version) &&
secure == ((InetAddressKey) object).secure;
}
@Override
public int hashCode() {
- return inetSocketAddress.hashCode() ^ version.hashCode() ^ secure.hashCode();
+ return host.hashCode() ^ port ^ version.hashCode() ^ secure.hashCode();
}
}
diff --git a/src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java b/src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java
new file mode 100644
index 0000000..c4fb2fd
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client.util;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+/**
+ * A {@link java.util.Set} with limited size. If the size is exceeded, an exception is thrown.
+ */
+public final class LimitedHashSet extends LinkedHashSet {
+
+ private static final long serialVersionUID = 1838128758142912702L;
+
+ private final int max;
+
+ public LimitedHashSet(int max) {
+ this.max = max;
+ }
+
+ @Override
+ public boolean add(E element) {
+ if (max < size()) {
+ throw new IllegalStateException("limit exceeded");
+ }
+ return super.add(element);
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> elements) {
+ boolean b = false;
+ for (E element : elements) {
+ if (max < size()) {
+ throw new IllegalStateException("limit exceeded");
+ }
+ b = b || super.add(element);
+ }
+ return b;
+ }
+}
diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java b/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java
new file mode 100644
index 0000000..c571627
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client.util;
+
+/**
+ * The network classes.
+ */
+public enum NetworkClass {
+
+ ANY, LOOPBACK, LOCAL, PUBLIC
+}
diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java b/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java
new file mode 100644
index 0000000..03baecc
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client.util;
+
+/**
+ * The TCP/IP network protocol versions.
+ */
+public enum NetworkProtocolVersion {
+
+ IPV4, IPV6, IPV46, NONE
+}
diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java b/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java
new file mode 100644
index 0000000..684f051
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright 2017 Jörg Prante
+ *
+ * Jörg Prante 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 org.xbib.netty.http.client.util;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Helper class for Java networking.
+ */
+public class NetworkUtils {
+
+ private static final Logger logger = Logger.getLogger(NetworkUtils.class.getName());
+
+ private static final String lf = System.lineSeparator();
+
+ private static final char[] hexDigit = new char[]{
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ private static final String IPV4_SETTING = "java.net.preferIPv4Stack";
+
+ private static final String IPV6_SETTING = "java.net.preferIPv6Addresses";
+
+ private static InetAddress localAddress;
+
+ public static void extendSystemProperties() {
+ InetAddress address;
+ try {
+ address = InetAddress.getLocalHost();
+ } catch (Exception e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ address = InetAddress.getLoopbackAddress();
+ }
+ localAddress = address;
+ try {
+ Map map = new HashMap<>();
+ map.put("net.localhost", address.getCanonicalHostName());
+ String hostname = address.getHostName();
+ map.put("net.hostname", hostname);
+ InetAddress[] hostnameAddresses = InetAddress.getAllByName(hostname);
+ int i = 0;
+ for (InetAddress hostnameAddress : hostnameAddresses) {
+ map.put("net.hostaddress." + (i++), hostnameAddress.getCanonicalHostName());
+ }
+ for (NetworkInterface networkInterface : getAllRunningAndUpInterfaces()) {
+ InetAddress inetAddress = getFirstNonLoopbackAddress(networkInterface, NetworkProtocolVersion.IPV4);
+ if (inetAddress != null) {
+ map.put("net." + networkInterface.getDisplayName(), inetAddress.getCanonicalHostName());
+ }
+ }
+ logger.log(Level.FINE, "found network properties for system properties: " + map);
+ for (Map.Entry entry : map.entrySet()) {
+ System.setProperty(entry.getKey(), entry.getValue());
+ }
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ }
+ }
+
+ private NetworkUtils() {
+ }
+
+ public static boolean isPreferIPv4() {
+ return Boolean.getBoolean(System.getProperty(IPV4_SETTING));
+ }
+
+ public static boolean isPreferIPv6() {
+ return Boolean.getBoolean(System.getProperty(IPV6_SETTING));
+ }
+
+ public static InetAddress getIPv4Localhost() throws UnknownHostException {
+ return getLocalhost(NetworkProtocolVersion.IPV4);
+ }
+
+ public static InetAddress getIPv6Localhost() throws UnknownHostException {
+ return getLocalhost(NetworkProtocolVersion.IPV6);
+ }
+
+ public static InetAddress getLocalhost(NetworkProtocolVersion ipversion) throws UnknownHostException {
+ return ipversion == NetworkProtocolVersion.IPV4 ?
+ InetAddress.getByName("127.0.0.1") : InetAddress.getByName("::1");
+ }
+
+ public static String getLocalHostName(String defaultHostName) {
+ if (localAddress == null) {
+ return defaultHostName;
+ }
+ String hostName = localAddress.getHostName();
+ if (hostName == null) {
+ return defaultHostName;
+ }
+ return hostName;
+ }
+
+ public static String getLocalHostAddress(String defaultHostAddress) {
+ if (localAddress == null) {
+ return defaultHostAddress;
+ }
+ String hostAddress = localAddress.getHostAddress();
+ if (hostAddress == null) {
+ return defaultHostAddress;
+ }
+ return hostAddress;
+ }
+
+ public static InetAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ public static NetworkClass getNetworkClass(InetAddress address) {
+ if (address == null || address.isAnyLocalAddress()) {
+ return NetworkClass.ANY;
+ }
+ if (address.isLoopbackAddress()) {
+ return NetworkClass.LOOPBACK;
+ }
+ if (address.isLinkLocalAddress() || address.isSiteLocalAddress()) {
+ return NetworkClass.LOCAL;
+ }
+ return NetworkClass.PUBLIC;
+ }
+
+ public static String format(InetAddress address) {
+ return format(address, -1);
+ }
+
+ public static String format(InetSocketAddress address) {
+ return format(address.getAddress(), address.getPort());
+ }
+
+ public static String format(InetAddress address, int port) {
+ Objects.requireNonNull(address);
+ StringBuilder sb = new StringBuilder();
+ if (port != -1 && address instanceof Inet6Address) {
+ sb.append(toUriString(address));
+ } else {
+ sb.append(toAddrString(address));
+ }
+ if (port != -1) {
+ sb.append(':').append(port);
+ }
+ return sb.toString();
+ }
+
+ public static String toUriString(InetAddress ip) {
+ if (ip instanceof Inet6Address) {
+ return "[" + toAddrString(ip) + "]";
+ }
+ return toAddrString(ip);
+ }
+
+ public static String toAddrString(InetAddress ip) {
+ if (ip == null) {
+ throw new NullPointerException("ip");
+ }
+ if (ip instanceof Inet4Address) {
+ byte[] bytes = ip.getAddress();
+ return (bytes[0] & 0xff) + "." + (bytes[1] & 0xff) + "." + (bytes[2] & 0xff) + "." + (bytes[3] & 0xff);
+ }
+ if (!(ip instanceof Inet6Address)) {
+ throw new IllegalArgumentException("ip");
+ }
+ byte[] bytes = ip.getAddress();
+ int[] hextets = new int[8];
+ for (int i = 0; i < hextets.length; i++) {
+ hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255;
+ }
+ compressLongestRunOfZeroes(hextets);
+ return hextetsToIPv6String(hextets);
+ }
+
+ public static boolean matchesNetwork(NetworkClass given, NetworkClass expected) {
+ switch (expected) {
+ case ANY:
+ return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.PUBLIC, NetworkClass.ANY).contains(given);
+ case PUBLIC:
+ return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.PUBLIC).contains(given);
+ case LOCAL:
+ return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL).contains(given);
+ case LOOPBACK:
+ return NetworkClass.LOOPBACK == given;
+ }
+ return false;
+ }
+
+ public static InetAddress getFirstNonLoopbackAddress(NetworkProtocolVersion ipversion) {
+ InetAddress address;
+ for (NetworkInterface networkInterface : getAllNetworkInterfaces()) {
+ try {
+ if (!networkInterface.isUp() || networkInterface.isLoopback()) {
+ continue;
+ }
+ } catch (Exception e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ continue;
+ }
+ address = getFirstNonLoopbackAddress(networkInterface, ipversion);
+ if (address != null) {
+ return address;
+ }
+ }
+ return null;
+ }
+
+ public static InetAddress getFirstNonLoopbackAddress(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) {
+ if (networkInterface == null) {
+ throw new IllegalArgumentException("network interface is null");
+ }
+ for (Enumeration addresses = networkInterface.getInetAddresses(); addresses.hasMoreElements(); ) {
+ InetAddress address = addresses.nextElement();
+ if (!address.isLoopbackAddress() && (address instanceof Inet4Address && ipVersion == NetworkProtocolVersion.IPV4) ||
+ (address instanceof Inet6Address && ipVersion == NetworkProtocolVersion.IPV6)) {
+ return address;
+ }
+ }
+ return null;
+ }
+
+ public static InetAddress getFirstAddress(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) {
+ if (networkInterface == null) {
+ throw new IllegalArgumentException("network interface is null");
+ }
+ for (Enumeration addresses = networkInterface.getInetAddresses(); addresses.hasMoreElements(); ) {
+ InetAddress address = addresses.nextElement();
+ if ((address instanceof Inet4Address && ipVersion == NetworkProtocolVersion.IPV4) ||
+ (address instanceof Inet6Address && ipVersion == NetworkProtocolVersion.IPV6)) {
+ return address;
+ }
+ }
+ return null;
+ }
+
+ public static boolean interfaceSupports(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) {
+ boolean supportsVersion = false;
+ if (networkInterface != null) {
+ Enumeration addresses = networkInterface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress address = addresses.nextElement();
+ if ((address instanceof Inet4Address && (ipVersion == NetworkProtocolVersion.IPV4)) ||
+ (address instanceof Inet6Address && (ipVersion == NetworkProtocolVersion.IPV6))) {
+ supportsVersion = true;
+ break;
+ }
+ }
+ }
+ return supportsVersion;
+ }
+
+ public static NetworkProtocolVersion getProtocolVersion() {
+ switch (findAvailableProtocols()) {
+ case IPV4:
+ return NetworkProtocolVersion.IPV4;
+ case IPV6:
+ return NetworkProtocolVersion.IPV6;
+ case IPV46:
+ if (Boolean.getBoolean(System.getProperty(IPV4_SETTING))) {
+ return NetworkProtocolVersion.IPV4;
+ }
+ if (Boolean.getBoolean(System.getProperty(IPV6_SETTING))) {
+ return NetworkProtocolVersion.IPV6;
+ }
+ return NetworkProtocolVersion.IPV6;
+ default:
+ break;
+ }
+ return NetworkProtocolVersion.NONE;
+ }
+
+ public static NetworkProtocolVersion findAvailableProtocols() {
+ boolean hasIPv4 = false;
+ boolean hasIPv6 = false;
+ for (InetAddress addr : getAllAvailableAddresses()) {
+ if (addr instanceof Inet4Address) {
+ hasIPv4 = true;
+ }
+ if (addr instanceof Inet6Address) {
+ hasIPv6 = true;
+ }
+ }
+ if (hasIPv4 && hasIPv6) {
+ return NetworkProtocolVersion.IPV46;
+ }
+ if (hasIPv4) {
+ return NetworkProtocolVersion.IPV4;
+ }
+ if (hasIPv6) {
+ return NetworkProtocolVersion.IPV6;
+ }
+ return NetworkProtocolVersion.NONE;
+ }
+
+ public static InetAddress resolveInetAddress(String hostname, String defaultValue) throws IOException {
+ String host = hostname;
+ if (host == null) {
+ host = defaultValue;
+ }
+ String origHost = host;
+ int pos = host.indexOf(':');
+ if (pos > 0) {
+ host = host.substring(0, pos - 1);
+ }
+ if ((host.startsWith("#") && host.endsWith("#")) || (host.startsWith("_") && host.endsWith("_"))) {
+ host = host.substring(1, host.length() - 1);
+ if ("local".equals(host)) {
+ return getLocalAddress();
+ } else if (host.startsWith("non_loopback")) {
+ if (host.toLowerCase(Locale.ROOT).endsWith(":ipv4")) {
+ return getFirstNonLoopbackAddress(NetworkProtocolVersion.IPV4);
+ } else if (host.toLowerCase(Locale.ROOT).endsWith(":ipv6")) {
+ return getFirstNonLoopbackAddress(NetworkProtocolVersion.IPV6);
+ } else {
+ return getFirstNonLoopbackAddress(getProtocolVersion());
+ }
+ } else {
+ NetworkProtocolVersion networkProtocolVersion = getProtocolVersion();
+ if (host.toLowerCase(Locale.ROOT).endsWith(":ipv4")) {
+ networkProtocolVersion = NetworkProtocolVersion.IPV4;
+ host = host.substring(0, host.length() - 5);
+ } else if (host.toLowerCase(Locale.ROOT).endsWith(":ipv6")) {
+ networkProtocolVersion = NetworkProtocolVersion.IPV6;
+ host = host.substring(0, host.length() - 5);
+ }
+ for (NetworkInterface ni : getInterfaces(NetworkUtils::isUp)) {
+ if (host.equals(ni.getName()) || host.equals(ni.getDisplayName())) {
+ if (ni.isLoopback()) {
+ return getFirstAddress(ni, networkProtocolVersion);
+ } else {
+ return getFirstNonLoopbackAddress(ni, networkProtocolVersion);
+ }
+ }
+ }
+ }
+ throw new IOException("failed to find network interface for [" + origHost + "]");
+ }
+ return InetAddress.getByName(host);
+ }
+
+ public static InetAddress resolvePublicHostAddress(String host) throws IOException {
+ InetAddress address = resolveInetAddress(host, null);
+ if (address == null || address.isAnyLocalAddress()) {
+ address = getFirstNonLoopbackAddress(NetworkProtocolVersion.IPV4);
+ if (address == null) {
+ address = getFirstNonLoopbackAddress(getProtocolVersion());
+ if (address == null) {
+ address = getLocalAddress();
+ if (address == null) {
+ return getLocalhost(NetworkProtocolVersion.IPV4);
+ }
+ }
+ }
+ }
+ return address;
+ }
+
+ private static List getAllNetworkInterfaces() {
+ return getInterfaces(n -> true);
+ }
+
+ public static List getAllRunningAndUpInterfaces() {
+ return getInterfaces(NetworkUtils::isUp);
+ }
+
+ public static List getInterfaces(Predicate predicate) {
+ List networkInterfaces = new ArrayList<>();
+ Enumeration interfaces;
+ try {
+ interfaces = NetworkInterface.getNetworkInterfaces();
+ } catch (Exception e) {
+ return networkInterfaces;
+ }
+ while (interfaces.hasMoreElements()) {
+ NetworkInterface networkInterface = interfaces.nextElement();
+ if (predicate.test(networkInterface)) {
+ networkInterfaces.add(networkInterface);
+ Enumeration subInterfaces = networkInterface.getSubInterfaces();
+ if (subInterfaces.hasMoreElements()) {
+ while (subInterfaces.hasMoreElements()) {
+ networkInterfaces.add(subInterfaces.nextElement());
+ }
+ }
+ }
+ }
+ sortInterfaces(networkInterfaces);
+ return networkInterfaces;
+ }
+
+ public static List getAllAvailableAddresses() {
+ List allAddresses = new ArrayList<>();
+ for (NetworkInterface networkInterface : getAllNetworkInterfaces()) {
+ Enumeration addrs = networkInterface.getInetAddresses();
+ while (addrs.hasMoreElements()) {
+ allAddresses.add(addrs.nextElement());
+ }
+ }
+ sortAddresses(allAddresses);
+ return allAddresses;
+ }
+
+ public static String displayNetworkInterfaces() {
+ StringBuilder sb = new StringBuilder();
+ for (NetworkInterface nic : getAllNetworkInterfaces()) {
+ sb.append(displayNetworkInterface(nic));
+ }
+ return sb.toString();
+ }
+
+ public static String displayNetworkInterface(NetworkInterface nic) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(lf).append(nic.getName()).append(lf);
+ if (!nic.getName().equals(nic.getDisplayName())) {
+ sb.append("\t").append(nic.getDisplayName()).append(lf);
+ }
+ sb.append("\t").append("flags ");
+ List flags = new ArrayList<>();
+ try {
+ if (nic.isUp()) {
+ flags.add("UP");
+ }
+ if (nic.supportsMulticast()) {
+ flags.add("MULTICAST");
+ }
+ if (nic.isLoopback()) {
+ flags.add("LOOPBACK");
+ }
+ if (nic.isPointToPoint()) {
+ flags.add("POINTTOPOINT");
+ }
+ if (nic.isVirtual()) {
+ flags.add("VIRTUAL");
+ }
+ } catch (Exception e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ }
+ sb.append(String.join(",", flags));
+ try {
+ sb.append(" mtu ").append(nic.getMTU()).append(lf);
+ } catch (SocketException e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ }
+ List addresses = nic.getInterfaceAddresses();
+ for (InterfaceAddress address : addresses) {
+ sb.append("\t").append(formatAddress(address)).append(lf);
+ }
+ try {
+ byte[] b = nic.getHardwareAddress();
+ if (b != null) {
+ sb.append("\t").append("ether ");
+ for (int i = 0; i < b.length; i++) {
+ if (i > 0) {
+ sb.append(":");
+ }
+ sb.append(hexDigit[(b[i] >> 4) & 0x0f]).append(hexDigit[b[i] & 0x0f]);
+ }
+ sb.append(lf);
+ }
+ } catch (SocketException e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ }
+ return sb.toString();
+ }
+
+ private static void sortInterfaces(List interfaces) {
+ interfaces.sort(Comparator.comparingInt(NetworkInterface::getIndex));
+ }
+
+ private static void sortAddresses(List addressList) {
+ addressList.sort((o1, o2) -> compareBytes(o1.getAddress(), o2.getAddress()));
+ }
+
+ private static String formatAddress(InterfaceAddress interfaceAddress) {
+ StringBuilder sb = new StringBuilder();
+ InetAddress address = interfaceAddress.getAddress();
+ if (address instanceof Inet6Address) {
+ sb.append("inet6 ").append(format(address))
+ .append(" prefixlen:").append(interfaceAddress.getNetworkPrefixLength());
+ } else {
+ int netmask = 0xFFFFFFFF << (32 - interfaceAddress.getNetworkPrefixLength());
+ byte[] b = new byte[] { (byte)(netmask >>> 24), (byte)(netmask >>> 16 & 0xFF),
+ (byte)(netmask >>> 8 & 0xFF), (byte)(netmask & 0xFF) };
+ sb.append("inet ").append(format(address));
+ try {
+ sb.append(" netmask:").append(format(InetAddress.getByAddress(b)));
+ } catch (UnknownHostException e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ }
+ InetAddress broadcast = interfaceAddress.getBroadcast();
+ if (broadcast != null) {
+ sb.append(" broadcast:").append(format(broadcast));
+ }
+ }
+ if (address.isLoopbackAddress()) {
+ sb.append(" scope:host");
+ } else if (address.isLinkLocalAddress()) {
+ sb.append(" scope:link");
+ } else if (address.isSiteLocalAddress()) {
+ sb.append(" scope:site");
+ }
+ return sb.toString();
+ }
+
+ private static boolean isUp(NetworkInterface networkInterface) {
+ try {
+ return networkInterface.isUp();
+ } catch (SocketException e) {
+ return false;
+ }
+ }
+
+ private static int compareBytes(byte[] left, byte[] right) {
+ for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) {
+ int a = left[i] & 0xff;
+ int b = right[j] & 0xff;
+ if (a != b) {
+ return a - b;
+ }
+ }
+ return left.length - right.length;
+ }
+
+ private static void compressLongestRunOfZeroes(int[] hextets) {
+ int bestRunStart = -1;
+ int bestRunLength = -1;
+ int runStart = -1;
+ for (int i = 0; i < hextets.length + 1; i++) {
+ if (i < hextets.length && hextets[i] == 0) {
+ if (runStart < 0) {
+ runStart = i;
+ }
+ } else if (runStart >= 0) {
+ int runLength = i - runStart;
+ if (runLength > bestRunLength) {
+ bestRunStart = runStart;
+ bestRunLength = runLength;
+ }
+ runStart = -1;
+ }
+ }
+ if (bestRunLength >= 2) {
+ Arrays.fill(hextets, bestRunStart, bestRunStart + bestRunLength, -1);
+ }
+ }
+
+ private static String hextetsToIPv6String(int[] hextets) {
+ StringBuilder sb = new StringBuilder(39);
+ boolean lastWasNumber = false;
+ for (int i = 0; i < hextets.length; i++) {
+ boolean b = hextets[i] >= 0;
+ if (b) {
+ if (lastWasNumber) {
+ sb.append(':');
+ }
+ sb.append(Integer.toHexString(hextets[i]));
+ } else {
+ if (i == 0 || lastWasNumber) {
+ sb.append("::");
+ }
+ }
+ lastWasNumber = b;
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/xbib/netty/http/client/util/package-info.java b/src/main/java/org/xbib/netty/http/client/util/package-info.java
new file mode 100644
index 0000000..403b658
--- /dev/null
+++ b/src/main/java/org/xbib/netty/http/client/util/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Utilities for Netty HTTP client.
+ */
+package org.xbib.netty.http.client.util;
diff --git a/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java b/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java
index baf2292..42df734 100644
--- a/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java
@@ -33,10 +33,9 @@ public class AkamaiTest {
private static final Logger logger = Logger.getLogger("");
@Test
- public void testAkamai() throws Exception {
+ public void testAkamaiHttps() throws Exception {
- // here we can not deal with server PUSH_PROMISE as response to headers, a go-away frame is written.
- // Probably because promised stream id is 2 which is smaller than 3?
+ // here we see server PUSH_PROMISE as response to headers, a go-away frame is written.
/*
----------------INBOUND--------------------
@@ -59,11 +58,17 @@ public class AkamaiTest {
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://http2.akamai.com/demo/h2_demo_frame.html")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
+ .onPushReceived((headers, fullHttpResponse) -> {
+ logger.log(Level.INFO, "received push promise: request headers = " + headers
+ + " status = " + fullHttpResponse.status()
+ + " response headers = " + fullHttpResponse.headers().entries()
+ );
+ })
.execute()
.get();
httpClient.close();
diff --git a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java b/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java
index 9300727..6298d3d 100644
--- a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java
@@ -15,7 +15,6 @@
*/
package org.xbib.netty.http.client.test;
-import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import org.xbib.netty.http.client.HttpRequestBuilder;
@@ -64,7 +63,7 @@ public class ElasticsearchTest {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
@@ -82,7 +81,7 @@ public class ElasticsearchTest {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
@@ -122,6 +121,6 @@ public class ElasticsearchTest {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e));
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e));
}
}
diff --git a/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java b/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java
index ddc4aee..cfa5b57 100644
--- a/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java
@@ -60,7 +60,7 @@ public class ExceptionTest {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
diff --git a/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java b/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java
index 9ad7564..57fe567 100644
--- a/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java
@@ -1,6 +1,5 @@
package org.xbib.netty.http.client.test;
-import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import org.xbib.netty.http.client.HttpRequestBuilder;
@@ -41,7 +40,8 @@ public class GoogleTest {
.build();
httpClient.prepareGet()
.setURL("http://www.google.com")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onHeaders(headers -> logger.log(Level.INFO, headers.toString()))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -51,7 +51,7 @@ public class GoogleTest {
httpClient.close();
}
- @Test
+
public void testGoogleWithoutFollowRedirects() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
@@ -68,13 +68,14 @@ public class GoogleTest {
httpClient.close();
}
+
@Test
public void testGoogleHttps1() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setURL("https://www.google.com")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -92,7 +93,7 @@ public class GoogleTest {
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://www.google.com")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -110,8 +111,7 @@ public class GoogleTest {
HttpRequestBuilder builder1 = httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://www.google.com")
- .setTimeout(10000)
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -120,19 +120,15 @@ public class GoogleTest {
HttpRequestBuilder builder2 = httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://www.google.com")
- .setTimeout(10000)
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
});
- // only sequential ... this sucks.
-
HttpRequestContext context1 = builder1.execute();
- context1.get();
-
HttpRequestContext context2 = builder2.execute();
+ context1.get();
context2.get();
httpClient.close();
diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java b/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java
index fe911bd..a4f5dd6 100644
--- a/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java
@@ -68,7 +68,8 @@ public class Http2FrameAdapterTest {
@Test
public void testHttp2FrameAdapter() throws Exception {
final int serverExpectedDataFrames = 1;
- final InetSocketAddress inetSocketAddress = new InetSocketAddress("http2-push.io", 443);
+ //final InetSocketAddress inetSocketAddress = new InetSocketAddress("http2-push.io", 443);
+ final InetSocketAddress inetSocketAddress = new InetSocketAddress("webtide.com", 443);
final CountDownLatch dataLatch = new CountDownLatch(serverExpectedDataFrames);
EventLoopGroup group = new NioEventLoopGroup();
Channel clientChannel = null;
diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java b/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java
index 6647873..fd8f1d2 100644
--- a/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java
@@ -40,7 +40,7 @@ public class Http2PushioTest {
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://http2-push.io")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
diff --git a/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java b/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java
new file mode 100644
index 0000000..b8e824d
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java
@@ -0,0 +1,64 @@
+package org.xbib.netty.http.client.test;
+
+import org.junit.Test;
+import org.xbib.netty.http.client.HttpClient;
+
+import java.nio.charset.StandardCharsets;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+
+/**
+ */
+public class HttpBinTest {
+
+ static {
+ System.setProperty("java.util.logging.SimpleFormatter.format",
+ "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
+ LogManager.getLogManager().reset();
+ Logger rootLogger = LogManager.getLogManager().getLogger("");
+ Handler handler = new ConsoleHandler();
+ handler.setFormatter(new SimpleFormatter());
+ rootLogger.addHandler(handler);
+ rootLogger.setLevel(Level.ALL);
+ for (Handler h : rootLogger.getHandlers()) {
+ handler.setFormatter(new SimpleFormatter());
+ h.setLevel(Level.ALL);
+ }
+ }
+
+ private static final Logger logger = Logger.getLogger("");
+
+ /**
+ * The reponse body should be
+ *
+ * {
+ * "cookies": {
+ * "name": "value"
+ * }
+ * }
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testHttpBin() throws Exception {
+ HttpClient httpClient = HttpClient.builder()
+ .build();
+ httpClient.prepareGet()
+ .setURL("http://httpbin.org/cookies/set?name=value")
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onCookie(cookie -> logger.log(Level.INFO, cookie.toString()))
+ .onHeaders(headers -> logger.log(Level.INFO, headers.toString()))
+ .onResponse(fullHttpResponse -> {
+ String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+ logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+ })
+ .execute()
+ .get();
+ httpClient.close();
+ }
+
+}
diff --git a/src/test/java/org/xbib/netty/http/client/test/InboundHttp2ToHttpAdapterTest.java b/src/test/java/org/xbib/netty/http/client/test/InboundHttp2ToHttpAdapterTest.java
deleted file mode 100644
index 20cbcb2..0000000
--- a/src/test/java/org/xbib/netty/http/client/test/InboundHttp2ToHttpAdapterTest.java
+++ /dev/null
@@ -1,306 +0,0 @@
-package org.xbib.netty.http.client.test;
-
-import io.netty.bootstrap.Bootstrap;
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.channel.ChannelInitializer;
-import io.netty.channel.ChannelPromise;
-import io.netty.channel.EventLoopGroup;
-import io.netty.channel.SimpleChannelInboundHandler;
-import io.netty.channel.nio.NioEventLoopGroup;
-import io.netty.channel.socket.ChannelInputShutdownReadComplete;
-import io.netty.channel.socket.nio.NioSocketChannel;
-import io.netty.handler.codec.http.DefaultFullHttpRequest;
-import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpMethod;
-import io.netty.handler.codec.http.HttpRequest;
-import io.netty.handler.codec.http.HttpVersion;
-import io.netty.handler.codec.http2.DefaultHttp2Connection;
-import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
-import io.netty.handler.codec.http2.Http2Connection;
-import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent;
-import io.netty.handler.codec.http2.Http2FrameLogger;
-import io.netty.handler.codec.http2.Http2SecurityUtil;
-import io.netty.handler.codec.http2.Http2Settings;
-import io.netty.handler.codec.http2.HttpConversionUtil;
-import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
-import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
-import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
-import io.netty.handler.logging.LogLevel;
-import io.netty.handler.logging.LoggingHandler;
-import io.netty.handler.ssl.ApplicationProtocolConfig;
-import io.netty.handler.ssl.ApplicationProtocolNames;
-import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
-import io.netty.handler.ssl.SslCloseCompletionEvent;
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslContextBuilder;
-import io.netty.handler.ssl.SslHandler;
-import io.netty.handler.ssl.SslProvider;
-import io.netty.handler.ssl.SupportedCipherSuiteFilter;
-import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
-import io.netty.util.internal.PlatformDependent;
-import org.junit.Test;
-
-import javax.net.ssl.SNIHostName;
-import javax.net.ssl.SNIServerName;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
-import java.net.InetSocketAddress;
-import java.net.URL;
-import java.util.AbstractMap;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogManager;
-import java.util.logging.Logger;
-import java.util.logging.SimpleFormatter;
-
-/**
- */
-public class InboundHttp2ToHttpAdapterTest {
-
- static {
- System.setProperty("java.util.logging.SimpleFormatter.format",
- "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
- LogManager.getLogManager().reset();
- Logger rootLogger = LogManager.getLogManager().getLogger("");
- Handler handler = new ConsoleHandler();
- handler.setFormatter(new SimpleFormatter());
- rootLogger.addHandler(handler);
- rootLogger.setLevel(Level.ALL);
- for (Handler h : rootLogger.getHandlers()) {
- handler.setFormatter(new SimpleFormatter());
- h.setLevel(Level.ALL);
- }
- }
-
- private static final Logger logger = Logger.getLogger("");
-
- @Test
- public void testInboundHttp2ToHttpAdapter() throws Exception {
- URL url = new URL("https://http2-push.io");
- final InetSocketAddress inetSocketAddress = new InetSocketAddress(url.getHost(), 443);
- EventLoopGroup group = new NioEventLoopGroup();
- Channel clientChannel = null;
- SettingsHandler settingsHandler = new SettingsHandler();
- ResponseHandler responseHandler = new ResponseHandler();
- try {
- Bootstrap bs = new Bootstrap();
- bs.group(group);
- bs.channel(NioSocketChannel.class);
- bs.handler(new ChannelInitializer() {
- @Override
- protected void initChannel(Channel ch) throws Exception {
- ch.pipeline().addLast(new TrafficLoggingHandler());
- SslContext sslContext = SslContextBuilder.forClient()
- .sslProvider(SslProvider.OPENSSL)
- .trustManager(InsecureTrustManagerFactory.INSTANCE)
- .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
- .applicationProtocolConfig(new ApplicationProtocolConfig(
- ApplicationProtocolConfig.Protocol.ALPN,
- ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
- ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
- ApplicationProtocolNames.HTTP_2))
- .build();
- SslHandler sslHandler = sslContext.newHandler(ch.alloc());
- SSLEngine engine = sslHandler.engine();
- String fullQualifiedHostname = inetSocketAddress.getHostName();
- SSLParameters params = engine.getSSLParameters();
- params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
- engine.setSSLParameters(params);
- ch.pipeline().addLast(sslHandler);
- ch.pipeline().addLast(new Http2NegotiationHandler(settingsHandler, responseHandler));
- }
- });
- clientChannel = bs.connect(inetSocketAddress).syncUninterruptibly().channel();
- settingsHandler.awaitSettings(clientChannel.newPromise());
- HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.valueOf("HTTP/2.0"),
- HttpMethod.GET, url.toExternalForm());
- logger.log(Level.FINE, "HTTP2: sending request");
- responseHandler.put(3, clientChannel.write(httpRequest), clientChannel.newPromise());
- clientChannel.flush();
- logger.log(Level.FINE, "HTTP2: waiting for responses");
- responseHandler.awaitResponses();
- logger.log(Level.FINE, "HTTP2: done");
- } finally {
- if (clientChannel != null) {
- clientChannel.close();
- }
- group.shutdownGracefully();
- }
- }
-
- private HttpToHttp2ConnectionHandler createHttp2ConnectionHandler() {
- final Http2Connection http2Connection = new DefaultHttp2Connection(false);
- return new HttpToHttp2ConnectionHandlerBuilder()
- .connection(http2Connection)
- .frameLogger(new Http2FrameLogger(LogLevel.INFO, "client"))
- .frameListener(new DelegatingDecompressorFrameListener(http2Connection,
- new InboundHttp2ToHttpAdapterBuilder(http2Connection)
- .maxContentLength(10 * 1024 * 1024)
- .propagateSettings(true)
- .validateHttpHeaders(false)
- .build()))
- .build();
- }
-
- class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler {
-
- private final SettingsHandler settingsHandler;
-
- private final ResponseHandler responseHandler;
-
- Http2NegotiationHandler(SettingsHandler settingsHandler, ResponseHandler responseHandler) {
- super("");
- this.settingsHandler = settingsHandler;
- this.responseHandler = responseHandler;
- }
-
- @Override
- protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
- if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
- ctx.pipeline().addLast(createHttp2ConnectionHandler());
- ctx.pipeline().addLast(settingsHandler);
- ctx.pipeline().addLast(new UserEventLogger());
- ctx.pipeline().addLast(responseHandler);
- logger.log(Level.FINE, "negotiated HTTP/2: pipeline = " + ctx.pipeline().names());
- return;
- }
- ctx.close();
- throw new IllegalStateException("unexpected protocol: " + protocol);
- }
- }
-
- class SettingsHandler extends SimpleChannelInboundHandler {
-
- private ChannelPromise promise;
-
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
- promise.setSuccess();
- ctx.pipeline().remove(this);
- }
-
- void awaitSettings(ChannelPromise promise) throws Exception {
- this.promise = promise;
- int timeout = 5000;
- if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
- throw new IllegalStateException("time out while waiting for HTTP/2 settings");
- }
- if (!promise.isSuccess()) {
- throw new RuntimeException(promise.cause());
- }
- }
- }
-
- class ResponseHandler extends SimpleChannelInboundHandler {
-
- private final Map> streamidPromiseMap;
-
- ResponseHandler() {
- this.streamidPromiseMap = PlatformDependent.newConcurrentHashMap();
- }
-
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception {
- Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
- if (streamId == null) {
- logger.log(Level.WARNING, () -> "stream ID missing");
- return;
- }
- Map.Entry entry = streamidPromiseMap.get(streamId);
- if (entry != null) {
- entry.getValue().setSuccess();
- } else {
- logger.log(Level.WARNING, () -> "stream id not found in promise map: " + streamId);
- }
- }
-
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
- logger.log(Level.FINE, () -> "exception caught " + cause.getMessage());
- }
-
- void put(int streamId, ChannelFuture channelFuture, ChannelPromise promise) {
- logger.log(Level.FINE, () -> "put stream ID " + streamId);
- streamidPromiseMap.put(streamId, new AbstractMap.SimpleEntry<>(channelFuture, promise));
- }
-
- void awaitResponses() {
- int timeout = 5000;
- Iterator>> iterator = streamidPromiseMap.entrySet().iterator();
- while (iterator.hasNext()) {
- Map.Entry> entry = iterator.next();
- ChannelFuture channelFuture = entry.getValue().getKey();
- if (!channelFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
- throw new IllegalStateException("time out while waiting to write for stream id " + entry.getKey());
- }
- if (!channelFuture.isSuccess()) {
- throw new RuntimeException(channelFuture.cause());
- }
- ChannelPromise promise = entry.getValue().getValue();
- if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
- throw new IllegalStateException("time out while waiting for response on stream id " + entry.getKey());
- }
- if (!promise.isSuccess()) {
- throw new RuntimeException(promise.cause());
- }
- iterator.remove();
- }
- }
- }
-
- class UserEventLogger extends ChannelInboundHandlerAdapter {
-
- @Override
- public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
- logger.log(Level.FINE, () -> "got user event " + evt);
- if (evt instanceof Http2ConnectionPrefaceWrittenEvent ||
- evt instanceof SslCloseCompletionEvent ||
- evt instanceof ChannelInputShutdownReadComplete) {
- // Expected events
- logger.log(Level.FINE, () -> "user event is expected: " + evt);
- return;
- }
- super.userEventTriggered(ctx, evt);
- }
- }
-
- class TrafficLoggingHandler extends LoggingHandler {
-
- TrafficLoggingHandler() {
- super("client", LogLevel.TRACE);
- }
-
- @Override
- public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
- ctx.fireChannelRegistered();
- }
-
- @Override
- public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
- ctx.fireChannelUnregistered();
- }
-
- @Override
- public void flush(ChannelHandlerContext ctx) throws Exception {
- ctx.flush();
- }
-
- @Override
- public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
- if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
- ctx.write(msg, promise);
- } else {
- super.write(ctx, msg, promise);
- }
- }
- }
-}
diff --git a/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java b/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
index bb93b8d..580f83b 100644
--- a/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
@@ -16,7 +16,6 @@
package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.FullHttpResponse;
-import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import org.xbib.netty.http.client.HttpRequestBuilder;
@@ -53,14 +52,13 @@ public class IndexHbzTest {
private static final Logger logger = Logger.getLogger("");
- @Test
public void testIndexHbz() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://index.hbz-nrw.de")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -77,7 +75,7 @@ public class IndexHbzTest {
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("https://index.hbz-nrw.de")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -123,7 +121,7 @@ public class IndexHbzTest {
.setVersion("HTTP/2.0")
.setURL("https://index.hbz-nrw.de")
.setTimeout(5000)
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -133,7 +131,6 @@ public class IndexHbzTest {
httpClient.close();
}
- @Test
public void testIndexHbzH2C() throws Exception {
// times out waiting for http2 settings frame
@@ -144,7 +141,7 @@ public class IndexHbzTest {
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("http://index.hbz-nrw.de")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -155,7 +152,7 @@ public class IndexHbzTest {
}
@Test
- public void testIndexHbzConcurrent() throws Exception {
+ public void testIndexHbzConcurrentHttp1() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
@@ -163,7 +160,7 @@ public class IndexHbzTest {
HttpRequestBuilder builder1 = httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://index.hbz-nrw.de")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -172,7 +169,7 @@ public class IndexHbzTest {
HttpRequestBuilder builder2 = httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://index.hbz-nrw.de")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
diff --git a/src/test/java/org/xbib/netty/http/client/test/WebtideTest.java b/src/test/java/org/xbib/netty/http/client/test/WebtideTest.java
new file mode 100644
index 0000000..d84ab20
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/WebtideTest.java
@@ -0,0 +1,60 @@
+package org.xbib.netty.http.client.test;
+
+import org.junit.Test;
+import org.xbib.netty.http.client.HttpClient;
+
+import java.nio.charset.StandardCharsets;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+
+/**
+ */
+public class WebtideTest {
+
+ static {
+ System.setProperty("java.util.logging.SimpleFormatter.format",
+ "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
+ LogManager.getLogManager().reset();
+ Logger rootLogger = LogManager.getLogManager().getLogger("");
+ Handler handler = new ConsoleHandler();
+ handler.setFormatter(new SimpleFormatter());
+ rootLogger.addHandler(handler);
+ rootLogger.setLevel(Level.FINE);
+ for (Handler h : rootLogger.getHandlers()) {
+ handler.setFormatter(new SimpleFormatter());
+ h.setLevel(Level.FINE);
+ }
+ }
+
+ private static final Logger logger = Logger.getLogger("");
+
+ @Test
+ public void testWebtide() throws Exception {
+ HttpClient httpClient = HttpClient.builder()
+ .build();
+
+ httpClient.prepareGet()
+ .setVersion("HTTP/2.0")
+ .setURL("https://webtide.com")
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onResponse(fullHttpResponse -> {
+ logger.log(Level.INFO, "status = " + fullHttpResponse.status()
+ + " response headers = " + fullHttpResponse.headers().entries()
+ );
+ })
+ .onPushReceived((headers, fullHttpResponse) -> {
+ logger.log(Level.INFO, "received push promise: request headers = " + headers
+ + " status = " + fullHttpResponse.status()
+ + " response headers = " + fullHttpResponse.headers().entries()
+ );
+ })
+ .execute()
+ .get();
+
+ httpClient.close();
+ }
+}
diff --git a/src/test/java/org/xbib/netty/http/client/test/XbibTest.java b/src/test/java/org/xbib/netty/http/client/test/XbibTest.java
index aab4bb4..8c0b263 100644
--- a/src/test/java/org/xbib/netty/http/client/test/XbibTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/XbibTest.java
@@ -92,6 +92,7 @@ public class XbibTest {
}
@Test
+ @Ignore
public void testXbibOrgWithProxy() throws Exception {
HttpClient httpClient = HttpClient.builder()
.setHttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080))
@@ -104,7 +105,7 @@ public class XbibTest {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
@@ -122,7 +123,7 @@ public class XbibTest {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
@@ -137,7 +138,7 @@ public class XbibTest {
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://xbib.org")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
@@ -148,7 +149,7 @@ public class XbibTest {
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://xbib.org")
- .onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+ .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);