much improved HTTP/2, cookies, more tests
This commit is contained in:
parent
4ca2ff395d
commit
8f1a7211e6
44 changed files with 2413 additions and 953 deletions
|
@ -65,8 +65,6 @@ jar {
|
||||||
|
|
||||||
test {
|
test {
|
||||||
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
|
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
|
||||||
include 'org/xbib/netty/http/client/test/Http2FrameAdapterTest*'
|
|
||||||
include 'org/xbib/netty/http/client/test/InboundHttp2ToHttpAdapterTest*'
|
|
||||||
testLogging {
|
testLogging {
|
||||||
showStandardStreams = false
|
showStandardStreams = false
|
||||||
exceptionFormat = 'full'
|
exceptionFormat = 'full'
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,4 +1,4 @@
|
||||||
#Mon Apr 17 15:12:33 CEST 2017
|
#Tue May 02 21:00:09 CEST 2017
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
172
gradlew
vendored
Executable file
172
gradlew
vendored
Executable file
|
@ -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" "$@"
|
84
gradlew.bat
vendored
Normal file
84
gradlew.bat
vendored
Normal file
|
@ -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
|
412
src/main/java/org/xbib/netty/http/client/Http2EventHandler.java
Normal file
412
src/main/java/org/xbib/netty/http/client/Http2EventHandler.java
Normal file
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
|
||||||
|
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
|
||||||
|
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
|
||||||
|
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
|
||||||
|
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* Promised requests MUST be authoritative, cacheable, and safe.
|
||||||
|
* See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
|
||||||
|
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
|
||||||
|
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* If a {@link RuntimeException} is thrown it will be logged and <strong>not propagated</strong>.
|
||||||
|
* 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
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||||
|
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||||
|
* </ul>
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FullHttpResponse> {
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(Http2Handler.class.getName());
|
|
||||||
|
|
||||||
private final Map<Integer, Entry<ChannelFuture, ChannelPromise>> 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<ChannelFuture, ChannelPromise> 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<Entry<Integer, Entry<ChannelFuture, ChannelPromise>>> iterator = streamidPromiseMap.entrySet().iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Entry<Integer, Entry<ChannelFuture, ChannelPromise>> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<FullHttpResponse> {
|
||||||
|
|
||||||
|
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<Http2Headers, ChannelPromise> 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<ChannelFuture, ChannelPromise> 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ package org.xbib.netty.http.client;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.buffer.ByteBufAllocator;
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.EventLoopGroup;
|
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.HttpRequest;
|
||||||
import io.netty.handler.codec.http.HttpResponse;
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
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.Future;
|
||||||
import io.netty.util.concurrent.FutureListener;
|
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.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URL;
|
import java.net.URI;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
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 Logger logger = Logger.getLogger(HttpClient.class.getName());
|
||||||
|
|
||||||
|
private static final AtomicInteger streamId = new AtomicInteger(3);
|
||||||
|
|
||||||
private final ByteBufAllocator byteBufAllocator;
|
private final ByteBufAllocator byteBufAllocator;
|
||||||
|
|
||||||
private final EventLoopGroup eventLoopGroup;
|
private final EventLoopGroup eventLoopGroup;
|
||||||
|
@ -58,7 +73,7 @@ public final class HttpClient implements Closeable {
|
||||||
private final HttpClientChannelPoolMap poolMap;
|
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,
|
HttpClient(ByteBufAllocator byteBufAllocator,
|
||||||
EventLoopGroup eventLoopGroup,
|
EventLoopGroup eventLoopGroup,
|
||||||
|
@ -68,6 +83,9 @@ public final class HttpClient implements Closeable {
|
||||||
this.byteBufAllocator = byteBufAllocator;
|
this.byteBufAllocator = byteBufAllocator;
|
||||||
this.eventLoopGroup = eventLoopGroup;
|
this.eventLoopGroup = eventLoopGroup;
|
||||||
this.poolMap = new HttpClientChannelPoolMap(this, httpClientChannelContext, bootstrap, maxConnections);
|
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) {
|
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");
|
logger.log(Level.FINE, () -> "closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispatch(HttpRequestContext httpRequestContext, HttpResponseListener httpResponseListener,
|
void dispatch(final HttpRequestContext httpRequestContext) {
|
||||||
ExceptionListener exceptionListener) {
|
final URI uri = httpRequestContext.getURI();
|
||||||
final URL url = httpRequestContext.getURL();
|
|
||||||
final HttpRequest httpRequest = httpRequestContext.getHttpRequest();
|
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<Cookie> 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()) {
|
if (httpRequestContext.isExpired()) {
|
||||||
httpRequestContext.fail("request expired");
|
httpRequestContext.fail("request expired");
|
||||||
}
|
}
|
||||||
|
@ -185,27 +210,31 @@ public final class HttpClient implements Closeable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HttpVersion version = httpRequestContext.getHttpRequest().protocolVersion();
|
HttpVersion version = httpRequestContext.getHttpRequest().protocolVersion();
|
||||||
InetAddressKey inetAddressKey = new InetAddressKey(url, version);
|
boolean secure = "https".equals(uri.getScheme());
|
||||||
// effectivly disable pool for HTTP/2
|
InetAddressKey inetAddressKey = new InetAddressKey(uri.getHost(), uri.getPort(), version, secure);
|
||||||
if (version.majorVersion() == 2) {
|
|
||||||
poolMap.remove(inetAddressKey);
|
|
||||||
}
|
|
||||||
final FixedChannelPool pool = poolMap.get(inetAddressKey);
|
final FixedChannelPool pool = poolMap.get(inetAddressKey);
|
||||||
logger.log(Level.FINE, () -> "connecting to " + inetAddressKey);
|
logger.log(Level.FINE, () -> "connecting to " + inetAddressKey);
|
||||||
Future<Channel> futureChannel = pool.acquire();
|
Future<Channel> futureChannel = pool.acquire();
|
||||||
futureChannel.addListener((FutureListener<Channel>) future -> {
|
futureChannel.addListener((FutureListener<Channel>) future -> {
|
||||||
|
final ExceptionListener exceptionListener = httpRequestContext.getExceptionListener();
|
||||||
if (future.isSuccess()) {
|
if (future.isSuccess()) {
|
||||||
Channel channel = future.getNow();
|
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.CHANNEL_POOL_ATTRIBUTE_KEY).set(pool);
|
||||||
channel.attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).set(httpRequestContext);
|
channel.attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).set(httpRequestContext);
|
||||||
if (httpResponseListener != null) {
|
HttpResponseListener httpResponseListener = httpRequestContext.getHttpResponseListener();
|
||||||
channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).set(httpResponseListener);
|
channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).set(httpResponseListener);
|
||||||
}
|
HttpPushListener httpPushListener = httpRequestContext.getHttpPushListener();
|
||||||
if (exceptionListener != null) {
|
channel.attr(HttpClientChannelContext.PUSH_LISTENER_ATTRIBUTE_KEY).set(httpPushListener);
|
||||||
channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).set(exceptionListener);
|
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()) {
|
if (httpRequestContext.isFailed()) {
|
||||||
logger.log(Level.FINE, () -> "detected fail, close now");
|
logger.log(Level.FINE, () -> "detected fail, close channel");
|
||||||
future.cancel(true);
|
future.cancel(true);
|
||||||
if (channel.isOpen()) {
|
if (channel.isOpen()) {
|
||||||
channel.close();
|
channel.close();
|
||||||
|
@ -227,22 +256,60 @@ public final class HttpClient implements Closeable {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (httpRequest.protocolVersion().majorVersion() == 2) {
|
} else if (httpRequest.protocolVersion().majorVersion() == 2) {
|
||||||
HttpClientChannelInitializer.Http2SettingsHandler http2SettingsHandler =
|
logger.log(Level.FINE, () -> "waiting for HTTP/2 settings");
|
||||||
poolMap.getHttpClientChannelInitializer().getHttp2SettingsHandler();
|
settingsPromise.await(httpRequestContext.getTimeout(), TimeUnit.MILLISECONDS);
|
||||||
if (http2SettingsHandler != null) {
|
logger.log(Level.FINE, () -> "waiting for HTTP/2 responses = " + httpRequestContext.getStreamIdPromiseMap().size());
|
||||||
logger.log(Level.FINE, "HTTP2: waiting for settings");
|
int timeout = httpRequestContext.getTimeout();
|
||||||
http2SettingsHandler.awaitSettings(httpRequestContext, exceptionListener);
|
for (Map.Entry<Integer, Map.Entry<ChannelFuture, ChannelPromise>> entry :
|
||||||
}
|
httpRequestContext.getStreamIdPromiseMap().entrySet()) {
|
||||||
Http2Handler http2Handler = poolMap.getHttpClientChannelInitializer().getHttp2Handler();
|
ChannelFuture channelFuture = entry.getValue().getKey();
|
||||||
if (http2Handler != null) {
|
if (channelFuture != null) {
|
||||||
logger.log(Level.FINE, () ->
|
logger.log(Level.FINE, "waiting for channel, stream ID = " + entry.getKey());
|
||||||
"HTTP2: trying to write, streamID=" + httpRequestContext.getStreamId() +
|
if (!channelFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
|
||||||
" request: " + httpRequest.toString());
|
IllegalStateException illegalStateException =
|
||||||
ChannelPromise channelPromise = channel.newPromise();
|
new IllegalStateException("time out while waiting to write for stream id " + entry.getKey());
|
||||||
http2Handler.put(httpRequestContext.getStreamId(), channel.write(httpRequest), channelPromise);
|
if (exceptionListener != null) {
|
||||||
channel.flush();
|
exceptionListener.onException(illegalStateException);
|
||||||
logger.log(Level.FINE, "HTTP2: waiting for responses");
|
httpRequestContext.fail(illegalStateException.getMessage());
|
||||||
http2Handler.awaitResponses(httpRequestContext, exceptionListener);
|
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 {
|
} else {
|
||||||
|
@ -262,7 +329,7 @@ public final class HttpClient implements Closeable {
|
||||||
HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET :
|
HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET :
|
||||||
httpRequestContext.getHttpRequest().method();
|
httpRequestContext.getHttpRequest().method();
|
||||||
if (httpRequestContext.getRedirectCount().getAndIncrement() < httpRequestContext.getMaxRedirects()) {
|
if (httpRequestContext.getRedirectCount().getAndIncrement() < httpRequestContext.getMaxRedirects()) {
|
||||||
dispatchRedirect(channel, method, new URL(redirUrl), httpRequestContext);
|
dispatchRedirect(method, URI.create(redirUrl), httpRequestContext);
|
||||||
} else {
|
} else {
|
||||||
httpRequestContext.fail("too many redirections");
|
httpRequestContext.fail("too many redirections");
|
||||||
final ChannelPool channelPool =
|
final ChannelPool channelPool =
|
||||||
|
@ -295,7 +362,7 @@ public final class HttpClient implements Closeable {
|
||||||
return location;
|
return location;
|
||||||
} else {
|
} else {
|
||||||
logger.log(Level.FINE, "(relative->absolute) redirect to " + location);
|
logger.log(Level.FINE, "(relative->absolute) redirect to " + location);
|
||||||
return makeAbsolute(httpRequestContext.getURL(), location);
|
return makeAbsolute(httpRequestContext.getURI(), location);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -303,35 +370,31 @@ public final class HttpClient implements Closeable {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dispatchRedirect(Channel channel, HttpMethod method, URL url,
|
private void dispatchRedirect(HttpMethod method, URI uri,
|
||||||
HttpRequestContext httpRequestContext) {
|
HttpRequestContext httpRequestContext) {
|
||||||
final String uri = httpRequestContext.getHttpRequest().protocolVersion().majorVersion() == 2 ?
|
final String uriStr = httpRequestContext.getHttpRequest().protocolVersion().majorVersion() == 2 ?
|
||||||
url.toExternalForm() : makeRelative(url);
|
uri.toASCIIString() : makeRelative(uri);
|
||||||
final HttpRequest httpRequest;
|
final HttpRequest httpRequest;
|
||||||
if (method.equals(httpRequestContext.getHttpRequest().method()) &&
|
if (method.equals(httpRequestContext.getHttpRequest().method()) &&
|
||||||
httpRequestContext.getHttpRequest() instanceof DefaultFullHttpRequest) {
|
httpRequestContext.getHttpRequest() instanceof DefaultFullHttpRequest) {
|
||||||
DefaultFullHttpRequest defaultFullHttpRequest = (DefaultFullHttpRequest) httpRequestContext.getHttpRequest();
|
DefaultFullHttpRequest defaultFullHttpRequest = (DefaultFullHttpRequest) httpRequestContext.getHttpRequest();
|
||||||
FullHttpRequest fullHttpRequest = defaultFullHttpRequest.copy();
|
FullHttpRequest fullHttpRequest = defaultFullHttpRequest.copy();
|
||||||
fullHttpRequest.setUri(uri);
|
fullHttpRequest.setUri(uriStr);
|
||||||
httpRequest = fullHttpRequest;
|
httpRequest = fullHttpRequest;
|
||||||
} else {
|
} else {
|
||||||
httpRequest = new DefaultHttpRequest(httpRequestContext.getHttpRequest().protocolVersion(), method, uri);
|
httpRequest = new DefaultHttpRequest(httpRequestContext.getHttpRequest().protocolVersion(), method, uriStr);
|
||||||
}
|
}
|
||||||
for (Map.Entry<String, String> e : httpRequestContext.getHttpRequest().headers().entries()) {
|
for (Map.Entry<String, String> e : httpRequestContext.getHttpRequest().headers().entries()) {
|
||||||
httpRequest.headers().add(e.getKey(), e.getValue());
|
httpRequest.headers().add(e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
httpRequest.headers().set(HttpHeaderNames.HOST, url.getHost());
|
httpRequest.headers().set(HttpHeaderNames.HOST, uri.getHost());
|
||||||
HttpRequestContext redirectContext = new HttpRequestContext(url, httpRequest,
|
HttpRequestContext redirectContext = new HttpRequestContext(uri, httpRequest,
|
||||||
httpRequestContext);
|
httpRequestContext);
|
||||||
logger.log(Level.FINE, "dispatchRedirect url = " + url + " with new request " + httpRequest.toString());
|
logger.log(Level.FINE, "dispatchRedirect url = " + uri + " with new request " + httpRequest.toString());
|
||||||
HttpResponseListener httpResponseListener =
|
dispatch(redirectContext);
|
||||||
channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
|
|
||||||
ExceptionListener exceptionListener =
|
|
||||||
channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
|
|
||||||
dispatch(redirectContext, httpResponseListener, exceptionListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String makeRelative(URL base) {
|
private String makeRelative(URI base) {
|
||||||
String uri = base.getPath();
|
String uri = base.getPath();
|
||||||
if (base.getQuery() != null) {
|
if (base.getQuery() != null) {
|
||||||
uri = uri + "?" + base.getQuery();
|
uri = uri + "?" + base.getQuery();
|
||||||
|
@ -339,7 +402,7 @@ public final class HttpClient implements Closeable {
|
||||||
return uri;
|
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");
|
String path = base.getPath() == null ? "/" : URLDecoder.decode(base.getPath(), "UTF-8");
|
||||||
if (location.startsWith("/")) {
|
if (location.startsWith("/")) {
|
||||||
path = location;
|
path = location;
|
||||||
|
@ -348,7 +411,7 @@ public final class HttpClient implements Closeable {
|
||||||
} else {
|
} else {
|
||||||
path += "/" + location;
|
path += "/" + location;
|
||||||
}
|
}
|
||||||
String scheme = base.getProtocol();
|
String scheme = base.getScheme();
|
||||||
StringBuilder sb = new StringBuilder(scheme).append("://").append(base.getHost());
|
StringBuilder sb = new StringBuilder(scheme).append("://").append(base.getHost());
|
||||||
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
|
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
|
||||||
if (defaultPort != -1 && base.getPort() != -1 && defaultPort != base.getPort()) {
|
if (defaultPort != -1 && base.getPort() != -1 && defaultPort != base.getPort()) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import io.netty.handler.proxy.Socks4ProxyHandler;
|
||||||
import io.netty.handler.proxy.Socks5ProxyHandler;
|
import io.netty.handler.proxy.Socks5ProxyHandler;
|
||||||
import io.netty.handler.ssl.CipherSuiteFilter;
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
import io.netty.handler.ssl.SslProvider;
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import org.xbib.netty.http.client.util.ClientAuthMode;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
@ -46,7 +47,7 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
|
||||||
|
|
||||||
private Bootstrap bootstrap;
|
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 int threads = 0;
|
||||||
|
|
||||||
private boolean tcpNodelay = DEFAULT_TCP_NODELAY;
|
private boolean tcpNodelay = DEFAULT_TCP_NODELAY;
|
||||||
|
@ -95,7 +96,7 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
|
||||||
|
|
||||||
private boolean useServerNameIdentification = DEFAULT_USE_SERVER_NAME_IDENTIFICATION;
|
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;
|
private HttpProxyHandler httpProxyHandler;
|
||||||
|
|
||||||
|
@ -252,8 +253,8 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpClientBuilder setSslClientAuthMode(SslClientAuthMode sslClientAuthMode) {
|
public HttpClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) {
|
||||||
this.sslClientAuthMode = sslClientAuthMode;
|
this.clientAuthMode = clientAuthMode;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +320,7 @@ public class HttpClientBuilder implements HttpClientChannelContextDefaults {
|
||||||
readTimeoutMillis, enableGzip, installHttp2Upgrade,
|
readTimeoutMillis, enableGzip, installHttp2Upgrade,
|
||||||
sslProvider, ciphers, cipherSuiteFilter, trustManagerFactory,
|
sslProvider, ciphers, cipherSuiteFilter, trustManagerFactory,
|
||||||
keyCertChainInputStream, keyInputStream, keyPassword,
|
keyCertChainInputStream, keyInputStream, keyPassword,
|
||||||
useServerNameIdentification, sslClientAuthMode,
|
useServerNameIdentification, clientAuthMode,
|
||||||
httpProxyHandler, socks4ProxyHandler, socks5ProxyHandler);
|
httpProxyHandler, socks4ProxyHandler, socks5ProxyHandler);
|
||||||
return new HttpClient(byteBufAllocator, eventLoopGroup, bootstrap, maxConnections, httpClientChannelContext);
|
return new HttpClient(byteBufAllocator, eventLoopGroup, bootstrap, maxConnections, httpClientChannelContext);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,12 @@ import io.netty.handler.proxy.Socks5ProxyHandler;
|
||||||
import io.netty.handler.ssl.CipherSuiteFilter;
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
import io.netty.handler.ssl.SslProvider;
|
import io.netty.handler.ssl.SslProvider;
|
||||||
import io.netty.util.AttributeKey;
|
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 java.io.InputStream;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
@ -39,6 +45,15 @@ final class HttpClientChannelContext {
|
||||||
static final AttributeKey<HttpResponseListener> RESPONSE_LISTENER_ATTRIBUTE_KEY =
|
static final AttributeKey<HttpResponseListener> RESPONSE_LISTENER_ATTRIBUTE_KEY =
|
||||||
AttributeKey.valueOf("httpClientResponseListener");
|
AttributeKey.valueOf("httpClientResponseListener");
|
||||||
|
|
||||||
|
static final AttributeKey<HttpHeadersListener> HEADER_LISTENER_ATTRIBUTE_KEY =
|
||||||
|
AttributeKey.valueOf("httpHeaderListener");
|
||||||
|
|
||||||
|
static final AttributeKey<CookieListener> COOKIE_LISTENER_ATTRIBUTE_KEY =
|
||||||
|
AttributeKey.valueOf("cookieListener");
|
||||||
|
|
||||||
|
static final AttributeKey<HttpPushListener> PUSH_LISTENER_ATTRIBUTE_KEY =
|
||||||
|
AttributeKey.valueOf("pushListener");
|
||||||
|
|
||||||
static final AttributeKey<ExceptionListener> EXCEPTION_LISTENER_ATTRIBUTE_KEY =
|
static final AttributeKey<ExceptionListener> EXCEPTION_LISTENER_ATTRIBUTE_KEY =
|
||||||
AttributeKey.valueOf("httpClientExceptionListener");
|
AttributeKey.valueOf("httpClientExceptionListener");
|
||||||
|
|
||||||
|
@ -74,7 +89,7 @@ final class HttpClientChannelContext {
|
||||||
|
|
||||||
private final boolean useServerNameIdentification;
|
private final boolean useServerNameIdentification;
|
||||||
|
|
||||||
private final SslClientAuthMode sslClientAuthMode;
|
private final ClientAuthMode clientAuthMode;
|
||||||
|
|
||||||
private final HttpProxyHandler httpProxyHandler;
|
private final HttpProxyHandler httpProxyHandler;
|
||||||
|
|
||||||
|
@ -98,7 +113,7 @@ final class HttpClientChannelContext {
|
||||||
InputStream keyInputStream,
|
InputStream keyInputStream,
|
||||||
String keyPassword,
|
String keyPassword,
|
||||||
boolean useServerNameIdentification,
|
boolean useServerNameIdentification,
|
||||||
SslClientAuthMode sslClientAuthMode,
|
ClientAuthMode clientAuthMode,
|
||||||
HttpProxyHandler httpProxyHandler,
|
HttpProxyHandler httpProxyHandler,
|
||||||
Socks4ProxyHandler socks4ProxyHandler,
|
Socks4ProxyHandler socks4ProxyHandler,
|
||||||
Socks5ProxyHandler socks5ProxyHandler) {
|
Socks5ProxyHandler socks5ProxyHandler) {
|
||||||
|
@ -118,7 +133,7 @@ final class HttpClientChannelContext {
|
||||||
this.keyInputStream = keyInputStream;
|
this.keyInputStream = keyInputStream;
|
||||||
this.keyPassword = keyPassword;
|
this.keyPassword = keyPassword;
|
||||||
this.useServerNameIdentification = useServerNameIdentification;
|
this.useServerNameIdentification = useServerNameIdentification;
|
||||||
this.sslClientAuthMode = sslClientAuthMode;
|
this.clientAuthMode = clientAuthMode;
|
||||||
this.httpProxyHandler = httpProxyHandler;
|
this.httpProxyHandler = httpProxyHandler;
|
||||||
this.socks4ProxyHandler = socks4ProxyHandler;
|
this.socks4ProxyHandler = socks4ProxyHandler;
|
||||||
this.socks5ProxyHandler = socks5ProxyHandler;
|
this.socks5ProxyHandler = socks5ProxyHandler;
|
||||||
|
@ -188,8 +203,8 @@ final class HttpClientChannelContext {
|
||||||
return useServerNameIdentification;
|
return useServerNameIdentification;
|
||||||
}
|
}
|
||||||
|
|
||||||
SslClientAuthMode getSslClientAuthMode() {
|
ClientAuthMode getClientAuthMode() {
|
||||||
return sslClientAuthMode;
|
return clientAuthMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpProxyHandler getHttpProxyHandler() {
|
HttpProxyHandler getHttpProxyHandler() {
|
||||||
|
|
|
@ -17,9 +17,12 @@ package org.xbib.netty.http.client;
|
||||||
|
|
||||||
import io.netty.handler.codec.http2.Http2SecurityUtil;
|
import io.netty.handler.codec.http2.Http2SecurityUtil;
|
||||||
import io.netty.handler.ssl.CipherSuiteFilter;
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
import io.netty.handler.ssl.SslProvider;
|
import io.netty.handler.ssl.SslProvider;
|
||||||
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
|
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
|
||||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
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;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
|
||||||
|
@ -105,7 +108,7 @@ public interface HttpClientChannelContextDefaults {
|
||||||
/**
|
/**
|
||||||
* Default SSL provider.
|
* Default SSL provider.
|
||||||
*/
|
*/
|
||||||
SslProvider DEFAULT_SSL_PROVIDER = SslProvider.OPENSSL;
|
SslProvider DEFAULT_SSL_PROVIDER = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
|
||||||
|
|
||||||
Iterable<String> DEFAULT_CIPHERS = Http2SecurityUtil.CIPHERS;
|
Iterable<String> DEFAULT_CIPHERS = Http2SecurityUtil.CIPHERS;
|
||||||
|
|
||||||
|
@ -118,5 +121,5 @@ public interface HttpClientChannelContextDefaults {
|
||||||
/**
|
/**
|
||||||
* Default for SSL client authentication.
|
* Default for SSL client authentication.
|
||||||
*/
|
*/
|
||||||
SslClientAuthMode DEFAULT_SSL_CLIENT_AUTH_MODE = SslClientAuthMode.NONE;
|
ClientAuthMode DEFAULT_SSL_CLIENT_AUTH_MODE = ClientAuthMode.NONE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package org.xbib.netty.http.client;
|
package org.xbib.netty.http.client;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||||
import io.netty.channel.ChannelInitializer;
|
import io.netty.channel.ChannelInitializer;
|
||||||
import io.netty.channel.ChannelPipeline;
|
import io.netty.channel.ChannelPipeline;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.SimpleChannelInboundHandler;
|
|
||||||
import io.netty.channel.socket.ChannelInputShutdownReadComplete;
|
import io.netty.channel.socket.ChannelInputShutdownReadComplete;
|
||||||
import io.netty.channel.socket.SocketChannel;
|
import io.netty.channel.socket.SocketChannel;
|
||||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
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.Http2Connection;
|
||||||
import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent;
|
import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent;
|
||||||
import io.netty.handler.codec.http2.Http2FrameLogger;
|
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.HttpToHttp2ConnectionHandler;
|
||||||
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
|
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.LogLevel;
|
||||||
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
import io.netty.handler.ssl.ApplicationProtocolConfig;
|
import io.netty.handler.ssl.ApplicationProtocolConfig;
|
||||||
import io.netty.handler.ssl.ApplicationProtocolNames;
|
import io.netty.handler.ssl.ApplicationProtocolNames;
|
||||||
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
|
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
|
||||||
import io.netty.handler.ssl.SslCloseCompletionEvent;
|
import io.netty.handler.ssl.SslCloseCompletionEvent;
|
||||||
import io.netty.handler.ssl.SslContext;
|
|
||||||
import io.netty.handler.ssl.SslContextBuilder;
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
import io.netty.handler.ssl.SslHandler;
|
import io.netty.handler.ssl.SslHandler;
|
||||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
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.Arrays;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -61,33 +61,41 @@ import javax.net.ssl.SSLException;
|
||||||
import javax.net.ssl.SSLParameters;
|
import javax.net.ssl.SSLParameters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Netty HTTP client channel initializer.
|
||||||
*/
|
*/
|
||||||
class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(HttpClientChannelInitializer.class.getName());
|
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 HttpClientChannelContext context;
|
||||||
|
|
||||||
private final Http1Handler http1Handler;
|
private final HttpHandler httpHandler;
|
||||||
|
|
||||||
private final Http2Handler http2Handler;
|
private final Http2ResponseHandler http2ResponseHandler;
|
||||||
|
|
||||||
private InetAddressKey key;
|
private InetAddressKey key;
|
||||||
|
|
||||||
private Http2SettingsHandler http2SettingsHandler;
|
/**
|
||||||
|
* Constructor for a new {@link HttpClientChannelInitializer}.
|
||||||
private UserEventLogger userEventLogger;
|
* @param context the HTTP client channel context
|
||||||
|
* @param httpHandler the HTTP 1.x handler
|
||||||
HttpClientChannelInitializer(HttpClientChannelContext context, Http1Handler http1Handler,
|
* @param http2ResponseHandler the HTTP 2 handler
|
||||||
Http2Handler http2Handler) {
|
*/
|
||||||
|
HttpClientChannelInitializer(HttpClientChannelContext context, HttpHandler httpHandler,
|
||||||
|
Http2ResponseHandler http2ResponseHandler) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.http1Handler = http1Handler;
|
this.httpHandler = httpHandler;
|
||||||
this.http2Handler = http2Handler;
|
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 {
|
void initChannel(SocketChannel ch, InetAddressKey key) throws Exception {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
initChannel(ch);
|
initChannel(ch);
|
||||||
|
@ -96,6 +104,9 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(SocketChannel ch) throws Exception {
|
protected void initChannel(SocketChannel ch) throws Exception {
|
||||||
logger.log(Level.FINE, () -> "initChannel with key = " + key);
|
logger.log(Level.FINE, () -> "initChannel with key = " + key);
|
||||||
|
if (key == null) {
|
||||||
|
throw new IllegalStateException("no key set for channel initialization");
|
||||||
|
}
|
||||||
ChannelPipeline pipeline = ch.pipeline();
|
ChannelPipeline pipeline = ch.pipeline();
|
||||||
pipeline.addLast(new TrafficLoggingHandler());
|
pipeline.addLast(new TrafficLoggingHandler());
|
||||||
if (context.getHttpProxyHandler() != null) {
|
if (context.getHttpProxyHandler() != null) {
|
||||||
|
@ -108,22 +119,12 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
pipeline.addLast(context.getSocks5ProxyHandler());
|
pipeline.addLast(context.getSocks5ProxyHandler());
|
||||||
}
|
}
|
||||||
pipeline.addLast(new ReadTimeoutHandler(context.getReadTimeoutMillis(), TimeUnit.MILLISECONDS));
|
pipeline.addLast(new ReadTimeoutHandler(context.getReadTimeoutMillis(), TimeUnit.MILLISECONDS));
|
||||||
http2SettingsHandler = new Http2SettingsHandler(ch.newPromise());
|
|
||||||
userEventLogger = new UserEventLogger();
|
|
||||||
if (context.getSslProvider() != null && key.isSecure()) {
|
if (context.getSslProvider() != null && key.isSecure()) {
|
||||||
configureEncrypted(ch);
|
configureEncrypted(ch);
|
||||||
} else {
|
} else {
|
||||||
configureClearText(ch);
|
configureClearText(ch);
|
||||||
}
|
}
|
||||||
logger.log(Level.FINE, () -> "initChannel pipeline handler names = " + ch.pipeline().names());
|
logger.log(Level.FINE, () -> "initChannel complete, pipeline handler names = " + ch.pipeline().names());
|
||||||
}
|
|
||||||
|
|
||||||
Http2SettingsHandler getHttp2SettingsHandler() {
|
|
||||||
return http2SettingsHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
Http2Handler getHttp2Handler() {
|
|
||||||
return http2Handler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureClearText(SocketChannel ch) {
|
private void configureClearText(SocketChannel ch) {
|
||||||
|
@ -134,6 +135,7 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
configureHttp1Pipeline(pipeline);
|
configureHttp1Pipeline(pipeline);
|
||||||
} else if (key.getVersion().majorVersion() == 2) {
|
} else if (key.getVersion().majorVersion() == 2) {
|
||||||
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
|
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
|
||||||
|
// using the upgrade handler means mixed HTTP 1 and HTTP 2 on the same connection
|
||||||
if (context.isInstallHttp2Upgrade()) {
|
if (context.isInstallHttp2Upgrade()) {
|
||||||
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
|
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
|
||||||
Http2ClientUpgradeCodec upgradeCodec =
|
Http2ClientUpgradeCodec upgradeCodec =
|
||||||
|
@ -154,65 +156,47 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
|
|
||||||
private void configureEncrypted(SocketChannel ch) throws SSLException {
|
private void configureEncrypted(SocketChannel ch) throws SSLException {
|
||||||
ChannelPipeline pipeline = ch.pipeline();
|
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) {
|
if (key.getVersion().majorVersion() == 2) {
|
||||||
final SslContext http2SslContext = SslContextBuilder.forClient()
|
sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig(
|
||||||
.sslProvider(context.getSslProvider())
|
ApplicationProtocolConfig.Protocol.ALPN,
|
||||||
.keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword())
|
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
|
||||||
.ciphers(context.getCiphers(), context.getCipherSuiteFilter())
|
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
|
||||||
.trustManager(context.getTrustManagerFactory())
|
ApplicationProtocolNames.HTTP_2,
|
||||||
.applicationProtocolConfig(new ApplicationProtocolConfig(
|
ApplicationProtocolNames.HTTP_1_1));
|
||||||
ApplicationProtocolConfig.Protocol.ALPN,
|
}
|
||||||
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
|
SslHandler sslHandler = sslContextBuilder.build().newHandler(ch.alloc());
|
||||||
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
|
SSLEngine engine = sslHandler.engine();
|
||||||
ApplicationProtocolNames.HTTP_2,
|
try {
|
||||||
ApplicationProtocolNames.HTTP_1_1))
|
if (context.isUseServerNameIdentification()) {
|
||||||
.build();
|
String fullQualifiedHostname = key.getInetSocketAddress().getHostName();
|
||||||
SslHandler sslHandler = http2SslContext.newHandler(ch.alloc());
|
SSLParameters params = engine.getSSLParameters();
|
||||||
try {
|
params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
|
||||||
SSLEngine engine = sslHandler.engine();
|
engine.setSSLParameters(params);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
pipeline.addLast(sslHandler);
|
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();
|
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
|
||||||
pipeline.addLast(http1connectionHandler);
|
pipeline.addLast(http1connectionHandler);
|
||||||
configureHttp1Pipeline(pipeline);
|
configureHttp1Pipeline(pipeline);
|
||||||
|
} else if (key.getVersion().majorVersion() == 2) {
|
||||||
|
pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,13 +208,12 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
new HttpObjectAggregator(context.getMaxContentLength(), false);
|
new HttpObjectAggregator(context.getMaxContentLength(), false);
|
||||||
httpObjectAggregator.setMaxCumulationBufferComponents(context.getMaxCompositeBufferComponents());
|
httpObjectAggregator.setMaxCumulationBufferComponents(context.getMaxCompositeBufferComponents());
|
||||||
pipeline.addLast(httpObjectAggregator);
|
pipeline.addLast(httpObjectAggregator);
|
||||||
pipeline.addLast(http1Handler);
|
pipeline.addLast(httpHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureHttp2Pipeline(ChannelPipeline pipeline) {
|
private void configureHttp2Pipeline(ChannelPipeline pipeline) {
|
||||||
pipeline.addLast(http2SettingsHandler);
|
pipeline.addLast(new UserEventLogger());
|
||||||
pipeline.addLast(userEventLogger);
|
pipeline.addLast(http2ResponseHandler);
|
||||||
pipeline.addLast(http2Handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpClientCodec createHttp1ConnectionHandler() {
|
private HttpClientCodec createHttp1ConnectionHandler() {
|
||||||
|
@ -241,13 +224,9 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
final Http2Connection http2Connection = new DefaultHttp2Connection(false);
|
final Http2Connection http2Connection = new DefaultHttp2Connection(false);
|
||||||
return new HttpToHttp2ConnectionHandlerBuilder()
|
return new HttpToHttp2ConnectionHandlerBuilder()
|
||||||
.connection(http2Connection)
|
.connection(http2Connection)
|
||||||
.frameLogger(frameLogger)
|
.frameLogger(new Http2FrameLogger(LogLevel.TRACE, HttpClientChannelInitializer.class))
|
||||||
.frameListener(new DelegatingDecompressorFrameListener(http2Connection,
|
.frameListener(new DelegatingDecompressorFrameListener(http2Connection,
|
||||||
new InboundHttp2ToHttpAdapterBuilder(http2Connection)
|
new Http2EventHandler(http2Connection, context.getMaxContentLength(), false)))
|
||||||
.maxContentLength(context.getMaxContentLength())
|
|
||||||
.propagateSettings(true)
|
|
||||||
.validateHttpHeaders(false)
|
|
||||||
.build()))
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,81 +242,37 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
|
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
|
||||||
ctx.pipeline().addLast(http2connectionHandler);
|
ctx.pipeline().addLast(http2connectionHandler);
|
||||||
configureHttp2Pipeline(ctx.pipeline());
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
|
if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
|
||||||
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
|
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
|
||||||
ctx.pipeline().addLast(http1connectionHandler);
|
ctx.pipeline().addLast(http1connectionHandler);
|
||||||
configureHttp1Pipeline(ctx.pipeline());
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
// close and fail
|
||||||
ctx.close();
|
ctx.close();
|
||||||
throw new IllegalStateException("unexpected protocol: " + protocol);
|
throw new IllegalStateException("unexpected protocol: " + protocol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
|
|
||||||
|
|
||||||
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
|
@Sharable
|
||||||
private class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
|
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
|
@Override
|
||||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||||
DefaultFullHttpRequest upgradeRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
|
DefaultFullHttpRequest upgradeRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
|
||||||
ctx.writeAndFlush(upgradeRequest);
|
ctx.writeAndFlush(upgradeRequest);
|
||||||
super.channelActive(ctx);
|
super.channelActive(ctx);
|
||||||
ctx.pipeline().remove(this);
|
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<SocketChannel> {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
logger.log(Level.FINE, () -> "exceptionCaught " + cause.getMessage());
|
||||||
ExceptionListener exceptionListener =
|
ExceptionListener exceptionListener =
|
||||||
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
|
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
|
||||||
logger.log(Level.FINE, () -> "exceptionCaught");
|
|
||||||
if (exceptionListener != null) {
|
if (exceptionListener != null) {
|
||||||
exceptionListener.onException(cause);
|
exceptionListener.onException(cause);
|
||||||
}
|
}
|
||||||
|
@ -360,6 +295,9 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Netty handler that logs user events and find expetced ones.
|
||||||
|
*/
|
||||||
@Sharable
|
@Sharable
|
||||||
private class UserEventLogger extends ChannelInboundHandlerAdapter {
|
private class UserEventLogger extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
@ -369,11 +307,46 @@ class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent ||
|
if (evt instanceof Http2ConnectionPrefaceWrittenEvent ||
|
||||||
evt instanceof SslCloseCompletionEvent ||
|
evt instanceof SslCloseCompletionEvent ||
|
||||||
evt instanceof ChannelInputShutdownReadComplete) {
|
evt instanceof ChannelInputShutdownReadComplete) {
|
||||||
// Expected events
|
// log expected events
|
||||||
logger.log(Level.FINE, () -> "user event is expected: " + evt);
|
logger.log(Level.FINE, () -> "user event is expected: " + evt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
super.userEventTriggered(ctx, evt);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.xbib.netty.http.client;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.channel.pool.ChannelPoolHandler;
|
import io.netty.channel.pool.ChannelPoolHandler;
|
||||||
import io.netty.channel.socket.SocketChannel;
|
import io.netty.channel.socket.SocketChannel;
|
||||||
|
import org.xbib.netty.http.client.util.InetAddressKey;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
@ -45,7 +46,7 @@ public class HttpClientChannelPoolHandler implements ChannelPoolHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelCreated(Channel ch) throws Exception {
|
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);
|
channelInitializer.initChannel((SocketChannel) ch, key);
|
||||||
int n = active.incrementAndGet();
|
int n = active.incrementAndGet();
|
||||||
if (n > peak) {
|
if (n > peak) {
|
||||||
|
@ -55,12 +56,12 @@ public class HttpClientChannelPoolHandler implements ChannelPoolHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelAcquired(Channel ch) throws Exception {
|
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
|
@Override
|
||||||
public void channelReleased(Channel ch) throws Exception {
|
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();
|
active.decrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.xbib.netty.http.client;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.channel.pool.AbstractChannelPoolMap;
|
import io.netty.channel.pool.AbstractChannelPoolMap;
|
||||||
import io.netty.channel.pool.FixedChannelPool;
|
import io.netty.channel.pool.FixedChannelPool;
|
||||||
|
import org.xbib.netty.http.client.util.InetAddressKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -49,7 +50,7 @@ public class HttpClientChannelPoolMap extends AbstractChannelPoolMap<InetAddress
|
||||||
@Override
|
@Override
|
||||||
protected FixedChannelPool newPool(InetAddressKey key) {
|
protected FixedChannelPool newPool(InetAddressKey key) {
|
||||||
this.httpClientChannelInitializer = new HttpClientChannelInitializer(httpClientChannelContext,
|
this.httpClientChannelInitializer = new HttpClientChannelInitializer(httpClientChannelContext,
|
||||||
new Http1Handler(httpClient), new Http2Handler(httpClient));
|
new HttpHandler(httpClient), new Http2ResponseHandler(httpClient));
|
||||||
this.httpClientChannelPoolHandler = new HttpClientChannelPoolHandler(httpClientChannelInitializer, key);
|
this.httpClientChannelPoolHandler = new HttpClientChannelPoolHandler(httpClientChannelInitializer, key);
|
||||||
return new FixedChannelPool(bootstrap.remoteAddress(key.getInetSocketAddress()),
|
return new FixedChannelPool(bootstrap.remoteAddress(key.getInetSocketAddress()),
|
||||||
httpClientChannelPoolHandler, maxConnections);
|
httpClientChannelPoolHandler, maxConnections);
|
||||||
|
|
|
@ -11,41 +11,57 @@ import io.netty.handler.codec.http.HttpHeaderValues;
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
import io.netty.handler.codec.http.HttpRequest;
|
import io.netty.handler.codec.http.HttpRequest;
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
|
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||||
|
import io.netty.handler.codec.http.QueryStringEncoder;
|
||||||
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
import io.netty.handler.codec.http2.HttpConversionUtil;
|
import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||||
import io.netty.util.AsciiString;
|
import io.netty.util.AsciiString;
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
|
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.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.net.URI;
|
||||||
import java.net.MalformedURLException;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.net.URL;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequestDefaults {
|
public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequestDefaults {
|
||||||
|
|
||||||
private static final AtomicInteger streamId = new AtomicInteger(3);
|
private static final Logger logger = Logger.getLogger(HttpClientRequestBuilder.class.getName());
|
||||||
|
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
private final ByteBufAllocator byteBufAllocator;
|
private final ByteBufAllocator byteBufAllocator;
|
||||||
|
|
||||||
|
private final AtomicInteger streamId;
|
||||||
|
|
||||||
private final DefaultHttpHeaders headers;
|
private final DefaultHttpHeaders headers;
|
||||||
|
|
||||||
private final List<String> removeHeaders;
|
private final List<String> removeHeaders;
|
||||||
|
|
||||||
|
private final Set<Cookie> cookies;
|
||||||
|
|
||||||
private final HttpMethod httpMethod;
|
private final HttpMethod httpMethod;
|
||||||
|
|
||||||
private int timeout = DEFAULT_TIMEOUT_MILLIS;
|
private int timeout = DEFAULT_TIMEOUT_MILLIS;
|
||||||
|
@ -60,9 +76,11 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
|
|
||||||
private int maxRedirects = DEFAULT_MAX_REDIRECT;
|
private int maxRedirects = DEFAULT_MAX_REDIRECT;
|
||||||
|
|
||||||
private URL url;
|
private URI uri;
|
||||||
|
|
||||||
private ByteBuf body;
|
private QueryStringEncoder queryStringEncoder;
|
||||||
|
|
||||||
|
private ByteBuf content;
|
||||||
|
|
||||||
private HttpRequest httpRequest;
|
private HttpRequest httpRequest;
|
||||||
|
|
||||||
|
@ -72,6 +90,12 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
|
|
||||||
private ExceptionListener exceptionListener;
|
private ExceptionListener exceptionListener;
|
||||||
|
|
||||||
|
private HttpHeadersListener httpHeadersListener;
|
||||||
|
|
||||||
|
private CookieListener cookieListener;
|
||||||
|
|
||||||
|
private HttpPushListener httpPushListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct HTTP client request builder.
|
* Construct HTTP client request builder.
|
||||||
*
|
*
|
||||||
|
@ -79,12 +103,15 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
* @param httpMethod HTTP method
|
* @param httpMethod HTTP method
|
||||||
* @param byteBufAllocator byte buf allocator
|
* @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.httpClient = httpClient;
|
||||||
this.httpMethod = httpMethod;
|
this.httpMethod = httpMethod;
|
||||||
this.byteBufAllocator = byteBufAllocator;
|
this.byteBufAllocator = byteBufAllocator;
|
||||||
|
this.streamId = new AtomicInteger(streamId);
|
||||||
this.headers = new DefaultHttpHeaders();
|
this.headers = new DefaultHttpHeaders();
|
||||||
this.removeHeaders = new ArrayList<>();
|
this.removeHeaders = new ArrayList<>();
|
||||||
|
this.cookies = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -93,24 +120,19 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getTimeout() {
|
|
||||||
return timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder setURL(String url) {
|
public HttpRequestBuilder setURL(String url) {
|
||||||
try {
|
this.uri = URI.create(url);
|
||||||
this.url = new URL(url);
|
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri, StandardCharsets.UTF_8);
|
||||||
} catch (MalformedURLException e) {
|
this.queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path());
|
||||||
throw new UncheckedIOException(e);
|
for (Map.Entry<String, List<String>> entry : queryStringDecoder.parameters().entrySet()) {
|
||||||
|
for (String value : entry.getValue()) {
|
||||||
|
queryStringEncoder.addParam(entry.getKey(), value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected URL getURL() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder addHeader(String name, Object value) {
|
public HttpRequestBuilder addHeader(String name, Object value) {
|
||||||
headers.add(name, value);
|
headers.add(name, value);
|
||||||
|
@ -129,6 +151,20 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
return this;
|
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
|
@Override
|
||||||
public HttpRequestBuilder contentType(String contentType) {
|
public HttpRequestBuilder contentType(String contentType) {
|
||||||
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
|
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||||
|
@ -141,10 +177,6 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected HttpVersion getVersion() {
|
|
||||||
return httpVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder acceptGzip(boolean gzip) {
|
public HttpRequestBuilder acceptGzip(boolean gzip) {
|
||||||
this.gzip = gzip;
|
this.gzip = gzip;
|
||||||
|
@ -157,20 +189,12 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isFollowRedirect() {
|
|
||||||
return followRedirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder setMaxRedirects(int maxRedirects) {
|
public HttpRequestBuilder setMaxRedirects(int maxRedirects) {
|
||||||
this.maxRedirects = maxRedirects;
|
this.maxRedirects = maxRedirects;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getMaxRedirects() {
|
|
||||||
return maxRedirects;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder setUserAgent(String userAgent) {
|
public HttpRequestBuilder setUserAgent(String userAgent) {
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
|
@ -179,60 +203,90 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder text(String text) throws IOException {
|
public HttpRequestBuilder text(String text) throws IOException {
|
||||||
setBody(text, HttpHeaderValues.TEXT_PLAIN);
|
content(text, HttpHeaderValues.TEXT_PLAIN);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder json(String json) throws IOException {
|
public HttpRequestBuilder json(String json) throws IOException {
|
||||||
setBody(json, HttpHeaderValues.APPLICATION_JSON);
|
content(json, HttpHeaderValues.APPLICATION_JSON);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder xml(String xml) throws IOException {
|
public HttpRequestBuilder xml(String xml) throws IOException {
|
||||||
setBody(xml, "application/xml");
|
content(xml, "application/xml");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder setBody(CharSequence charSequence, String contentType) throws IOException {
|
public HttpRequestBuilder content(CharSequence charSequence, String contentType) throws IOException {
|
||||||
setBody(charSequence.toString().getBytes(CharsetUtil.UTF_8), AsciiString.of(contentType));
|
content(charSequence.toString().getBytes(CharsetUtil.UTF_8), AsciiString.of(contentType));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder setBody(byte[] buf, String contentType) throws IOException {
|
public HttpRequestBuilder content(byte[] buf, String contentType) throws IOException {
|
||||||
setBody(buf, AsciiString.of(contentType));
|
content(buf, AsciiString.of(contentType));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilder setBody(ByteBuf body, String contentType) throws IOException {
|
public HttpRequestBuilder content(ByteBuf body, String contentType) throws IOException {
|
||||||
setBody(body, AsciiString.of(contentType));
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequest build() {
|
public HttpRequest build() {
|
||||||
if (url == null) {
|
if (uri == null) {
|
||||||
throw new IllegalStateException("URL not set");
|
throw new IllegalStateException("URL not set");
|
||||||
}
|
}
|
||||||
if (url.getHost() == null) {
|
if (uri.getHost() == null) {
|
||||||
throw new IllegalStateException("URL host not set: " + url);
|
throw new IllegalStateException("URL host not set: " + uri);
|
||||||
}
|
}
|
||||||
DefaultHttpRequest httpRequest = createHttpRequest();
|
DefaultHttpRequest httpRequest = createHttpRequest();
|
||||||
String scheme = url.getProtocol();
|
String scheme = uri.getScheme();
|
||||||
StringBuilder sb = new StringBuilder(url.getHost());
|
StringBuilder sb = new StringBuilder(uri.getHost());
|
||||||
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
|
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
|
||||||
if (defaultPort != -1 && url.getPort() != -1 && defaultPort != url.getPort()) {
|
if (defaultPort != -1 && uri.getPort() != -1 && defaultPort != uri.getPort()) {
|
||||||
sb.append(":").append(url.getPort());
|
sb.append(":").append(uri.getPort());
|
||||||
}
|
}
|
||||||
if (httpVersion.majorVersion() == 2) {
|
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().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,
|
httpRequest.headers().add(HttpHeaderNames.DATE,
|
||||||
DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT"))));
|
DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT"))));
|
||||||
if (userAgent != null) {
|
if (userAgent != null) {
|
||||||
|
@ -258,31 +312,57 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
return httpRequest;
|
return httpRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultHttpRequest createHttpRequest() {
|
@Override
|
||||||
// Regarding request-target URI:
|
public HttpRequestContext execute() {
|
||||||
// RFC https://tools.ietf.org/html/rfc7230#section-5.3.2
|
if (httpRequest == null) {
|
||||||
// would allow url.toExternalForm as absolute-form,
|
httpRequest = build();
|
||||||
// but some servers do not support that. So, we create origin-form.
|
}
|
||||||
// But for HTTP/2, we should create the absolute-form, otherwise
|
if (httpResponseListener == null) {
|
||||||
// netty will throw "java.lang.IllegalArgumentException: :scheme must be specified."
|
httpResponseListener = httpRequestContext;
|
||||||
String requestTarget = toOriginForm(url);
|
}
|
||||||
return body == null ?
|
httpRequestContext = new HttpRequestContext(uri, httpRequest, streamId,
|
||||||
new DefaultHttpRequest(httpVersion, httpMethod, requestTarget) :
|
new AtomicBoolean(false),
|
||||||
new DefaultFullHttpRequest(httpVersion, httpMethod, requestTarget, body);
|
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 <T> CompletableFuture<T> execute(Function<FullHttpResponse, T> supplier) {
|
||||||
|
final CompletableFuture<T> 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();
|
StringBuilder sb = new StringBuilder();
|
||||||
String path = base.getPath() != null && !base.getPath().isEmpty() ? base.getPath() : "/";
|
String pathAndQuery = queryStringEncoder.toString();
|
||||||
String query = base.getQuery();
|
sb.append(pathAndQuery.isEmpty() ? "/" : pathAndQuery);
|
||||||
String ref = base.getRef();
|
String ref = uri.getFragment();
|
||||||
if (path.charAt(0) != '/') {
|
|
||||||
sb.append('/');
|
|
||||||
}
|
|
||||||
sb.append(path);
|
|
||||||
if (query != null && !query.isEmpty()) {
|
|
||||||
sb.append('?').append(query);
|
|
||||||
}
|
|
||||||
if (ref != null && !ref.isEmpty()) {
|
if (ref != null && !ref.isEmpty()) {
|
||||||
sb.append('#').append(ref);
|
sb.append('#').append(ref);
|
||||||
}
|
}
|
||||||
|
@ -293,60 +373,18 @@ public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequest
|
||||||
headers.add(name, value);
|
headers.add(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setBody(CharSequence charSequence, AsciiString contentType) throws IOException {
|
private void content(CharSequence charSequence, AsciiString contentType) throws IOException {
|
||||||
setBody(charSequence.toString().getBytes(CharsetUtil.UTF_8), contentType);
|
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);
|
ByteBuf buffer = byteBufAllocator.buffer(buf.length).writeBytes(buf);
|
||||||
setBody(buffer, contentType);
|
content(buffer, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setBody(ByteBuf body, AsciiString contentType) throws IOException {
|
private void content(ByteBuf body, AsciiString contentType) throws IOException {
|
||||||
this.body = body;
|
this.content = body;
|
||||||
addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
|
addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
|
||||||
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
|
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 <T> CompletableFuture<T> execute(Function<FullHttpResponse, T> supplier) {
|
|
||||||
final CompletableFuture<T> completableFuture = new CompletableFuture<>();
|
|
||||||
onResponse(response -> completableFuture.complete(supplier.apply(response)));
|
|
||||||
onError(completableFuture::completeExceptionally);
|
|
||||||
execute();
|
|
||||||
return completableFuture;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,21 +20,29 @@ import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||||
import io.netty.channel.pool.ChannelPool;
|
import io.netty.channel.pool.ChannelPool;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
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.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Netty channel handler for HTTP 1.1.
|
* HTTP 1.x Netty channel handler.
|
||||||
*/
|
*/
|
||||||
@ChannelHandler.Sharable
|
@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;
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
Http1Handler(HttpClient httpClient) {
|
HttpHandler(HttpClient httpClient) {
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,16 +60,34 @@ final class Http1Handler extends ChannelInboundHandlerAdapter {
|
||||||
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
|
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
|
||||||
if (msg instanceof FullHttpResponse) {
|
if (msg instanceof FullHttpResponse) {
|
||||||
FullHttpResponse httpResponse = (FullHttpResponse) msg;
|
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 =
|
HttpResponseListener httpResponseListener =
|
||||||
ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
|
ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
|
||||||
if (httpResponseListener != null) {
|
if (httpResponseListener != null) {
|
||||||
|
logger.log(Level.FINE, () -> "firing onResponse");
|
||||||
httpResponseListener.onResponse(httpResponse);
|
httpResponseListener.onResponse(httpResponse);
|
||||||
}
|
}
|
||||||
|
logger.log(Level.FINE, () -> "trying redirect");
|
||||||
if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
|
if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.log(Level.FINE, () -> "success");
|
httpRequestContext.success("response finished");
|
||||||
httpRequestContext.success("response arrived");
|
|
||||||
final ChannelPool channelPool =
|
final ChannelPool channelPool =
|
||||||
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
|
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
|
||||||
channelPool.release(ctx.channel());
|
channelPool.release(ctx.channel());
|
|
@ -18,6 +18,12 @@ package org.xbib.netty.http.client;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpRequest;
|
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.io.IOException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
@ -39,6 +45,10 @@ public interface HttpRequestBuilder {
|
||||||
|
|
||||||
HttpRequestBuilder removeHeader(String name);
|
HttpRequestBuilder removeHeader(String name);
|
||||||
|
|
||||||
|
HttpRequestBuilder addParam(String name, String value);
|
||||||
|
|
||||||
|
HttpRequestBuilder addCookie(Cookie cookie);
|
||||||
|
|
||||||
HttpRequestBuilder contentType(String contentType);
|
HttpRequestBuilder contentType(String contentType);
|
||||||
|
|
||||||
HttpRequestBuilder acceptGzip(boolean gzip);
|
HttpRequestBuilder acceptGzip(boolean gzip);
|
||||||
|
@ -49,7 +59,7 @@ public interface HttpRequestBuilder {
|
||||||
|
|
||||||
HttpRequestBuilder setUserAgent(String userAgent);
|
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;
|
HttpRequestBuilder text(String text) throws IOException;
|
||||||
|
|
||||||
|
@ -57,14 +67,20 @@ public interface HttpRequestBuilder {
|
||||||
|
|
||||||
HttpRequestBuilder xml(String xmlText) throws IOException;
|
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 onResponse(HttpResponseListener httpResponseListener);
|
||||||
|
|
||||||
|
HttpRequestBuilder onException(ExceptionListener exceptionListener);
|
||||||
|
|
||||||
|
HttpRequestBuilder onPushReceived(HttpPushListener httpPushListener);
|
||||||
|
|
||||||
HttpRequest build();
|
HttpRequest build();
|
||||||
|
|
||||||
HttpRequestContext execute();
|
HttpRequestContext execute();
|
||||||
|
|
|
@ -15,16 +15,32 @@
|
||||||
*/
|
*/
|
||||||
package org.xbib.netty.http.client;
|
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.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpRequest;
|
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.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
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 static final Logger logger = Logger.getLogger(HttpRequestContext.class.getName());
|
||||||
|
|
||||||
private final URL url;
|
private final URI uri;
|
||||||
|
|
||||||
private final HttpRequest httpRequest;
|
private final HttpRequest httpRequest;
|
||||||
|
|
||||||
|
@ -53,19 +69,43 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
|
||||||
|
|
||||||
private final CountDownLatch latch;
|
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<Integer, Map.Entry<ChannelFuture, ChannelPromise>> promiseMap;
|
||||||
|
|
||||||
|
private final Map<Integer, Map.Entry<Http2Headers, ChannelPromise>> pushMap;
|
||||||
|
|
||||||
|
private ChannelPromise settingsPromise;
|
||||||
|
|
||||||
|
private Collection<Cookie> cookies;
|
||||||
|
|
||||||
|
private Map<Integer, FullHttpResponse> httpResponses;
|
||||||
|
|
||||||
private Long stopTime;
|
private Long stopTime;
|
||||||
|
|
||||||
HttpRequestContext(URL url, HttpRequest httpRequest,
|
HttpRequestContext(URI uri, HttpRequest httpRequest, AtomicInteger streamId,
|
||||||
AtomicBoolean succeeded, AtomicBoolean failed,
|
AtomicBoolean succeeded, AtomicBoolean failed,
|
||||||
int timeout, Long startTime,
|
int timeout, Long startTime,
|
||||||
boolean followRedirect, int maxRedirects, AtomicInteger redirectCount,
|
boolean followRedirect, int maxRedirects, AtomicInteger redirectCount,
|
||||||
CountDownLatch latch, Integer streamId) {
|
CountDownLatch latch,
|
||||||
this.url = url;
|
HttpResponseListener httpResponseListener,
|
||||||
|
ExceptionListener exceptionListener,
|
||||||
|
HttpHeadersListener httpHeadersListener,
|
||||||
|
CookieListener cookieListener,
|
||||||
|
HttpPushListener httpPushListener) {
|
||||||
|
this.uri = uri;
|
||||||
this.httpRequest = httpRequest;
|
this.httpRequest = httpRequest;
|
||||||
|
this.streamId = streamId;
|
||||||
this.succeeded = succeeded;
|
this.succeeded = succeeded;
|
||||||
this.failed = failed;
|
this.failed = failed;
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
|
@ -74,12 +114,24 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
|
||||||
this.maxRedirects = maxRedirects;
|
this.maxRedirects = maxRedirects;
|
||||||
this.redirectCount = redirectCount;
|
this.redirectCount = redirectCount;
|
||||||
this.latch = latch;
|
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.httpRequest = httpRequest;
|
||||||
|
this.streamId = httpRequestContext.streamId;
|
||||||
this.succeeded = httpRequestContext.succeeded;
|
this.succeeded = httpRequestContext.succeeded;
|
||||||
this.failed = httpRequestContext.failed;
|
this.failed = httpRequestContext.failed;
|
||||||
this.failed.lazySet(false); // reset
|
this.failed.lazySet(false); // reset
|
||||||
|
@ -89,17 +141,105 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
|
||||||
this.maxRedirects = httpRequestContext.maxRedirects;
|
this.maxRedirects = httpRequestContext.maxRedirects;
|
||||||
this.redirectCount = httpRequestContext.redirectCount;
|
this.redirectCount = httpRequestContext.redirectCount;
|
||||||
this.latch = httpRequestContext.latch;
|
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() {
|
public URI getURI() {
|
||||||
return url;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequest getHttpRequest() {
|
public HttpRequest getHttpRequest() {
|
||||||
return httpRequest;
|
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<Integer, Map.Entry<ChannelFuture, ChannelPromise>> 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<Integer, Map.Entry<Http2Headers, ChannelPromise>> 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<Cookie> getCookies() {
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Cookie> 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() {
|
public int getTimeout() {
|
||||||
return timeout;
|
return timeout;
|
||||||
}
|
}
|
||||||
|
@ -144,7 +284,7 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
|
||||||
return latch;
|
return latch;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getStreamId() {
|
public AtomicInteger getStreamId() {
|
||||||
return streamId;
|
return streamId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,12 +302,15 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
|
||||||
logger.log(Level.FINE, () -> "success because of " + reason);
|
logger.log(Level.FINE, () -> "success because of " + reason);
|
||||||
if (succeeded.compareAndSet(false, true)) {
|
if (succeeded.compareAndSet(false, true)) {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fail(String reason) {
|
public void fail(String reason) {
|
||||||
logger.log(Level.FINE, () -> "failed because of " + 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)) {
|
if (failed.compareAndSet(false, true)) {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
|
@ -175,11 +318,10 @@ public final class HttpRequestContext implements HttpResponseListener, HttpReque
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(FullHttpResponse fullHttpResponse) {
|
public void onResponse(FullHttpResponse fullHttpResponse) {
|
||||||
this.httpResponse = fullHttpResponse;
|
this.httpResponses.put(streamId.get(), fullHttpResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FullHttpResponse getHttpResponse() {
|
public Map<Integer, FullHttpResponse> getHttpResponses() {
|
||||||
return httpResponse;
|
return httpResponses;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.xbib.netty.http.client;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.xbib.netty.http.client;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Listeners for Netty HTTP client.
|
||||||
|
*/
|
||||||
|
package org.xbib.netty.http.client.listener;
|
|
@ -13,11 +13,11 @@
|
||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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
|
NONE, WANT, NEED
|
||||||
}
|
}
|
|
@ -13,66 +13,64 @@
|
||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.xbib.netty.http.client;
|
package org.xbib.netty.http.client.util;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
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 {
|
public class InetAddressKey {
|
||||||
|
|
||||||
private final InetSocketAddress inetSocketAddress;
|
private final String host;
|
||||||
|
|
||||||
|
private final int port;
|
||||||
|
|
||||||
private final HttpVersion version;
|
private final HttpVersion version;
|
||||||
|
|
||||||
private final Boolean secure;
|
private final Boolean secure;
|
||||||
|
|
||||||
InetAddressKey(URL url, HttpVersion version) {
|
private InetSocketAddress inetSocketAddress;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
InetAddressKey(InetSocketAddress inetSocketAddress, HttpVersion version, boolean secure) {
|
public InetAddressKey(String host, int port, HttpVersion version, boolean secure) {
|
||||||
this.inetSocketAddress = inetSocketAddress;
|
this.host = host;
|
||||||
|
this.port = port == -1 ? secure ? 443 : 80 : port;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.secure = secure;
|
this.secure = secure;
|
||||||
}
|
}
|
||||||
|
|
||||||
InetSocketAddress getInetSocketAddress() {
|
public InetSocketAddress getInetSocketAddress() {
|
||||||
|
if (inetSocketAddress == null) {
|
||||||
|
this.inetSocketAddress = new InetSocketAddress(host, port);
|
||||||
|
}
|
||||||
return inetSocketAddress;
|
return inetSocketAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpVersion getVersion() {
|
public HttpVersion getVersion() {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isSecure() {
|
public boolean isSecure() {
|
||||||
return secure;
|
return secure;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return inetSocketAddress + " (version:" + version + ",secure:" + secure + ")";
|
return host + ":" + port + " (version:" + version + ",secure:" + secure + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object object) {
|
public boolean equals(Object object) {
|
||||||
return object instanceof InetAddressKey &&
|
return object instanceof InetAddressKey &&
|
||||||
inetSocketAddress.equals(((InetAddressKey) object).inetSocketAddress) &&
|
host.equals(((InetAddressKey) object).host) &&
|
||||||
|
port == ((InetAddressKey) object).port &&
|
||||||
version.equals(((InetAddressKey) object).version) &&
|
version.equals(((InetAddressKey) object).version) &&
|
||||||
secure == ((InetAddressKey) object).secure;
|
secure == ((InetAddressKey) object).secure;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return inetSocketAddress.hashCode() ^ version.hashCode() ^ secure.hashCode();
|
return host.hashCode() ^ port ^ version.hashCode() ^ secure.hashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<E> extends LinkedHashSet<E> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
596
src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java
Normal file
596
src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java
Normal file
|
@ -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<String, String> 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<String, String> 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<InetAddress> 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<InetAddress> 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<InetAddress> 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<NetworkInterface> getAllNetworkInterfaces() {
|
||||||
|
return getInterfaces(n -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<NetworkInterface> getAllRunningAndUpInterfaces() {
|
||||||
|
return getInterfaces(NetworkUtils::isUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<NetworkInterface> getInterfaces(Predicate<NetworkInterface> predicate) {
|
||||||
|
List<NetworkInterface> networkInterfaces = new ArrayList<>();
|
||||||
|
Enumeration<NetworkInterface> 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<NetworkInterface> subInterfaces = networkInterface.getSubInterfaces();
|
||||||
|
if (subInterfaces.hasMoreElements()) {
|
||||||
|
while (subInterfaces.hasMoreElements()) {
|
||||||
|
networkInterfaces.add(subInterfaces.nextElement());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortInterfaces(networkInterfaces);
|
||||||
|
return networkInterfaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<InetAddress> getAllAvailableAddresses() {
|
||||||
|
List<InetAddress> allAddresses = new ArrayList<>();
|
||||||
|
for (NetworkInterface networkInterface : getAllNetworkInterfaces()) {
|
||||||
|
Enumeration<InetAddress> 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<String> 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<InterfaceAddress> 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<NetworkInterface> interfaces) {
|
||||||
|
interfaces.sort(Comparator.comparingInt(NetworkInterface::getIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sortAddresses(List<InetAddress> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Utilities for Netty HTTP client.
|
||||||
|
*/
|
||||||
|
package org.xbib.netty.http.client.util;
|
|
@ -33,10 +33,9 @@ public class AkamaiTest {
|
||||||
private static final Logger logger = Logger.getLogger("");
|
private static final Logger logger = Logger.getLogger("");
|
||||||
|
|
||||||
@Test
|
@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.
|
// here we see 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?
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
----------------INBOUND--------------------
|
----------------INBOUND--------------------
|
||||||
|
@ -59,11 +58,17 @@ public class AkamaiTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("https://http2.akamai.com/demo/h2_demo_frame.html")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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()
|
.execute()
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package org.xbib.netty.http.client.test;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.xbib.netty.http.client.HttpClient;
|
import org.xbib.netty.http.client.HttpClient;
|
||||||
import org.xbib.netty.http.client.HttpRequestBuilder;
|
import org.xbib.netty.http.client.HttpRequestBuilder;
|
||||||
|
@ -64,7 +63,7 @@ public class ElasticsearchTest {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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()
|
.execute()
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
@ -82,7 +81,7 @@ public class ElasticsearchTest {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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()
|
.execute()
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
@ -122,6 +121,6 @@ public class ElasticsearchTest {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class ExceptionTest {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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()
|
.execute()
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.xbib.netty.http.client.test;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.xbib.netty.http.client.HttpClient;
|
import org.xbib.netty.http.client.HttpClient;
|
||||||
import org.xbib.netty.http.client.HttpRequestBuilder;
|
import org.xbib.netty.http.client.HttpRequestBuilder;
|
||||||
|
@ -41,7 +40,8 @@ public class GoogleTest {
|
||||||
.build();
|
.build();
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setURL("http://www.google.com")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -51,7 +51,7 @@ public class GoogleTest {
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGoogleWithoutFollowRedirects() throws Exception {
|
public void testGoogleWithoutFollowRedirects() throws Exception {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
HttpClient httpClient = HttpClient.builder()
|
||||||
.build();
|
.build();
|
||||||
|
@ -68,13 +68,14 @@ public class GoogleTest {
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGoogleHttps1() throws Exception {
|
public void testGoogleHttps1() throws Exception {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
HttpClient httpClient = HttpClient.builder()
|
||||||
.build();
|
.build();
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setURL("https://www.google.com")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -92,7 +93,7 @@ public class GoogleTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("https://www.google.com")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -110,8 +111,7 @@ public class GoogleTest {
|
||||||
HttpRequestBuilder builder1 = httpClient.prepareGet()
|
HttpRequestBuilder builder1 = httpClient.prepareGet()
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("https://www.google.com")
|
.setURL("https://www.google.com")
|
||||||
.setTimeout(10000)
|
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
|
||||||
.onResponse(fullHttpResponse -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -120,19 +120,15 @@ public class GoogleTest {
|
||||||
HttpRequestBuilder builder2 = httpClient.prepareGet()
|
HttpRequestBuilder builder2 = httpClient.prepareGet()
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("https://www.google.com")
|
.setURL("https://www.google.com")
|
||||||
.setTimeout(10000)
|
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
|
||||||
.onResponse(fullHttpResponse -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
});
|
});
|
||||||
|
|
||||||
// only sequential ... this sucks.
|
|
||||||
|
|
||||||
HttpRequestContext context1 = builder1.execute();
|
HttpRequestContext context1 = builder1.execute();
|
||||||
context1.get();
|
|
||||||
|
|
||||||
HttpRequestContext context2 = builder2.execute();
|
HttpRequestContext context2 = builder2.execute();
|
||||||
|
context1.get();
|
||||||
context2.get();
|
context2.get();
|
||||||
|
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
|
|
@ -68,7 +68,8 @@ public class Http2FrameAdapterTest {
|
||||||
@Test
|
@Test
|
||||||
public void testHttp2FrameAdapter() throws Exception {
|
public void testHttp2FrameAdapter() throws Exception {
|
||||||
final int serverExpectedDataFrames = 1;
|
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);
|
final CountDownLatch dataLatch = new CountDownLatch(serverExpectedDataFrames);
|
||||||
EventLoopGroup group = new NioEventLoopGroup();
|
EventLoopGroup group = new NioEventLoopGroup();
|
||||||
Channel clientChannel = null;
|
Channel clientChannel = null;
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class Http2PushioTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("https://http2-push.io")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
|
|
@ -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
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "cookies": {
|
||||||
|
* "name": "value"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<Channel>() {
|
|
||||||
@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<Http2Settings> {
|
|
||||||
|
|
||||||
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<FullHttpResponse> {
|
|
||||||
|
|
||||||
private final Map<Integer, Map.Entry<ChannelFuture, ChannelPromise>> 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<ChannelFuture, ChannelPromise> 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<Map.Entry<Integer, Map.Entry<ChannelFuture, ChannelPromise>>> iterator = streamidPromiseMap.entrySet().iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Map.Entry<Integer, Map.Entry<ChannelFuture, ChannelPromise>> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,7 +16,6 @@
|
||||||
package org.xbib.netty.http.client.test;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.xbib.netty.http.client.HttpClient;
|
import org.xbib.netty.http.client.HttpClient;
|
||||||
import org.xbib.netty.http.client.HttpRequestBuilder;
|
import org.xbib.netty.http.client.HttpRequestBuilder;
|
||||||
|
@ -53,14 +52,13 @@ public class IndexHbzTest {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger("");
|
private static final Logger logger = Logger.getLogger("");
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIndexHbz() throws Exception {
|
public void testIndexHbz() throws Exception {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
HttpClient httpClient = HttpClient.builder()
|
||||||
.build();
|
.build();
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://index.hbz-nrw.de")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -77,7 +75,7 @@ public class IndexHbzTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("https://index.hbz-nrw.de")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -123,7 +121,7 @@ public class IndexHbzTest {
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("https://index.hbz-nrw.de")
|
.setURL("https://index.hbz-nrw.de")
|
||||||
.setTimeout(5000)
|
.setTimeout(5000)
|
||||||
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
.onResponse(fullHttpResponse -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -133,7 +131,6 @@ public class IndexHbzTest {
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIndexHbzH2C() throws Exception {
|
public void testIndexHbzH2C() throws Exception {
|
||||||
|
|
||||||
// times out waiting for http2 settings frame
|
// times out waiting for http2 settings frame
|
||||||
|
@ -144,7 +141,7 @@ public class IndexHbzTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/2.0")
|
.setVersion("HTTP/2.0")
|
||||||
.setURL("http://index.hbz-nrw.de")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -155,7 +152,7 @@ public class IndexHbzTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIndexHbzConcurrent() throws Exception {
|
public void testIndexHbzConcurrentHttp1() throws Exception {
|
||||||
|
|
||||||
HttpClient httpClient = HttpClient.builder()
|
HttpClient httpClient = HttpClient.builder()
|
||||||
.build();
|
.build();
|
||||||
|
@ -163,7 +160,7 @@ public class IndexHbzTest {
|
||||||
HttpRequestBuilder builder1 = httpClient.prepareGet()
|
HttpRequestBuilder builder1 = httpClient.prepareGet()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://index.hbz-nrw.de")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -172,7 +169,7 @@ public class IndexHbzTest {
|
||||||
HttpRequestBuilder builder2 = httpClient.prepareGet()
|
HttpRequestBuilder builder2 = httpClient.prepareGet()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://index.hbz-nrw.de")
|
.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 -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,6 +92,7 @@ public class XbibTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore
|
||||||
public void testXbibOrgWithProxy() throws Exception {
|
public void testXbibOrgWithProxy() throws Exception {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
HttpClient httpClient = HttpClient.builder()
|
||||||
.setHttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080))
|
.setHttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080))
|
||||||
|
@ -104,7 +105,7 @@ public class XbibTest {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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()
|
.execute()
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
@ -122,7 +123,7 @@ public class XbibTest {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
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()
|
.execute()
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
|
@ -137,7 +138,7 @@ public class XbibTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://xbib.org")
|
.setURL("http://xbib.org")
|
||||||
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
.onResponse(fullHttpResponse -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
@ -148,7 +149,7 @@ public class XbibTest {
|
||||||
httpClient.prepareGet()
|
httpClient.prepareGet()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://xbib.org")
|
.setURL("http://xbib.org")
|
||||||
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
.onResponse(fullHttpResponse -> {
|
.onResponse(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
|
|
Loading…
Reference in a new issue