From 94d6e90a0dfd3eb339961e9a6af5ac061ec52168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Thu, 20 Oct 2022 10:19:32 +0200 Subject: [PATCH] initial commit --- .gitignore | 16 + LICENSE.txt | 202 +++++++ README.md | 68 +++ build.gradle | 46 ++ config/checkstyle/checkstyle.xml | 321 ++++++++++ gradle.properties | 5 + gradle/compile/java.gradle | 30 + gradle/documentation/asciidoc.gradle | 13 + gradle/ide/idea.gradle | 8 + gradle/publish/ivy.gradle | 27 + gradle/publish/maven.gradle | 64 ++ gradle/publish/sonatype.gradle | 11 + gradle/quality/sonarqube.gradle | 50 ++ gradle/repositories/maven.gradle | 4 + gradle/test/jmh.gradle | 22 + gradle/test/junit5.gradle | 36 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 240 ++++++++ gradlew.bat | 91 +++ net-http-client-netty-secure/build.gradle | 7 + .../src/main/java/module-info.java | 27 + .../secure/ClientSecureSocketProvider.java | 21 + .../client/netty/secure/HttpsRequest.java | 23 + .../netty/secure/HttpsRequestBuilder.java | 18 + .../client/netty/secure/HttpsResponse.java | 22 + .../netty/secure/HttpsResponseBuilder.java | 20 + .../secure/JdkClientSecureSocketProvider.java | 50 ++ .../netty/secure/NettyHttpsClientConfig.java | 159 +++++ .../http1/Https1ChannelInitializer.java | 260 ++++++++ .../netty/secure/http1/Https1Interaction.java | 32 + .../http2/Https2ChannelInitializer.java | 187 ++++++ .../http2/Https2ChildChannelInitializer.java | 29 + .../netty/secure/http2/Https2Interaction.java | 37 ++ ...t.http.client.netty.HttpChannelInitializer | 2 + ...nt.netty.secure.ClientSecureSocketProvider | 1 + .../http/netty/client/secure/AkamaiTest.java | 39 ++ .../client/secure/ExponentialBackOffTest.java | 155 +++++ .../client/secure/FileDescriptorLeakTest.java | 43 ++ .../netty/client/secure/Http2FramesTest.java | 117 ++++ .../http/netty/client/secure/HttpBinTest.java | 58 ++ .../http/netty/client/secure/Https1Test.java | 295 +++++++++ .../http/netty/client/secure/Https2Test.java | 116 ++++ .../netty/client/secure/JdkClientTest.java | 71 +++ .../http/netty/client/secure/MockBackOff.java | 76 +++ .../netty/client/secure/MockBackOffTest.java | 25 + .../netty/client/secure/SimpleHttp1Test.java | 257 ++++++++ .../netty/client/secure/SimpleHttp2Test.java | 348 +++++++++++ .../netty/client/secure/ThreadLeakTest.java | 36 ++ .../http/netty/client/secure/WebtideTest.java | 369 ++++++++++++ ...nt.netty.secure.ClientSecureSocketProvider | 1 + .../src/test/resources/logging.properties | 5 + net-http-client-netty/build.gradle | 5 + .../src/main/java/module-info.java | 27 + .../http/client/netty/BaseInteraction.java | 429 +++++++++++++ .../http/client/netty/BoundedChannelPool.java | 395 ++++++++++++ .../client/netty/ClientTransportProvider.java | 13 + .../client/netty/HttpChannelInitializer.java | 20 + .../netty/HttpChannelPoolInitializer.java | 6 + .../netty/HttpChunkContentCompressor.java | 27 + .../net/http/client/netty/HttpRequest.java | 267 +++++++++ .../http/client/netty/HttpRequestBuilder.java | 438 ++++++++++++++ .../net/http/client/netty/HttpResponse.java | 81 +++ .../client/netty/HttpResponseBuilder.java | 73 +++ .../net/http/client/netty/Interaction.java | 61 ++ .../http/client/netty/NettyCustomizer.java | 46 ++ .../http/client/netty/NettyHttpClient.java | 260 ++++++++ .../client/netty/NettyHttpClientBuilder.java | 177 ++++++ .../client/netty/NettyHttpClientConfig.java | 333 +++++++++++ .../netty/NioClientTransportProvider.java | 23 + .../org/xbib/net/http/client/netty/Pool.java | 27 + .../xbib/net/http/client/netty/StreamIds.java | 71 +++ .../client/netty/TrafficLoggingHandler.java | 43 ++ .../xbib/net/http/client/netty/UserAgent.java | 43 ++ .../netty/http1/Http1ChannelInitializer.java | 102 ++++ .../http/client/netty/http1/Http1Handler.java | 47 ++ .../client/netty/http1/Http1Interaction.java | 255 ++++++++ .../netty/http2/Http2ChannelInitializer.java | 84 +++ .../http2/Http2ChildChannelInitializer.java | 38 ++ .../http/client/netty/http2/Http2Handler.java | 50 ++ .../client/netty/http2/Http2Interaction.java | 254 ++++++++ .../client/netty/http2/Http2Messages.java | 44 ++ ....http.client.netty.ClientTransportProvider | 1 + ...t.http.client.netty.HttpChannelInitializer | 2 + .../xbib/net/http/client/netty/Http1Test.java | 37 ++ .../xbib/net/http/client/netty/Http2Test.java | 43 ++ .../src/test/resources/logging.properties | 5 + net-http-client-simple/build.gradle | 3 + .../src/main/java/module-info.java | 6 + .../net/http/client/jdk/JdkHttpClient.java | 28 + .../http/client/jdk/JdkHttpClientBuilder.java | 39 ++ .../http/client/jdk/JdkHttpClientConfig.java | 183 ++++++ net-http-client/build.gradle | 3 + .../src/main/java/module-info.java | 7 + .../org/xbib/net/http/client/BackOff.java | 65 ++ .../xbib/net/http/client/BaseHttpRequest.java | 65 ++ .../http/client/BaseHttpRequestBuilder.java | 124 ++++ .../xbib/net/http/client/ClientAuthMode.java | 8 + .../net/http/client/ExceptionListener.java | 7 + .../net/http/client/ExponentialBackOff.java | 489 +++++++++++++++ .../org/xbib/net/http/client/HttpClient.java | 12 + .../org/xbib/net/http/client/HttpRequest.java | 29 + .../net/http/client/HttpRequestBuilder.java | 23 + .../xbib/net/http/client/HttpResponse.java | 34 ++ .../net/http/client/HttpResponseBuilder.java | 11 + .../net/http/client/ResponseListener.java | 7 + .../xbib/net/http/client/TimeoutListener.java | 7 + .../net/http/client/cookie/CookieDecoder.java | 255 ++++++++ .../net/http/client/cookie/CookieEncoder.java | 199 +++++++ .../http/client/ClientCookieDecoderTest.java | 274 +++++++++ .../http/client/ClientCookieEncoderTest.java | 43 ++ net-http-netty-boringssl/build.gradle | 8 + .../src/main/java/module-info.java | 17 + .../BoringSSLClientSecureSocketProvider.java | 63 ++ .../BoringSSLServerSecureSocketProvider.java | 61 ++ ...nt.netty.secure.ClientSecureSocketProvider | 1 + ...y.client.secure.ServerSecureSocketProvider | 1 + net-http-netty-conscrypt/build.gradle | 5 + .../src/main/java/module-info.java | 15 + .../ConscryptClientSecureSocketProvider.java | 57 ++ .../ConscryptServerSecureSocketProvider.java | 57 ++ .../META-INF/services/java.security.Provider | 1 + ...nt.netty.secure.ClientSecureSocketProvider | 1 + ...er.netty.secure.ServerSecureSocketProvider | 1 + .../net/http/netty/conscrypt/Http1Test.java | 38 ++ .../src/test/resources/logging.properties | 5 + net-http-netty-epoll/build.gradle | 5 + .../src/main/java/module-info.java | 14 + .../epoll/EpollClientTransportProvider.java | 25 + .../epoll/EpollServerTransportProvider.java | 26 + ....http.client.netty.ClientTransportProvider | 1 + ....http.server.netty.ServerTransportProvider | 1 + net-http-netty-kqueue/build.gradle | 5 + .../src/main/java/module-info.java | 14 + .../kqueue/KqueueClientTransportProvider.java | 25 + .../kqueue/KqueueServerTransportProvider.java | 25 + ....http.client.netty.ClientTransportProvider | 1 + ....http.server.netty.ServerTransportProvider | 1 + .../build.gradle | 4 + .../src/main/java/module-info.java | 12 + .../config/ConfigApplicationModule.java | 70 +++ .../build.gradle | 5 + .../src/main/java/module-info.java | 12 + .../application/database/BaseDatabase.java | 283 +++++++++ .../server/application/database/Database.java | 37 ++ .../database/DatabaseApplicationModule.java | 103 ++++ .../server/application/database/Table.java | 203 +++++++ net-http-server-application-web/build.gradle | 33 + .../src/main/application/400.gtpl | 3 + .../src/main/application/403.gtpl | 3 + .../src/main/application/404.gtpl | 3 + .../src/main/application/500.gtpl | 29 + .../application/demo/auth/form/index.gtpl | 29 + .../application/demo/auth/form/success.gtpl | 12 + .../src/main/application/index.gtpl | 5 + .../src/main/java/module-info.java | 16 + .../server/application/web/Bootstrap.java | 199 +++++++ .../application/web/WebApplication.java | 59 ++ .../web/WebApplicationBuilder.java | 43 ++ ...org.xbib.net.http.server.ApplicationModule | 3 + ...t.http.server.netty.HttpChannelInitializer | 4 + ....http.server.netty.ServerTransportProvider | 1 + .../org.xbib.net.security.CertificateProvider | 1 + .../services/org.xbib.settings.SettingsLoader | 2 + .../src/main/resources/logging.properties | 8 + .../src/test/resources/logging.properties | 5 + net-http-server-netty-secure/build.gradle | 13 + .../src/main/java/module-info.java | 24 + .../server/netty/secure/HttpsAddress.java | 382 ++++++++++++ .../server/netty/secure/HttpsRequest.java | 27 + .../netty/secure/HttpsRequestBuilder.java | 86 +++ .../secure/JdkServerSecureSocketProvider.java | 45 ++ .../netty/secure/NettyHttpsServerConfig.java | 172 ++++++ .../secure/ServerNameIndicationHandler.java | 64 ++ .../secure/ServerSecureSocketProvider.java | 21 + .../http1/Https1ChannelInitializer.java | 117 ++++ .../netty/secure/http1/Https1Handler.java | 104 ++++ .../http2/Https2ChannelInitializer.java | 120 ++++ .../http2/Https2ChildChannelInitializer.java | 52 ++ .../netty/secure/http2/Https2Handler.java | 83 +++ .../netty/secure/http2/Https2Messages.java | 36 ++ ...NettyHttps2ServerMultiRequestLoadTest.java | 107 ++++ .../secure/test/NettyHttps2ServerTest.java | 106 ++++ .../NettyHttpsServerMultiRequestLoadTest.java | 197 ++++++ .../secure/test/NettyHttpsServerTest.java | 264 ++++++++ ...t.http.server.netty.HttpChannelInitializer | 4 + ....http.server.netty.ServerTransportProvider | 1 + .../org.xbib.net.security.CertificateProvider | 2 + .../src/test/resources/logging.properties | 5 + net-http-server-netty/build.gradle | 12 + .../src/main/java/module-info.java | 30 + .../server/netty/HttpChannelInitializer.java | 11 + .../net/http/server/netty/HttpRequest.java | 56 ++ .../http/server/netty/HttpRequestBuilder.java | 106 ++++ .../net/http/server/netty/HttpResponse.java | 35 ++ .../server/netty/HttpResponseBuilder.java | 285 +++++++++ .../http/server/netty/IdleTimeoutHandler.java | 32 + .../http/server/netty/NettyCustomizer.java | 52 ++ .../http/server/netty/NettyHttpServer.java | 217 +++++++ .../server/netty/NettyHttpServerBuilder.java | 152 +++++ .../server/netty/NettyHttpServerConfig.java | 184 ++++++ .../netty/NioServerTransportProvider.java | 24 + .../server/netty/ServerTransportProvider.java | 13 + .../server/netty/TrafficLoggingHandler.java | 43 ++ .../server/netty/buffer/NettyDataBuffer.java | 324 ++++++++++ .../netty/buffer/NettyDataBufferFactory.java | 128 ++++ .../netty/http1/Http1ChannelInitializer.java | 69 +++ .../http/server/netty/http1/Http1Handler.java | 103 ++++ .../netty/http1/HttpPipelinedRequest.java | 63 ++ .../netty/http1/HttpPipelinedResponse.java | 77 +++ .../netty/http1/HttpPipeliningHandler.java | 119 ++++ .../netty/http2/Http2ChannelInitializer.java | 109 ++++ .../http2/Http2ChildChannelInitializer.java | 48 ++ .../http/server/netty/http2/Http2Handler.java | 82 +++ .../server/netty/http2/Http2Messages.java | 46 ++ .../org.xbib.net.buffer.DataBufferFactory | 1 + .../NettyHttp2ServerMultiRequestLoadTest.java | 97 +++ .../http/netty/test/NettyHttp2ServerTest.java | 87 +++ .../test/NettyHttpServerFailureTest.java | 112 ++++ .../NettyHttpServerMultiRequestLoadTest.java | 97 +++ .../http/netty/test/NettyHttpServerTest.java | 87 +++ .../pipelining/HttpPipeliningHandlerTest.java | 265 +++++++++ .../simple/SimpleHttp2ClientServerTest.java | 220 +++++++ .../SimpleHttpClientServerFailureTest.java | 255 ++++++++ .../simple/SimpleHttpClientServerTest.java | 207 +++++++ ...t.http.server.netty.HttpChannelInitializer | 2 + ....http.server.netty.ServerTransportProvider | 1 + .../src/test/resources/logging.properties | 5 + net-http-server-nio/build.gradle | 9 + .../src/main/java/module-info.java | 8 + .../xbib/net/http/server/nio/HttpRequest.java | 44 ++ .../http/server/nio/HttpRequestBuilder.java | 99 +++ .../net/http/server/nio/HttpResponse.java | 39 ++ .../http/server/nio/HttpResponseBuilder.java | 117 ++++ .../net/http/server/nio/NioHttpServer.java | 241 ++++++++ .../http/server/nio/NioHttpServerBuilder.java | 28 + .../net/http/server/nio/demo/ByteArray.java | 88 +++ .../http/server/nio/demo/ClientBootstrap.java | 33 + .../net/http/server/nio/demo/EventLoop.java | 72 +++ .../http/server/nio/demo/EventLoopGroup.java | 52 ++ .../net/http/server/nio/demo/HttpContext.java | 51 ++ .../net/http/server/nio/demo/HttpHeader.java | 45 ++ .../net/http/server/nio/demo/HttpHeaders.java | 55 ++ .../net/http/server/nio/demo/HttpMethod.java | 5 + .../net/http/server/nio/demo/HttpRequest.java | 50 ++ .../server/nio/demo/HttpRequestHandler.java | 5 + .../server/nio/demo/HttpRequestParser.java | 210 +++++++ .../http/server/nio/demo/HttpResponse.java | 83 +++ .../net/http/server/nio/demo/HttpServer.java | 19 + .../server/nio/demo/HttpServerHandler.java | 37 ++ .../http/server/nio/demo/ServerBootstrap.java | 48 ++ .../http/server/nio/demo/SocketContext.java | 53 ++ .../http/server/nio/demo/SocketHandler.java | 21 + .../nio/demo/SocketHandlerProvider.java | 5 + .../net/http/server/nio/demo/UriHandler.java | 21 + .../xbib/net/http/nio/test/ByteArrayTest.java | 63 ++ .../http/nio/test/HttpRequestParserTest.java | 109 ++++ .../net/http/nio/test/NioHttpServerTest.java | 75 +++ net-http-server-simple-secure/build.gradle | 11 + .../src/main/java/module-info.java | 9 + .../server/simple/secure/HttpsAddress.java | 258 ++++++++ .../server/simple/secure/HttpsRequest.java | 27 + .../simple/secure/HttpsRequestBuilder.java | 79 +++ .../simple/secure/SimpleHttpsServer.java | 60 ++ .../secure/SimpleHttpsServerBuilder.java | 15 + .../simple/secure/SimpleHttpsServerTest.java | 72 +++ .../org.xbib.net.security.CertificateProvider | 1 + .../src/test/resources/logging.properties | 5 + net-http-server-simple/build.gradle | 9 + .../src/main/java/module-info.java | 7 + .../net/http/server/simple/HttpRequest.java | 46 ++ .../server/simple/HttpRequestBuilder.java | 100 ++++ .../net/http/server/simple/HttpResponse.java | 30 + .../server/simple/HttpResponseBuilder.java | 121 ++++ .../http/server/simple/SimpleHttpServer.java | 252 ++++++++ .../simple/SimpleHttpServerBuilder.java | 28 + .../server/simple/test/HttpRouterTest.java | 65 ++ .../simple/test/SimpleHttpServerTest.java | 76 +++ .../src/test/resources/logging.properties | 9 + net-http-server/build.gradle | 3 + .../src/main/java/module-info.java | 29 + .../org/xbib/net/http/server/Application.java | 52 ++ .../net/http/server/ApplicationBuilder.java | 32 + .../net/http/server/ApplicationModule.java | 24 + .../xbib/net/http/server/BaseApplication.java | 314 ++++++++++ .../http/server/BaseApplicationBuilder.java | 166 ++++++ .../http/server/BaseApplicationModule.java | 46 ++ .../xbib/net/http/server/BaseAttributes.java | 24 + .../xbib/net/http/server/BaseHttpDomain.java | 68 +++ .../http/server/BaseHttpDomainBuilder.java | 51 ++ .../xbib/net/http/server/BaseHttpRequest.java | 97 +++ .../http/server/BaseHttpRequestBuilder.java | 227 +++++++ .../net/http/server/BaseHttpResponse.java | 10 + .../http/server/BaseHttpResponseBuilder.java | 301 ++++++++++ .../http/server/BaseHttpSecurityDomain.java | 26 + .../server/BaseHttpSecurityDomainBuilder.java | 30 + .../http/server/BaseHttpServerContext.java | 272 +++++++++ .../xbib/net/http/server/BaseHttpService.java | 88 +++ .../http/server/BaseHttpServiceBuilder.java | 69 +++ .../org/xbib/net/http/server/HttpDomain.java | 18 + .../net/http/server/HttpDomainBuilder.java | 16 + .../net/http/server/HttpErrorHandler.java | 4 + .../xbib/net/http/server/HttpException.java | 51 ++ .../org/xbib/net/http/server/HttpHandler.java | 6 + .../org/xbib/net/http/server/HttpRequest.java | 43 ++ .../net/http/server/HttpRequestBuilder.java | 48 ++ .../xbib/net/http/server/HttpResponse.java | 10 + .../net/http/server/HttpResponseBuilder.java | 67 +++ .../net/http/server/HttpSecurityDomain.java | 9 + .../org/xbib/net/http/server/HttpServer.java | 14 + .../net/http/server/HttpServerBuilder.java | 6 + .../net/http/server/HttpServerConfig.java | 75 +++ .../net/http/server/HttpServerContext.java | 53 ++ .../org/xbib/net/http/server/HttpService.java | 19 + .../net/http/server/HttpServiceBuilder.java | 19 + .../server/MissingHostHeaderException.java | 11 + .../org/xbib/net/http/server/Resolver.java | 6 + .../org/xbib/net/http/server/Service.java | 56 ++ .../http/server/UnknownExpectException.java | 11 + .../net/http/server/auth/BaseUserProfile.java | 231 +++++++ .../auth/BasicAuthenticationHandler.java | 56 ++ .../auth/FormAuthenticationHandler.java | 84 +++ .../auth/LoginAuthenticationHandler.java | 79 +++ .../net/http/server/cookie/CookieDecoder.java | 128 ++++ .../net/http/server/cookie/CookieEncoder.java | 90 +++ .../cookie/CookieSignatureException.java | 9 + .../server/cookie/CookieSignatureUtil.java | 44 ++ .../server/cookie/IncomingCookieHandler.java | 37 ++ .../server/cookie/OutgoingCookieHandler.java | 29 + .../server/decorate/AbstractUnwrappable.java | 38 ++ .../decorate/DecoratingHttpService.java | 58 ++ .../server/decorate/DecoratingService.java | 16 + .../net/http/server/decorate/Unwrappable.java | 72 +++ .../server/handler/BadRequestHandler.java | 19 + .../http/server/handler/ForbiddenHandler.java | 19 + .../handler/InternalServerErrorHandler.java | 39 ++ .../http/server/handler/NotFoundHandler.java | 21 + .../server/handler/NotImplementedHandler.java | 21 + .../server/handler/UnauthorizedHandler.java | 19 + .../handler/VersionNotSupportedHandler.java | 20 + .../http/server/ldap/CallbackHandlerImpl.java | 35 ++ .../server/ldap/Krb5LoginConfiguration.java | 29 + .../http/server/ldap/LdapAuthenticator.java | 117 ++++ .../http/server/ldap/LdapContextFactory.java | 210 +++++++ .../net/http/server/ldap/LdapException.java | 15 + .../http/server/ldap/LdapGroupMapping.java | 81 +++ .../http/server/ldap/LdapGroupsProvider.java | 121 ++++ .../xbib/net/http/server/ldap/LdapRealm.java | 59 ++ .../xbib/net/http/server/ldap/LdapSearch.java | 174 ++++++ .../net/http/server/ldap/LdapUserMapping.java | 49 ++ .../http/server/ldap/LdapUsersProvider.java | 104 ++++ .../persist/AbstractPersistenceStore.java | 77 +++ .../xbib/net/http/server/persist/Codec.java | 16 + .../http/server/persist/PersistenceStore.java | 16 + .../server/persist/file/FileJsonCodec.java | 75 +++ .../file/FileJsonPersistenceStore.java | 21 + .../persist/file/FilePropertiesCodec.java | 97 +++ .../persist/memory/MemoryPropertiesCodec.java | 74 +++ .../xbib/net/http/server/realm/BaseRealm.java | 4 + .../http/server/realm/BaseRealmBuilder.java | 4 + .../server/render/HttpResponseRenderer.java | 24 + .../resource/AbstractResourceHandler.java | 449 ++++++++++++++ .../http/server/resource/BaseResource.java | 155 +++++ .../resource/ClassLoaderResourceHandler.java | 186 ++++++ .../server/resource/FileResourceHandler.java | 218 +++++++ .../server/resource/HtmlTemplateResource.java | 161 +++++ .../resource/HtmlTemplateResourceHandler.java | 61 ++ .../server/resource/HttpServerResource.java | 11 + .../http/server/resource/MethodHandler.java | 34 ++ .../server/resource/ResourceResolver.java | 14 + .../resource/WebRootResourceResolver.java | 134 +++++ .../negotiate/AcceptHeaderOverride.java | 28 + .../negotiate/ContentTypeNegotiator.java | 129 ++++ .../resource/negotiate/LocaleNegotiator.java | 32 + .../resource/negotiate/MediaRangeSpec.java | 185 ++++++ .../resource/negotiate/Negotiation.java | 60 ++ .../resource/negotiate/VariantSpec.java | 40 ++ .../server/route/BaseDomainsByAddress.java | 11 + .../net/http/server/route/BaseHttpRoute.java | 352 +++++++++++ .../server/route/BaseHttpRouteResolver.java | 138 +++++ .../net/http/server/route/BaseHttpRouter.java | 240 ++++++++ .../server/route/BaseHttpRouterBuilder.java | 56 ++ .../http/server/route/DomainsByAddress.java | 8 + .../xbib/net/http/server/route/HttpRoute.java | 19 + .../http/server/route/HttpRouteResolver.java | 43 ++ .../net/http/server/route/HttpRouter.java | 28 + .../http/server/route/HttpRouterBuilder.java | 13 + .../net/http/server/session/BaseSession.java | 222 +++++++ .../session/IncomingSessionHandler.java | 168 ++++++ .../session/OutgoingSessionHandler.java | 179 ++++++ .../xbib/net/http/server/session/Session.java | 19 + .../http/server/session/SessionListener.java | 8 + .../net/http/server/session/SessionUtil.java | 25 + .../session/file/FileJsonSessionCodec.java | 127 ++++ .../server/session/jdbc/JdbcSessionCodec.java | 181 ++++++ .../memory/MemoryPropertiesSessionCodec.java | 96 +++ .../util/BlockingThreadPoolExecutor.java | 69 +++ .../server/validate/HttpRequestValidator.java | 30 + .../net/http/server/ldap/LdapRealmTest.java | 63 ++ .../xbib/net/http/server/ldap/LdapTest.java | 39 ++ .../route/base/BaseHttpRouteResolverTest.java | 273 +++++++++ .../src/test/resources/logging.properties | 5 + net-http-template-groovy/build.gradle | 4 + .../src/main/java/module-info.java | 13 + .../groovy/DefaultMarkupTemplate.java | 239 ++++++++ .../groovy/DefaultTemplateResolver.java | 53 ++ ...oovyHttpResonseStatusTemplateResource.java | 99 +++ .../groovy/GroovyHttpStatusHandler.java | 35 ++ .../GroovyInternalServerErrorHandler.java | 40 ++ .../groovy/GroovyMarkupTemplateHandler.java | 102 ++++ .../GroovyTemplateApplicationModule.java | 54 ++ .../groovy/GroovyTemplateRenderer.java | 38 ++ .../groovy/GroovyTemplateResource.java | 140 +++++ .../groovy/GroovyTemplateResourceHandler.java | 24 + .../groovy/GroovyTemplateService.java | 21 + .../groovy/GroovyTemplateServiceBuilder.java | 85 +++ net-http/build.gradle | 4 + net-http/src/main/java/module-info.java | 7 + .../java/org/xbib/net/http/HttpAddress.java | 173 ++++++ .../org/xbib/net/http/HttpHeaderNames.java | 363 +++++++++++ .../org/xbib/net/http/HttpHeaderValues.java | 231 +++++++ .../java/org/xbib/net/http/HttpHeaders.java | 93 +++ .../java/org/xbib/net/http/HttpMethod.java | 6 + .../org/xbib/net/http/HttpResponseStatus.java | 555 +++++++++++++++++ .../org/xbib/net/http/HttpStatusClass.java | 103 ++++ .../java/org/xbib/net/http/HttpVersion.java | 254 ++++++++ .../java/org/xbib/net/http/cookie/Cookie.java | 150 +++++ .../org/xbib/net/http/cookie/CookieBox.java | 11 + .../xbib/net/http/cookie/CookieDecoder.java | 41 ++ .../xbib/net/http/cookie/CookieEncoder.java | 29 + .../net/http/cookie/CookieHeaderNames.java | 18 + .../org/xbib/net/http/cookie/CookieUtil.java | 181 ++++++ .../xbib/net/http/cookie/DefaultCookie.java | 232 ++++++++ .../org/xbib/net/http/cookie/SameSite.java | 5 + .../java/org/xbib/net/http/package-info.java | 4 + .../xbib/net/http/test/MimeParserTest.java | 11 + .../src/test/resources/hamster.a.templates | 58 ++ .../src/test/resources/hamster.b.templates | 563 ++++++++++++++++++ .../src/test/resources/hamster.c.templates | 60 ++ .../src/test/resources/logging.properties | 6 + net-http/src/test/resources/whale.a.templates | 454 ++++++++++++++ settings.gradle | 70 +++ 442 files changed, 34467 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 build.gradle create mode 100644 config/checkstyle/checkstyle.xml create mode 100644 gradle.properties create mode 100644 gradle/compile/java.gradle create mode 100644 gradle/documentation/asciidoc.gradle create mode 100644 gradle/ide/idea.gradle create mode 100644 gradle/publish/ivy.gradle create mode 100644 gradle/publish/maven.gradle create mode 100644 gradle/publish/sonatype.gradle create mode 100644 gradle/quality/sonarqube.gradle create mode 100644 gradle/repositories/maven.gradle create mode 100644 gradle/test/jmh.gradle create mode 100644 gradle/test/junit5.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 net-http-client-netty-secure/build.gradle create mode 100644 net-http-client-netty-secure/src/main/java/module-info.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/ClientSecureSocketProvider.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequest.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequestBuilder.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponse.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponseBuilder.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/JdkClientSecureSocketProvider.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/NettyHttpsClientConfig.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1ChannelInitializer.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1Interaction.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChannelInitializer.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChildChannelInitializer.java create mode 100644 net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2Interaction.java create mode 100644 net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer create mode 100644 net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/AkamaiTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ExponentialBackOffTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/FileDescriptorLeakTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Http2FramesTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/HttpBinTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https1Test.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https2Test.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/JdkClientTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOff.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOffTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp1Test.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp2Test.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ThreadLeakTest.java create mode 100644 net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/WebtideTest.java create mode 100644 net-http-client-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider create mode 100644 net-http-client-netty-secure/src/test/resources/logging.properties create mode 100644 net-http-client-netty/build.gradle create mode 100644 net-http-client-netty/src/main/java/module-info.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BaseInteraction.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BoundedChannelPool.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/ClientTransportProvider.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelInitializer.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelPoolInitializer.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChunkContentCompressor.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequest.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequestBuilder.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponse.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponseBuilder.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Interaction.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyCustomizer.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClient.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientBuilder.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientConfig.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NioClientTransportProvider.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Pool.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/StreamIds.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/TrafficLoggingHandler.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/UserAgent.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1ChannelInitializer.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Handler.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Interaction.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChannelInitializer.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChildChannelInitializer.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Handler.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Interaction.java create mode 100644 net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Messages.java create mode 100644 net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider create mode 100644 net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer create mode 100644 net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http1Test.java create mode 100644 net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http2Test.java create mode 100644 net-http-client-netty/src/test/resources/logging.properties create mode 100644 net-http-client-simple/build.gradle create mode 100644 net-http-client-simple/src/main/java/module-info.java create mode 100644 net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClient.java create mode 100644 net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientBuilder.java create mode 100644 net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientConfig.java create mode 100644 net-http-client/build.gradle create mode 100644 net-http-client/src/main/java/module-info.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/BackOff.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequest.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequestBuilder.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/ClientAuthMode.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/ExceptionListener.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/ExponentialBackOff.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/HttpClient.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/HttpRequest.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/HttpRequestBuilder.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/HttpResponse.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/HttpResponseBuilder.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/ResponseListener.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/TimeoutListener.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieDecoder.java create mode 100644 net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieEncoder.java create mode 100644 net-http-client/src/test/java/org/xbib/net/http/client/ClientCookieDecoderTest.java create mode 100644 net-http-client/src/test/java/org/xbib/net/http/client/ClientCookieEncoderTest.java create mode 100644 net-http-netty-boringssl/build.gradle create mode 100644 net-http-netty-boringssl/src/main/java/module-info.java create mode 100644 net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLClientSecureSocketProvider.java create mode 100644 net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLServerSecureSocketProvider.java create mode 100644 net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider create mode 100644 net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.netty.client.secure.ServerSecureSocketProvider create mode 100644 net-http-netty-conscrypt/build.gradle create mode 100644 net-http-netty-conscrypt/src/main/java/module-info.java create mode 100644 net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptClientSecureSocketProvider.java create mode 100644 net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptServerSecureSocketProvider.java create mode 100644 net-http-netty-conscrypt/src/main/resources/META-INF/services/java.security.Provider create mode 100644 net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider create mode 100644 net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider create mode 100644 net-http-netty-conscrypt/src/test/java/org/xbib/net/http/netty/conscrypt/Http1Test.java create mode 100644 net-http-netty-conscrypt/src/test/resources/logging.properties create mode 100644 net-http-netty-epoll/build.gradle create mode 100644 net-http-netty-epoll/src/main/java/module-info.java create mode 100644 net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollClientTransportProvider.java create mode 100644 net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollServerTransportProvider.java create mode 100644 net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider create mode 100644 net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider create mode 100644 net-http-netty-kqueue/build.gradle create mode 100644 net-http-netty-kqueue/src/main/java/module-info.java create mode 100644 net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueClientTransportProvider.java create mode 100644 net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueServerTransportProvider.java create mode 100644 net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider create mode 100644 net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider create mode 100644 net-http-server-application-config/build.gradle create mode 100644 net-http-server-application-config/src/main/java/module-info.java create mode 100644 net-http-server-application-config/src/main/java/org/xbib/net/http/server/application/config/ConfigApplicationModule.java create mode 100644 net-http-server-application-database/build.gradle create mode 100644 net-http-server-application-database/src/main/java/module-info.java create mode 100644 net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/BaseDatabase.java create mode 100644 net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Database.java create mode 100644 net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/DatabaseApplicationModule.java create mode 100644 net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Table.java create mode 100644 net-http-server-application-web/build.gradle create mode 100644 net-http-server-application-web/src/main/application/400.gtpl create mode 100644 net-http-server-application-web/src/main/application/403.gtpl create mode 100644 net-http-server-application-web/src/main/application/404.gtpl create mode 100644 net-http-server-application-web/src/main/application/500.gtpl create mode 100644 net-http-server-application-web/src/main/application/demo/auth/form/index.gtpl create mode 100644 net-http-server-application-web/src/main/application/demo/auth/form/success.gtpl create mode 100644 net-http-server-application-web/src/main/application/index.gtpl create mode 100644 net-http-server-application-web/src/main/java/module-info.java create mode 100644 net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/Bootstrap.java create mode 100644 net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java create mode 100644 net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplicationBuilder.java create mode 100644 net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.ApplicationModule create mode 100644 net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer create mode 100644 net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider create mode 100644 net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider create mode 100644 net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.settings.SettingsLoader create mode 100644 net-http-server-application-web/src/main/resources/logging.properties create mode 100644 net-http-server-application-web/src/test/resources/logging.properties create mode 100644 net-http-server-netty-secure/build.gradle create mode 100644 net-http-server-netty-secure/src/main/java/module-info.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsAddress.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequest.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequestBuilder.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/JdkServerSecureSocketProvider.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/NettyHttpsServerConfig.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerNameIndicationHandler.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerSecureSocketProvider.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1ChannelInitializer.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1Handler.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChannelInitializer.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChildChannelInitializer.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Handler.java create mode 100644 net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Messages.java create mode 100644 net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerMultiRequestLoadTest.java create mode 100644 net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerTest.java create mode 100644 net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerMultiRequestLoadTest.java create mode 100644 net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerTest.java create mode 100644 net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer create mode 100644 net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider create mode 100644 net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider create mode 100644 net-http-server-netty-secure/src/test/resources/logging.properties create mode 100644 net-http-server-netty/build.gradle create mode 100644 net-http-server-netty/src/main/java/module-info.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpChannelInitializer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponse.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponseBuilder.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/IdleTimeoutHandler.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyCustomizer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerBuilder.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerConfig.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NioServerTransportProvider.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/ServerTransportProvider.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/TrafficLoggingHandler.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBuffer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBufferFactory.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1ChannelInitializer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1Handler.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedRequest.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedResponse.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipeliningHandler.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChannelInitializer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChildChannelInitializer.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Handler.java create mode 100644 net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Messages.java create mode 100644 net-http-server-netty/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerMultiRequestLoadTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerFailureTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerMultiRequestLoadTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/pipelining/HttpPipeliningHandlerTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttp2ClientServerTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerFailureTest.java create mode 100644 net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerTest.java create mode 100644 net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer create mode 100644 net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider create mode 100644 net-http-server-netty/src/test/resources/logging.properties create mode 100644 net-http-server-nio/build.gradle create mode 100644 net-http-server-nio/src/main/java/module-info.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequest.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequestBuilder.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponse.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponseBuilder.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServer.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServerBuilder.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ByteArray.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ClientBootstrap.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoop.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoopGroup.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpContext.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeader.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeaders.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpMethod.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequest.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestHandler.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestParser.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpResponse.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServer.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServerHandler.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ServerBootstrap.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketContext.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandler.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandlerProvider.java create mode 100644 net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/UriHandler.java create mode 100644 net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/ByteArrayTest.java create mode 100644 net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/HttpRequestParserTest.java create mode 100644 net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/NioHttpServerTest.java create mode 100644 net-http-server-simple-secure/build.gradle create mode 100644 net-http-server-simple-secure/src/main/java/module-info.java create mode 100644 net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsAddress.java create mode 100644 net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequest.java create mode 100644 net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequestBuilder.java create mode 100644 net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServer.java create mode 100644 net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerBuilder.java create mode 100644 net-http-server-simple-secure/src/test/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerTest.java create mode 100644 net-http-server-simple-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider create mode 100644 net-http-server-simple-secure/src/test/resources/logging.properties create mode 100644 net-http-server-simple/build.gradle create mode 100644 net-http-server-simple/src/main/java/module-info.java create mode 100644 net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequest.java create mode 100644 net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequestBuilder.java create mode 100644 net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponse.java create mode 100644 net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponseBuilder.java create mode 100644 net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServer.java create mode 100644 net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServerBuilder.java create mode 100644 net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/HttpRouterTest.java create mode 100644 net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/SimpleHttpServerTest.java create mode 100644 net-http-server-simple/src/test/resources/logging.properties create mode 100644 net-http-server/build.gradle create mode 100644 net-http-server/src/main/java/module-info.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/Application.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ApplicationBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ApplicationModule.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationModule.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseAttributes.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomain.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomainBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequest.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequestBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponse.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponseBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomain.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomainBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpService.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServiceBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpDomain.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpDomainBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpErrorHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpException.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpRequest.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpRequestBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpResponse.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpResponseBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpSecurityDomain.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpServer.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpServerBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpServerConfig.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpServerContext.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpService.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/HttpServiceBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/MissingHostHeaderException.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/Resolver.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/Service.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/UnknownExpectException.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/auth/BasicAuthenticationHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/auth/FormAuthenticationHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieDecoder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieEncoder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureException.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureUtil.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/cookie/OutgoingCookieHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/decorate/AbstractUnwrappable.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingHttpService.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingService.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/decorate/Unwrappable.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/BadRequestHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/ForbiddenHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/InternalServerErrorHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/NotFoundHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/NotImplementedHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/UnauthorizedHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/handler/VersionNotSupportedHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/CallbackHandlerImpl.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/Krb5LoginConfiguration.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapAuthenticator.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapContextFactory.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapException.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupMapping.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupsProvider.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapRealm.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapSearch.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUserMapping.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUsersProvider.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/AbstractPersistenceStore.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/PersistenceStore.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealm.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealmBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/render/HttpResponseRenderer.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/AbstractResourceHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/BaseResource.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/ClassLoaderResourceHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/FileResourceHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResourceHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/HttpServerResource.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/MethodHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/ResourceResolver.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/WebRootResourceResolver.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/AcceptHeaderOverride.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/ContentTypeNegotiator.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/LocaleNegotiator.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/MediaRangeSpec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/Negotiation.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/VariantSpec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/BaseDomainsByAddress.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/DomainsByAddress.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRoute.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouteResolver.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouter.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterBuilder.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/BaseSession.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/Session.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/SessionListener.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/SessionUtil.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/util/BlockingThreadPoolExecutor.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/validate/HttpRequestValidator.java create mode 100644 net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapRealmTest.java create mode 100644 net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapTest.java create mode 100644 net-http-server/src/test/java/org/xbib/net/http/server/route/base/BaseHttpRouteResolverTest.java create mode 100644 net-http-server/src/test/resources/logging.properties create mode 100644 net-http-template-groovy/build.gradle create mode 100644 net-http-template-groovy/src/main/java/module-info.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultTemplateResolver.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpStatusHandler.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyInternalServerErrorHandler.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyMarkupTemplateHandler.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateRenderer.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResourceHandler.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateService.java create mode 100644 net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateServiceBuilder.java create mode 100644 net-http/build.gradle create mode 100644 net-http/src/main/java/module-info.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpAddress.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpHeaderNames.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpHeaderValues.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpHeaders.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpMethod.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpResponseStatus.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpStatusClass.java create mode 100644 net-http/src/main/java/org/xbib/net/http/HttpVersion.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/Cookie.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/CookieBox.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/CookieDecoder.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/CookieEncoder.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/CookieHeaderNames.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/CookieUtil.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/DefaultCookie.java create mode 100644 net-http/src/main/java/org/xbib/net/http/cookie/SameSite.java create mode 100644 net-http/src/main/java/org/xbib/net/http/package-info.java create mode 100644 net-http/src/test/java/org/xbib/net/http/test/MimeParserTest.java create mode 100644 net-http/src/test/resources/hamster.a.templates create mode 100644 net-http/src/test/resources/hamster.b.templates create mode 100644 net-http/src/test/resources/hamster.c.templates create mode 100644 net-http/src/test/resources/logging.properties create mode 100644 net-http/src/test/resources/whale.a.templates create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..021874f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/.settings +/.classpath +/.project +/.gradle +**/data +**/work +**/logs +**/.idea +**/target +**/out +**/build +.DS_Store +*.iml +*~ +*.key +*.crt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..466671c --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Java Net API for servers and clients + +## A consolidated Uniform Resource Locator implementation for Java + +A Uniform Resource Locator (URL) is a compact representation of the +location and access method for a resource available via the Internet. + +Historically, there are many different forms of internet resource representations, for example, +the URL (RFC 1738 as of 1994), the URI (RFC 2396 as of 1998), and IRI (RFC 3987 as of 2005), +and most of them have updated specifications. + +This Java implementation serves as a universal point of handling all +different forms. It follows the syntax of the Uniform Resource Identifier (RFC 3986) +in accordance with the https://url.spec.whatwg.org/[WHATWG URL standard]. + +This alternative implementation of Uniform Resource Locator combines the features of the vanilla URI/URL Java SDK implementations +but removes it peculiarities and deficiencies, such as `java.lang.IllegalArgumentException: Illegal character in path at ... at java.net.URI.create()` + +Normalization, NIO charset encoding/decoding, IPv6, an extensive set of schemes, and path matching have been added. + +Fast building and parsing URLs, improved percent decoding/encoding, and URI templating features are included, to make +this library also useful in URI and IRI contexts. + +While parsing and building, you have better control about address resolving. Only explicit `resolveFromhost` methods +will execute host lookup queries against DNS resolvers, otherwise, no resolving will occur under the hood. + +You can build URLs with a fluent API, for example + +``` +URL.http().host("foo.com").toUrlString() +``` + +And you can parse URLs with a fluent API, for exmaple + +``` +URL url = URL.parser().parse("file:///foo/bar?foo=bar#fragment"); +``` + +There is no external dependency. The size of the jar library is ~118k. The only dependency on `java.net` are the classes + +``` +java.net.IDN +java.net.Inet4Address +java.net.Inet6Address +java.net.InetAddress +``` + +which might get re-implemented in another library at a later time, in a project like Netty DNS resolver. + +## A simple HTTP server + +## A netty-based HTTP server + +# License + +Copyright (C) 2018 Jörg Prante + +Licensed 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. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7b2a7dd --- /dev/null +++ b/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + maven { + url 'https://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.gradle.plugin:gradle-plugin-shadow:1.1.1" + } +} + +plugins { + id "de.marcphilipp.nexus-publish" version "0.4.0" + id "io.codearte.nexus-staging" version "0.21.1" + id "org.xbib.gradle.plugin.asciidoctor" version "2.5.2.1" +} + +wrapper { + gradleVersion = libs.versions.gradle.get() + distributionType = Wrapper.DistributionType.ALL +} + +ext { + user = 'xbib' + name = 'net' + description = 'Network classes for Java' + inceptionYear = '2016' + url = 'https://github.com/' + user + '/' + name + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git' + issueManagementSystem = 'Github' + issueManagementUrl = ext.scmUrl + '/issues' + licenseName = 'The Apache License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +} + +subprojects { + apply from: rootProject.file('gradle/ide/idea.gradle') + apply from: rootProject.file('gradle/repositories/maven.gradle') + apply from: rootProject.file('gradle/compile/java.gradle') + apply from: rootProject.file('gradle/test/junit5.gradle') + apply from: rootProject.file('gradle/documentation/asciidoc.gradle') + apply from: rootProject.file('gradle/publish/maven.gradle') +} +apply from: rootProject.file('gradle/publish/sonatype.gradle') diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..14a1edc --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7f9ea7d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +group = org.xbib +name = net +version = 3.0.0 + +org.gradle.warning.mode = ALL diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle new file mode 100644 index 0000000..9cd3f3b --- /dev/null +++ b/gradle/compile/java.gradle @@ -0,0 +1,30 @@ +apply plugin: 'java-library' + +java { + modularity.inferModulePath.set(true) +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.withType(JavaCompile) { + options.compilerArgs.add('-Xlint:all,-exports') +} + +javadoc { + options.addStringOption('Xdoclint:none', '-quiet') +} diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle new file mode 100644 index 0000000..87196cf --- /dev/null +++ b/gradle/documentation/asciidoc.gradle @@ -0,0 +1,13 @@ +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +asciidoctor { + attributes 'source-highlighter': 'coderay', + toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img' +} \ No newline at end of file diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle new file mode 100644 index 0000000..5bd2095 --- /dev/null +++ b/gradle/ide/idea.gradle @@ -0,0 +1,8 @@ +apply plugin: 'idea' + +idea { + module { + outputDir file('build/classes/java/main') + testOutputDir file('build/classes/java/test') + } +} diff --git a/gradle/publish/ivy.gradle b/gradle/publish/ivy.gradle new file mode 100644 index 0000000..fe0a848 --- /dev/null +++ b/gradle/publish/ivy.gradle @@ -0,0 +1,27 @@ +apply plugin: 'ivy-publish' + +publishing { + repositories { + ivy { + url = "https://xbib.org/repo" + } + } + publications { + ivy(IvyPublication) { + from components.java + descriptor { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + author { + name = 'Jörg Prante' + url = 'http://example.com/users/jane' + } + descriptor.description { + text = rootProject.ext.description + } + } + } + } +} \ No newline at end of file diff --git a/gradle/publish/maven.gradle b/gradle/publish/maven.gradle new file mode 100644 index 0000000..3786359 --- /dev/null +++ b/gradle/publish/maven.gradle @@ -0,0 +1,64 @@ + +apply plugin: "de.marcphilipp.nexus-publish" + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + pom { + name = project.name + description = rootProject.ext.description + url = rootProject.ext.url + inceptionYear = rootProject.ext.inceptionYear + packaging = 'jar' + organization { + name = 'xbib' + url = 'https://xbib.org' + } + developers { + developer { + id = 'jprante' + name = 'Jörg Prante' + email = 'joergprante@gmail.com' + url = 'https://github.com/jprante' + } + } + scm { + url = rootProject.ext.scmUrl + connection = rootProject.ext.scmConnection + developerConnection = rootProject.ext.scmDeveloperConnection + } + issueManagement { + system = rootProject.ext.issueManagementSystem + url = rootProject.ext.issueManagementUrl + } + licenses { + license { + name = rootProject.ext.licenseName + url = rootProject.ext.licenseUrl + distribution = 'repo' + } + } + } + } + } +} + +if (project.hasProperty("signing.keyId")) { + apply plugin: 'signing' + signing { + sign publishing.publications.mavenJava + } +} + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + nexusPublishing { + repositories { + sonatype { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } + } + } +} diff --git a/gradle/publish/sonatype.gradle b/gradle/publish/sonatype.gradle new file mode 100644 index 0000000..e1813f3 --- /dev/null +++ b/gradle/publish/sonatype.gradle @@ -0,0 +1,11 @@ + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + + apply plugin: 'io.codearte.nexus-staging' + + nexusStaging { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } +} diff --git a/gradle/quality/sonarqube.gradle b/gradle/quality/sonarqube.gradle new file mode 100644 index 0000000..ec439b2 --- /dev/null +++ b/gradle/quality/sonarqube.gradle @@ -0,0 +1,50 @@ + +subprojects { + + sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.junit.reportsPath", "build/test-results/test/" + } + } + + + tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + + tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + + checkstyle { + //configFile = rootProject.file('config/checkstyle/checkstyle.xml') + ignoreFailures = true + showViolations = true + } + + spotbugs { + effort = "max" + reportLevel = "low" + //includeFilter = file("findbugs-exclude.xml") + } + + tasks.withType(com.github.spotbugs.SpotBugsTask) { + ignoreFailures = true + reports { + xml.enabled = false + html.enabled = true + } + } +} \ No newline at end of file diff --git a/gradle/repositories/maven.gradle b/gradle/repositories/maven.gradle new file mode 100644 index 0000000..ec58acb --- /dev/null +++ b/gradle/repositories/maven.gradle @@ -0,0 +1,4 @@ +repositories { + mavenLocal() + mavenCentral() +} diff --git a/gradle/test/jmh.gradle b/gradle/test/jmh.gradle new file mode 100644 index 0000000..8c38e5c --- /dev/null +++ b/gradle/test/jmh.gradle @@ -0,0 +1,22 @@ +sourceSets { + jmh { + java.srcDirs = ['src/jmh/java'] + resources.srcDirs = ['src/jmh/resources'] + compileClasspath += sourceSets.main.runtimeClasspath + } +} + +dependencies { + jmhImplementation 'org.openjdk.jmh:jmh-core:1.34' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.34' +} + +task jmh(type: JavaExec, group: 'jmh', dependsOn: jmhClasses) { + mainClass.set('org.openjdk.jmh.Main') + classpath = sourceSets.jmh.compileClasspath + sourceSets.jmh.runtimeClasspath + project.file('build/reports/jmh').mkdirs() + args '-rf', 'json' + args '-rff', project.file('build/reports/jmh/result.json') +} + +classes.finalizedBy(jmhClasses) diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle new file mode 100644 index 0000000..ea3ee77 --- /dev/null +++ b/gradle/test/junit5.gradle @@ -0,0 +1,36 @@ +dependencies { + testImplementation libs.junit.jupiter.api + testImplementation libs.junit.jupiter.params + testImplementation libs.hamcrest + testRuntimeOnly libs.junit.jupiter.engine +} + +test { + useJUnitPlatform() + failFast = false + jvmArgs '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED', + '--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED', + '--add-exports=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED', + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED' + systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties' + systemProperty 'io.netty.tryReflectionSetAccessible', 'true' + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nTest result: ${result.resultType}" + println "Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 zcmb5WV{~QRw(p$^Dz@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 execute + +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 + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/net-http-client-netty-secure/build.gradle b/net-http-client-netty-secure/build.gradle new file mode 100644 index 0000000..c5325aa --- /dev/null +++ b/net-http-client-netty-secure/build.gradle @@ -0,0 +1,7 @@ +dependencies { + api project(':net-http-client-netty') + api libs.net.security + api libs.netty.codec.http2 + api libs.netty.handler.proxy + testImplementation project(':net-http-netty-boringssl') +} diff --git a/net-http-client-netty-secure/src/main/java/module-info.java b/net-http-client-netty-secure/src/main/java/module-info.java new file mode 100644 index 0000000..240d4e2 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/module-info.java @@ -0,0 +1,27 @@ +import org.xbib.net.http.client.netty.HttpChannelInitializer; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; +import org.xbib.net.http.client.netty.secure.JdkClientSecureSocketProvider; +import org.xbib.net.http.client.netty.secure.http1.Https1ChannelInitializer; +import org.xbib.net.http.client.netty.secure.http2.Https2ChannelInitializer; + +module org.xbib.net.http.client.netty.secure { + exports org.xbib.net.http.client.netty.secure; + exports org.xbib.net.http.client.netty.secure.http1; + exports org.xbib.net.http.client.netty.secure.http2; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.client; + requires org.xbib.net.http.client.netty; + requires org.xbib.net.security; + requires io.netty.handler; + requires io.netty.codec.http; + requires io.netty.codec.http2; + requires io.netty.handler.proxy; + requires io.netty.transport; + requires java.logging; + requires io.netty.common; + uses ClientSecureSocketProvider; + provides ClientSecureSocketProvider with JdkClientSecureSocketProvider; + uses HttpChannelInitializer; + provides HttpChannelInitializer with Https1ChannelInitializer, Https2ChannelInitializer; +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/ClientSecureSocketProvider.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/ClientSecureSocketProvider.java new file mode 100644 index 0000000..74d10c3 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/ClientSecureSocketProvider.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.client.netty.secure; + +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; +import java.security.Provider; +import org.xbib.net.http.HttpAddress; + +public interface ClientSecureSocketProvider { + + String name(); + + Provider securityProvider(HttpAddress address); + + SslProvider sslProvider(HttpAddress address); + + Iterable ciphers(HttpAddress address); + + CipherSuiteFilter cipherSuiteFilter(HttpAddress address); + + String[] protocols(HttpAddress address); +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequest.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequest.java new file mode 100644 index 0000000..b09d26e --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequest.java @@ -0,0 +1,23 @@ +package org.xbib.net.http.client.netty.secure; + +import javax.net.ssl.SSLSession; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.client.netty.HttpRequest; + +public class HttpsRequest extends HttpRequest { + + private final HttpsRequestBuilder builder; + + protected HttpsRequest(HttpsRequestBuilder builder, HttpHeaders headers) { + super(builder, headers); + this.builder = builder; + } + + public static HttpsRequestBuilder builder() { + return new HttpsRequestBuilder(); + } + + public SSLSession getSSLSession() { + return builder.sslSession; + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequestBuilder.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequestBuilder.java new file mode 100644 index 0000000..59397e1 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsRequestBuilder.java @@ -0,0 +1,18 @@ +package org.xbib.net.http.client.netty.secure; + +import javax.net.ssl.SSLSession; +import org.xbib.net.http.client.netty.HttpRequestBuilder; + +public class HttpsRequestBuilder extends HttpRequestBuilder { + + SSLSession sslSession; + + public HttpsRequestBuilder setSSLSession(SSLSession sslSession) { + this.sslSession = sslSession; + return this; + } + + public HttpsRequest build() { + return new HttpsRequest(this, validateHeaders()); + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponse.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponse.java new file mode 100644 index 0000000..3fec1af --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponse.java @@ -0,0 +1,22 @@ +package org.xbib.net.http.client.netty.secure; + +import javax.net.ssl.SSLSession; +import org.xbib.net.http.client.netty.HttpResponse; + +public class HttpsResponse extends HttpResponse { + + private final HttpsResponseBuilder builder; + + protected HttpsResponse(HttpsResponseBuilder builder) { + super(builder); + this.builder = builder; + } + + public static HttpsResponseBuilder builder() { + return new HttpsResponseBuilder(); + } + + public SSLSession getSSLSession() { + return builder.sslSession; + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponseBuilder.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponseBuilder.java new file mode 100644 index 0000000..e638828 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/HttpsResponseBuilder.java @@ -0,0 +1,20 @@ +package org.xbib.net.http.client.netty.secure; + +import javax.net.ssl.SSLSession; +import org.xbib.net.http.client.netty.HttpResponseBuilder; + +public class HttpsResponseBuilder extends HttpResponseBuilder { + + SSLSession sslSession; + + public HttpsResponseBuilder setSSLSession(SSLSession sslSession) { + this.sslSession = sslSession; + return this; + } + + @Override + public HttpsResponse build() { + super.build(); + return new HttpsResponse(this); + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/JdkClientSecureSocketProvider.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/JdkClientSecureSocketProvider.java new file mode 100644 index 0000000..4573c95 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/JdkClientSecureSocketProvider.java @@ -0,0 +1,50 @@ +package org.xbib.net.http.client.netty.secure; + +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import java.security.Provider; +import java.util.Arrays; +import javax.net.ssl.SSLSocketFactory; +import org.xbib.net.http.HttpAddress; + +public class JdkClientSecureSocketProvider implements ClientSecureSocketProvider { + + // https://convincingbits.wordpress.com/2016/02/17/ssl-tls-with-java-7-and-the-death-of-sslv2hello/ + static { + System.setProperty("https.protocol", "TLSv1"); + } + + public JdkClientSecureSocketProvider() { + } + + @Override + public String name() { + return "JDK"; + } + + @Override + public Provider securityProvider(HttpAddress httpAddress) { + return null; + } + + @Override + public SslProvider sslProvider(HttpAddress httpAddress) { + return SslProvider.JDK; + } + + @Override + public Iterable ciphers(HttpAddress httpAddress) { + return Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + } + + @Override + public CipherSuiteFilter cipherSuiteFilter(HttpAddress httpAddress) { + return SupportedCipherSuiteFilter.INSTANCE; + } + + @Override + public String[] protocols(HttpAddress httpAddress) { + return new String[] { "TLSv1.3", "TLSv1.2" }; + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/NettyHttpsClientConfig.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/NettyHttpsClientConfig.java new file mode 100644 index 0000000..1998260 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/NettyHttpsClientConfig.java @@ -0,0 +1,159 @@ +package org.xbib.net.http.client.netty.secure; + +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.AttributeKey; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.TrustManagerFactory; +import org.xbib.net.http.client.ClientAuthMode; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; + +public class NettyHttpsClientConfig extends NettyHttpClientConfig { + + private static final Logger logger = Logger.getLogger(NettyHttpsClientConfig.class.getName()); + + public static final AttributeKey ATTRIBUTE_KEY_SSL_HANDLER = AttributeKey.valueOf("_ssl_handler"); + + private static TrustManagerFactory TRUST_MANAGER_FACTORY; + + static { + try { + TRUST_MANAGER_FACTORY = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (Exception e) { + TRUST_MANAGER_FACTORY = null; + } + } + + private TrustManagerFactory trustManagerFactory = TRUST_MANAGER_FACTORY; + + private String secureSocketProviderName = "JDK"; + + private KeyStore trustManagerKeyStore = null; + + private ClientAuthMode clientAuthMode = ClientAuthMode.NONE; + + private InputStream keyCertChainInputStream; + + private InputStream keyInputStream; + + private String keyPassword; + + private boolean protocolNegotiationEnabled = false; + + /* + * Automatically selects the protocol from our secure socket providers. + */ + private String[] secureProtocolName = null; + + public NettyHttpsClientConfig() { + } + + public NettyHttpsClientConfig setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + public NettyHttpsClientConfig trustInsecure() { + this.trustManagerFactory = InsecureTrustManagerFactory.INSTANCE; + return this; + } + + public TrustManagerFactory getTrustManagerFactory() { + initializeTrustManagerFactory(); + return trustManagerFactory; + } + + public NettyHttpsClientConfig setTrustManagerKeyStore(KeyStore trustManagerKeyStore) { + this.trustManagerKeyStore = trustManagerKeyStore; + return this; + } + + public KeyStore getTrustManagerKeyStore() { + return trustManagerKeyStore; + } + + public NettyHttpsClientConfig setSecureSocketProviderName(String secureSocketProviderName) { + this.secureSocketProviderName = secureSocketProviderName; + return this; + } + + public String getSecureSocketProviderName() { + return secureSocketProviderName; + } + + public NettyHttpsClientConfig setSecureProtocolName(String[] secureProtocolName) { + this.secureProtocolName = secureProtocolName; + return this; + } + + public String[] getSecureProtocolName() { + return secureProtocolName; + } + + public NettyHttpsClientConfig setKeyCert(InputStream keyCertChainInputStream, + InputStream keyInputStream) { + this.keyCertChainInputStream = keyCertChainInputStream; + this.keyInputStream = keyInputStream; + return this; + } + + public InputStream getKeyCertChainInputStream() { + return keyCertChainInputStream; + } + + public InputStream getKeyInputStream() { + return keyInputStream; + } + + public NettyHttpsClientConfig setKeyCert(InputStream keyCertChainInputStream, + InputStream keyInputStream, + String keyPassword) { + this.keyCertChainInputStream = keyCertChainInputStream; + this.keyInputStream = keyInputStream; + this.keyPassword = keyPassword; + return this; + } + + public String getKeyPassword() { + return keyPassword; + } + + public NettyHttpsClientConfig setClientAuthMode(ClientAuthMode clientAuthMode) { + this.clientAuthMode = clientAuthMode; + return this; + } + + public ClientAuthMode getClientAuthMode() { + return clientAuthMode; + } + + public NettyHttpsClientConfig setProtocolNegotiation(boolean negotiationEnabled) { + this.protocolNegotiationEnabled = negotiationEnabled; + return this; + } + + public boolean isProtocolNegotiationEnabled() { + return protocolNegotiationEnabled; + } + + /** + * Initialize trust manager factory once per client lifecycle. + */ + private void initializeTrustManagerFactory() { + if (trustManagerFactory != null) { + try { + trustManagerFactory.init(trustManagerKeyStore); + logger.log(Level.FINE, "trust manager factory initialized with key store " + trustManagerFactory); + } catch (KeyStoreException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } else { + logger.log(Level.INFO, "no trust manager factory present"); + } + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1ChannelInitializer.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1ChannelInitializer.java new file mode 100644 index 0000000..8190f6f --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1ChannelInitializer.java @@ -0,0 +1,260 @@ +package org.xbib.net.http.client.netty.secure.http1; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.proxy.Socks5ProxyHandler; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.handler.timeout.ReadTimeoutHandler; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.security.Provider; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpChannelInitializer; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyCustomizer; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.http2.Http2Messages; +import org.xbib.net.http.client.netty.http1.Http1Handler; +import org.xbib.net.http.client.netty.TrafficLoggingHandler; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +public class Https1ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Https1ChannelInitializer.class.getName()); + + public Https1ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress address) { + return HttpVersion.HTTP_1_1.equals(address.getVersion()) && address.isSecure(); + } + + @Override + public Interaction newInteraction(NettyHttpClient client, HttpAddress httpAddress) { + return new Https1Interaction(client, httpAddress); + } + + @Override + public void init(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + NettyCustomizer nettyCustomizer, + Interaction interaction) throws IOException { + NettyHttpsClientConfig nettyHttpClientConfig = (NettyHttpsClientConfig) nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + if (nettyHttpClientConfig.isDebug()) { + pipeline.addLast("client-traffic", new TrafficLoggingHandler(LogLevel.DEBUG)); + } + int readTimeoutMilllis = nettyHttpClientConfig.getSocketConfig().getReadTimeoutMillis(); + if (readTimeoutMilllis > 0) { + pipeline.addLast("client-read-timeout", new ReadTimeoutHandler(readTimeoutMilllis / 1000)); + } + int socketTimeoutMillis = nettyHttpClientConfig.getSocketConfig().getSocketTimeoutMillis(); + if (socketTimeoutMillis > 0) { + pipeline.addLast("client-idle-timeout", new IdleStateHandler(socketTimeoutMillis / 1000, + socketTimeoutMillis / 1000, socketTimeoutMillis / 1000)); + } + if (nettyHttpClientConfig.getHttpProxyHandler() != null) { + pipeline.addLast("client-http-proxy", nettyHttpClientConfig.getHttpProxyHandler()); + } + if (nettyHttpClientConfig.getSocks4ProxyHandler() != null) { + pipeline.addLast("client-socks4-proxy", nettyHttpClientConfig.getSocks4ProxyHandler()); + } + if (nettyHttpClientConfig.getSocks5ProxyHandler() != null) { + Socks5ProxyHandler socks5ProxyHandler = nettyHttpClientConfig.getSocks5ProxyHandler(); + pipeline.addLast("client-socks5-proxy", socks5ProxyHandler); + } + configureEncrypted(channel, httpAddress, nettyHttpClient, interaction); + if (nettyCustomizer != null) { + nettyCustomizer.afterChannelInitialized(channel); + } + if (nettyHttpClientConfig.isDebug()) { + logger.log(Level.FINE, "HTTP 1.1 secure channel initialized: " + + " address=" + httpAddress + + " pipeline=" + pipeline.names()); + } + } + + private void configureEncrypted(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + Interaction interaction) throws IOException { + NettyHttpsClientConfig nettyHttpClientConfig = (NettyHttpsClientConfig) nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + try { + SslHandler sslHandler = createSslHandler(nettyHttpClientConfig, httpAddress); + channel.attr(NettyHttpsClientConfig.ATTRIBUTE_KEY_SSL_HANDLER).set(sslHandler); + pipeline.addLast("client-ssl-handler", sslHandler); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + if (nettyHttpClientConfig.isProtocolNegotiationEnabled()) { + ApplicationProtocolNegotiationHandler negotiationHandler = + new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws IOException { + logger.log(Level.FINEST, "configuring pipeline for negotiated protocol " + protocol); + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + configureHttp2(ctx.channel(), httpAddress, nettyHttpClient, interaction); + return; + } + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + configurePlain(ctx.channel(), nettyHttpClient, interaction); + return; + } + ctx.close(); + throw new IllegalStateException("protocol not accepted: " + protocol); + } + }; + pipeline.addLast("client-negotiation", negotiationHandler); + } else { + configurePlain(channel, nettyHttpClient, interaction); + } + } + + private SslHandler createSslHandler(NettyHttpsClientConfig nettyHttpClientConfig, + HttpAddress httpAddress) throws IOException { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + ClientSecureSocketProvider clientSecureSocketProvider = null; + for (ClientSecureSocketProvider provider : ServiceLoader.load(ClientSecureSocketProvider.class)) { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "trying secure socket provider = " + provider.name()); + } + if (nettyHttpClientConfig.getSecureSocketProviderName().equals(provider.name())) { + sslContextBuilder.sslProvider(provider.sslProvider(httpAddress)) + .ciphers(provider.ciphers(httpAddress), provider.cipherSuiteFilter(httpAddress)); + if (nettyHttpClientConfig.isProtocolNegotiationEnabled()) { + sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1)); + } + if (provider.securityProvider(httpAddress) != null) { + Provider p = provider.securityProvider(httpAddress); + sslContextBuilder.sslContextProvider(p); + } + if (nettyHttpClientConfig.getTrustManagerFactory() != null) { + sslContextBuilder.trustManager(nettyHttpClientConfig.getTrustManagerFactory()); + } + clientSecureSocketProvider = provider; + } + } + InetSocketAddress peer = httpAddress.getInetSocketAddress(); + SslHandler sslHandler = sslContextBuilder.build() + .newHandler(nettyHttpClientConfig.getByteBufAllocator(), peer.getHostName(), peer.getPort()); + SSLEngine engine = sslHandler.engine(); + SSLParameters params = engine.getSSLParameters(); + params.setEndpointIdentificationAlgorithm("HTTPS"); + List sniServerNames = new ArrayList<>(); + sniServerNames.add(new SNIHostName(httpAddress.getHost())); // only single host_name allowed + params.setServerNames(sniServerNames); + engine.setSSLParameters(params); + switch (nettyHttpClientConfig.getClientAuthMode()) { + case NEED: + engine.setNeedClientAuth(true); + break; + case WANT: + engine.setWantClientAuth(true); + break; + default: + break; + } + if (clientSecureSocketProvider != null) { + engine.setEnabledProtocols(clientSecureSocketProvider.protocols(httpAddress)); + } + if (nettyHttpClientConfig.getSecureProtocolName() != null) { + String[] enabledProtocols = nettyHttpClientConfig.getSecureProtocolName(); + engine.setEnabledProtocols(enabledProtocols); + logger.log(Level.FINEST, "TLS: configured protocol = " + + Arrays.asList(nettyHttpClientConfig.getSecureProtocolName())); + } + sslHandler.setHandshakeTimeoutMillis(nettyHttpClientConfig.getSocketConfig().getSslHandshakeTimeoutMillis()); + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "TLS: selected secure socket provider = " + + (clientSecureSocketProvider != null ? clientSecureSocketProvider.name() : "")); + logger.log(Level.FINEST, "TLS:" + + " enabled protocols = " + Arrays.asList(engine.getEnabledProtocols()) + + " supported protocols = " + Arrays.asList(engine.getSupportedProtocols()) + + " application protocol = " + engine.getApplicationProtocol() + + " handshake application protocol = " + engine.getHandshakeApplicationProtocol()); + logger.log(Level.FINEST, "TLS: client need auth = " + + engine.getNeedClientAuth() + " client want auth = " + engine.getWantClientAuth()); + } + return sslHandler; + } + + private void configureHttp2(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + Interaction interaction) throws IOException { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + ChannelInitializer initializer = new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + throw new IllegalStateException(); + } + }; + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer) + .initialSettings(nettyHttpClientConfig.getHttp2Settings()); + if (nettyHttpClientConfig.isDebug()) { + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "client-frame")); + } + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.autoAckSettingsFrame(true).build(); + pipeline.addLast("client-multiplex", multiplexCodec); + pipeline.addLast("client-messages", new Http2Messages(interaction)); + // simulate we are ready for HTTP/2 + interaction.settingsReceived(Http2Settings.defaultSettings()); + } + + private void configurePlain(Channel channel, + NettyHttpClient nettyHttpClient, + Interaction interaction) throws IOException { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("http-client-chunk-writer", + new ChunkedWriteHandler()); + pipeline.addLast("http-client-codec", new HttpClientCodec(nettyHttpClientConfig.getMaxInitialLineLength(), + nettyHttpClientConfig.getMaxHeadersSize(), nettyHttpClientConfig.getMaxChunkSize())); + if (nettyHttpClientConfig.isGzipEnabled()) { + pipeline.addLast("http-client-decompressor", new HttpContentDecompressor()); + } + HttpObjectAggregator httpObjectAggregator = + new HttpObjectAggregator(nettyHttpClientConfig.getMaxContentLength(), false); + httpObjectAggregator.setMaxCumulationBufferComponents(nettyHttpClientConfig.getMaxCompositeBufferComponents()); + pipeline.addLast("http-client-aggregator", httpObjectAggregator); + pipeline.addLast("http-client-response", new Http1Handler(interaction)); + interaction.settingsReceived(null); + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1Interaction.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1Interaction.java new file mode 100644 index 0000000..8046f97 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http1/Https1Interaction.java @@ -0,0 +1,32 @@ +package org.xbib.net.http.client.netty.secure.http1; + +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; +import javax.net.ssl.SSLSession; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.client.netty.HttpResponseBuilder; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.http1.Http1Interaction; +import org.xbib.net.http.client.netty.http2.Http2Interaction; +import org.xbib.net.http.client.netty.secure.HttpsResponse; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import org.xbib.net.http.client.netty.secure.http2.Https2Interaction; + +public class Https1Interaction extends Http1Interaction { + + public Https1Interaction(NettyHttpClient nettyHttpClient, HttpAddress httpAddress) { + super(nettyHttpClient, httpAddress); + } + + @Override + protected HttpResponseBuilder newHttpResponseBuilder(Channel channel) { + SslHandler sslHandler = channel.attr(NettyHttpsClientConfig.ATTRIBUTE_KEY_SSL_HANDLER).get(); + SSLSession sslSession = sslHandler != null ? sslHandler.engine().getSession() : null; + return HttpsResponse.builder().setSSLSession(sslSession); + } + + @Override + protected Http2Interaction upgradeInteraction() { + return new Https2Interaction(nettyHttpClient, httpAddress); + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChannelInitializer.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChannelInitializer.java new file mode 100644 index 0000000..9905ad8 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChannelInitializer.java @@ -0,0 +1,187 @@ +package org.xbib.net.http.client.netty.secure.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpChannelInitializer; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyCustomizer; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.http2.Http2Messages; +import org.xbib.net.http.client.netty.TrafficLoggingHandler; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +public class Https2ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Https2ChannelInitializer.class.getName()); + + public Https2ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress address) { + return HttpVersion.HTTP_2_0.equals(address.getVersion()) && address.isSecure(); + } + + @Override + public Interaction newInteraction(NettyHttpClient client, HttpAddress httpAddress) { + return new Https2Interaction(client, httpAddress); + } + + @Override + public void init(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + NettyCustomizer nettyCustomizer, + Interaction interaction) { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + if (nettyHttpClientConfig.isDebug()) { + channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); + } + configureEncrypted(channel, httpAddress, nettyHttpClient, interaction); + if (nettyCustomizer != null) { + nettyCustomizer.afterChannelInitialized(channel); + } + if (nettyHttpClientConfig.isDebug()) { + logger.log(Level.FINE, "HTTP/2 secure channel initialized: address = " + httpAddress + + " pipeline = " + channel.pipeline().names()); + } + } + + private void configureEncrypted(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + Interaction interaction) { + NettyHttpsClientConfig nettyHttpClientConfig = (NettyHttpsClientConfig) nettyHttpClient.getClientConfig(); + try { + SslHandler sslHandler = createSslHandler(nettyHttpClientConfig, httpAddress); + channel.attr(NettyHttpsClientConfig.ATTRIBUTE_KEY_SSL_HANDLER).set(sslHandler); + channel.pipeline().addLast("client-ssl-handler", sslHandler); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + configurePlain(channel, nettyHttpClient, interaction); + } + + private SslHandler createSslHandler(NettyHttpsClientConfig nettyHttpClientConfig, + HttpAddress httpAddress) throws IOException { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + ClientSecureSocketProvider clientSecureSocketProvider = null; + for (ClientSecureSocketProvider provider : ServiceLoader.load(ClientSecureSocketProvider.class)) { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "trying secure socket provider = " + provider.name()); + } + if (nettyHttpClientConfig.getSecureSocketProviderName().equals(provider.name())) { + sslContextBuilder.sslProvider(provider.sslProvider(httpAddress)) + .ciphers(provider.ciphers(httpAddress), provider.cipherSuiteFilter(httpAddress)); + sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2)); + if (provider.securityProvider(httpAddress) != null) { + sslContextBuilder.sslContextProvider(provider.securityProvider(httpAddress)); + } + if (nettyHttpClientConfig.getTrustManagerFactory() != null) { + sslContextBuilder.trustManager(nettyHttpClientConfig.getTrustManagerFactory()); + } + clientSecureSocketProvider = provider; + } + } + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "selected secure socket provider = " + + (clientSecureSocketProvider != null ? clientSecureSocketProvider.name() : "")); + } + InetSocketAddress peer = httpAddress.getInetSocketAddress(); + SslHandler sslHandler = sslContextBuilder.build() + .newHandler(nettyHttpClientConfig.getByteBufAllocator(), peer.getHostName(), peer.getPort()); + SSLEngine engine = sslHandler.engine(); + SSLParameters params = engine.getSSLParameters(); + params.setEndpointIdentificationAlgorithm("HTTPS"); + List sniServerNames = new ArrayList<>(); + sniServerNames.add(new SNIHostName(httpAddress.getHost())); // only single host_name allowed + params.setServerNames(sniServerNames); + engine.setSSLParameters(params); + switch (nettyHttpClientConfig.getClientAuthMode()) { + case NEED: + engine.setNeedClientAuth(true); + break; + case WANT: + engine.setWantClientAuth(true); + break; + default: + break; + } + if (clientSecureSocketProvider != null) { + engine.setEnabledProtocols(clientSecureSocketProvider.protocols(httpAddress)); + } + if (nettyHttpClientConfig.getSecureProtocolName() != null) { + String[] enabledProtocols = nettyHttpClientConfig.getSecureProtocolName(); + engine.setEnabledProtocols(enabledProtocols); + logger.log(Level.FINEST, "TLS: configured protocol = " + + Arrays.asList(nettyHttpClientConfig.getSecureProtocolName())); + } + sslHandler.setHandshakeTimeoutMillis(nettyHttpClientConfig.getSocketConfig().getSslHandshakeTimeoutMillis()); + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "TLS: selected secure socket provider = " + + (clientSecureSocketProvider != null ? clientSecureSocketProvider.name() : "")); + logger.log(Level.FINEST, "TLS: " + + " enabled protocols = " + Arrays.asList(engine.getEnabledProtocols()) + + " supported protocols = " + Arrays.asList(engine.getSupportedProtocols()) + + " application protocol = " + engine.getApplicationProtocol() + + " handshake application protocol = " + engine.getHandshakeApplicationProtocol()); + logger.log(Level.FINEST, "TLS: client need auth = " + + engine.getNeedClientAuth() + " client want auth = " + engine.getWantClientAuth()); + } + return sslHandler; + } + + private void configurePlain(Channel channel, + NettyHttpClient nettyHttpClient, + Interaction interaction) { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelInitializer initializer = new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + throw new IllegalStateException(); + } + }; + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer) + .initialSettings(nettyHttpClientConfig.getHttp2Settings()); + if (nettyHttpClientConfig.isDebug()) { + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "client-frame")); + } + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder + .autoAckSettingsFrame(true) + .autoAckPingFrame(true) + .gracefulShutdownTimeoutMillis(30000L) + .build(); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("client-multiplex", multiplexCodec); + pipeline.addLast("client-messages", new Http2Messages(interaction)); + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChildChannelInitializer.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChildChannelInitializer.java new file mode 100644 index 0000000..c722839 --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2ChildChannelInitializer.java @@ -0,0 +1,29 @@ +package org.xbib.net.http.client.netty.secure.http2; + +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.http2.Http2ChildChannelInitializer; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +public class Https2ChildChannelInitializer extends Http2ChildChannelInitializer { + + public Https2ChildChannelInitializer(NettyHttpClientConfig clientConfig, Interaction interaction, Channel parentChannel) { + super(clientConfig, interaction, parentChannel); + } + + /** + * Initialize child channel for HTTP/2, copy the SSL handler attribute so it can be found in interactions. + * + * @param ch the {@link Channel} which was registered. + */ + @Override + protected void initChannel(Channel ch) { + super.initChannel(ch); + SslHandler sslHandler = parentChannel.attr(NettyHttpsClientConfig.ATTRIBUTE_KEY_SSL_HANDLER).get(); + if (sslHandler != null) { + ch.attr(NettyHttpsClientConfig.ATTRIBUTE_KEY_SSL_HANDLER).set(sslHandler); + } + } +} diff --git a/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2Interaction.java b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2Interaction.java new file mode 100644 index 0000000..a5f66df --- /dev/null +++ b/net-http-client-netty-secure/src/main/java/org/xbib/net/http/client/netty/secure/http2/Https2Interaction.java @@ -0,0 +1,37 @@ +package org.xbib.net.http.client.netty.secure.http2; + +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.client.netty.HttpResponseBuilder; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.http2.Http2ChildChannelInitializer; +import org.xbib.net.http.client.netty.http2.Http2Interaction; +import org.xbib.net.http.client.netty.secure.HttpsResponse; +import org.xbib.net.http.client.netty.secure.HttpsResponseBuilder; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +public class Https2Interaction extends Http2Interaction { + + public Https2Interaction(NettyHttpClient nettyHttpClient, HttpAddress httpAddress) { + super(nettyHttpClient, httpAddress); + } + + @Override + protected Http2ChildChannelInitializer newHttp2ChildChannelInitializer(NettyHttpClientConfig clientConfig, + Http2Interaction interaction, + Channel parentChannel) { + return new Https2ChildChannelInitializer(clientConfig, interaction, parentChannel); + } + + @Override + protected HttpResponseBuilder newHttpResponseBuilder(Channel channel) { + SslHandler sslHandler = channel.attr(NettyHttpsClientConfig.ATTRIBUTE_KEY_SSL_HANDLER).get(); + HttpsResponseBuilder builder = HttpsResponse.builder(); + if (sslHandler != null) { + builder.setSSLSession(sslHandler.engine().getSession()); + } + return builder; + } +} diff --git a/net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer b/net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer new file mode 100644 index 0000000..b6525a7 --- /dev/null +++ b/net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer @@ -0,0 +1,2 @@ +org.xbib.net.http.client.netty.secure.http1.Https1ChannelInitializer +org.xbib.net.http.client.netty.secure.http2.Https2ChannelInitializer \ No newline at end of file diff --git a/net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider b/net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider new file mode 100644 index 0000000..de19131 --- /dev/null +++ b/net-http-client-netty-secure/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider @@ -0,0 +1 @@ +org.xbib.net.http.client.netty.secure.JdkClientSecureSocketProvider \ No newline at end of file diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/AkamaiTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/AkamaiTest.java new file mode 100644 index 0000000..0cb0648 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/AkamaiTest.java @@ -0,0 +1,39 @@ +package org.xbib.net.http.netty.client.secure; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +public class AkamaiTest { + + private static final Logger logger = Logger.getLogger(AkamaiTest.class.getName()); + + /** + * Problems with akamai: + * failing: Cannot invoke "io.netty.handler.codec.http2.AbstractHttp2StreamChannel.fireChildRead(io.netty.handler.codec.http2.Http2Frame)" because "channel" is null * demo/h2_demo_frame.html sends no content, only a push promise, and does not continue + * + * @throws IOException if test fails + */ + @Test + void testAkamai() throws IOException { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://http2.akamai.com/demo/h2_demo_frame.html") + .setVersion("HTTP/2.0") + .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8))) + .build(); + client.execute(request).get().close(); + } + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ExponentialBackOffTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ExponentialBackOffTest.java new file mode 100644 index 0000000..a90d66f --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ExponentialBackOffTest.java @@ -0,0 +1,155 @@ +package org.xbib.net.http.netty.client.secure; + +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.BackOff; +import org.xbib.net.http.client.ExponentialBackOff; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Tests {@link ExponentialBackOff}. + */ +class ExponentialBackOffTest { + + @Test + void testConstructor() { + ExponentialBackOff backOffPolicy = new ExponentialBackOff(); + assertEquals(ExponentialBackOff.DEFAULT_INITIAL_INTERVAL_MILLIS, + backOffPolicy.getInitialIntervalMillis()); + assertEquals(ExponentialBackOff.DEFAULT_INITIAL_INTERVAL_MILLIS, + backOffPolicy.getCurrentIntervalMillis()); + assertEquals(ExponentialBackOff.DEFAULT_RANDOMIZATION_FACTOR, + backOffPolicy.getRandomizationFactor(), 1); + assertEquals(ExponentialBackOff.DEFAULT_MULTIPLIER, backOffPolicy.getMultiplier(), 1); + assertEquals( + ExponentialBackOff.DEFAULT_MAX_INTERVAL_MILLIS, backOffPolicy.getMaxIntervalMillis()); + assertEquals(ExponentialBackOff.DEFAULT_MAX_ELAPSED_TIME_MILLIS, + backOffPolicy.getMaxElapsedTimeMillis()); + } + + @Test + void testBuilder() { + ExponentialBackOff backOffPolicy = new ExponentialBackOff.Builder().build(); + assertEquals(ExponentialBackOff.DEFAULT_INITIAL_INTERVAL_MILLIS, + backOffPolicy.getInitialIntervalMillis()); + assertEquals(ExponentialBackOff.DEFAULT_INITIAL_INTERVAL_MILLIS, + backOffPolicy.getCurrentIntervalMillis()); + assertEquals(ExponentialBackOff.DEFAULT_RANDOMIZATION_FACTOR, + backOffPolicy.getRandomizationFactor(), 1); + assertEquals(ExponentialBackOff.DEFAULT_MULTIPLIER, backOffPolicy.getMultiplier(), 1); + assertEquals(ExponentialBackOff.DEFAULT_MAX_INTERVAL_MILLIS, backOffPolicy.getMaxIntervalMillis()); + assertEquals(ExponentialBackOff.DEFAULT_MAX_ELAPSED_TIME_MILLIS, + backOffPolicy.getMaxElapsedTimeMillis()); + + int testInitialInterval = 1; + double testRandomizationFactor = 0.1; + double testMultiplier = 5.0; + int testMaxInterval = 10; + int testMaxElapsedTime = 900000; + + backOffPolicy = new ExponentialBackOff.Builder() + .setInitialIntervalMillis(testInitialInterval) + .setRandomizationFactor(testRandomizationFactor) + .setMultiplier(testMultiplier) + .setMaxIntervalMillis(testMaxInterval) + .setMaxElapsedTimeMillis(testMaxElapsedTime) + .build(); + assertEquals(testInitialInterval, backOffPolicy.getInitialIntervalMillis()); + assertEquals(testInitialInterval, backOffPolicy.getCurrentIntervalMillis()); + assertEquals(testRandomizationFactor, backOffPolicy.getRandomizationFactor(), 1); + assertEquals(testMultiplier, backOffPolicy.getMultiplier(), 1); + assertEquals(testMaxInterval, backOffPolicy.getMaxIntervalMillis()); + assertEquals(testMaxElapsedTime, backOffPolicy.getMaxElapsedTimeMillis()); + } + + @Test + void testBackOff() { + int testInitialInterval = 500; + double testRandomizationFactor = 0.1; + double testMultiplier = 2.0; + int testMaxInterval = 5000; + int testMaxElapsedTime = 900000; + + ExponentialBackOff backOffPolicy = new ExponentialBackOff.Builder() + .setInitialIntervalMillis(testInitialInterval) + .setRandomizationFactor(testRandomizationFactor) + .setMultiplier(testMultiplier) + .setMaxIntervalMillis(testMaxInterval) + .setMaxElapsedTimeMillis(testMaxElapsedTime) + .build(); + int[] expectedResults = {500, 1000, 2000, 4000, 5000, 5000, 5000, 5000, 5000, 5000}; + for (int expected : expectedResults) { + assertEquals(expected, backOffPolicy.getCurrentIntervalMillis()); + // Assert that the next back off falls in the expected range. + int minInterval = (int) (expected - (testRandomizationFactor * expected)); + int maxInterval = (int) (expected + (testRandomizationFactor * expected)); + long actualInterval = backOffPolicy.nextBackOffMillis(); + assertTrue(minInterval <= actualInterval && actualInterval <= maxInterval); + } + } + + @Test + void testGetRandomizedInterval() { + // 33% chance of being 1. + assertEquals(1, ExponentialBackOff.getRandomValueFromInterval(0.5, 0, 2)); + assertEquals(1, ExponentialBackOff.getRandomValueFromInterval(0.5, 0.33, 2)); + // 33% chance of being 2. + assertEquals(2, ExponentialBackOff.getRandomValueFromInterval(0.5, 0.34, 2)); + assertEquals(2, ExponentialBackOff.getRandomValueFromInterval(0.5, 0.66, 2)); + // 33% chance of being 3. + assertEquals(3, ExponentialBackOff.getRandomValueFromInterval(0.5, 0.67, 2)); + assertEquals(3, ExponentialBackOff.getRandomValueFromInterval(0.5, 0.99, 2)); + } + + @Test + void testGetElapsedTimeMillis() { + ExponentialBackOff backOffPolicy = new ExponentialBackOff.Builder().setNanoClock(new MyNanoClock()).build(); + long elapsedTimeMillis = backOffPolicy.getElapsedTimeMillis(); + assertEquals(1000, elapsedTimeMillis); + } + + @Test + void testMaxElapsedTime() { + ExponentialBackOff backOffPolicy = + new ExponentialBackOff.Builder().setNanoClock(new MyNanoClock(10000)).build(); + assertTrue(backOffPolicy.nextBackOffMillis() != BackOff.STOP); + // Change the currentElapsedTimeMillis to be 0 ensuring that the elapsed time will be greater + // than the max elapsed time. + backOffPolicy.setStartTimeNanos(0); + assertEquals(BackOff.STOP, backOffPolicy.nextBackOffMillis()); + } + + @Test + void testBackOffOverflow() { + int testInitialInterval = Integer.MAX_VALUE / 2; + double testMultiplier = 2.1; + int testMaxInterval = Integer.MAX_VALUE; + ExponentialBackOff backOffPolicy = new ExponentialBackOff.Builder() + .setInitialIntervalMillis(testInitialInterval) + .setMultiplier(testMultiplier) + .setMaxIntervalMillis(testMaxInterval) + .build(); + backOffPolicy.nextBackOffMillis(); + // Assert that when an overflow is possible the current interval is set to the max interval. + assertEquals(testMaxInterval, backOffPolicy.getCurrentIntervalMillis()); + } + + static class MyNanoClock implements ExponentialBackOff.NanoClock { + + private int i = 0; + private long startSeconds; + + MyNanoClock() { + } + + MyNanoClock(long startSeconds) { + this.startSeconds = startSeconds; + } + + public long nanoTime() { + return (startSeconds + i++) * 1000000000; + } + } + +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/FileDescriptorLeakTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/FileDescriptorLeakTest.java new file mode 100644 index 0000000..1ff4bb6 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/FileDescriptorLeakTest.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.netty.client.secure; + +import com.sun.management.UnixOperatingSystemMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +class FileDescriptorLeakTest { + + private static final Logger logger = Logger.getLogger(FileDescriptorLeakTest.class.getName()); + + @Test + void testFileLeak() throws Exception { + OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); + for (int i = 0; i < 3; i++) { + if (os instanceof UnixOperatingSystemMXBean) { + logger.info("before: number of open file descriptor : " + ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount()); + } + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://xbib.org") + .setResponseListener(resp -> logger.log(Level.INFO, "got response: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8))) + .build(); + client.execute(request).get().close(); + } + if (os instanceof UnixOperatingSystemMXBean){ + logger.info("after: number of open file descriptor : " + ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount()); + } + } + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Http2FramesTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Http2FramesTest.java new file mode 100644 index 0000000..7a39312 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Http2FramesTest.java @@ -0,0 +1,117 @@ +package org.xbib.net.http.netty.client.secure; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2FrameAdapter; +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.logging.LogLevel; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +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 java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import org.junit.jupiter.api.Test; + +class Http2FramesTest { + + private static final Logger logger = Logger.getLogger(Http2FramesTest.class.getName()); + + @Test + void testHttp2Frames() throws Exception { + final InetSocketAddress inetSocketAddress = new InetSocketAddress("webtide.com", 443); + CompletableFuture completableFuture = new CompletableFuture<>(); + EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + Channel clientChannel = null; + try { + Bootstrap bootstrap = new Bootstrap() + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .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); + Http2FrameAdapter frameAdapter = new Http2FrameAdapter() { + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) { + logger.log(Level.FINE, "settings received, now writing request"); + Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class); + handler.encoder().writeHeaders(ctx, 3, + new DefaultHttp2Headers().method(HttpMethod.GET.asciiName()) + .path("/") + .scheme("https") + .authority(inetSocketAddress.getHostName()), + 0, true, ctx.newPromise()); + ctx.channel().flush(); + } + + @Override + public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, + boolean endOfStream) throws Http2Exception { + int i = super.onDataRead(ctx, streamId, data, padding, endOfStream); + if (endOfStream) { + completableFuture.complete(true); + } + return i; + } + }; + Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder() + .server(false) + .frameListener(frameAdapter) + .frameLogger(new Http2FrameLogger(LogLevel.INFO, "client")); + ch.pipeline().addLast(builder.build()); + } + }); + logger.log(Level.INFO, () -> "connecting"); + clientChannel = bootstrap.connect(inetSocketAddress).sync().channel(); + logger.log(Level.INFO, () -> "waiting for end of stream"); + completableFuture.get(); + logger.log(Level.INFO, () -> "done"); + } finally { + if (clientChannel != null) { + clientChannel.close(); + } + eventLoopGroup.shutdownGracefully(); + } + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/HttpBinTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/HttpBinTest.java new file mode 100644 index 0000000..515bffb --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/HttpBinTest.java @@ -0,0 +1,58 @@ +package org.xbib.net.http.netty.client.secure; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import org.xbib.net.http.cookie.Cookie; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HttpBinTest { + + private static final Logger logger = Logger.getLogger(HttpBinTest.class.getName()); + + /** + * Test httpbin.org "Set-Cookie:" header after redirection of URL. + * + * The reponse body should be + *

+     *   {
+     *     "cookies": {
+     *       "name": "value"
+     *     }
+     *   }
+     * 
+ * @throws IOException if test fails + */ + @Test + void testHttpBinCookies() throws IOException { + AtomicBoolean success = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("http://httpbin.org/cookies/set?name=value") + .setResponseListener(resp -> { + logger.log(Level.INFO, "got HTTP/2 response: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8)); + for (Cookie cookie : resp.getCookies()) { + logger.log(Level.INFO, "got cookie: " + cookie.toString()); + if ("name".equals(cookie.name()) && ("value".equals(cookie.value()))) { + success.set(true); + } + } + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(success.get()); + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https1Test.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https1Test.java new file mode 100644 index 0000000..9ddfe9c --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https1Test.java @@ -0,0 +1,295 @@ +package org.xbib.net.http.netty.client.secure; + +import io.netty.handler.proxy.Socks5ProxyHandler; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.security.cert.Certificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLPeerUnverifiedException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.SocketConfig; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.client.HttpResponse; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.HttpsResponse; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class Https1Test { + + private static final Logger logger = Logger.getLogger(Https1Test.class.getName()); + + @Test + void testXbib() throws Exception { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://xbib.org") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " ssl = " + dumpCertificates((HttpsResponse) resp))) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testGoogleHttp() throws Exception { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setProtocolNegotiation(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("http://google.de") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8))) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testGoogleUpgradeHttps() throws Exception { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setProtocolNegotiation(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://www.google.de/") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " ssl = " + dumpCertificates((HttpsResponse) resp))) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testDNB() throws Exception { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + Map map = Map.of( + "version", "1.1", + "operation", "searchRetrieve", + "recordSchema", "MARC21plus-1-xml", + "query", "iss=00280836" + ); + HttpRequest request = HttpRequest.get() + .setURL("http://services.dnb.de/sru/zdb") + .setParameters(map) + .setResponseListener(resp -> logger.log(Level.INFO, + "got response: " + resp.getHeaders() + + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " status=" + resp.getStatus())) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testHebisGetRequest() throws Exception { + // we test HEBIS here with strange certificate setup and TLS 1.2 only + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()){ + HttpRequest request = HttpRequest.post() + .setURL("https://hebis.rz.uni-frankfurt.de/HEBCGI/vuefl_recv_data.pl") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " ssl = " + dumpCertificates((HttpsResponse) resp)) + ) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testSequentialRequests() throws Exception { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + for (int i = 0; i <10; i++) { + HttpRequest request = HttpRequest.get().setURL("https://xbib.org") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " ssl = " + dumpCertificates((HttpsResponse) resp))) + .build(); + client.execute(request).get(); + } + } + } + + @Test + void testParallelRequests() throws Exception { + AtomicInteger counter = new AtomicInteger(); + NettyHttpClientConfig config = new NettyHttpsClientConfig(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request1 = HttpRequest.builder(HttpMethod.GET) + .setURL("https://xbib.org") + .setVersion("HTTP/1.1") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " counter = " + counter.incrementAndGet() + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " ssl = " + dumpCertificates((HttpsResponse) resp))) + .build(); + HttpRequest request2 = HttpRequest.builder(HttpMethod.GET) + .setURL("https://xbib.org") + .setVersion("HTTP/1.1") + .setResponseListener(resp -> + logger.log(Level.INFO, + "got response: " + + " counter = " + counter.incrementAndGet() + + " status = " + resp.getStatus() + + " headers = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " ssl = " + dumpCertificates((HttpsResponse) resp))) + .build(); + for (int i = 0; i < 5; i++) { + client.execute(request1); + client.execute(request2); + } + Thread.sleep(1000L); + } + assertEquals(10, counter.get()); + } + + @Test + void testXbibOrgWithCompletableFuture() throws IOException { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient httpClient = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://xbib.org") + .build(); + String result = httpClient.execute(request, response -> response.getBodyAsChars(StandardCharsets.UTF_8).toString()) + .exceptionally(Throwable::getMessage) + .join(); + logger.info("got result = " + result); + } + // TODO 15 sec timeout on closing event loop group, why? + } + + @Test + void testXbibOrgWithCompletableFutureAndGoogleSearch() throws IOException { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient httpClient = NettyHttpClient.builder() + .setConfig(config) + .build()) { + final Function stringFunction = + response -> response.getBodyAsChars(StandardCharsets.UTF_8).toString(); + HttpRequest request = HttpRequest.get() + .setURL("https://xbib.org") + .build(); + final CompletableFuture completableFuture = httpClient.execute(request, stringFunction) + .exceptionally(Throwable::getMessage) + .thenCompose(content -> { + try { + return httpClient.execute(HttpRequest.get() + .setURL("https://www.google.de/") + .addParameter("query", content.substring(0, 15)) + .build(), stringFunction); + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + return null; + } + }); + String result = completableFuture.join(); + logger.info("got result = " + result); + } + } + + @Disabled("proxy is down") + @Test + void testXbibOrgWithProxy() throws IOException { + SocketConfig socketConfig = new SocketConfig(); + socketConfig.setConnectTimeoutMillis(30000); + socketConfig.setReadTimeoutMillis(30000); + Socks5ProxyHandler handler = new Socks5ProxyHandler(new InetSocketAddress("178.162.202.44", 1695)); + handler.setConnectTimeoutMillis(30000L); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setSocketConfig(socketConfig) + .setSocks5ProxyHandler(handler) + .setDebug(true); + try (NettyHttpClient httpClient = NettyHttpClient.builder() + .setConfig(config) + .build()) { + httpClient.execute(HttpRequest.get() + .setURL("https://xbib.org") + .setResponseListener(resp -> logger.log(Level.INFO, "status = " + resp.getStatus() + + " response body = " + resp.getBodyAsChars(StandardCharsets.UTF_8))) + .build()) + .get(); + } + } + + private String dumpCertificates(HttpsResponse httpsResponse) { + StringBuilder sb = new StringBuilder(); + try { + for (Certificate certificate : httpsResponse.getSSLSession().getPeerCertificates()) { + if (certificate instanceof X509Certificate) { + X509Certificate c = (X509Certificate) certificate; + sb.append("subjects=").append(c.getSubjectAlternativeNames()); + sb.append(",issuers=").append(c.getIssuerAlternativeNames()); + sb.append(",not before=").append(c.getNotBefore()); + sb.append(",not after=").append(c.getNotAfter()); + sb.append("\n"); + } + } + } catch (SSLPeerUnverifiedException | CertificateParsingException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + return sb.toString(); + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https2Test.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https2Test.java new file mode 100644 index 0000000..9ffa0d6 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/Https2Test.java @@ -0,0 +1,116 @@ +package org.xbib.net.http.netty.client.secure; + +import io.netty.handler.codec.http.HttpMethod; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Https2Test { + + private static final Logger logger = Logger.getLogger(Https2Test.class.getName()); + + @Disabled + @Test + void testXbib() throws Exception { + // the xbib server does not offer HTTP/2 so this does not work! + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://xbib.org/") + .setVersion("HTTP/2.0") + .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8))) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testGoogleFollwRedirect() throws Exception { + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://google.com") + .setVersion("HTTP/2.0") + .setFollowRedirect(true) // default is true, https://www.google.com/ + .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8))) + .build(); + client.execute(request).get().close(); + } + } + + @Test + void testHttp1WithTlsV13() throws Exception { + AtomicBoolean success = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setSecureProtocolName(new String[] { "TLSv1.3" }) + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://google.com") + .setVersion("HTTP/2.0") + .setFollowRedirect(true) // default is true, https://www.google.com/ + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8)); + success.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(success.get()); + } + + @Test + void testParallelRequestsAndClientClose() throws IOException { + AtomicBoolean success1 = new AtomicBoolean(); + AtomicBoolean success2 = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request1 = HttpRequest.get() + .setURL("https://google.com") + .setVersion("HTTP/2.0") + .setFollowRedirect(true) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response1: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8)); + success1.set(true); + }) + .build(); + HttpRequest request2 = HttpRequest.get() + .setURL("https://google.com") + .setVersion("HTTP/2.0") + .setFollowRedirect(true) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response2: " + + resp.getHeaders() + resp.getBodyAsChars(StandardCharsets.UTF_8)); + success2.set(true); + }) + .build(); + client.execute(request1); + client.execute(request2); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/JdkClientTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/JdkClientTest.java new file mode 100644 index 0000000..c916b62 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/JdkClientTest.java @@ -0,0 +1,71 @@ +package org.xbib.net.http.netty.client.secure; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; + +/** + * Testing the JDK 11+ HTTP client for comparison purposes. + */ +public class JdkClientTest { + + private static final Logger logger = Logger.getLogger(JdkClientTest.class.getName()); + + static { + System.setProperty("javax.net.debug", "true"); + } + + @Test + public void testDNB() throws Exception { + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); + Map map = Map.of( + "version", "1.1", + "operation", "searchRetrieve", + "recordSchema", "MARC21plus-1-xml", + "query", "iss = 00280836" + ); + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI("https://services.dnb.de/sru/zdb")) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(buildFormDataFromMap(map)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + logger.log(Level.INFO, Integer.toString(response.statusCode())); + logger.log(Level.INFO, response.body()); + } + + @Test + void testHebisGetRequest() throws Exception { + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI("https://hebis.rz.uni-frankfurt.de/HEBCGI/vuefl_recv_data.pl")) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + logger.log(Level.INFO, Integer.toString(response.statusCode())); + logger.log(Level.INFO, response.body()); + } + + private static HttpRequest.BodyPublisher buildFormDataFromMap(Map data) { + var builder = new StringBuilder(); + for (Map.Entry entry : data.entrySet()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); + builder.append("="); + builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)); + } + return HttpRequest.BodyPublishers.ofString(builder.toString()); + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOff.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOff.java new file mode 100644 index 0000000..2cf4800 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOff.java @@ -0,0 +1,76 @@ +package org.xbib.net.http.netty.client.secure; + + +import org.xbib.net.http.client.BackOff; + +/** + * Mock for {@link BackOff} that always returns a fixed number. + * + *

+ * Implementation is not thread-safe. + *

+ * + */ +public class MockBackOff implements BackOff { + + /** Fixed back-off milliseconds. */ + private long backOffMillis; + + /** Maximum number of tries before returning {@link #STOP}. */ + private int maxTries = 10; + + /** Number of tries so far. */ + private int numTries; + + @Override + public void reset() { + numTries = 0; + } + + @Override + public long nextBackOffMillis() { + if (numTries >= maxTries || backOffMillis == STOP) { + return STOP; + } + numTries++; + return backOffMillis; + } + + /** + * Sets the fixed back-off milliseconds (defaults to {@code 0}). + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public MockBackOff setBackOffMillis(long backOffMillis) { + //Preconditions.checkArgument(backOffMillis == STOP || backOffMillis >= 0); + this.backOffMillis = backOffMillis; + return this; + } + + /** + * Sets the maximum number of tries before returning {@link #STOP} (defaults to {@code 10}). + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public MockBackOff setMaxTries(int maxTries) { + //Preconditions.checkArgument(maxTries >= 0); + this.maxTries = maxTries; + return this; + } + + /** Returns the maximum number of tries before returning {@link #STOP}. */ + public final int getMaxTries() { + return numTries; + } + + /** Returns the number of tries so far. */ + public final int getNumberOfTries() { + return numTries; + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOffTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOffTest.java new file mode 100644 index 0000000..4dbf518 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/MockBackOffTest.java @@ -0,0 +1,25 @@ +package org.xbib.net.http.netty.client.secure; + +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.BackOff; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests {@link MockBackOff}. + */ +class MockBackOffTest { + + @Test + void testNextBackOffMillis() throws IOException { + subtestNextBackOffMillis(0, new MockBackOff()); + subtestNextBackOffMillis(BackOff.STOP, new MockBackOff().setBackOffMillis(BackOff.STOP)); + subtestNextBackOffMillis(42, new MockBackOff().setBackOffMillis(42)); + } + + private void subtestNextBackOffMillis(long expectedValue, BackOff backOffPolicy) throws IOException { + for (int i = 0; i < 10; i++) { + assertEquals(expectedValue, backOffPolicy.nextBackOffMillis()); + } + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp1Test.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp1Test.java new file mode 100644 index 0000000..c619d53 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp1Test.java @@ -0,0 +1,257 @@ +package org.xbib.net.http.netty.client.secure; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.AttributeKey; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SimpleHttp1Test { + + private static final Logger logger = Logger.getLogger(SimpleHttp1Test.class.getName()); + + @AfterAll + void checkThreads() { + Set threadSet = Thread.getAllStackTraces().keySet(); + logger.log(Level.INFO, "threads = " + threadSet.size() ); + threadSet.forEach( thread -> { + if (thread.getName().equals("ObjectCleanerThread")) { + logger.log(Level.INFO, thread.toString()); + } + }); + } + + @Test + void testHttp1() throws Exception { + Client client = new Client(); + try { + HttpTransport transport = client.newTransport("google.de", 80); + transport.onResponse(msg -> logger.log(Level.INFO, + "got response: " + msg.status().code() + " headers=" + msg.headers().entries())); + transport.connect(); + sendRequest(transport); + transport.awaitResponse(); + } finally { + client.shutdown(); + } + } + + private void sendRequest(HttpTransport transport) { + Channel channel = transport.channel(); + if (channel == null) { + return; + } + String host = transport.inetSocketAddress().getHostString(); + int port = transport.inetSocketAddress().getPort(); + String uri = "http://" + host + ":" + port; + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + request.headers().add(HttpHeaderNames.HOST, host + ":" + port); + request.headers().add(HttpHeaderNames.USER_AGENT, "Java"); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); + logger.log(Level.INFO, () -> "writing request = " + request); + if (channel.isWritable()) { + channel.writeAndFlush(request); + } + } + + private final AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); + + interface ResponseWriter { + void write(FullHttpResponse msg); + } + + class Client { + private final EventLoopGroup eventLoopGroup; + + private final Bootstrap bootstrap; + + private final List transports; + + Client() { + eventLoopGroup = new NioEventLoopGroup(); + HttpResponseHandler httpResponseHandler = new HttpResponseHandler(); + Initializer initializer = new Initializer(httpResponseHandler); + bootstrap = new Bootstrap() + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(initializer); + transports = new ArrayList<>(); + } + + Bootstrap bootstrap() { + return bootstrap; + } + + void shutdown() { + close(); + eventLoopGroup.shutdownGracefully(); + try { + eventLoopGroup.awaitTermination(10L, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + + HttpTransport newTransport(String host, int port) { + HttpTransport transport = new HttpTransport(this, new InetSocketAddress(host, port)); + transports.add(transport); + return transport; + } + + void close() { + for (HttpTransport transport : transports) { + transport.close(); + } + transports.clear(); + } + } + + class HttpTransport { + + private final Client client; + + private final InetSocketAddress inetSocketAddress; + + private Channel channel; + + private CompletableFuture promise; + + private ResponseWriter responseWriter; + + HttpTransport(Client client, InetSocketAddress inetSocketAddress ) { + this.client = client; + this.inetSocketAddress = inetSocketAddress; + } + + InetSocketAddress inetSocketAddress() { + return inetSocketAddress; + } + + void connect() throws InterruptedException { + channel = client.bootstrap().connect(inetSocketAddress).sync().await().channel(); + channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); + promise = new CompletableFuture<>(); + } + + Channel channel() { + return channel; + } + + void onResponse(ResponseWriter responseWriter) { + this.responseWriter = responseWriter; + } + + void responseReceived(FullHttpResponse msg) { + if (responseWriter != null) { + responseWriter.write(msg); + } + } + + void awaitResponse() { + if (promise != null) { + try { + promise.get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + } + + void complete() { + if (promise != null) { + promise.complete(true); + } + } + + void fail(Throwable throwable) { + if (promise != null) { + promise.completeExceptionally(throwable); + } + } + + void close() { + if (channel != null) { + channel.close(); + } + } + } + + class Initializer extends ChannelInitializer { + + private HttpResponseHandler httpResponseHandler; + + Initializer(HttpResponseHandler httpResponseHandler) { + this.httpResponseHandler = httpResponseHandler; + } + + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new HttpClientCodec()); + ch.pipeline().addLast(new HttpObjectAggregator(1048576)); + ch.pipeline().addLast(httpResponseHandler); + } + } + + class HttpResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { + HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + if (msg.content().isReadable()) { + transport.responseReceived(msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.complete(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(new IOException("channel closed")); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(cause); + ctx.channel().close(); + } + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp2Test.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp2Test.java new file mode 100644 index 0000000..95c2909 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/SimpleHttp2Test.java @@ -0,0 +1,348 @@ +package org.xbib.net.http.netty.client.secure; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +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.Http2ConnectionHandler; +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.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.AttributeKey; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLException; +import org.junit.jupiter.api.Test; + +class SimpleHttp2Test { + + private static final Logger logger = Logger.getLogger(SimpleHttp2Test.class.getName()); + + @Test + void testHttp2WithUpgrade() throws Exception { + Client client = new Client(); + try { + Http2Transport transport = client.newTransport("webtide.com", 443); + transport.onResponse(string -> logger.log(Level.INFO, "got messsage: " + string)); + transport.connect(); + transport.awaitSettings(); + sendRequest(transport); + transport.awaitResponses(); + transport.close(); + } finally { + client.shutdown(); + } + } + + private void sendRequest(Http2Transport transport) { + Channel channel = transport.channel(); + if (channel == null) { + return; + } + Integer streamId = transport.nextStream(); + String host = transport.inetSocketAddress().getHostString(); + int port = transport.inetSocketAddress().getPort(); + String uri = "https://" + host + ":" + port; + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + request.headers().add(HttpHeaderNames.HOST, host + ":" + port); + request.headers().add(HttpHeaderNames.USER_AGENT, "Java"); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); + request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE); + if (streamId != null) { + request.headers().add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId)); + } + logger.log(Level.INFO, () -> "writing request = " + request); + channel.writeAndFlush(request); + } + + private final AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); + + interface ResponseWriter { + void write(String string); + } + + class Client { + private final EventLoopGroup eventLoopGroup; + + private final Bootstrap bootstrap; + + Client() { + eventLoopGroup = new NioEventLoopGroup(); + Http2SettingsHandler http2SettingsHandler = new Http2SettingsHandler(); + Http2ResponseHandler http2ResponseHandler = new Http2ResponseHandler(); + Initializer initializer = new Initializer(http2SettingsHandler, http2ResponseHandler); + bootstrap = new Bootstrap() + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(initializer); + } + + Bootstrap bootstrap() { + return bootstrap; + } + + void shutdown() { + eventLoopGroup.shutdownGracefully(); + } + + Http2Transport newTransport(String host, int port) { + return new Http2Transport(this, new InetSocketAddress(host, port)); + } + } + + class Http2Transport { + + private final Client client; + + private final InetSocketAddress inetSocketAddress; + + private Channel channel; + + CompletableFuture settingsPromise; + + private final SortedMap> streamidPromiseMap; + + private final AtomicInteger streamIdCounter; + + private ResponseWriter responseWriter; + + Http2Transport(Client client, InetSocketAddress inetSocketAddress) { + this.client = client; + this.inetSocketAddress = inetSocketAddress; + streamidPromiseMap = new TreeMap<>(); + streamIdCounter = new AtomicInteger(3); + } + + InetSocketAddress inetSocketAddress() { + return inetSocketAddress; + } + + void connect() throws InterruptedException { + channel = client.bootstrap().connect(inetSocketAddress).sync().await().channel(); + channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); + settingsPromise = new CompletableFuture<>(); + } + + Channel channel() { + return channel; + } + + Integer nextStream() { + Integer streamId = streamIdCounter.getAndAdd(2); + streamidPromiseMap.put(streamId, new CompletableFuture<>()); + return streamId; + } + + void onResponse(ResponseWriter responseWriter) { + this.responseWriter = responseWriter; + } + + void settingsReceived(Channel channel, Http2Settings http2Settings) { + if (settingsPromise != null) { + settingsPromise.complete(true); + } else { + logger.log(Level.WARNING, "settings received but no promise present"); + } + } + + void awaitSettings() { + if (settingsPromise != null) { + try { + logger.log(Level.INFO, "waiting for settings"); + settingsPromise.get(5, TimeUnit.SECONDS); + logger.log(Level.INFO, "settings received"); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + settingsPromise.completeExceptionally(e); + } + } else { + logger.log(Level.WARNING, "waiting for settings but no promise present"); + } + } + + void responseReceived(Integer streamId, String message) { + if (streamId == null) { + logger.log(Level.WARNING, "unexpected message received: " + message); + return; + } + CompletableFuture promise = streamidPromiseMap.get(streamId); + if (promise == null) { + logger.log(Level.WARNING, "message received for unknown stream id " + streamId); + } else { + if (responseWriter != null) { + responseWriter.write(message); + } + promise.complete(true); + } + } + + void awaitResponse(Integer streamId) { + if (streamId == null) { + return; + } + CompletableFuture promise = streamidPromiseMap.get(streamId); + if (promise != null) { + try { + logger.log(Level.INFO, "waiting for response for stream id=" + streamId); + promise.get(5, TimeUnit.SECONDS); + logger.log(Level.INFO, "response for stream id=" + streamId + " received"); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.log(Level.WARNING, "streamId=" + streamId + " " + e.getMessage(), e); + } finally { + streamidPromiseMap.remove(streamId); + } + } + } + + void awaitResponses() { + logger.log(Level.INFO, "waiting for all stream ids " + streamidPromiseMap.keySet()); + for (int streamId : streamidPromiseMap.keySet()) { + awaitResponse(streamId); + } + } + + void fail(Throwable throwable) { + for (CompletableFuture promise : streamidPromiseMap.values()) { + promise.completeExceptionally(throwable); + } + } + + void close() { + if (channel != null) { + channel.close(); + } + } + } + + class Initializer extends ChannelInitializer { + + private final Http2SettingsHandler http2SettingsHandler; + + private final Http2ResponseHandler http2ResponseHandler; + + Initializer(Http2SettingsHandler http2SettingsHandler, Http2ResponseHandler http2ResponseHandler) { + this.http2SettingsHandler = http2SettingsHandler; + this.http2ResponseHandler = http2ResponseHandler; + } + + @Override + protected void initChannel(SocketChannel ch) { + DefaultHttp2Connection http2Connection = new DefaultHttp2Connection(false); + Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.INFO, "client"); + Http2ConnectionHandler http2ConnectionHandler = new HttpToHttp2ConnectionHandlerBuilder() + .connection(http2Connection) + .frameLogger(frameLogger) + .frameListener(new DelegatingDecompressorFrameListener(http2Connection, + new InboundHttp2ToHttpAdapterBuilder(http2Connection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build())) + .build(); + + try { + SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .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(); + ch.pipeline().addLast(sslContext.newHandler(ch.alloc())); + ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler("") { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(http2ConnectionHandler, http2SettingsHandler, http2ResponseHandler); + return; + } + ctx.close(); + throw new IllegalStateException("unknown protocol: " + protocol); + } + }; + ch.pipeline().addLast(negotiationHandler); + } catch (SSLException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + } + + class Http2SettingsHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) { + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.settingsReceived(ctx.channel(), http2Settings); + ctx.pipeline().remove(this); + } + } + + class Http2ResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + if (msg.content().isReadable()) { + transport.responseReceived(streamId, msg.content().toString(StandardCharsets.UTF_8)); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // do nothing + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(new IOException("channel closed")); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(cause); + ctx.channel().close(); + } + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ThreadLeakTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ThreadLeakTest.java new file mode 100644 index 0000000..133e547 --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/ThreadLeakTest.java @@ -0,0 +1,36 @@ +package org.xbib.net.http.netty.client.secure; + +import java.io.IOException; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ThreadLeakTest { + + private static final Logger logger = Logger.getLogger(ThreadLeakTest.class.getName()); + + @Test + void testForLeaks() throws IOException { + NettyHttpClientConfig config = new NettyHttpsClientConfig(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + } + } + + @BeforeAll + @AfterAll + void checkThreads() { + Set threadSet = Thread.getAllStackTraces().keySet(); + logger.log(Level.INFO, "threads = " + threadSet.size() ); + threadSet.forEach( thread -> logger.log(Level.INFO, thread.toString())); + } +} diff --git a/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/WebtideTest.java b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/WebtideTest.java new file mode 100644 index 0000000..beea72a --- /dev/null +++ b/net-http-client-netty-secure/src/test/java/org/xbib/net/http/netty/client/secure/WebtideTest.java @@ -0,0 +1,369 @@ +package org.xbib.net.http.netty.client.secure; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; +import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; +import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +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.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.AttributeKey; +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLException; +import org.junit.jupiter.api.Test; + +class WebtideTest { + + private static final Logger logger = Logger.getLogger(WebtideTest.class.getName()); + + /** + * Netty standalone demo to connect to https://webtide.com + * and negotiate HTTP/2 and receive responses as HTTP objects. + */ + @Test + void testWebtideHttps() throws Exception { + try (Client client = new Client()) { + InetSocketAddress address = new InetSocketAddress("google.com", 443); + Http2Transport transport = new Http2Transport(client.bootstrap, address); + transport.onResponse(string -> logger.log(Level.INFO, "got response for request = " + string)); + logger.log(Level.FINE, "connected"); + transport.connect(); + logger.log(Level.FINE, "waiting for settings"); + transport.awaitSettings(); + sendRequest(transport); + logger.log(Level.FINE, "waiting for responses"); + transport.awaitResponses(); + logger.log(Level.FINE, "close"); + transport.close(); + } + } + + private void sendRequest(Http2Transport transport) { + Channel channel = transport.channel(); + if (channel == null) { + return; + } + Integer streamId = transport.nextStream(); + String host = transport.inetSocketAddress().getHostString(); + int port = transport.inetSocketAddress().getPort(); + String uri = "https://" + host + ":" + port; + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + request.headers().add(HttpHeaderNames.HOST, host + ":" + port); + request.headers().add(HttpHeaderNames.USER_AGENT, "Java"); + if (streamId != null) { + request.headers().add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId)); + } + logger.log(Level.FINE, "request prepared and ready for sending " + request); + channel.writeAndFlush(request); + } + + private final AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); + + interface ResponseWriter { + void write(String string); + } + + class Client implements Closeable { + + private final EventLoopGroup eventLoopGroup; + + private final Bootstrap bootstrap; + + Client() { + eventLoopGroup = new NioEventLoopGroup(); + Initializer initializer = new Initializer(); + bootstrap = new Bootstrap() + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(initializer); + } + + @Override + public void close() throws IOException { + eventLoopGroup.shutdownGracefully(); + } + } + + class Http2Transport { + + private final Bootstrap bootstrap; + + private final InetSocketAddress inetSocketAddress; + + private final SortedMap> streamidPromiseMap; + + private final AtomicInteger streamIdCounter; + + private final CompletableFuture settingsPromise; + + private Channel channel; + + private ResponseWriter responseWriter; + + Http2Transport(Bootstrap bootstrap, InetSocketAddress inetSocketAddress) { + this.bootstrap = bootstrap; + this.inetSocketAddress = inetSocketAddress; + this.streamidPromiseMap = new TreeMap<>(); + this.streamIdCounter = new AtomicInteger(3); + this.settingsPromise = new CompletableFuture<>(); + } + + InetSocketAddress inetSocketAddress() { + return inetSocketAddress; + } + + void connect() throws InterruptedException { + channel = bootstrap.connect(inetSocketAddress).sync().await().channel(); + channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); + } + + Channel channel() { + return channel; + } + + Integer nextStream() { + Integer streamId = streamIdCounter.getAndAdd(2); + streamidPromiseMap.put(streamId, new CompletableFuture<>()); + return streamId; + } + + void onResponse(ResponseWriter responseWriter) { + this.responseWriter = responseWriter; + } + + void settingsReceived(Channel channel, Http2Settings http2Settings) { + settingsPromise.complete(true); + } + + void awaitSettings() { + try { + settingsPromise.get(5, TimeUnit.SECONDS); + logger.log(Level.INFO, "settings received"); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + settingsPromise.completeExceptionally(e); + } + } + + void responseReceived(Integer streamId, String message) { + if (streamId == null) { + logger.log(Level.WARNING, "unexpected message received: " + message); + return; + } + CompletableFuture promise = streamidPromiseMap.get(streamId); + if (promise == null) { + logger.log(Level.WARNING, "message received for unknown stream id " + streamId); + } else { + if (responseWriter != null) { + responseWriter.write(message); + } + promise.complete(true); + } + } + + void awaitResponse(Integer streamId) { + if (streamId == null) { + return; + } + CompletableFuture promise = streamidPromiseMap.get(streamId); + if (promise != null) { + try { + logger.log(Level.INFO, "waiting for response for stream id=" + streamId); + promise.get(5, TimeUnit.SECONDS); + logger.log(Level.INFO, "response for stream id=" + streamId + " received"); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.log(Level.WARNING, "streamId=" + streamId + " " + e.getMessage(), e); + } finally { + streamidPromiseMap.remove(streamId); + } + } + } + + void awaitResponses() { + logger.log(Level.INFO, "waiting for all stream ids " + streamidPromiseMap.keySet()); + for (int streamId : streamidPromiseMap.keySet()) { + awaitResponse(streamId); + } + } + + void fail(Throwable throwable) { + for (CompletableFuture promise : streamidPromiseMap.values()) { + promise.completeExceptionally(throwable); + } + } + + void close() { + if (channel != null) { + channel.close(); + } + } + } + + class Initializer extends ChannelInitializer { + + @Override + protected void initChannel(SocketChannel ch) { + + try { + SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .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(); + ch.pipeline().addLast(sslContext.newHandler(ch.alloc())); + ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler("") { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + logger.log(Level.INFO, "ALPN negotiated protocol = " + protocol); + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + DefaultHttp2Connection http2Connection = new DefaultHttp2Connection(false); + Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.INFO, "client"); + Http2ConnectionHandler http2ConnectionHandler = new HttpToHttp2ConnectionHandlerBuilder() + .connection(http2Connection) + .frameLogger(frameLogger) + .frameListener(new DelegatingDecompressorFrameListener(http2Connection, + new InboundHttp2ToHttpAdapterBuilder(http2Connection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build())) + .build(); + Http2SettingsHandler http2SettingsHandler = new Http2SettingsHandler(); + Http2ResponseHandler http2ResponseHandler = new Http2ResponseHandler(); + Http2ResponseMessages http2ResponseMessages = new Http2ResponseMessages(); + ctx.pipeline().addLast(http2ConnectionHandler, http2SettingsHandler, + http2ResponseHandler, http2ResponseMessages); + logger.log(Level.INFO, "HTTP/2 pipeline set up = " + ctx.channel().pipeline().names()); + return; + } + ctx.close(); + throw new IllegalStateException("unknown protocol: " + protocol); + } + }; + ch.pipeline().addLast(negotiationHandler); + } catch (SSLException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + } + + class Http2SettingsHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) { + logger.log(Level.INFO, "got settings = " + http2Settings); + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.settingsReceived(ctx.channel(), http2Settings); + ctx.pipeline().remove(this); + } + } + + class Http2ResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { + logger.log(Level.INFO, "got full http response = " + msg); + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + if (msg.content().isReadable()) { + transport.responseReceived(streamId, msg.content().toString(StandardCharsets.UTF_8)); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + logger.log(Level.INFO, "channel read complete"); + // do nothing + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + logger.log(Level.INFO, "channel inactive"); + ctx.fireChannelInactive(); + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(new IOException("channel closed")); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(cause); + ctx.channel().close(); + } + } + + + class Http2ResponseMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + logger.log(Level.FINEST, "received msg = " + msg.getClass().getName()); + if (msg instanceof DefaultHttp2SettingsFrame) { + DefaultHttp2SettingsFrame settingsFrame = (DefaultHttp2SettingsFrame) msg; + logger.log(Level.FINEST, "received settings "); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + logger.log(Level.FINEST, "received event = " + evt.getClass().getName()); + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + Http2ConnectionPrefaceAndSettingsFrameWrittenEvent event = + (Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) evt; + logger.log(Level.FINEST, "received preface and setting written event " + event); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.FINEST, "received exception " + cause); + } + } + +} diff --git a/net-http-client-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider b/net-http-client-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider new file mode 100644 index 0000000..824b301 --- /dev/null +++ b/net-http-client-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.boringssl.BoringSSLClientSecureSocketProvider \ No newline at end of file diff --git a/net-http-client-netty-secure/src/test/resources/logging.properties b/net-http-client-netty-secure/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-client-netty-secure/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-client-netty/build.gradle b/net-http-client-netty/build.gradle new file mode 100644 index 0000000..89890d2 --- /dev/null +++ b/net-http-client-netty/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':net-http-client') + api libs.netty.codec.http2 + api libs.netty.handler.proxy +} diff --git a/net-http-client-netty/src/main/java/module-info.java b/net-http-client-netty/src/main/java/module-info.java new file mode 100644 index 0000000..df0f1b2 --- /dev/null +++ b/net-http-client-netty/src/main/java/module-info.java @@ -0,0 +1,27 @@ +import org.xbib.net.http.client.netty.ClientTransportProvider; +import org.xbib.net.http.client.netty.http1.Http1ChannelInitializer; +import org.xbib.net.http.client.netty.http2.Http2ChannelInitializer; +import org.xbib.net.http.client.netty.HttpChannelInitializer; +import org.xbib.net.http.client.netty.NioClientTransportProvider; + +module org.xbib.net.http.client.netty { + exports org.xbib.net.http.client.netty; + exports org.xbib.net.http.client.netty.http1; + exports org.xbib.net.http.client.netty.http2; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.client; + requires io.netty.buffer; + requires io.netty.common; + requires io.netty.transport; + requires io.netty.handler; + requires io.netty.codec; + requires io.netty.codec.http; + requires io.netty.codec.http2; + requires io.netty.handler.proxy; + requires java.logging; + uses ClientTransportProvider; + provides ClientTransportProvider with NioClientTransportProvider; + uses HttpChannelInitializer; + provides HttpChannelInitializer with Http1ChannelInitializer, Http2ChannelInitializer; +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BaseInteraction.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BaseInteraction.java new file mode 100644 index 0000000..1fafd96 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BaseInteraction.java @@ -0,0 +1,429 @@ +package org.xbib.net.http.client.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.Http2Settings; +import java.io.IOException; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.xbib.net.PercentDecoder; +import org.xbib.net.URL; +import org.xbib.net.URLSyntaxException; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.client.BackOff; +import org.xbib.net.http.client.HttpResponse; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieBox; + +public abstract class BaseInteraction implements Interaction { + + private static final Logger logger = Logger.getLogger(BaseInteraction.class.getName()); + + protected final NettyHttpClient nettyHttpClient; + + protected final HttpAddress httpAddress; + + protected Throwable throwable; + + protected final Map streamIds; + + protected HttpRequest httpRequest; + + protected Channel channel; + + private CookieBox cookieBox; + + protected ChannelPromise settingsPromise; + + protected Http2Settings http2Settings; + + protected CompletableFuture future; + + public BaseInteraction(NettyHttpClient nettyHttpClient, HttpAddress httpAddress) { + this.nettyHttpClient = nettyHttpClient; + this.httpAddress = httpAddress; + this.streamIds = new ConcurrentHashMap<>(); + } + + @Override + public void setSettingsPromise(ChannelPromise settingsPromise) { + this.settingsPromise = settingsPromise; + } + + @Override + public HttpAddress getHttpAddress() { + return httpAddress; + } + + public void setFuture(CompletableFuture future) { + this.future = future; + } + + public CompletableFuture getFuture() { + return future; + } + + /** + * Method for executing the request and respond in a completable future. + * + * @param request request + * @param supplier supplier + * @param supplier result + * @return completable future + */ + @Override + public CompletableFuture execute(HttpRequest request, Function supplier) + throws IOException { + Objects.requireNonNull(request); + this.httpRequest = request; + Objects.requireNonNull(supplier); + final CompletableFuture completableFuture = new CompletableFuture<>(); + request.setResponseListener(response -> { + if (response != null) { + completableFuture.complete(supplier.apply(response)); + } else { + completableFuture.cancel(true); + } + get(); + cancel(); + }); + request.setTimeoutListener(req -> completableFuture.completeExceptionally(new TimeoutException())); + request.setExceptionListener(completableFuture::completeExceptionally); + execute(request); + return completableFuture; + } + + @Override + public void close() throws IOException { + logger.log(Level.FINE, "closing interaction " + this); + get(); + //cancel(); + releaseChannel(channel, true); + if (future != null) { + future.complete(null); + } + } + + @Override + public boolean isFailed() { + return throwable != null; + } + + @Override + public Throwable getFailure() { + return throwable; + } + + /** + * The underlying network layer failed. + * So we fail all (open) promises. + * @param throwable the exception + */ + @Override + public void fail(Channel channel, Throwable throwable) { + // do not fail more than once + if (this.throwable != null) { + return; + } + this.throwable = throwable; + logger.log(Level.SEVERE, "channel " + channel + " failing: " + throwable.getMessage(), throwable); + for (StreamIds streamIds : streamIds.values()) { + streamIds.fail(throwable); + } + if (future != null) { + future.completeExceptionally(throwable); + } + } + + @Override + public void inactive(Channel channel) { + // do nothing + } + + @Override + public Interaction get() { + return get(nettyHttpClient.getClientConfig().getSocketConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public Interaction get(long value, TimeUnit timeUnit) { + if (!streamIds.isEmpty()) { + for (Map.Entry entry : streamIds.entrySet()) { + StreamIds streamIds = entry.getValue(); + if (!streamIds.isClosed()) { + for (Integer key : streamIds.keys()) { + String requestKey = getRequestKey(entry.getKey(), key); + try { + CompletableFuture timeoutFuture = streamIds.get(key); + Boolean timeout = timeoutFuture.get(value, timeUnit); + if (timeout) { + completeRequest(requestKey); + } else { + completeRequestTimeout(requestKey, new TimeoutException()); + } + } catch (TimeoutException e) { + completeRequestTimeout(requestKey, new TimeoutException()); + } catch (Exception e) { + completeRequestExceptionally(requestKey, e); + streamIds.fail(e); + } finally { + streamIds.remove(key); + } + } + streamIds.close(); + } + } + } + nettyHttpClient.remove(this); + return this; + } + + @Override + public void cancel() { + if (!streamIds.isEmpty()) { + for (Map.Entry entry : streamIds.entrySet()) { + StreamIds streamIds = entry.getValue(); + for (Integer key : streamIds.keys()) { + try { + streamIds.get(key).cancel(true); + } catch (Exception e) { + completeRequestExceptionally(getRequestKey(entry.getKey(), key), e); + streamIds.fail(e); + } finally { + streamIds.remove(key); + } + } + streamIds.close(); + } + streamIds.clear(); + } + } + + protected abstract String getRequestKey(String channelId, Integer streamId); + + protected Channel acquireChannel(HttpRequest request) throws IOException { + Channel channel; + if (nettyHttpClient.hasPooledNodes()) { + channel = nextChannel(); + this.channel = channel; + } else { + channel = this.channel; + if (channel == null) { + channel = nextChannel(); + } + this.channel = channel; + } + return channel; + } + + protected Channel newChannel(HttpAddress httpAddress) throws IOException { + if (httpAddress != null) { + try { + return nettyHttpClient.getBootstrap() + .handler(nettyHttpClient.newChannelInitializer(httpAddress, this)) + .connect(httpAddress.getInetSocketAddress()).sync().await().channel(); + } catch (InterruptedException e) { + throw new IOException(e); + } + } else { + if (nettyHttpClient.hasPooledNodes()) { + try { + return nettyHttpClient.getPool().acquire(); + } catch (Exception e) { + throw new IOException(e); + } + } else { + throw new UnsupportedOperationException(); + } + } + } + + protected void releaseChannel(Channel channel, boolean close) throws IOException{ + if (channel == null) { + return; + } + if (nettyHttpClient.hasPooledNodes()) { + try { + nettyHttpClient.getPool().release(channel, close); + } catch (Exception e) { + throw new IOException(e); + } + } else if (close) { + channel.close(); + } + } + + protected abstract Channel nextChannel() throws IOException; + + protected HttpRequest continuation(HttpRequest request, HttpResponse httpResponse) throws URLSyntaxException { + if (httpResponse == null) { + return null; + } + if (request == null) { + // push promise or something else + return null; + } + try { + if (request.canRedirect()) { + int status = httpResponse.getStatus().code(); + switch (status) { + case 300: + case 301: + case 302: + case 303: + case 305: + case 307: + case 308: + String location = httpResponse.getHeaders().get(HttpHeaderNames.LOCATION); + location = new PercentDecoder(StandardCharsets.UTF_8.newDecoder()).decode(location); + if (location != null) { + logger.log(Level.FINE, "found redirect location: " + location); + URL redirUrl = URL.base(request.getURL()).resolve(location); + HttpMethod method = httpResponse.getStatus().code() == 303 ? HttpMethod.GET : request.getMethod(); + HttpRequestBuilder newHttpRequestHttpRequestBuilder = HttpRequest.builder(method, request) + .setURL(redirUrl); + request.getURL().getQueryParams().forEach(pair -> + newHttpRequestHttpRequestBuilder.addParameter(pair.getKey(), pair.getValue()) + ); + request.cookies().forEach(newHttpRequestHttpRequestBuilder::addCookie); + HttpRequest newHttpRequest = newHttpRequestHttpRequestBuilder.build(); + StringBuilder hostAndPort = new StringBuilder(); + hostAndPort.append(redirUrl.getHost()); + if (redirUrl.getPort() != null) { + hostAndPort.append(':').append(redirUrl.getPort()); + } + newHttpRequest.getHeaders().set(HttpHeaderNames.HOST, hostAndPort.toString()); + logger.log(Level.FINE, "redirect url: " + redirUrl); + return newHttpRequest; + } + break; + default: + break; + } + } + } catch (MalformedInputException | UnmappableCharacterException e) { + this.throwable = e; + } + return null; + } + + protected HttpRequest retry(HttpRequest request, HttpResponse httpResponse) { + if (httpResponse == null) { + // no response present, invalid in any way + return null; + } + if (request == null) { + // push promise or something else + return null; + } + if (request.isBackOff()) { + BackOff backOff = request.getBackOff() != null ? + request.getBackOff() : + nettyHttpClient.getClientConfig().getBackOff(); + int status = httpResponse.getStatus ().code(); + switch (status) { + case 403: + case 404: + case 500: + case 502: + case 503: + case 504: + case 507: + case 509: + if (backOff != null) { + long millis = backOff.nextBackOffMillis(); + if (millis != BackOff.STOP) { + logger.log(Level.FINE, () -> "status = " + status + " backing off request by " + millis + " milliseconds"); + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // ignore + } + return request; + } + } + break; + default: + break; + } + } + return null; + } + + private void completeRequest(String requestKey) { + if (requestKey != null) { + if (httpRequest != null && httpRequest.getCompletableFuture() != null) { + httpRequest.getCompletableFuture().complete(httpRequest); + } + } + } + + private void completeRequestExceptionally(String requestKey, Throwable throwable) { + if (requestKey != null) { + httpRequest.onException(throwable); + } + } + + private void completeRequestTimeout(String requestKey, TimeoutException timeoutException) { + if (requestKey != null) { + httpRequest.onTimeout(); + } + } + + @Override + public void setCookieBox(CookieBox cookieBox) { + this.cookieBox = cookieBox; + } + + @Override + public CookieBox getCookieBox() { + return cookieBox; + } + + protected void addCookie(Cookie cookie) { + if (cookieBox == null) { + this.cookieBox = new CookieBox(); + } + cookieBox.add(cookie); + } + + protected List matchCookiesFromBox(HttpRequest request) { + return cookieBox == null ? Collections.emptyList() : cookieBox.stream().filter(cookie -> + matchCookie(request.getURL(), cookie)).collect(Collectors.toList()); + } + + protected List matchCookies(HttpRequest request) { + return request.cookies().stream().filter(cookie -> + matchCookie(request.getURL(), cookie)).collect(Collectors.toList()); + } + + private boolean matchCookie(URL url, Cookie cookie) { + boolean domainMatch = cookie.domain() == null || url.getHost().endsWith(cookie.domain()); + if (!domainMatch) { + return false; + } + if (cookie.path() != null) { + boolean pathMatch = "/".equals(cookie.path()) || url.getPath().startsWith(cookie.path()); + if (!pathMatch) { + return false; + } + } + boolean secureScheme = "https".equals(url.getScheme()); + return (secureScheme && cookie.isSecure()) || (!secureScheme && !cookie.isSecure()); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BoundedChannelPool.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BoundedChannelPool.java new file mode 100644 index 0000000..b7f381a --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/BoundedChannelPool.java @@ -0,0 +1,395 @@ +package org.xbib.net.http.client.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.pool.ChannelPoolHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; +import java.io.IOException; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; + +public class BoundedChannelPool implements Pool { + + private static final Logger logger = Logger.getLogger(BoundedChannelPool.class.getName()); + + private final Semaphore semaphore; + + private final HttpVersion httpVersion; + + private ChannelPoolHandler channelPoolhandler; + + private final List nodes; + + private final int numberOfNodes; + + private final int retriesPerNode; + + private final Map bootstraps; + + private final Map> channels; + + private final Map> availableChannels; + + private final Map counts; + + private final Map failedCounts; + + private final Lock lock; + + private PoolKeySelector poolKeySelector; + + /** + * A bounded channel pool. + * + * @param semaphore the level of concurrency + * @param httpVersion the HTTP version of the pool connections + * @param nodes the endpoint nodes, any element may contain the port (followed after ":") + * to override the defaultPort argument + * @param retriesPerNode the max count of the subsequent connection failures to the node before + * the node will be excluded from the pool. If set to 0, the value is ignored. + * @param poolKeySelectorType pool key selector type + */ + public BoundedChannelPool(Semaphore semaphore, + HttpVersion httpVersion, + List nodes, + int retriesPerNode, + PoolKeySelectorType poolKeySelectorType) { + this.semaphore = semaphore; + this.httpVersion = httpVersion; + this.nodes = nodes; + this.retriesPerNode = retriesPerNode; + switch (poolKeySelectorType) { + case RANDOM: + this.poolKeySelector = new RandomPoolKeySelector(); + break; + case ROUNDROBIN: + this.poolKeySelector = new RoundRobinKeySelector(); + break; + } + this.lock = new ReentrantLock(); + if (nodes == null || nodes.isEmpty()) { + throw new IllegalArgumentException("nodes must not be empty"); + } + this.numberOfNodes = nodes.size(); + bootstraps = new HashMap<>(numberOfNodes); + channels = new ConcurrentHashMap<>(numberOfNodes); + availableChannels = new ConcurrentHashMap<>(numberOfNodes); + counts = new ConcurrentHashMap<>(numberOfNodes); + failedCounts = new ConcurrentHashMap<>(numberOfNodes); + } + + /** + * Initialize pool. + * + * @param bootstrap bootstrap instance + * @param channelPoolHandler channel pool handler being notified upon new connection is created + */ + public void init(Bootstrap bootstrap, ChannelPoolHandler channelPoolHandler, int channelCount) throws IOException { + this.channelPoolhandler = channelPoolHandler; + for (HttpAddress node : nodes) { + HttpChannelPoolInitializer initializer = new HttpChannelPoolInitializer(node, channelPoolHandler); + bootstraps.put(node, bootstrap.clone().remoteAddress(node.getInetSocketAddress()) + .handler(initializer)); + availableChannels.put(node, new ConcurrentLinkedQueue<>()); + counts.put(node, 0); + failedCounts.put(node, 0); + } + if (channelCount <= 0) { + throw new IllegalArgumentException("channel count must be greater zero, but got " + channelCount); + } + for (int i = 0; i < channelCount; i++) { + Channel channel = newConnection(); + if (channel == null) { + throw new ConnectException("failed to prepare channels"); + } + HttpAddress key = channel.attr(POOL_ATTRIBUTE_KEY).get(); + if (channel.isActive()) { + Queue channelQueue = availableChannels.get(key); + if (channelQueue != null) { + channelQueue.add(channel); + } + } else { + channel.close(); + } + } + logger.log(Level.FINE,"pool: prepared " + channelCount + " channels: " + availableChannels); + } + + @Override + public HttpVersion getVersion() { + return httpVersion; + } + + @Override + public Channel acquire() throws Exception { + Channel channel = null; + if (semaphore.tryAcquire()) { + if ((channel = poll()) == null) { + channel = newConnection(); + } + if (channel == null) { + semaphore.release(); + throw new ConnectException(); + } else { + if (channelPoolhandler != null) { + channelPoolhandler.channelAcquired(channel); + } + } + } + return channel; + } + + @Override + public void release(Channel channel, boolean close) throws Exception { + try { + if (channel != null) { + if (channel.isActive()) { + HttpAddress key = channel.attr(POOL_ATTRIBUTE_KEY).get(); + if (key != null) { + Queue channelQueue = availableChannels.get(key); + if (channelQueue != null) { + channelQueue.add(channel); + } + } + } else if (channel.isOpen() && close) { + logger.log(Level.FINE, "closing channel " + channel); + channel.close(); + } + if (channelPoolhandler != null) { + channelPoolhandler.channelReleased(channel); + } + } + } finally { + semaphore.release(); + } + } + + @Override + public void close() throws IOException { + lock.lock(); + try { + logger.log(Level.FINE, "closing pool"); + int count = 0; + Set channelSet = new HashSet<>(); + for (Map.Entry> entry : availableChannels.entrySet()) { + channelSet.addAll(entry.getValue()); + } + for (Map.Entry> entry : channels.entrySet()) { + channelSet.addAll(entry.getValue()); + } + for (Channel channel : channelSet) { + if (channel != null && channel.isOpen()) { + logger.log(Level.FINE, "trying to abort channel " + channel); + if (httpVersion.majorVersion() == 2) { + // be polite, send a go away frame + DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(0); + ChannelPromise channelPromise = channel.newPromise(); + channel.writeAndFlush(goAwayFrame, channelPromise); + try { + channelPromise.get(); + logger.log(Level.FINE, "goaway frame sent to " + channel); + } catch (ExecutionException e) { + logger.log(Level.FINE, e.getMessage(), e); + } catch (InterruptedException e) { + throw new IOException(e); + } + } + channel.close(); + count++; + } + } + availableChannels.clear(); + channels.clear(); + bootstraps.clear(); + counts.clear(); + logger.log(Level.FINE, "closed pool (found " + count + " connections open)"); + } finally { + lock.unlock(); + } + } + + private Channel newConnection() throws ConnectException { + Channel channel = null; + HttpAddress key = null; + int min = Integer.MAX_VALUE; + Integer next; + for (int j = 0; j < numberOfNodes; j++) { + HttpAddress nextKey = poolKeySelector.key(); + next = counts.get(nextKey); + if (next == null || next == 0) { + key = nextKey; + break; + } else if (next < min) { + min = next; + key = nextKey; + } + } + if (key != null) { + logger.log(Level.FINE, "trying connection to " + key); + try { + channel = connect(key); + } catch (Exception e) { + logger.log(Level.WARNING, "failed to create a new connection to " + key + ": " + e.toString()); + if (retriesPerNode > 0) { + int selectedNodeFailedConnAttemptsCount = failedCounts.get(key) + 1; + failedCounts.put(key, selectedNodeFailedConnAttemptsCount); + if (selectedNodeFailedConnAttemptsCount > retriesPerNode) { + logger.log(Level.WARNING, "failed to connect to the node " + key + " " + + selectedNodeFailedConnAttemptsCount + " times, " + + "excluding the node from the connection pool"); + counts.put(key, Integer.MAX_VALUE); + boolean allNodesExcluded = true; + for (HttpAddress node : nodes) { + if (counts.get(node) < Integer.MAX_VALUE) { + allNodesExcluded = false; + break; + } + } + if (allNodesExcluded) { + logger.log(Level.SEVERE, "no nodes left in the connection pool"); + } + } + } + if (e instanceof ConnectException) { + throw (ConnectException) e; + } else { + throw new ConnectException(e.getMessage()); + } + } + } + if (channel != null) { + channel.closeFuture().addListener(new CloseChannelListener(key, channel)); + channel.attr(POOL_ATTRIBUTE_KEY).set(key); + channels.computeIfAbsent(key, node -> new ArrayList<>()).add(channel); + counts.put(key, counts.get(key) + 1); + if (retriesPerNode > 0) { + failedCounts.put(key, 0); + } + } + return channel; + } + + private Channel connect(HttpAddress key) throws Exception { + Bootstrap bootstrap = bootstraps.get(key); + if (bootstrap != null) { + return bootstrap.connect().sync().channel(); + } + return null; + } + + private Channel poll() { + Queue channelQueue; + Channel channel; + for (int j = 0; j < numberOfNodes; j++) { + HttpAddress key = poolKeySelector.key(); + channelQueue = availableChannels.get(key); + if (channelQueue != null) { + channel = channelQueue.poll(); + if (channel != null && channel.isActive()) { + return channel; + } + } else { + logger.log(Level.WARNING, "what happened? channel queue is null?"); + } + } + return null; + } + + private interface PoolKeySelector { + HttpAddress key(); + } + + private class RandomPoolKeySelector implements PoolKeySelector { + + @Override + public HttpAddress key() { + int r = ThreadLocalRandom.current().nextInt(numberOfNodes); + return nodes.get(r % numberOfNodes); + } + } + + private class RoundRobinKeySelector implements PoolKeySelector { + + int r = 0; + + @Override + public HttpAddress key() { + return nodes.get(r++ % numberOfNodes); + } + } + + private class CloseChannelListener implements ChannelFutureListener { + + private final HttpAddress key; + + private final Channel channel; + + private CloseChannelListener(HttpAddress key, Channel channel) { + this.key = key; + this.channel = channel; + } + + @Override + public void operationComplete(ChannelFuture future) { + logger.log(Level.FINE,"connection to " + key + " closed"); + lock.lock(); + try { + if (counts.containsKey(key)) { + counts.put(key, counts.get(key) - 1); + } + List channels = BoundedChannelPool.this.channels.get(key); + if (channels != null) { + channels.remove(channel); + } + semaphore.release(); + } finally { + lock.unlock(); + } + } + } + + static class HttpChannelPoolInitializer extends ChannelInitializer { + + private final HttpAddress key; + + private final ChannelPoolHandler channelPoolHandler; + + HttpChannelPoolInitializer(HttpAddress key, ChannelPoolHandler channelPoolHandler) { + this.key = key; + this.channelPoolHandler = channelPoolHandler; + } + + @Override + protected void initChannel(SocketChannel channel) throws Exception { + if (!channel.eventLoop().inEventLoop()) { + throw new IllegalStateException(); + } + channel.attr(Pool.POOL_ATTRIBUTE_KEY).set(key); + if (channelPoolHandler != null) { + channelPoolHandler.channelCreated(channel); + } + } + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/ClientTransportProvider.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/ClientTransportProvider.java new file mode 100644 index 0000000..29506be --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/ClientTransportProvider.java @@ -0,0 +1,13 @@ +package org.xbib.net.http.client.netty; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import java.util.concurrent.ThreadFactory; + +public interface ClientTransportProvider { + + EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory); + + Class createSocketChannelClass(); + +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelInitializer.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelInitializer.java new file mode 100644 index 0000000..7b00293 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelInitializer.java @@ -0,0 +1,20 @@ +package org.xbib.net.http.client.netty; + +import io.netty.channel.Channel; +import java.io.Closeable; +import java.io.IOException; +import org.xbib.net.http.HttpAddress; + +public interface HttpChannelInitializer { + + boolean supports(HttpAddress httpAddress); + + Interaction newInteraction(NettyHttpClient client, HttpAddress httpAdress); + + void init(Channel channel, + HttpAddress httpAddress, + NettyHttpClient client, + NettyCustomizer nettyCustomizer, + Interaction interaction) throws IOException; + +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelPoolInitializer.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelPoolInitializer.java new file mode 100644 index 0000000..2ec6310 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChannelPoolInitializer.java @@ -0,0 +1,6 @@ +package org.xbib.net.http.client.netty; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.pool.ChannelPoolHandler; +import io.netty.channel.socket.SocketChannel; +import org.xbib.net.http.HttpAddress; diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChunkContentCompressor.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChunkContentCompressor.java new file mode 100644 index 0000000..71a0fc1 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpChunkContentCompressor.java @@ -0,0 +1,27 @@ +package org.xbib.net.http.client.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.HttpContentCompressor; + +/** + * Be sure you place the HttpChunkContentCompressor before the ChunkedWriteHandler. + */ +public class HttpChunkContentCompressor extends HttpContentCompressor { + + public HttpChunkContentCompressor() { + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf) { + ByteBuf byteBuf = (ByteBuf) msg; + if (byteBuf.isReadable()) { + msg = new DefaultHttpContent(byteBuf); + } + } + super.write(ctx, msg, promise); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequest.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequest.java new file mode 100644 index 0000000..3b79cda --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequest.java @@ -0,0 +1,267 @@ +package org.xbib.net.http.client.netty; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.Request; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.BackOff; +import org.xbib.net.http.client.ExceptionListener; +import org.xbib.net.http.client.HttpResponse; +import org.xbib.net.http.client.ResponseListener; +import org.xbib.net.http.client.TimeoutListener; +import org.xbib.net.http.cookie.Cookie; + +/** + * HTTP client request. + */ +public class HttpRequest implements org.xbib.net.http.client.HttpRequest, Closeable { + + private final HttpRequestBuilder builder; + + private final HttpHeaders headers; + + private CompletableFuture completableFuture; + + private int redirectCount; + + protected HttpRequest(HttpRequestBuilder builder, HttpHeaders headers) { + this.builder = builder; + this.headers = headers; + } + + @Override + public URL getURL() { + return builder.url; + } + + @Override + public HttpVersion getVersion() { + return builder.httpVersion; + } + + @Override + public HttpMethod getMethod() { + return builder.httpMethod; + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public ParameterBuilder getParameters() { + return builder.parameterBuilder; + } + + public Collection cookies() { + return builder.cookies; + } + + @Override + public InetSocketAddress getLocalAddress() { + return null; // unused + } + + @Override + public InetSocketAddress getRemoteAddress() { + return null; // unused + } + + @Override + public URL getBaseURL() { + return builder.url; + } + + public ByteBuffer getBody() { + return builder.body; + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return charset.decode(getBody()); + } + + public CharBuffer getBodyAsChars(Charset charset, int offset, int size) { + ByteBuffer slicedBuffer = (getBody().duplicate().position(offset)).slice(); + slicedBuffer.limit(size); + return charset.decode(slicedBuffer); + } + + @SuppressWarnings("unchecked") + @Override + public R as(Class cl) { + return (R) this; + } + + public List getBodyData() { + return builder.bodyData; + } + + public boolean isFollowRedirect() { + return builder.followRedirect; + } + + public boolean isBackOff() { + return builder.backOff != null; + } + + public BackOff getBackOff() { + return builder.backOff; + } + + public boolean canRedirect() { + if (!builder.followRedirect) { + return false; + } + if (redirectCount >= builder.maxRedirects) { + return false; + } + redirectCount++; + return true; + } + + public void release() { + // nothing to do + } + + @Override + public void close() throws IOException { + release(); + } + + @Override + public String toString() { + return "HttpNettyRequest[url=" + builder.url + + ",version=" + builder.httpVersion + + ",method=" + builder.httpMethod + + ",headers=" + headers.entries() + + ",content=" + (builder.body != null && builder.body.remaining() >= 16 ? + getBodyAsChars(StandardCharsets.UTF_8, 0, 16) + "..." : + builder.body != null ? getBodyAsChars(StandardCharsets.UTF_8) : "") + + "]"; + } + + public HttpRequest setCompletableFuture(CompletableFuture completableFuture) { + this.completableFuture = completableFuture; + return this; + } + + public CompletableFuture getCompletableFuture() { + return completableFuture; + } + + public void setResponseListener(ResponseListener responseListener) { + builder.responseListener = responseListener; + } + + public void onResponse(HttpResponse httpResponse) { + if (builder.responseListener != null) { + builder.responseListener.onResponse(httpResponse); + } + if (completableFuture != null) { + completableFuture.complete(this); + } + } + + public void setExceptionListener(ExceptionListener exceptionListener) { + builder.exceptionListener = exceptionListener; + } + + public void onException(Throwable throwable) { + if (builder.exceptionListener != null) { + builder.exceptionListener.onException(throwable); + } + if (completableFuture != null) { + completableFuture.completeExceptionally(throwable); + } + } + + public void setTimeoutListener(TimeoutListener timeoutListener) { + builder.timeoutListener = timeoutListener; + } + + public void onTimeout() { + if (builder.timeoutListener != null) { + builder.timeoutListener.onTimeout(this); + } + if (completableFuture != null) { + if (builder.timeoutMillis > 0L) { + completableFuture.completeOnTimeout(this, builder.timeoutMillis, TimeUnit.MILLISECONDS); + } else { + completableFuture.completeOnTimeout(this, 15L, TimeUnit.SECONDS); + } + } + } + + public static HttpRequestBuilder get() { + return builder(HttpMethod.GET); + } + + public static HttpRequestBuilder put() { + return builder(HttpMethod.PUT); + } + + public static HttpRequestBuilder post() { + return builder(HttpMethod.POST); + } + + public static HttpRequestBuilder delete() { + return builder(HttpMethod.DELETE); + } + + public static HttpRequestBuilder head() { + return builder(HttpMethod.HEAD); + } + + public static HttpRequestBuilder patch() { + return builder(HttpMethod.PATCH); + } + + public static HttpRequestBuilder trace() { + return builder(HttpMethod.TRACE); + } + + public static HttpRequestBuilder options() { + return builder(HttpMethod.OPTIONS); + } + + public static HttpRequestBuilder connect() { + return builder(HttpMethod.CONNECT); + } + + public static HttpRequestBuilder builder(HttpMethod httpMethod) { + return builder(PooledByteBufAllocator.DEFAULT, httpMethod); + } + + public static HttpRequestBuilder builder(HttpMethod httpMethod, HttpRequest httpRequest) { + return builder(PooledByteBufAllocator.DEFAULT, httpMethod) + .setVersion(httpRequest.builder.httpVersion) + .setURL(httpRequest.builder.url) + .setHeaders(httpRequest.headers) + .content(httpRequest.builder.body) + .setResponseListener(httpRequest.builder.responseListener) + .setTimeoutListener(httpRequest.builder.timeoutListener, httpRequest.builder.timeoutMillis) + .setExceptionListener(httpRequest.builder.exceptionListener); + } + + public static HttpRequestBuilder builder(ByteBufAllocator allocator, HttpMethod httpMethod) { + return new HttpRequestBuilder(allocator).setMethod(httpMethod); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequestBuilder.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequestBuilder.java new file mode 100644 index 0000000..6c07e53 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpRequestBuilder.java @@ -0,0 +1,438 @@ +package org.xbib.net.http.client.netty; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import io.netty.handler.codec.http2.HttpConversionUtil; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.URL; +import org.xbib.net.URLBuilder; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.BackOff; +import org.xbib.net.http.client.ExceptionListener; +import org.xbib.net.http.client.HttpResponse; +import org.xbib.net.http.client.ResponseListener; +import org.xbib.net.http.client.TimeoutListener; +import org.xbib.net.http.cookie.Cookie; + +public class HttpRequestBuilder implements org.xbib.net.http.client.HttpRequestBuilder { + + private static final URL DEFAULT_URL = URL.from("http://localhost"); + + private static final String DEFAULT_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8"; + + final ByteBufAllocator allocator; + + HttpAddress httpAddress; + + URL url; + + String requestPath; + + final Collection cookies; + + HttpMethod httpMethod; + + HttpHeaders headers; + + HttpVersion httpVersion; + + final List removeHeaders; + + String userAgent; + + boolean keepalive; + + boolean gzip; + + String contentType; + + ParameterBuilder parameterBuilder; + + ByteBuffer body; + + final List bodyData; + + boolean followRedirect; + + int maxRedirects; + + boolean enableBackOff; + + BackOff backOff; + + ResponseListener responseListener; + + ExceptionListener exceptionListener; + + TimeoutListener timeoutListener; + + long timeoutMillis; + + protected HttpRequestBuilder() { + this(ByteBufAllocator.DEFAULT); + } + + protected HttpRequestBuilder(ByteBufAllocator allocator) { + this.allocator = allocator; + this.httpMethod = HttpMethod.GET; + this.httpVersion = HttpVersion.HTTP_1_1; + this.userAgent = UserAgent.getUserAgent(); + this.gzip = false; + this.keepalive = true; + this.url = DEFAULT_URL; + this.followRedirect = true; + this.maxRedirects = 10; + this.headers = new HttpHeaders(); + this.removeHeaders = new ArrayList<>(); + this.cookies = new HashSet<>(); + this.bodyData = new ArrayList<>(); + this.contentType = DEFAULT_FORM_CONTENT_TYPE; + this.parameterBuilder = Parameter.builder(); + this.timeoutMillis = 0L; + } + + @Override + public HttpRequestBuilder setAddress(HttpAddress httpAddress) { + this.httpAddress = httpAddress; + try { + this.url = URL.builder() + .scheme(httpAddress.isSecure() ? "https" : "http") + .host(httpAddress.getInetSocketAddress().getHostString()) + .port(httpAddress.getInetSocketAddress().getPort()) + .build(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + this.httpVersion = httpAddress.getVersion(); + return this; + } + + public HttpRequestBuilder setURL(String url) { + return setURL(URL.from(url)); + } + + @Override + public HttpRequestBuilder setURL(URL url) { + this.url = url; + return this; + } + + @Override + public HttpRequestBuilder setRequestPath(String requestPath) { + this.requestPath = requestPath; + return this; + } + + public HttpRequestBuilder setMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + public HttpRequestBuilder setVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public HttpRequestBuilder setVersion(String httpVersion) { + this.httpVersion = HttpVersion.valueOf(httpVersion); + return this; + } + + public HttpRequestBuilder setHeaders(Map headers) { + headers.forEach(this::addHeader); + return this; + } + + public HttpRequestBuilder setHeaders(HttpHeaders headers) { + this.headers = headers; + return this; + } + + public HttpRequestBuilder addHeader(String name, String value) { + this.headers.add(name, value); + return this; + } + + public HttpRequestBuilder setHeader(String name, String value) { + this.headers.set(name, value); + return this; + } + + public HttpRequestBuilder removeHeader(String name) { + removeHeaders.add(name); + return this; + } + + public HttpRequestBuilder contentType(String contentType) { + Objects.requireNonNull(contentType); + this.contentType = contentType; + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + return this; + } + + public HttpRequestBuilder contentType(String contentType, Charset charset) { + Objects.requireNonNull(contentType); + Objects.requireNonNull(charset); + this.contentType = contentType; + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=" + charset.name().toLowerCase()); + return this; + } + + @Override + public HttpRequestBuilder setParameterBuilder(ParameterBuilder parameterBuilder) { + this.parameterBuilder = parameterBuilder; + return this; + } + + public HttpRequestBuilder setParameters(Map parameters) { + parameters.forEach(this::addParameter); + return this; + } + + @SuppressWarnings("unchecked") + public HttpRequestBuilder addParameter(String name, Object value) { + Objects.requireNonNull(name); + Objects.requireNonNull(value); + Collection collection; + if (!(value instanceof Collection)) { + collection = Collections.singletonList(value); + } else { + collection = (Collection) value; + } + collection.forEach(v -> parameterBuilder.add(name, v)); + return this; + } + + public HttpRequestBuilder addRawParameter(String name, String value) { + Objects.requireNonNull(name); + Objects.requireNonNull(value); + parameterBuilder.add(name, value); + return this; + } + + public HttpRequestBuilder addBasicAuthorization(String name, String password) { + String encoding = Base64.getEncoder().encodeToString((name + ":" + password).getBytes(StandardCharsets.UTF_8)); + this.headers.add(HttpHeaderNames.AUTHORIZATION, "Basic " + encoding); + return this; + } + + @Override + public HttpRequestBuilder setBody(ByteBuffer byteBuffer) { + this.body = byteBuffer; + return this; + } + + /** + * For multipart MIME body data. + * + * @param data a mime body + * @return this + */ + public HttpRequestBuilder addBodyData(InterfaceHttpData data) { + bodyData.add(data); + return this; + } + + public HttpRequestBuilder addCookie(Cookie cookie) { + cookies.add(cookie); + return this; + } + + public HttpRequestBuilder acceptGzip(boolean gzip) { + this.gzip = gzip; + return this; + } + + public HttpRequestBuilder keepAlive(boolean keepalive) { + this.keepalive = keepalive; + return this; + } + + public HttpRequestBuilder setFollowRedirect(boolean followRedirect) { + this.followRedirect = followRedirect; + return this; + } + + public HttpRequestBuilder setMaxRedirects(int maxRedirects) { + this.maxRedirects = maxRedirects; + return this; + } + + public HttpRequestBuilder enableBackOff(boolean enableBackOff) { + this.enableBackOff = enableBackOff; + return this; + } + + public HttpRequestBuilder setBackOff(BackOff backOff) { + this.backOff = backOff; + return this; + } + + public HttpRequestBuilder setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public HttpRequestBuilder text(String text) { + if (text == null) { + return this; + } + ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(text); + content(byteBuf, HttpHeaderValues.TEXT_PLAIN); + return this; + } + + public HttpRequestBuilder json(String json) { + if (json == null) { + return this; + } + ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(json); + content(byteBuf, HttpHeaderValues.APPLICATION_JSON); + return this; + } + + public HttpRequestBuilder xml(String xml) { + if (xml == null) { + return this; + } + ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(xml); + content(byteBuf, "application/xml"); + return this; + } + + public HttpRequestBuilder content(CharSequence charSequence, CharSequence contentType) { + if (charSequence == null) { + return this; + } + content(charSequence.toString().getBytes(HttpUtil.getCharset(contentType, StandardCharsets.UTF_8)), contentType.toString()); + return this; + } + + public HttpRequestBuilder content(CharSequence charSequence, CharSequence contentType, Charset charset) { + if (charSequence == null) { + return this; + } + content(charSequence.toString().getBytes(charset), contentType.toString()); + return this; + } + + public HttpRequestBuilder content(byte[] buf, String contentType) { + if (buf == null) { + return this; + } + content(ByteBuffer.wrap(buf), contentType); + return this; + } + + public HttpRequestBuilder content(ByteBuffer content, String contentType) { + if (content == null) { + return this; + } + setBody(content); + addHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(content.remaining())); + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + return this; + } + + public HttpRequestBuilder content(ByteBuffer content) { + if (content == null) { + return this; + } + this.body = content; + return this; + } + + public HttpRequestBuilder setResponseListener(ResponseListener responseListener) { + this.responseListener = responseListener; + return this; + } + + public HttpRequestBuilder setExceptionListener(ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + return this; + } + + public HttpRequestBuilder setTimeoutListener(TimeoutListener timeoutListener, long timeoutMillis) { + this.timeoutListener = timeoutListener; + this.timeoutMillis = timeoutMillis; + return this; + } + + public HttpRequest build() { + return new HttpRequest(this, validateHeaders()); + } + + protected HttpHeaders validateHeaders() { + Parameter parameter = parameterBuilder.build(); + HttpHeaders validatedHeaders = HttpHeaders.of(headers); + if (url != null) { + // add our URI parameters to the URL + URLBuilder urlBuilder = url.mutator(); + if (requestPath != null) { + urlBuilder.path(requestPath); + } + parameter.forEach(e -> urlBuilder.queryParam(e.getKey(), e.getValue())); + url = urlBuilder.build(); + String scheme = url.getScheme(); + if (httpVersion.majorVersion() == 2) { + validatedHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme); + } + validatedHeaders.set(HttpHeaderNames.HOST, url.getHostInfo()); + } + validatedHeaders.set(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); + if (userAgent != null) { + validatedHeaders.set(HttpHeaderNames.USER_AGENT, userAgent); + } + if (gzip) { + validatedHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, "gzip"); + } + if (httpMethod.name().equals(HttpMethod.POST.name())) { + content(parameter.getAsQueryString(), contentType); + } + int length = body != null ? body.remaining() : 0; + if (!validatedHeaders.containsHeader(HttpHeaderNames.CONTENT_LENGTH) && !validatedHeaders.containsHeader(HttpHeaderNames.TRANSFER_ENCODING)) { + if (length < 0) { + validatedHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, "chunked"); + } else { + validatedHeaders.set(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length)); + } + } + if (!validatedHeaders.containsHeader(HttpHeaderNames.ACCEPT)) { + validatedHeaders.set(HttpHeaderNames.ACCEPT, "*/*"); + } + // RFC 2616 Section 14.10 + // "An HTTP/1.1 client that does not support persistent connections MUST include the "close" connection + // option in every request message." + if (httpVersion.majorVersion() == 1 && !keepalive) { + validatedHeaders.set(HttpHeaderNames.CONNECTION, "close"); + } + // at last, forced removal of unwanted headers + for (String headerName : removeHeaders) { + validatedHeaders.remove(headerName); + } + return validatedHeaders; + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponse.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponse.java new file mode 100644 index 0000000..4fd0b59 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponse.java @@ -0,0 +1,81 @@ +package org.xbib.net.http.client.netty; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.CookieBox; +import org.xbib.net.util.ByteBufferInputStream; + +public class HttpResponse implements org.xbib.net.http.client.HttpResponse, Closeable { + + private final HttpResponseBuilder builder; + + protected HttpResponse(HttpResponseBuilder builder) { + this.builder = builder; + } + + public static HttpResponseBuilder builder() { + return new HttpResponseBuilder(); + } + + public SocketAddress getLocalAddress() { + return builder.localAddress; + } + + public SocketAddress getRemoteAddress() { + return builder.remoteAddress; + } + + @Override + public HttpAddress getAddress() { + return builder.httpAddress; + } + + @Override + public HttpResponseStatus getStatus() { + return builder.httpStatus; + } + + @Override + public HttpHeaders getHeaders() { + return builder.httpHeaders; + } + + @Override + public CookieBox getCookies() { + return builder.cookieBox; + } + + @Override + public ByteBuffer getBody() { + return builder.byteBuffer; + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return charset.decode(builder.byteBuffer); + } + + @Override + public InputStream getBodyAsStream() { + return new ByteBufferInputStream(builder.byteBuffer); + } + + @Override + public void release() { + // nothing to do + } + + @Override + public void close() throws IOException { + release(); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponseBuilder.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponseBuilder.java new file mode 100644 index 0000000..cf34236 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/HttpResponseBuilder.java @@ -0,0 +1,73 @@ +package org.xbib.net.http.client.netty; + +import java.io.InputStream; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.CookieBox; + +public class HttpResponseBuilder { + + SocketAddress localAddress; + + SocketAddress remoteAddress; + + HttpAddress httpAddress; + + HttpResponseStatus httpStatus; + + HttpHeaders httpHeaders; + + CookieBox cookieBox; + + ByteBuffer byteBuffer; + + CharBuffer charBuffer; + + InputStream inputStream; + + protected HttpResponseBuilder() { + } + + public HttpResponseBuilder setLocalAddress(SocketAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + public HttpResponseBuilder setRemoteAddress(SocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + + public HttpResponseBuilder setHttpAddress(HttpAddress httpAddress) { + this.httpAddress = httpAddress; + return this; + } + + public HttpResponseBuilder setStatus(HttpResponseStatus httpResponseStatus) { + this.httpStatus = httpResponseStatus; + return this; + } + + public HttpResponseBuilder setCookieBox(CookieBox cookieBox) { + this.cookieBox = cookieBox; + return this; + } + + public HttpResponseBuilder setHeaders(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + + public HttpResponseBuilder setByteBuffer(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + return this; + } + + public HttpResponse build() { + return new HttpResponse(this); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Interaction.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Interaction.java new file mode 100644 index 0000000..200e88b --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Interaction.java @@ -0,0 +1,61 @@ +package org.xbib.net.http.client.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.client.HttpResponse; +import org.xbib.net.http.cookie.CookieBox; + +public interface Interaction extends Closeable { + + HttpAddress getHttpAddress(); + + Interaction execute(HttpRequest httpRequest) throws IOException; + + CompletableFuture execute(HttpRequest httpRequest, Function supplier) throws IOException; + + void settingsPrefaceWritten() throws IOException; + + void setSettingsPromise(ChannelPromise channelPromise); + + void waitForSettings(long value, TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException; + + void settingsReceived(Http2Settings http2Settings) throws IOException; + + void responseReceived(Channel channel, Integer streamId, FullHttpResponse fullHttpResponse) throws IOException; + + void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers); + + void fail(Channel channel, Throwable throwable); + + void inactive(Channel channel); + + void setCookieBox(CookieBox cookieBox); + + CookieBox getCookieBox(); + + void setFuture(CompletableFuture future); + + CompletableFuture getFuture(); + + Interaction get(); + + Interaction get(long value, TimeUnit timeUnit); + + void cancel(); + + boolean isFailed(); + + Throwable getFailure(); + +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyCustomizer.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyCustomizer.java new file mode 100644 index 0000000..40c2300 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyCustomizer.java @@ -0,0 +1,46 @@ +package org.xbib.net.http.client.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; + +/** + * Strategy interface to customize netty {@link Bootstrap} and {@link Channel} via callback hooks. + * Extending the NettyCustomizer API + * Contrary to other driver options, the options available in this class should be considered as advanced feature and as such, + * they should only be modified by expert users. A misconfiguration introduced by the means of this API can have unexpected + * results and cause the driver to completely fail to connect. + */ +public interface NettyCustomizer { + + /** + * Hook invoked each time the driver creates a new Connection and configures a new instance of Bootstrap for it. This hook + * is called after the driver has applied all {@link java.net.SocketOption}s. This is a good place to add extra + * {@link io.netty.channel.ChannelOption}s to the {@link Bootstrap}. + * + * @param bootstrap must not be {@code null}. + */ + default void afterBootstrapInitialized(Bootstrap bootstrap) { + } + + /** + * Hook invoked each time the driver initializes the channel. This hook is called after the driver has registered all its + * internal channel handlers, and applied the configured options. + * + * @param channel must not be {@code null}. + */ + default void afterChannelInitialized(Channel channel) { + } + + /** + * Hook invoked each time a full HTTP request is received in a Netty handler pipeline. + * Useful to adjust headers in a Netty way. + * + * @param ctx the channel context + * @param fullHttpRequest the full HTTP request + */ + default void afterFullHttpRequestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) { + } + +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClient.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClient.java new file mode 100644 index 0000000..f293fcb --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClient.java @@ -0,0 +1,260 @@ +package org.xbib.net.http.client.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.pool.ChannelPoolHandler; +import io.netty.util.concurrent.Future; +import java.io.Closeable; +import java.io.IOException; +import java.net.ConnectException; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.client.HttpClient; +import org.xbib.net.http.client.HttpResponse; + +public class NettyHttpClient implements HttpClient, Closeable { + + private static final Logger logger = Logger.getLogger(NettyHttpClient.class.getName()); + + private final NettyHttpClientBuilder builder; + + private final EventLoopGroup eventLoopGroup; + + private final Bootstrap bootstrap; + + private final AtomicBoolean closed; + + private final HttpChannelInitializer httpChannelInitializer; + + private final ServiceLoader httpChannelInitializerServiceLoader; + + private Pool pool; + + private final List interactions; + + NettyHttpClient(NettyHttpClientBuilder builder, + EventLoopGroup eventLoopGroup, + Bootstrap bootstrap) throws IOException { + this.builder = builder; + this.eventLoopGroup = eventLoopGroup; + this.bootstrap = bootstrap; + this.closed = new AtomicBoolean(false); + this.httpChannelInitializer = builder.httpChannelInitializer; + this.httpChannelInitializerServiceLoader = ServiceLoader.load(HttpChannelInitializer.class); + createBoundedPool(builder.nettyHttpClientConfig, bootstrap); + this.interactions = new CopyOnWriteArrayList<>(); + } + + public static NettyHttpClientBuilder builder() { + return new NettyHttpClientBuilder(); + } + + public NettyHttpClient getClient() { + return this; + } + + public Bootstrap getBootstrap() { + return bootstrap; + } + + public NettyHttpClientConfig getClientConfig() { + return builder.nettyHttpClientConfig; + } + + public Pool getPool() { + return pool; + } + + public boolean hasPooledNodes() { + return pool != null && !builder.nettyHttpClientConfig.getPoolNodes().isEmpty(); + } + + public ChannelInitializer newChannelInitializer(HttpAddress httpAddress, Interaction interaction) { + return new ChannelInitializer<>() { + @Override + protected void initChannel(Channel channel) throws Exception { + interaction.setSettingsPromise(channel.newPromise()); + lookupChannelInitializer(httpAddress) + .init(channel, httpAddress, getClient(), builder.nettyCustomizer, interaction); + } + }; + } + + /** + * Execute a HTTP request and return a {@link CompletableFuture}. + * + * @param request the request + * @param supplier the function for the response + * @param the result of the function for the response + * @return the completable future + * @throws IOException if the request fails to be executed. + */ + @Override + public CompletableFuture execute(HttpRequest request, + Function supplier) throws IOException { + HttpAddress httpAddress = HttpAddress.of(request.getURL(), request.getVersion()); + HttpChannelInitializer initializer = lookupChannelInitializer(httpAddress); + Interaction interaction = initializer.newInteraction(this, httpAddress); + interactions.add(interaction); + return interaction.execute(request, supplier); + } + + /** + * Execute HTTP request. + * + * @param request the HTTP request + * @return an interaction + * @throws IOException if execution fails + */ + public Interaction execute(HttpRequest request) throws IOException { + HttpAddress httpAddress = HttpAddress.of(request.getURL(), request.getVersion()); + HttpChannelInitializer initializer = lookupChannelInitializer(httpAddress); + Interaction interaction = initializer.newInteraction(this, httpAddress); + CompletableFuture future = new CompletableFuture<>(); + interaction.setFuture(future); + interactions.add(interaction); + return interaction.execute(request); + } + + /** + * For following redirects, construct a new interaction on a given request URL.. + * + * @param interaction the previous interaction + * @param request the new request for continuing the request. + * @throws IOException if continuation fails + */ + public void continuation(Interaction interaction, HttpRequest request) throws IOException { + HttpAddress httpAddress = HttpAddress.of(request.getURL(), request.getVersion()); + HttpChannelInitializer initializer = lookupChannelInitializer(httpAddress); + Interaction next = initializer.newInteraction(this, httpAddress); + next.setCookieBox(interaction.getCookieBox()); + next.execute(request); + next.get(); + closeAndRemove(next); + } + + /** + * Retry interaction. + * + * @param interaction the interaction to retry + * @param request the request to retry + * @throws IOException if retry failed + */ + public void retry(Interaction interaction, HttpRequest request) throws IOException { + interaction.execute(request); + interaction.get(); + closeAndRemove(interaction); + } + + @Override + public void close() throws IOException { + long amount = 15; + TimeUnit timeUnit = TimeUnit.SECONDS; + if (closed.compareAndSet(false, true)) { + try { + for (Interaction interaction : interactions) { + logger.log(Level.FINER, "waiting for unfinshed interaction " + interaction); + //interaction.get(); + interaction.close(); + } + if (hasPooledNodes()) { + logger.log(Level.FINER, "closing pool"); + pool.close(); + } + Future future = eventLoopGroup.shutdownGracefully(0L, amount, timeUnit); + future.await(amount, timeUnit); + if (future.isSuccess()) { + logger.log(Level.FINER, "event loop group closed"); + } else { + logger.log(Level.WARNING, "timeout when closing event loop group"); + } + } catch (Exception e) { + throw new IOException(e); + } + } + } + + private void closeAndRemove(Interaction interaction) { + try { + interaction.close(); + remove(interaction); + } catch (Exception e) { + logger.log(Level.SEVERE, "unable to close interaction: " + e.getMessage(), e); + } + } + + void remove(Interaction interaction) { + interactions.remove(interaction); + } + + + private HttpChannelInitializer lookupChannelInitializer(HttpAddress httpAddress) { + if (httpChannelInitializer != null || httpAddress == null) { + return httpChannelInitializer; + } + for (HttpChannelInitializer initializer : httpChannelInitializerServiceLoader) { + if (initializer.supports(httpAddress)) { + return initializer; + } + } + throw new IllegalStateException("no channel initializer found for address " + httpAddress + ", check service provider"); + } + + private void createBoundedPool(NettyHttpClientConfig nettyHttpClientConfig, + Bootstrap bootstrap) throws IOException { + List nodes = nettyHttpClientConfig.getPoolNodes(); + if (nodes == null || nodes.isEmpty()) { + return; + } + Integer limit = nettyHttpClientConfig.getPoolNodeConnectionLimit(); + if (limit == null || limit < 1) { + limit = 1; + } + Semaphore semaphore = new Semaphore(limit); + Integer retries = nettyHttpClientConfig.getRetriesPerPoolNode(); + if (retries == null || retries < 0) { + retries = 0; + } + Integer nodeConnectionLimit = nettyHttpClientConfig.getPoolNodeConnectionLimit(); + if (nodeConnectionLimit == null || nodeConnectionLimit == 0) { + nodeConnectionLimit = nodes.size(); + } + this.pool = new BoundedChannelPool(semaphore, nettyHttpClientConfig.getPoolVersion(), + nodes, retries, nettyHttpClientConfig.getPoolKeySelectorType()); + try { + this.pool.init(bootstrap, new NettyClientChannelPoolHandler(), nodeConnectionLimit); + } catch (ConnectException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + + private class NettyClientChannelPoolHandler implements ChannelPoolHandler { + + @Override + public void channelReleased(Channel channel) { + } + + @Override + public void channelAcquired(Channel channel) { + } + + @Override + public void channelCreated(Channel channel) throws IOException { + HttpAddress httpAddress = channel.attr(Pool.POOL_ATTRIBUTE_KEY).get(); + HttpChannelInitializer initializer = lookupChannelInitializer(httpAddress); + Interaction interaction = initializer.newInteraction(getClient(), httpAddress); + initializer.init(channel, httpAddress, getClient(), builder.nettyCustomizer, interaction); + } + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientBuilder.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientBuilder.java new file mode 100644 index 0000000..6c361bb --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientBuilder.java @@ -0,0 +1,177 @@ +package org.xbib.net.http.client.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import java.io.IOException; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.ServiceLoader; +import java.util.concurrent.ThreadFactory; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.util.NamedThreadFactory; + +public class NettyHttpClientBuilder { + + private static final Logger logger = Logger.getLogger(NettyHttpClientBuilder.class.getName()); + + NettyHttpClientConfig nettyHttpClientConfig; + + ByteBufAllocator byteBufAllocator; + + EventLoopGroup eventLoopGroup; + + Class socketChannelClass; + + HttpChannelInitializer httpChannelInitializer; + + NettyCustomizer nettyCustomizer; + + NettyHttpClientBuilder() { + } + + public NettyHttpClientBuilder setConfig(NettyHttpClientConfig nettyHttpClientConfig) { + this.nettyHttpClientConfig = nettyHttpClientConfig; + return this; + } + + /** + * Set Netty's ByteBuf allocator. + * + * @param byteBufAllocator the byte buf allocator + * @return this builder + */ + public NettyHttpClientBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = byteBufAllocator; + return this; + } + + public NettyHttpClientBuilder setEventLoop(EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = eventLoopGroup; + return this; + } + + public NettyHttpClientBuilder setChannelClass(Class socketChannelClass) { + this.socketChannelClass = socketChannelClass; + return this; + } + + public NettyHttpClientBuilder addPoolNode(HttpAddress httpAddress) { + nettyHttpClientConfig.addPoolNode(httpAddress); + nettyHttpClientConfig.setPoolVersion(httpAddress.getVersion()); + nettyHttpClientConfig.setPoolSecure(httpAddress.isSecure()); + return this; + } + + public NettyHttpClientBuilder setHttpChannelInitializer(HttpChannelInitializer httpChannelInitializer) { + this.httpChannelInitializer = httpChannelInitializer; + return this; + } + + public NettyHttpClientBuilder setNettyCustomizer(NettyCustomizer nettyCustomizer) { + this.nettyCustomizer = nettyCustomizer; + return this; + } + + public NettyHttpClient build() throws IOException { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "installed security providers = " + + Arrays.stream(Security.getProviders()).map(Provider::getName).collect(Collectors.toList())); + } + if (nettyHttpClientConfig == null) { + nettyHttpClientConfig = createEmptyConfig(); + } + if (byteBufAllocator == null) { + byteBufAllocator = ByteBufAllocator.DEFAULT; + } + EventLoopGroup myEventLoopGroup = createEventLoopGroup(nettyHttpClientConfig, eventLoopGroup); + Class mySocketChannelClass = createChannelClass(nettyHttpClientConfig, socketChannelClass); + Bootstrap bootstrap = createBootstrap(nettyHttpClientConfig, byteBufAllocator, myEventLoopGroup, mySocketChannelClass); + if (nettyCustomizer != null) { + nettyCustomizer.afterBootstrapInitialized(bootstrap); + } + return new NettyHttpClient(this, myEventLoopGroup, bootstrap); + } + + protected NettyHttpClientConfig createEmptyConfig() { + return new NettyHttpClientConfig(); + } + + private static EventLoopGroup createEventLoopGroup(NettyHttpClientConfig clientConfig, + EventLoopGroup eventLoopGroup) { + if (eventLoopGroup != null) { + return eventLoopGroup; + } + EventLoopGroup myEventLoopGroup = null; + ThreadFactory threadFactory = new NamedThreadFactory("org-xbib-net-http-netty-client"); + ServiceLoader transportProviders = ServiceLoader.load(ClientTransportProvider.class); + for (ClientTransportProvider serverTransportProvider : transportProviders) { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "found event loop group provider = " + serverTransportProvider); + } + if (clientConfig.getTransportProviderName() == null || clientConfig.getTransportProviderName().equals(serverTransportProvider.getClass().getName())) { + myEventLoopGroup = serverTransportProvider.createEventLoopGroup(clientConfig.getThreadCount(), threadFactory); + break; + } + } + if (myEventLoopGroup == null) { + myEventLoopGroup = new NioEventLoopGroup(clientConfig.getThreadCount(), threadFactory); + } + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "event loop group class: " + myEventLoopGroup.getClass().getName()); + } + return myEventLoopGroup; + } + + private static Class createChannelClass(NettyHttpClientConfig clientConfig, + Class socketChannelClass) { + if (socketChannelClass != null) { + return socketChannelClass; + } + Class myChannelClass = null; + ServiceLoader transportProviders = ServiceLoader.load(ClientTransportProvider.class); + for (ClientTransportProvider transportProvider : transportProviders) { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "found socket channel provider = " + transportProvider); + } + if (clientConfig.getTransportProviderName() == null || clientConfig.getTransportProviderName().equals(transportProvider.getClass().getName())) { + myChannelClass = transportProvider.createSocketChannelClass(); + break; + } + } + if (myChannelClass == null) { + myChannelClass = NioSocketChannel.class; + } + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "socket channel class: " + myChannelClass.getName()); + } + return myChannelClass; + } + + private static Bootstrap createBootstrap(NettyHttpClientConfig nettyHttpClientConfig, + ByteBufAllocator byteBufAllocator, + EventLoopGroup eventLoopGroup, + Class socketChannelClass) { + return new Bootstrap() + .group(eventLoopGroup) + .channel(socketChannelClass) + .option(ChannelOption.ALLOCATOR, byteBufAllocator) + .option(ChannelOption.TCP_NODELAY, nettyHttpClientConfig.socketConfig.isTcpNodelay()) + .option(ChannelOption.SO_KEEPALIVE, nettyHttpClientConfig.socketConfig.isKeepAlive()) + .option(ChannelOption.SO_REUSEADDR, nettyHttpClientConfig.socketConfig.isReuseAddr()) + .option(ChannelOption.SO_LINGER, nettyHttpClientConfig.socketConfig.getLinger()) + .option(ChannelOption.SO_SNDBUF, nettyHttpClientConfig.socketConfig.getTcpSendBufferSize()) + .option(ChannelOption.SO_RCVBUF, nettyHttpClientConfig.socketConfig.getTcpReceiveBufferSize()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyHttpClientConfig.socketConfig.getConnectTimeoutMillis()) + .option(ChannelOption.WRITE_BUFFER_WATER_MARK, nettyHttpClientConfig.getWriteBufferWaterMark()); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientConfig.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientConfig.java new file mode 100644 index 0000000..04c9765 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NettyHttpClientConfig.java @@ -0,0 +1,333 @@ +package org.xbib.net.http.client.netty; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.proxy.Socks4ProxyHandler; +import io.netty.handler.proxy.Socks5ProxyHandler; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.SocketConfig; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.BackOff; + +public class NettyHttpClientConfig { + + /** + * If frame logging /traffic logging is enabled or not. + */ + private boolean debug = false; + + /** + * Default debug log level. + */ + private LogLevel debugLogLevel = LogLevel.DEBUG; + + SocketConfig socketConfig = new SocketConfig(); + + private String transportProviderName = null; + + /** + * If set to 0, then Netty will decide about thread count. + * Default is Runtime.getRuntime().availableProcessors() * 2 + */ + private int threadCount = 0; + + /** + * Set HTTP initial line length to 4k. + * See {@link io.netty.handler.codec.http.HttpClientCodec}. + */ + private int maxInitialLineLength = 4 * 1024; + + /** + * Set HTTP maximum headers size to 8k. + * See {@link io.netty.handler.codec.http.HttpClientCodec}. + */ + private int maxHeadersSize = 8 * 1024; + + /** + * Set HTTP chunk maximum size to 8k. + * See {@link io.netty.handler.codec.http.HttpClientCodec}. + */ + private int maxChunkSize = 8 * 1024; + + /** + * Set maximum content length to 256 MB. + */ + private int maxContentLength = 256 * 1024 * 1024; + + /** + * This is Netty's default. + */ + private int maxCompositeBufferComponents = 1024; + + /** + * Default for gzip codec is false + */ + private boolean gzipEnabled = false; + + private ByteBufAllocator byteBufAllocator; + + private HttpProxyHandler httpProxyHandler; + + private Socks4ProxyHandler socks4ProxyHandler; + + private Socks5ProxyHandler socks5ProxyHandler; + + private List poolNodes = new ArrayList<>(); + + private Pool.PoolKeySelectorType poolKeySelectorType = Pool.PoolKeySelectorType.ROUNDROBIN; + + private Integer poolNodeConnectionLimit; + + private Integer retriesPerPoolNode = 0; + + private HttpVersion poolVersion = HttpVersion.HTTP_1_1; + + private Boolean poolSecure = false; + + private Http2Settings http2Settings = Http2Settings.defaultSettings(); + + private WriteBufferWaterMark writeBufferWaterMark = WriteBufferWaterMark.DEFAULT; + + private BackOff backOff = BackOff.ZERO_BACKOFF; + + public NettyHttpClientConfig() { + this.byteBufAllocator = ByteBufAllocator.DEFAULT; + } + + public void setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = byteBufAllocator; + } + + public ByteBufAllocator getByteBufAllocator() { + return byteBufAllocator; + } + + public NettyHttpClientConfig setDebug(boolean debug) { + this.debug = debug; + return this; + } + + public NettyHttpClientConfig enableDebug() { + this.debug = true; + return this; + } + + public NettyHttpClientConfig disableDebug() { + this.debug = false; + return this; + } + + public boolean isDebug() { + return debug; + } + + public NettyHttpClientConfig setDebugLogLevel(LogLevel debugLogLevel) { + this.debugLogLevel = debugLogLevel; + return this; + } + + public LogLevel getDebugLogLevel() { + return debugLogLevel; + } + + public NettyHttpClientConfig setTransportProviderName(String transportProviderName) { + this.transportProviderName = transportProviderName; + return this; + } + + public String getTransportProviderName() { + return transportProviderName; + } + + public NettyHttpClientConfig setThreadCount(int threadCount) { + this.threadCount = threadCount; + return this; + } + + public int getThreadCount() { + return threadCount; + } + + public NettyHttpClientConfig setSocketConfig(SocketConfig socketConfig) { + this.socketConfig = socketConfig; + return this; + } + + public SocketConfig getSocketConfig() { + return socketConfig; + } + + public NettyHttpClientConfig setMaxInitialLineLength(int maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + return this; + } + + public int getMaxInitialLineLength() { + return maxInitialLineLength; + } + + public NettyHttpClientConfig setMaxHeadersSize(int maxHeadersSize) { + this.maxHeadersSize = maxHeadersSize; + return this; + } + + public int getMaxHeadersSize() { + return maxHeadersSize; + } + + public NettyHttpClientConfig setMaxChunkSize(int maxChunkSize) { + this.maxChunkSize = maxChunkSize; + return this; + } + + public int getMaxChunkSize() { + return maxChunkSize; + } + + public NettyHttpClientConfig setMaxContentLength(int maxContentLength) { + this.maxContentLength = maxContentLength; + return this; + } + + public int getMaxContentLength() { + return maxContentLength; + } + + public NettyHttpClientConfig setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + this.maxCompositeBufferComponents = maxCompositeBufferComponents; + return this; + } + + public int getMaxCompositeBufferComponents() { + return maxCompositeBufferComponents; + } + + public NettyHttpClientConfig setGzipEnabled(boolean gzipEnabled) { + this.gzipEnabled = gzipEnabled; + return this; + } + + public boolean isGzipEnabled() { + return gzipEnabled; + } + + public NettyHttpClientConfig setHttp2Settings(Http2Settings http2Settings) { + this.http2Settings = http2Settings; + return this; + } + + public Http2Settings getHttp2Settings() { + return http2Settings; + } + + + public NettyHttpClientConfig setHttpProxyHandler(HttpProxyHandler httpProxyHandler) { + this.httpProxyHandler = httpProxyHandler; + return this; + } + + public HttpProxyHandler getHttpProxyHandler() { + return httpProxyHandler; + } + + public NettyHttpClientConfig setSocks4ProxyHandler(Socks4ProxyHandler socks4ProxyHandler) { + this.socks4ProxyHandler = socks4ProxyHandler; + return this; + } + + public Socks4ProxyHandler getSocks4ProxyHandler() { + return socks4ProxyHandler; + } + + public NettyHttpClientConfig setSocks5ProxyHandler(Socks5ProxyHandler socks5ProxyHandler) { + this.socks5ProxyHandler = socks5ProxyHandler; + return this; + } + + public Socks5ProxyHandler getSocks5ProxyHandler() { + return socks5ProxyHandler; + } + + public NettyHttpClientConfig setPoolNodes(List poolNodes) { + this.poolNodes = poolNodes; + return this; + } + + public List getPoolNodes() { + return poolNodes; + } + + public NettyHttpClientConfig setPoolKeySelectorType(Pool.PoolKeySelectorType poolKeySelectorType) { + this.poolKeySelectorType = poolKeySelectorType; + return this; + } + + public Pool.PoolKeySelectorType getPoolKeySelectorType() { + return poolKeySelectorType; + } + + public NettyHttpClientConfig addPoolNode(HttpAddress poolNodeAddress) { + this.poolNodes.add(poolNodeAddress); + return this; + } + + public NettyHttpClientConfig setPoolNodeConnectionLimit(Integer poolNodeConnectionLimit) { + this.poolNodeConnectionLimit = poolNodeConnectionLimit; + return this; + } + + public Integer getPoolNodeConnectionLimit() { + return poolNodeConnectionLimit; + } + + public NettyHttpClientConfig setRetriesPerPoolNode(Integer retriesPerPoolNode) { + this.retriesPerPoolNode = retriesPerPoolNode; + return this; + } + + public Integer getRetriesPerPoolNode() { + return retriesPerPoolNode; + } + + public NettyHttpClientConfig setPoolVersion(HttpVersion poolVersion) { + this.poolVersion = poolVersion; + return this; + } + + public HttpVersion getPoolVersion() { + return poolVersion; + } + + public NettyHttpClientConfig setPoolSecure(boolean poolSecure) { + this.poolSecure = poolSecure; + return this; + } + + public boolean isPoolSecure() { + return poolSecure; + } + + public NettyHttpClientConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + this.writeBufferWaterMark = writeBufferWaterMark; + return this; + } + + public WriteBufferWaterMark getWriteBufferWaterMark() { + return writeBufferWaterMark; + } + + public NettyHttpClientConfig setBackOff(BackOff backOff) { + this.backOff = backOff; + return this; + } + + public BackOff getBackOff() { + return backOff; + } + +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NioClientTransportProvider.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NioClientTransportProvider.java new file mode 100644 index 0000000..38175a4 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/NioClientTransportProvider.java @@ -0,0 +1,23 @@ +package org.xbib.net.http.client.netty; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import java.util.concurrent.ThreadFactory; + +public class NioClientTransportProvider implements ClientTransportProvider { + + public NioClientTransportProvider() { + } + + @Override + public EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + return new NioEventLoopGroup(nThreads, threadFactory); + } + + @Override + public Class createSocketChannelClass() { + return NioSocketChannel.class; + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Pool.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Pool.java new file mode 100644 index 0000000..6268304 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/Pool.java @@ -0,0 +1,27 @@ +package org.xbib.net.http.client.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.pool.ChannelPoolHandler; +import io.netty.util.AttributeKey; +import java.io.Closeable; +import java.io.IOException; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; + +public interface Pool extends Closeable { + + AttributeKey POOL_ATTRIBUTE_KEY = AttributeKey.valueOf("__pool"); + + void init(Bootstrap bootstrap, ChannelPoolHandler channelPoolHandler, int count) throws IOException; + + HttpVersion getVersion(); + + Channel acquire() throws Exception; + + void release(Channel channel, boolean close) throws Exception; + + enum PoolKeySelectorType { + RANDOM, ROUNDROBIN + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/StreamIds.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/StreamIds.java new file mode 100644 index 0000000..b1178a1 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/StreamIds.java @@ -0,0 +1,71 @@ +package org.xbib.net.http.client.netty; + +import java.util.Set; +import java.util.SortedMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class StreamIds { + + private final AtomicInteger streamId; + + private final SortedMap> sortedMap; + + public StreamIds() { + this.streamId = new AtomicInteger(3); + this.sortedMap = new ConcurrentSkipListMap<>(); + } + + public CompletableFuture get(Integer key) { + return sortedMap.get(key); + } + + public Set keys() { + return sortedMap.keySet(); + } + + public Integer lastKey() { + return sortedMap.isEmpty() ? null : sortedMap.lastKey(); + } + + public void put(Integer key, CompletableFuture promise) { + sortedMap.put(key, promise); + } + + public void remove(Integer key) { + if (key != null) { + sortedMap.remove(key); + } + } + + public Integer nextStreamId() { + int streamId = this.streamId.getAndAdd(2); + if (streamId == Integer.MIN_VALUE) { + // reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE + this.streamId.set(3); + streamId = 3; + } + sortedMap.put(streamId, new CompletableFuture<>()); + return streamId; + } + + public void fail(Throwable throwable) { + for (CompletableFuture promise : sortedMap.values()) { + promise.completeExceptionally(throwable); + } + } + + public void close() { + sortedMap.clear(); + } + + public boolean isClosed() { + return sortedMap.isEmpty(); + } + + @Override + public String toString() { + return "StreamIds[id=" + streamId + ",map=" + sortedMap + "]"; + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/TrafficLoggingHandler.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/TrafficLoggingHandler.java new file mode 100644 index 0000000..d1cb977 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/TrafficLoggingHandler.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.client.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +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. + */ +@ChannelHandler.Sharable +public class TrafficLoggingHandler extends LoggingHandler { + + public TrafficLoggingHandler(LogLevel level) { + super("client", level); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.fireChannelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + ctx.fireChannelUnregistered(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { + ctx.write(msg, promise); + } else { + super.write(ctx, msg, promise); + } + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/UserAgent.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/UserAgent.java new file mode 100644 index 0000000..13ac93c --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/UserAgent.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.client.netty; + +import io.netty.bootstrap.Bootstrap; +import java.util.Optional; + +/** + * HTTP client user agent. + */ +public final class UserAgent { + + /** + * The default value for {@code User-Agent}. + */ + private static final String USER_AGENT = String.format("HttpNettyClient/%s (Java/%s/%s) (Netty/%s)", + httpClientVersion(), javaVendor(), javaVersion(), nettyVersion()); + + private UserAgent() { + } + + public static String getUserAgent() { + return USER_AGENT; + } + + private static String httpClientVersion() { + return Optional.ofNullable(UserAgent.class.getPackage().getImplementationVersion()) + .orElse("unknown"); + } + + private static String javaVendor() { + return Optional.ofNullable(System.getProperty("java.vendor")) + .orElse("unknown"); + } + + private static String javaVersion() { + return Optional.ofNullable(System.getProperty("java.version")) + .orElse("unknown"); + } + + private static String nettyVersion() { + return Optional.ofNullable(Bootstrap.class.getPackage().getImplementationVersion()) + .orElse("unknown"); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1ChannelInitializer.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1ChannelInitializer.java new file mode 100644 index 0000000..303ad13 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1ChannelInitializer.java @@ -0,0 +1,102 @@ +package org.xbib.net.http.client.netty.http1; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.proxy.Socks5ProxyHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.handler.timeout.ReadTimeoutHandler; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpChannelInitializer; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyCustomizer; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.TrafficLoggingHandler; + +public class Http1ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Http1ChannelInitializer.class.getName()); + + public Http1ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress httpAddress) { + return HttpVersion.HTTP_1_1.equals(httpAddress.getVersion()) && !httpAddress.isSecure(); + } + + @Override + public Interaction newInteraction(NettyHttpClient client, HttpAddress httpAddress) { + return new Http1Interaction(client, httpAddress); + } + + @Override + public void init(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + NettyCustomizer nettyCustomizer, + Interaction interaction) throws IOException { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + if (nettyHttpClientConfig.isDebug()) { + pipeline.addLast("client-traffic", new TrafficLoggingHandler(LogLevel.DEBUG)); + } + int readTimeoutMilllis = nettyHttpClientConfig.getSocketConfig().getReadTimeoutMillis(); + if (readTimeoutMilllis > 0) { + pipeline.addLast("client-read-timeout", new ReadTimeoutHandler(readTimeoutMilllis / 1000)); + } + int socketTimeoutMillis = nettyHttpClientConfig.getSocketConfig().getSocketTimeoutMillis(); + if (socketTimeoutMillis > 0) { + pipeline.addLast("client-idle-timeout", new IdleStateHandler(socketTimeoutMillis / 1000, + socketTimeoutMillis / 1000, socketTimeoutMillis / 1000)); + } + if (nettyHttpClientConfig.getHttpProxyHandler() != null) { + pipeline.addLast("client-http-proxy", nettyHttpClientConfig.getHttpProxyHandler()); + } + if (nettyHttpClientConfig.getSocks4ProxyHandler() != null) { + pipeline.addLast("client-socks4-proxy", nettyHttpClientConfig.getSocks4ProxyHandler()); + } + if (nettyHttpClientConfig.getSocks5ProxyHandler() != null) { + Socks5ProxyHandler socks5ProxyHandler = nettyHttpClientConfig.getSocks5ProxyHandler(); + pipeline.addLast("client-socks5-proxy", socks5ProxyHandler); + } + configurePlain(channel, nettyHttpClient, interaction); + if (nettyCustomizer != null) { + nettyCustomizer.afterChannelInitialized(channel); + } + if (nettyHttpClientConfig.isDebug()) { + logger.log(Level.FINE, "HTTP 1.1 plain channel initialized: " + + " address=" + httpAddress + + " pipeline=" + pipeline.names()); + } + } + + private void configurePlain(Channel channel, + NettyHttpClient nettyHttpClient, + Interaction interaction) throws IOException { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("http-client-chunk-writer", + new ChunkedWriteHandler()); + pipeline.addLast("http-client-codec", new HttpClientCodec(nettyHttpClientConfig.getMaxInitialLineLength(), + nettyHttpClientConfig.getMaxHeadersSize(), nettyHttpClientConfig.getMaxChunkSize())); + if (nettyHttpClientConfig.isGzipEnabled()) { + pipeline.addLast("http-client-decompressor", new HttpContentDecompressor()); + } + HttpObjectAggregator httpObjectAggregator = + new HttpObjectAggregator(nettyHttpClientConfig.getMaxContentLength(), false); + httpObjectAggregator.setMaxCumulationBufferComponents(nettyHttpClientConfig.getMaxCompositeBufferComponents()); + pipeline.addLast("http-client-aggregator", httpObjectAggregator); + pipeline.addLast("http-client-response", new Http1Handler(interaction)); + interaction.settingsReceived(null); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Handler.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Handler.java new file mode 100644 index 0000000..1d083c8 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Handler.java @@ -0,0 +1,47 @@ +package org.xbib.net.http.client.netty.http1; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpResponse; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.client.netty.Interaction; + +@ChannelHandler.Sharable +public class Http1Handler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Http1Handler.class.getName()); + + private final Interaction interaction; + + public Http1Handler(Interaction interaction) { + this.interaction = interaction; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpResponse) { + FullHttpResponse httpResponse = (FullHttpResponse) msg; + try { + interaction.responseReceived(ctx.channel(), null, httpResponse); + } finally { + httpResponse.release(); + } + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + interaction.fail(ctx.channel(), cause); + ctx.close(); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Interaction.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Interaction.java new file mode 100644 index 0000000..4ed2e2c --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http1/Http1Interaction.java @@ -0,0 +1,255 @@ +package org.xbib.net.http.client.netty.http1; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import java.io.IOException; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.URLSyntaxException; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.client.cookie.CookieDecoder; +import org.xbib.net.http.client.cookie.CookieEncoder; +import org.xbib.net.http.client.netty.BaseInteraction; +import org.xbib.net.http.client.netty.HttpResponseBuilder; +import org.xbib.net.http.client.netty.StreamIds; +import org.xbib.net.http.client.netty.http2.Http2Interaction; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.HttpResponse; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyHttpClient; + +public class Http1Interaction extends BaseInteraction { + + private static final Logger logger = Logger.getLogger(Http1Interaction.class.getName()); + + private final HttpDataFactory httpDataFactory; + + public Http1Interaction(NettyHttpClient nettyHttpClient, HttpAddress httpAddress) { + super(nettyHttpClient, httpAddress); + this.httpDataFactory = new DefaultHttpDataFactory(); + } + + @Override + public Interaction execute(HttpRequest request) throws IOException { + if (throwable != null) { + logger.log(Level.WARNING, throwable.getMessage(), throwable); + return this; + } + httpRequest = request; + Channel channel = acquireChannel(request); + try { + // if http2Settings is present, we have a HTTP-2 upgrade + waitForSettings(5L, TimeUnit.SECONDS); + if (http2Settings != null) { + Http2Interaction interaction = upgradeInteraction(); + interaction.executeRequest(request, channel); + return interaction; + } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new IOException(e); + } + return executeRequest(request, channel); + } + + public Interaction executeRequest(HttpRequest request, Channel channel) throws IOException { + final String channelId = channel.id().toString(); + streamIds.putIfAbsent(channelId, new StreamIds()); + // Some HTTP 1 servers do not understand URIs in HTTP command line in spite of RFC 7230. + // The "origin form" requires a "Host" header. + // Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2. + // The reason is that Netty derives the HTTP/2 scheme header from the absolute form. + String uri = request.getVersion().majorVersion() == 1 ? request.getURL().relativeReference() : request.getURL().toExternalForm(); + HttpVersion httpVersion = HttpVersion.valueOf(request.getVersion().text()); + HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod().name()); + DefaultFullHttpRequest fullHttpRequest = request.getBody() == null ? + new DefaultFullHttpRequest(httpVersion, httpMethod, uri) : + new DefaultFullHttpRequest(httpVersion, httpMethod, uri, Unpooled.wrappedBuffer(request.getBody())); + HttpPostRequestEncoder httpPostRequestEncoder = null; + final Integer streamId = streamIds.get(channelId).nextStreamId(); + if (streamId == null) { + throw new IllegalStateException("stream id is null"); + } + // add matching cookies from box (previous requests) and new cookies from request builder + Collection cookies = new ArrayList<>(); + cookies.addAll(matchCookiesFromBox(request)); + cookies.addAll(matchCookies(request)); + if (!cookies.isEmpty()) { + request.getHeaders().set(HttpHeaderNames.COOKIE, CookieEncoder.STRICT.encode(cookies)); + } + request.getHeaders().entries().forEach(p -> fullHttpRequest.headers().add(p.getKey(), p.getValue())); + if (request.getBody() == null && !request.getBodyData().isEmpty()) { + try { + httpPostRequestEncoder = new HttpPostRequestEncoder(httpDataFactory, fullHttpRequest, true); + httpPostRequestEncoder.setBodyHttpDatas(request.getBodyData()); + httpPostRequestEncoder.finalizeRequest(); + } catch (HttpPostRequestEncoder.ErrorDataEncoderException e) { + throw new IOException(e); + } + } + if (!channel.isWritable()) { + logger.log(Level.WARNING, "channel not writable"); + return this; + } + channel.write(fullHttpRequest); + if (httpPostRequestEncoder != null && httpPostRequestEncoder.isChunked()) { + channel.write(httpPostRequestEncoder); + } + channel.flush(); + if (httpPostRequestEncoder != null) { + httpPostRequestEncoder.cleanFiles(); + } + return this; + } + + @Override + public void settingsPrefaceWritten() { + logger.log(Level.FINEST, "settings/preface written"); + } + + @Override + public void waitForSettings(long value, TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException { + if (settingsPromise != null) { + logger.log(Level.FINEST, "waiting for settings, promise = " + settingsPromise); + settingsPromise.get(value, timeUnit); + } + } + + @Override + public void settingsReceived(Http2Settings http2Settings) { + this.http2Settings = http2Settings; + if (settingsPromise != null) { + logger.log(Level.FINEST, "received settings " + http2Settings + " for promise " + settingsPromise); + if (!settingsPromise.isDone()) { + settingsPromise.setSuccess(); + } + } else { + logger.log(Level.WARNING, "settings received but no promise present"); + } + } + + @Override + public void responseReceived(Channel channel, Integer streamId, FullHttpResponse fullHttpResponse) { + if (throwable != null) { + logger.log(Level.WARNING, "throwable not null", throwable); + return; + } + HttpResponse httpResponse = null; + try { + // streamID is expected to be null, last request on memory + // is expected to be current, remove request from memory + for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + addCookie(cookie); + } + HttpResponseStatus httpStatus = HttpResponseStatus.valueOf(fullHttpResponse.status().code()); + HttpHeaders httpHeaders = new HttpHeaders(); + fullHttpResponse.headers().iteratorCharSequence().forEachRemaining(e -> httpHeaders.add(e.getKey(), e.getValue().toString())); + httpResponse = newHttpResponseBuilder(channel) + .setHttpAddress(httpAddress) + .setLocalAddress(channel.localAddress()) + .setRemoteAddress(channel.remoteAddress()) + .setCookieBox(getCookieBox()) + .setStatus(httpStatus) + .setHeaders(httpHeaders) + .setByteBuffer(fullHttpResponse.content().nioBuffer()) + .build(); + httpRequest.onResponse(httpResponse); + // check for retry / continue + try { + HttpRequest retryRequest = retry(httpRequest, httpResponse); + if (retryRequest != null) { + // retry transport, wait for completion + nettyHttpClient.retry(this, retryRequest); + } else { + HttpRequest continueRequest = continuation(httpRequest, httpResponse); + if (continueRequest != null) { + // continue with new transport, synchronous call here, + // wait for completion + nettyHttpClient.continuation(this, continueRequest); + } + } + } catch (URLSyntaxException | IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + // acknowledge success, if possible + String channelId = channel.id().toString(); + StreamIds streamIds = super.streamIds.get(channelId); + if (streamIds != null) { + Integer lastKey = streamIds.lastKey(); + if (lastKey != null) { + CompletableFuture promise = streamIds.get(lastKey); + if (promise != null) { + promise.complete(true); + } + } + } + } finally { + if (httpResponse != null) { + httpResponse.release(); + } + } + } + + @Override + public void pushPromiseReceived(Channel channel, Integer streamId, + Integer promisedStreamId, Http2Headers headers) { + } + + @Override + protected String getRequestKey(String channelId, Integer streamId) { + return null; + } + + @Override + protected Channel nextChannel() throws IOException { + Channel channel = newChannel(httpAddress); + if (channel == null) { + ConnectException connectException; + if (httpAddress != null) { + connectException = new ConnectException("unable to connect to " + httpAddress); + } else if (nettyHttpClient.hasPooledNodes()) { + connectException = new ConnectException("unable to get channel from pool"); + } else { + // API misuse + connectException = new ConnectException("unable to get channel"); + } + this.throwable = connectException; + throw connectException; + } + return channel; + } + + @Override + public void close() throws IOException { + httpDataFactory.cleanAllHttpData(); + super.close(); + } + + protected HttpResponseBuilder newHttpResponseBuilder(Channel channel) { + return HttpResponse.builder(); + } + + protected Http2Interaction upgradeInteraction() { + return new Http2Interaction(nettyHttpClient, httpAddress); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChannelInitializer.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChannelInitializer.java new file mode 100644 index 0000000..232b986 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChannelInitializer.java @@ -0,0 +1,84 @@ +package org.xbib.net.http.client.netty.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.logging.LogLevel; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpChannelInitializer; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyCustomizer; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.TrafficLoggingHandler; + +public class Http2ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); + + public Http2ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress httpAddress) { + return HttpVersion.HTTP_2_0.equals(httpAddress.getVersion()) && !httpAddress.isSecure(); + } + + @Override + public Interaction newInteraction(NettyHttpClient client, HttpAddress httpAddress) { + return new Http2Interaction(client, httpAddress); + } + + @Override + public void init(Channel channel, + HttpAddress httpAddress, + NettyHttpClient nettyHttpClient, + NettyCustomizer nettyCustomizer, + Interaction interaction) { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelPipeline pipeline = channel.pipeline(); + if (nettyHttpClientConfig.isDebug()) { + pipeline.addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); + } + configurePlain(channel, nettyHttpClient, interaction); + if (nettyCustomizer != null) { + nettyCustomizer.afterChannelInitialized(channel); + } + if (nettyHttpClientConfig.isDebug()) { + logger.log(Level.FINE, "HTTP/2 plain channel initialized: address = " + httpAddress + + " pipeline = " + pipeline.names()); + } + } + + private void configurePlain(Channel channel, + NettyHttpClient nettyHttpClient, + Interaction interaction) { + NettyHttpClientConfig nettyHttpClientConfig = nettyHttpClient.getClientConfig(); + ChannelInitializer initializer = new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + throw new IllegalStateException(); + } + }; + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer) + .initialSettings(nettyHttpClientConfig.getHttp2Settings()); + if (nettyHttpClientConfig.isDebug()) { + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "client-frame")); + } + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder + .autoAckPingFrame(true) + .autoAckSettingsFrame(true) + .decoupleCloseAndGoAway(false) + .gracefulShutdownTimeoutMillis(30000L) + .build(); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("client-multiplex", multiplexCodec); + pipeline.addLast("client-messages", new Http2Messages(interaction)); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChildChannelInitializer.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChildChannelInitializer.java new file mode 100644 index 0000000..76eed9f --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2ChildChannelInitializer.java @@ -0,0 +1,38 @@ +package org.xbib.net.http.client.netty.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; + +public class Http2ChildChannelInitializer extends ChannelInitializer { + + private final NettyHttpClientConfig clientConfig; + + private final Interaction interaction; + + protected final Channel parentChannel; + + public Http2ChildChannelInitializer(NettyHttpClientConfig clientConfig, Interaction interaction, Channel parentChannel) { + this.clientConfig = clientConfig; + this.interaction = interaction; + this.parentChannel = parentChannel; + } + + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast("child-client-frame-converter", + new Http2StreamFrameToHttpObjectCodec(false)); + p.addLast("child-client-decompressor", + new HttpContentDecompressor()); + p.addLast("child-client-chunk-aggregator", + new HttpObjectAggregator(clientConfig.getMaxContentLength())); + p.addLast("child-client-response-handler", + new Http2Handler(interaction)); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Handler.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Handler.java new file mode 100644 index 0000000..ff97485 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Handler.java @@ -0,0 +1,50 @@ +package org.xbib.net.http.client.netty.http2; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http2.HttpConversionUtil; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.client.netty.Interaction; + +@ChannelHandler.Sharable +public class Http2Handler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Http2Handler.class.getName()); + + private final Interaction interaction; + + public Http2Handler(Interaction interaction) { + this.interaction = interaction; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpResponse) { + FullHttpResponse httpResponse = (FullHttpResponse) msg; + try { + Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + interaction.responseReceived(ctx.channel(), streamId, httpResponse); + } finally { + httpResponse.release(); + } + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelInactive(); + interaction.inactive(ctx.channel()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.FINE, "exception caught"); + interaction.fail(ctx.channel(), cause); + ctx.close(); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Interaction.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Interaction.java new file mode 100644 index 0000000..ebd5612 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Interaction.java @@ -0,0 +1,254 @@ +package org.xbib.net.http.client.netty.http2; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.codec.http2.HttpConversionUtil; +import java.io.IOException; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.URLSyntaxException; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.client.cookie.CookieDecoder; +import org.xbib.net.http.client.cookie.CookieEncoder; +import org.xbib.net.http.client.netty.BaseInteraction; +import org.xbib.net.http.client.netty.HttpResponseBuilder; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.StreamIds; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.HttpResponse; +import org.xbib.net.http.client.netty.Interaction; +import org.xbib.net.http.client.netty.NettyHttpClient; + +public class Http2Interaction extends BaseInteraction { + + private static final Logger logger = Logger.getLogger(Http2Interaction.class.getName()); + + public Http2Interaction(NettyHttpClient nettyHttpClient, HttpAddress httpAddress) { + super(nettyHttpClient, httpAddress); + } + + @Override + public Interaction execute(HttpRequest request) throws IOException { + if (throwable != null) { + return this; + } + Channel channel = acquireChannel(request); + try { + waitForSettings(5L, TimeUnit.SECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new IOException(e); + } + return executeRequest(request, channel); + } + + public Interaction executeRequest(HttpRequest request, Channel channel) throws IOException { + this.httpRequest = request; + final String channelId = channel.id().toString(); + streamIds.putIfAbsent(channelId, new StreamIds()); + ChannelInitializer initializer = newHttp2ChildChannelInitializer(nettyHttpClient.getClientConfig(), this, channel); + Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(channel) + .handler(initializer).open().syncUninterruptibly().getNow(); + CharSequence method = request.getMethod().name(); + String scheme = request.getURL().getScheme(); + String authority = request.getURL().getHost() + (request.getURL().getPort() != null ? ":" + request.getURL().getPort() : ""); + String relative = request.getURL().relativeReference(); + String path = relative.isEmpty() ? "/" : relative; + Http2Headers http2Headers = new DefaultHttp2Headers() + .method(method).scheme(scheme).authority(authority).path(path); + StreamIds streamIds = super.streamIds.get(channelId); + if (streamIds == null) { + throw new IllegalStateException(); + } + final Integer streamId = streamIds.nextStreamId(); + if (streamId == null) { + throw new IllegalStateException(); + } + http2Headers.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); + // add matching cookies from box (previous requests) and new cookies from request builder + Collection cookies = new ArrayList<>(); + cookies.addAll(matchCookiesFromBox(request)); + cookies.addAll(matchCookies(request)); + if (!cookies.isEmpty()) { + request.getHeaders().set(HttpHeaderNames.COOKIE, CookieEncoder.STRICT.encode(cookies)); + } + DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders(); + request.getHeaders().entries().forEach(p -> httpHeaders.set(p.getKey(), p.getValue())); + HttpConversionUtil.toHttp2Headers(httpHeaders, http2Headers); + boolean hasContent = request.getBody() != null && request.getBody().remaining() > 0; + DefaultHttp2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(http2Headers, !hasContent); + DefaultHttp2DataFrame dataFrame; + childChannel.write(headersFrame); + if (hasContent) { + dataFrame = new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(request.getBody()), true); + childChannel.write(dataFrame); + } + childChannel.flush(); + if (nettyHttpClient.hasPooledNodes()) { + releaseChannel(channel, false); + } + return this; + } + + @Override + public void settingsPrefaceWritten() { + logger.log(Level.FINEST, "settings/preface written"); + } + + @Override + public void waitForSettings(long value, TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException { + if (settingsPromise != null) { + logger.log(Level.FINEST, "waiting for settings, promise = " + settingsPromise); + settingsPromise.get(value, timeUnit); + } + } + + @Override + public void settingsReceived(Http2Settings http2Settings) { + this.http2Settings = http2Settings; + if (settingsPromise != null) { + logger.log(Level.FINEST, "received settings for promise = " + settingsPromise); + settingsPromise.setSuccess(); + } else { + logger.log(Level.WARNING, "settings received but no promise present"); + } + } + + @Override + public void responseReceived(Channel channel, Integer streamId, FullHttpResponse fullHttpResponse) { + if (throwable != null) { + logger.log(Level.WARNING, "throwable is not null?", throwable); + return; + } + if (streamId == null) { + logger.log(Level.WARNING, "stream ID is null?"); + return; + } + HttpResponse httpResponse = null; + try { + // format of childchan channel ID is "/" + String channelId = channel.id().toString(); + int pos = channelId.indexOf('/'); + channelId = pos > 0 ? channelId.substring(0, pos) : channelId; + StreamIds streamIds = super.streamIds.get(channelId); + if (streamIds == null) { + // should never happen + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "stream ID is null? channelId = " + channelId); + } + return; + } + for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + addCookie(cookie); + } + HttpResponseStatus httpStatus = HttpResponseStatus.valueOf(fullHttpResponse.status().code()); + HttpHeaders httpHeaders = new HttpHeaders(); + fullHttpResponse.headers().iteratorCharSequence().forEachRemaining(e -> httpHeaders.add(e.getKey(), e.getValue().toString())); + httpResponse = newHttpResponseBuilder(channel) + .setHttpAddress(httpAddress) + .setCookieBox(getCookieBox()) + .setStatus(httpStatus) + .setHeaders(httpHeaders) + .setByteBuffer(fullHttpResponse.content().nioBuffer()) + .build(); + CompletableFuture promise = streamIds.get(streamId); + try { + httpRequest.onResponse(httpResponse); + HttpRequest retryRequest = retry(httpRequest, httpResponse); + if (retryRequest != null) { + // retry transport, wait for completion + nettyHttpClient.retry(this, retryRequest); + } else { + HttpRequest continueRequest = continuation(httpRequest, httpResponse); + if (continueRequest != null) { + // continue with new transport, synchronous call here, wait for completion + nettyHttpClient.continuation(this, continueRequest); + } + } + if (promise != null) { + promise.complete(true); + } else { + // when transport is closed, stream IDs will be emptied + logger.log(Level.FINE, "promise is null, streamIDs lost"); + } + } catch (URLSyntaxException | IOException e) { + if (promise != null) { + promise.completeExceptionally(e); + } else { + logger.log(Level.FINE, "promise is null, can't abort"); + } + } finally { + streamIds.remove(streamId); + } + } finally { + if (httpResponse != null) { + httpResponse.release(); + } + } + } + + + @Override + public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) { + String channelId = channel.id().toString(); + StreamIds streamIds = super.streamIds.get(channelId); + if (streamIds != null) { + streamIds.put(promisedStreamId, new CompletableFuture<>()); + } + } + + @Override + protected String getRequestKey(String channelId, Integer streamId) { + return channelId + "#" + streamId; + } + + @Override + protected Channel nextChannel() throws IOException { + Channel channel = newChannel(httpAddress); + if (channel == null) { + ConnectException connectException; + if (httpAddress != null) { + connectException = new ConnectException("unable to connect to " + httpAddress); + } else if (nettyHttpClient.hasPooledNodes()) { + connectException = new ConnectException("unable to get channel from pool"); + } else { + // API misuse + connectException = new ConnectException("unable to get channel"); + } + this.throwable = connectException; + throw connectException; + } + return channel; + } + + protected Http2ChildChannelInitializer newHttp2ChildChannelInitializer(NettyHttpClientConfig clientConfig, + Http2Interaction interaction, + Channel parentChannel) { + return new Http2ChildChannelInitializer(clientConfig, interaction, parentChannel); + } + + protected HttpResponseBuilder newHttpResponseBuilder(Channel channel) { + return HttpResponse.builder(); + } +} diff --git a/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Messages.java b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Messages.java new file mode 100644 index 0000000..601e6d4 --- /dev/null +++ b/net-http-client-netty/src/main/java/org/xbib/net/http/client/netty/http2/Http2Messages.java @@ -0,0 +1,44 @@ +package org.xbib.net.http.client.netty.http2; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.client.netty.Interaction; + +public class Http2Messages extends ChannelInboundHandlerAdapter { + + private static final Logger logger = Logger.getLogger(Http2Messages.class.getName()); + + private final Interaction interaction; + + public Http2Messages(Interaction interaction) { + this.interaction = interaction; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof DefaultHttp2SettingsFrame) { + DefaultHttp2SettingsFrame settingsFrame = (DefaultHttp2SettingsFrame) msg; + interaction.settingsReceived(settingsFrame.settings()); + logger.log(Level.FINEST, "received settings "); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + Http2ConnectionPrefaceAndSettingsFrameWrittenEvent event = + (Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) evt; + logger.log(Level.FINEST, "received preface and setting written event " + event); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + interaction.fail(ctx.channel(), cause); + } +} diff --git a/net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider b/net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider new file mode 100644 index 0000000..5cb2cc7 --- /dev/null +++ b/net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.client.netty.NioClientTransportProvider \ No newline at end of file diff --git a/net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer b/net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer new file mode 100644 index 0000000..6ff1dd7 --- /dev/null +++ b/net-http-client-netty/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.HttpChannelInitializer @@ -0,0 +1,2 @@ +org.xbib.net.http.client.netty.http1.Http1ChannelInitializer +org.xbib.net.http.client.netty.http2.Http2ChannelInitializer \ No newline at end of file diff --git a/net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http1Test.java b/net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http1Test.java new file mode 100644 index 0000000..e182989 --- /dev/null +++ b/net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http1Test.java @@ -0,0 +1,37 @@ +package org.xbib.net.http.client.netty; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Http1Test { + + private static final Logger logger = Logger.getLogger(Http1Test.class.getName()); + + @Test + void testHttpGetRequest() throws Exception { + NettyHttpClientConfig config = new NettyHttpClientConfig() + .setDebug(true); + AtomicBoolean received = new AtomicBoolean(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("http://httpbin.org") + .setResponseListener(resp -> { + logger.log(Level.INFO, + "local address = " + resp.getLocalAddress() + + " got response = " + resp.getHeaders() + + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " status=" + resp.getStatus()); + received.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } +} diff --git a/net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http2Test.java b/net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http2Test.java new file mode 100644 index 0000000..9ae9293 --- /dev/null +++ b/net-http-client-netty/src/test/java/org/xbib/net/http/client/netty/Http2Test.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.client.netty; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.HttpVersion; + +class Http2Test { + + private static final Logger logger = Logger.getLogger(Http2Test.class.getName()); + + /** + * HTTP/2 cleartext is not support by many servers. + * This will return HTTP/1.1 Bad request and we run into a timeout. + */ + @Test + void testCleartext() { + Assertions.assertThrows(IOException.class, () -> { + NettyHttpClientConfig config = new NettyHttpClientConfig() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("http://httpbin.org") + .setVersion(HttpVersion.HTTP_2_0) + .setResponseListener(resp -> { + logger.log(Level.INFO, + "local address = " + resp.getLocalAddress() + + " got respons =: " + resp.getHeaders() + + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " status=" + resp.getStatus()); + }) + .build(); + client.execute(request).get().close(); + } + }); + } +} diff --git a/net-http-client-netty/src/test/resources/logging.properties b/net-http-client-netty/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-client-netty/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-client-simple/build.gradle b/net-http-client-simple/build.gradle new file mode 100644 index 0000000..0343483 --- /dev/null +++ b/net-http-client-simple/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':net-http-client') +} diff --git a/net-http-client-simple/src/main/java/module-info.java b/net-http-client-simple/src/main/java/module-info.java new file mode 100644 index 0000000..3492a33 --- /dev/null +++ b/net-http-client-simple/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module org.xbib.net.http.client.jdk { + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.client; + requires java.logging; +} diff --git a/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClient.java b/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClient.java new file mode 100644 index 0000000..e4104ab --- /dev/null +++ b/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClient.java @@ -0,0 +1,28 @@ +package org.xbib.net.http.client.jdk; + +import org.xbib.net.http.client.HttpClient; +import org.xbib.net.http.client.HttpRequest; +import org.xbib.net.http.client.HttpResponse; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class JdkHttpClient implements HttpClient { + + private final JdkHttpClientBuilder builder; + + JdkHttpClient(JdkHttpClientBuilder builder) { + this.builder = builder; + } + + @Override + public CompletableFuture execute(HttpRequest request, Function supplier) throws IOException { + return null; + } + + @Override + public void close() throws IOException { + + } +} diff --git a/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientBuilder.java b/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientBuilder.java new file mode 100644 index 0000000..7246953 --- /dev/null +++ b/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientBuilder.java @@ -0,0 +1,39 @@ +package org.xbib.net.http.client.jdk; + +import java.io.IOException; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class JdkHttpClientBuilder { + + private static final Logger logger = Logger.getLogger(JdkHttpClientBuilder.class.getName()); + + JdkHttpClientConfig jdkHttpClientConfig; + + JdkHttpClientBuilder() { + } + + public JdkHttpClientBuilder setConfig(JdkHttpClientConfig JdkHttpClientConfig) { + this.jdkHttpClientConfig = JdkHttpClientConfig; + return this; + } + + public JdkHttpClient build() throws IOException { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "installed security providers = " + + Arrays.stream(Security.getProviders()).map(Provider::getName).collect(Collectors.toList())); + } + if (jdkHttpClientConfig == null) { + jdkHttpClientConfig = createEmptyConfig(); + } + return new JdkHttpClient(this); + } + + protected JdkHttpClientConfig createEmptyConfig() { + return new JdkHttpClientConfig(); + } +} diff --git a/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientConfig.java b/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientConfig.java new file mode 100644 index 0000000..a7c71b8 --- /dev/null +++ b/net-http-client-simple/src/main/java/org/xbib/net/http/client/jdk/JdkHttpClientConfig.java @@ -0,0 +1,183 @@ +package org.xbib.net.http.client.jdk; + +import org.xbib.net.SocketConfig; +import org.xbib.net.http.client.BackOff; + +import java.util.logging.Level; + +public class JdkHttpClientConfig { + + /** + * If frame logging /traffic logging is enabled or not. + */ + private boolean debug = false; + + /** + * Default debug log level. + */ + private Level debugLogLevel = Level.FINE; + + SocketConfig socketConfig = new SocketConfig(); + + private String transportProviderName = null; + + /** + * If set to 0, then Netty will decide about thread count. + * Default is Runtime.getRuntime().availableProcessors() * 2 + */ + private int threadCount = 0; + + /** + * Set HTTP initial line length to 4k. + */ + private int maxInitialLineLength = 4 * 1024; + + /** + * Set HTTP maximum headers size to 8k. + */ + private int maxHeadersSize = 8 * 1024; + + /** + * Set HTTP chunk maximum size to 8k. + */ + private int maxChunkSize = 8 * 1024; + + /** + * Set maximum content length to 256 MB. + */ + private int maxContentLength = 256 * 1024 * 1024; + + /** + * This is Netty's default. + */ + private int maxCompositeBufferComponents = 1024; + + /** + * Default for gzip codec is true + */ + private boolean gzipEnabled = false; + + private BackOff backOff = BackOff.ZERO_BACKOFF; + + public JdkHttpClientConfig() { + } + + public JdkHttpClientConfig setDebug(boolean debug) { + this.debug = debug; + return this; + } + + public JdkHttpClientConfig enableDebug() { + this.debug = true; + return this; + } + + public JdkHttpClientConfig disableDebug() { + this.debug = false; + return this; + } + + public boolean isDebug() { + return debug; + } + + public JdkHttpClientConfig setDebugLogLevel(Level debugLogLevel) { + this.debugLogLevel = debugLogLevel; + return this; + } + + public Level getDebugLogLevel() { + return debugLogLevel; + } + + public JdkHttpClientConfig setTransportProviderName(String transportProviderName) { + this.transportProviderName = transportProviderName; + return this; + } + + public String getTransportProviderName() { + return transportProviderName; + } + + public JdkHttpClientConfig setThreadCount(int threadCount) { + this.threadCount = threadCount; + return this; + } + + public int getThreadCount() { + return threadCount; + } + + public JdkHttpClientConfig setSocketConfig(SocketConfig socketConfig) { + this.socketConfig = socketConfig; + return this; + } + + public SocketConfig getSocketConfig() { + return socketConfig; + } + + public JdkHttpClientConfig setMaxInitialLineLength(int maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + return this; + } + + public int getMaxInitialLineLength() { + return maxInitialLineLength; + } + + public JdkHttpClientConfig setMaxHeadersSize(int maxHeadersSize) { + this.maxHeadersSize = maxHeadersSize; + return this; + } + + public int getMaxHeadersSize() { + return maxHeadersSize; + } + + public JdkHttpClientConfig setMaxChunkSize(int maxChunkSize) { + this.maxChunkSize = maxChunkSize; + return this; + } + + public int getMaxChunkSize() { + return maxChunkSize; + } + + public JdkHttpClientConfig setMaxContentLength(int maxContentLength) { + this.maxContentLength = maxContentLength; + return this; + } + + public int getMaxContentLength() { + return maxContentLength; + } + + public JdkHttpClientConfig setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + this.maxCompositeBufferComponents = maxCompositeBufferComponents; + return this; + } + + public int getMaxCompositeBufferComponents() { + return maxCompositeBufferComponents; + } + + public JdkHttpClientConfig setGzipEnabled(boolean gzipEnabled) { + this.gzipEnabled = gzipEnabled; + return this; + } + + public boolean isGzipEnabled() { + return gzipEnabled; + } + + public JdkHttpClientConfig setBackOff(BackOff backOff) { + this.backOff = backOff; + return this; + } + + public BackOff getBackOff() { + return backOff; + } + +} diff --git a/net-http-client/build.gradle b/net-http-client/build.gradle new file mode 100644 index 0000000..5929d8a --- /dev/null +++ b/net-http-client/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':net-http') +} diff --git a/net-http-client/src/main/java/module-info.java b/net-http-client/src/main/java/module-info.java new file mode 100644 index 0000000..2747904 --- /dev/null +++ b/net-http-client/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module org.xbib.net.http.client { + exports org.xbib.net.http.client; + exports org.xbib.net.http.client.cookie; + requires org.xbib.net; + requires org.xbib.net.http; + requires java.logging; +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/BackOff.java b/net-http-client/src/main/java/org/xbib/net/http/client/BackOff.java new file mode 100644 index 0000000..def8b67 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/BackOff.java @@ -0,0 +1,65 @@ +package org.xbib.net.http.client; + +/** + * Back-off policy when retrying an operation. + */ +public interface BackOff { + + /** + * Indicates that no more retries should be made for use in {@link #nextBackOffMillis()}. */ + long STOP = -1L; + + /** + * Reset to initial state. + */ + void reset(); + + /** + * Gets the number of milliseconds to wait before retrying the operation or {@link #STOP} to + * indicate that no retries should be made. + * + * @return milliseconds before operation retry + * + *

+ * Example usage: + *

+ * + *
+     long backOffMillis = backoff.nextBackOffMillis();
+     if (backOffMillis == Backoff.STOP) {
+     // do not retry operation
+     } else {
+     // sleep for backOffMillis milliseconds and retry operation
+     }
+     * 
+ */ + long nextBackOffMillis(); + + /** + * Fixed back-off policy whose back-off time is always zero, meaning that the operation is retried + * immediately without waiting. + */ + BackOff ZERO_BACKOFF = new BackOff() { + + public void reset() { + } + + public long nextBackOffMillis() { + return 0; + } + }; + + /** + * Fixed back-off policy that always returns {@code #STOP} for {@link #nextBackOffMillis()}, + * meaning that the operation should not be retried. + */ + BackOff STOP_BACKOFF = new BackOff() { + + public void reset() { + } + + public long nextBackOffMillis() { + return STOP; + } + }; +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequest.java b/net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequest.java new file mode 100644 index 0000000..5606077 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequest.java @@ -0,0 +1,65 @@ +package org.xbib.net.http.client; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +public abstract class BaseHttpRequest implements HttpRequest { + + protected final BaseHttpRequestBuilder builder; + + protected BaseHttpRequest(BaseHttpRequestBuilder builder) { + this.builder = builder; + } + + @Override + public InetSocketAddress getLocalAddress() { + return builder.localAddress; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return builder.remoteAddress; + } + + @Override + public URL getBaseURL() { + return builder.url; + } + + @Override + public HttpVersion getVersion() { + return builder.httpVersion; + } + + @Override + public HttpMethod getMethod() { + return builder.httpMethod; + } + + @Override + public HttpHeaders getHeaders() { + return builder.httpHeaders; + } + + @Override + public ParameterBuilder getParameters() { + return builder.parameterBuilder; + } + + @Override + public ByteBuffer getBody() { + return builder.byteBuffer; + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return builder.byteBuffer != null ? charset.decode(builder.byteBuffer) : null; + } +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequestBuilder.java b/net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequestBuilder.java new file mode 100644 index 0000000..0e153a0 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/BaseHttpRequestBuilder.java @@ -0,0 +1,124 @@ +package org.xbib.net.http.client; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +public abstract class BaseHttpRequestBuilder implements HttpRequestBuilder { + + HttpAddress httpAddress; + + InetSocketAddress localAddress; + + InetSocketAddress remoteAddress; + + URL url; + + String requestPath; + + ParameterBuilder parameterBuilder; + + Integer sequenceId; + + Integer streamId; + + Long requestId; + + HttpVersion httpVersion; + + HttpMethod httpMethod; + + HttpHeaders httpHeaders = new HttpHeaders(); + + ByteBuffer byteBuffer; + + protected BaseHttpRequestBuilder() { + } + + public BaseHttpRequestBuilder setVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public HttpVersion getVersion() { + return httpVersion; + } + + public BaseHttpRequestBuilder setMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + public HttpMethod getMethod() { + return httpMethod; + } + + public BaseHttpRequestBuilder setHeaders(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + + public BaseHttpRequestBuilder addHeader(String key, String value) { + this.httpHeaders.add(key, value); + return this; + } + + @Override + public BaseHttpRequestBuilder setAddress(HttpAddress httpAddress) { + this.httpAddress = httpAddress; + return this; + } + @Override + public BaseHttpRequestBuilder setURL(URL url) { + this.url = url; + return this; + } + + @Override + public BaseHttpRequestBuilder setRequestPath(String requestPath) { + this.requestPath = requestPath; + return this; + } + + @Override + public BaseHttpRequestBuilder setParameterBuilder(ParameterBuilder parameterBuilder) { + this.parameterBuilder = parameterBuilder; + return this; + } + + @Override + public BaseHttpRequestBuilder setBody(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + return this; + } + + public BaseHttpRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + public BaseHttpRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + + public BaseHttpRequestBuilder setSequenceId(Integer sequenceId) { + this.sequenceId = sequenceId; + return this; + } + + public BaseHttpRequestBuilder setStreamId(Integer streamId) { + this.streamId = streamId; + return this; + } + + public BaseHttpRequestBuilder setRequestId(Long requestId) { + this.requestId = requestId; + return this; + } +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/ClientAuthMode.java b/net-http-client/src/main/java/org/xbib/net/http/client/ClientAuthMode.java new file mode 100644 index 0000000..f5c6156 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/ClientAuthMode.java @@ -0,0 +1,8 @@ +package org.xbib.net.http.client; + +/** + * Client authentication modes, useful for SSL channels. + */ +public enum ClientAuthMode { + NONE, WANT, NEED +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/ExceptionListener.java b/net-http-client/src/main/java/org/xbib/net/http/client/ExceptionListener.java new file mode 100644 index 0000000..13441ed --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/ExceptionListener.java @@ -0,0 +1,7 @@ +package org.xbib.net.http.client; + +@FunctionalInterface +public interface ExceptionListener { + + void onException(Throwable throwable); +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/ExponentialBackOff.java b/net-http-client/src/main/java/org/xbib/net/http/client/ExponentialBackOff.java new file mode 100644 index 0000000..f022f19 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/ExponentialBackOff.java @@ -0,0 +1,489 @@ +package org.xbib.net.http.client; + +/** + * Implementation of {@link BackOff} that increases the back off period for each retry attempt using + * a randomization function that grows exponentially. + * + *

+ * {@link #nextBackOffMillis()} is calculated using the following formula: + *

+ * + *
+ randomized_interval =
+ retry_interval * (random value in range [1 - randomization_factor, 1 + randomization_factor])
+ * 
+ * + *

+ * In other words {@link #nextBackOffMillis()} will range between the randomization factor + * percentage below and above the retry interval. For example, using 2 seconds as the base retry + * interval and 0.5 as the randomization factor, the actual back off period used in the next retry + * attempt will be between 1 and 3 seconds. + *

+ * + *

+ * Note: max_interval caps the retry_interval and not the randomized_interval. + *

+ * + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * {@link BackOff#STOP}. The elapsed time can be reset by calling {@link #reset()}. + *

+ * + *

+ * Example: The default retry_interval is .5 seconds, default randomization_factor is 0.5, default + * multiplier is 1.5 and the default max_interval is 1 minute. For 10 tries the sequence will be + * (values in seconds) and assuming we go over the max_elapsed_time on the 10th try: + *

+ * + *
+ request#     retry_interval     randomized_interval
+
+ 1             0.5                [0.25,   0.75]
+ 2             0.75               [0.375,  1.125]
+ 3             1.125              [0.562,  1.687]
+ 4             1.687              [0.8435, 2.53]
+ 5             2.53               [1.265,  3.795]
+ 6             3.795              [1.897,  5.692]
+ 7             5.692              [2.846,  8.538]
+ 8             8.538              [4.269, 12.807]
+ 9            12.807              [6.403, 19.210]
+ 10           19.210              {@link BackOff#STOP}
+ * 
+ * + *

+ * Implementation is not thread-safe. + *

+ */ +public class ExponentialBackOff implements BackOff { + + /** The default initial interval value in milliseconds (0.5 seconds). */ + public static final int DEFAULT_INITIAL_INTERVAL_MILLIS = 500; + + /** + * The default randomization factor (0.5 which results in a random period ranging between 50% + * below and 50% above the retry interval). + */ + public static final double DEFAULT_RANDOMIZATION_FACTOR = 0.5; + + /** The default multiplier value (1.5 which is 50% increase per back off). */ + public static final double DEFAULT_MULTIPLIER = 1.5; + + /** The default maximum back off time in milliseconds (1 minute). */ + public static final int DEFAULT_MAX_INTERVAL_MILLIS = 60000; + + /** The default maximum elapsed time in milliseconds (15 minutes). */ + public static final int DEFAULT_MAX_ELAPSED_TIME_MILLIS = 900000; + + /** The current retry interval in milliseconds. */ + private int currentIntervalMillis; + + /** The initial retry interval in milliseconds. */ + private final int initialIntervalMillis; + + /** + * The randomization factor to use for creating a range around the retry interval. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ */ + private final double randomizationFactor; + + /** The value to multiply the current interval with for each retry attempt. */ + private final double multiplier; + + /** + * The maximum value of the back off period in milliseconds. Once the retry interval reaches this + * value it stops increasing. + */ + private final int maxIntervalMillis; + + /** + * The system time in nanoseconds. It is calculated when an ExponentialBackOffPolicy instance is + * created and is reset when {@link #reset()} is called. + */ + private long startTimeNanos; + + /** + * The maximum elapsed time after instantiating {@link ExponentialBackOff} or calling + * {@link #reset()} after which {@link #nextBackOffMillis()} returns {@link BackOff#STOP}. + */ + private final int maxElapsedTimeMillis; + + /** Nano clock. */ + private final NanoClock nanoClock; + + /** + * Creates an instance of ExponentialBackOffPolicy using default values. + * + *

+ * To override the defaults use {@link Builder}. + *

+ * + *
    + *
  • {@code initialIntervalMillis} defaults to {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}
  • + *
  • {@code randomizationFactor} defaults to {@link #DEFAULT_RANDOMIZATION_FACTOR}
  • + *
  • {@code multiplier} defaults to {@link #DEFAULT_MULTIPLIER}
  • + *
  • {@code maxIntervalMillis} defaults to {@link #DEFAULT_MAX_INTERVAL_MILLIS}
  • + *
  • {@code maxElapsedTimeMillis} defaults in {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}
  • + *
+ */ + public ExponentialBackOff() { + this(new Builder()); + } + + /** + * @param builder builder + */ + private ExponentialBackOff(Builder builder) { + initialIntervalMillis = builder.initialIntervalMillis; + randomizationFactor = builder.randomizationFactor; + multiplier = builder.multiplier; + maxIntervalMillis = builder.maxIntervalMillis; + maxElapsedTimeMillis = builder.maxElapsedTimeMillis; + nanoClock = builder.nanoClock; + reset(); + } + + /** + * Sets the interval back to the initial retry interval and restarts the timer. + */ + public final void reset() { + currentIntervalMillis = initialIntervalMillis; + startTimeNanos = nanoClock.nanoTime(); + } + + public void setStartTimeNanos(long startTimeNanos) { + this.startTimeNanos = startTimeNanos; + } + + /** + * {@inheritDoc} + * + *

+ * This method calculates the next back off interval using the formula: randomized_interval = + * retry_interval +/- (randomization_factor * retry_interval) + *

+ * + *

+ * Subclasses may override if a different algorithm is required. + *

+ */ + public long nextBackOffMillis() { + // Make sure we have not gone over the maximum elapsed time. + if (getElapsedTimeMillis() > maxElapsedTimeMillis) { + return STOP; + } + int randomizedInterval = + getRandomValueFromInterval(randomizationFactor, Math.random(), currentIntervalMillis); + incrementCurrentInterval(); + return randomizedInterval; + } + + /** + * Returns a random value from the interval [randomizationFactor * currentInterval, + * randomizationFactor * currentInterval]. + * @param randomizationFactor the randomization factor + * @param random scaling factor + * @param currentIntervalMillis milliseconds + * @return random value + */ + public static int getRandomValueFromInterval(double randomizationFactor, double random, int currentIntervalMillis) { + double delta = randomizationFactor * currentIntervalMillis; + double minInterval = currentIntervalMillis - delta; + double maxInterval = currentIntervalMillis + delta; + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + return (int) (minInterval + (random * (maxInterval - minInterval + 1))); + } + + /** + * Returns the initial retry interval in milliseconds. + * @return interval milliseconds + */ + public final int getInitialIntervalMillis() { + return initialIntervalMillis; + } + + /** + * Returns the randomization factor to use for creating a range around the retry interval. + * @return randomization factor + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ */ + public final double getRandomizationFactor() { + return randomizationFactor; + } + + /** + * Returns the current retry interval in milliseconds. + * @return current interval in milliseconds + */ + public final int getCurrentIntervalMillis() { + return currentIntervalMillis; + } + + /** + * Returns the value to multiply the current interval with for each retry attempt. + * @return multiplier + */ + public final double getMultiplier() { + return multiplier; + } + + /** + * Returns the maximum value of the back off period in milliseconds. Once the current interval + * reaches this value it stops increasing. + * @return maximum interval value in milliseconds + */ + public final int getMaxIntervalMillis() { + return maxIntervalMillis; + } + + /** + * Returns the maximum elapsed time in milliseconds. + * @return maximum elapsed time in milliseconds + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * {@link BackOff#STOP}. The elapsed time can be reset by calling {@link #reset()}. + *

+ */ + public final int getMaxElapsedTimeMillis() { + return maxElapsedTimeMillis; + } + + /** + * Returns the elapsed time in milliseconds since an {@link ExponentialBackOff} instance is + * created and is reset when {@link #reset()} is called. + * @return the elapsed time in milliseconds + *

+ * The elapsed time is computed using {@link System#nanoTime()}. + *

+ */ + public final long getElapsedTimeMillis() { + return (nanoClock.nanoTime() - startTimeNanos) / 1000000; + } + + /** + * Increments the current interval by multiplying it with the multiplier. + */ + private void incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if (currentIntervalMillis >= maxIntervalMillis / multiplier) { + currentIntervalMillis = maxIntervalMillis; + } else { + currentIntervalMillis *= multiplier; + } + } + + /** + * Nano clock which can be used to measure elapsed time in nanoseconds. + * + *

+ * The default system implementation can be accessed at {@link #SYSTEM}. Alternative implementations + * may be used for testing. + *

+ * + */ + public interface NanoClock { + + /** + * Returns the current value of the most precise available system timer, in nanoseconds for use to + * measure elapsed time, to match the behavior of {@link System#nanoTime()}. + * @return value of timer in nanoseconds + */ + long nanoTime(); + + /** + * Provides the default System implementation of a nano clock by using {@link System#nanoTime()}. + */ + NanoClock SYSTEM = System::nanoTime; + } + + /** + * Builder for {@link ExponentialBackOff}. + * + *

+ * Implementation is not thread-safe. + *

+ */ + public static class Builder { + + /** The initial retry interval in milliseconds. */ + private int initialIntervalMillis = DEFAULT_INITIAL_INTERVAL_MILLIS; + + /** + * The randomization factor to use for creating a range around the retry interval. + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ */ + private double randomizationFactor = DEFAULT_RANDOMIZATION_FACTOR; + + /** + * The value to multiply the current interval with for each retry attempt. + */ + private double multiplier = DEFAULT_MULTIPLIER; + + /** + * The maximum value of the back off period in milliseconds. Once the retry interval reaches + * this value it stops increasing. + */ + private int maxIntervalMillis = DEFAULT_MAX_INTERVAL_MILLIS; + + /** + * The maximum elapsed time in milliseconds after instantiating {@link ExponentialBackOff} or + * calling {@link #reset()} after which {@link #nextBackOffMillis()} returns + * {@link BackOff#STOP}. + */ + private int maxElapsedTimeMillis = DEFAULT_MAX_ELAPSED_TIME_MILLIS; + + /** + * Nano clock. + */ + private NanoClock nanoClock = NanoClock.SYSTEM; + + public Builder() { + } + + /** + * Builds a new instance of {@link ExponentialBackOff}. + * @return an {@link ExponentialBackOff} instance + */ + public ExponentialBackOff build() { + if (initialIntervalMillis <= 0) { + throw new IllegalArgumentException(); + } + if (!(0 <= randomizationFactor && randomizationFactor < 1)) { + throw new IllegalArgumentException(); + } + if (multiplier < 1) { + throw new IllegalArgumentException(); + } + if ((maxIntervalMillis < initialIntervalMillis)) { + throw new IllegalArgumentException(); + } + if (maxElapsedTimeMillis <= 0) { + throw new IllegalArgumentException(); + } + return new ExponentialBackOff(this); + } + + /** + * Sets the initial retry interval in milliseconds. The default value is + * {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. Must be {@code > 0}. + * @param initialIntervalMillis interval milliseconds + * @return the builder + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setInitialIntervalMillis(int initialIntervalMillis) { + this.initialIntervalMillis = initialIntervalMillis; + return this; + } + + /** + * Sets the randomization factor to use for creating a range around the retry interval. The + * default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}. Must fall in the range + * {@code 0 <= randomizationFactor < 1}. + * @param randomizationFactor the randomization factor + * @return the builder + * + *

+ * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% + * above the retry interval. + *

+ * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setRandomizationFactor(double randomizationFactor) { + this.randomizationFactor = randomizationFactor; + return this; + } + + /** + * Sets the value to multiply the current interval with for each retry attempt. The default + * value is {@link #DEFAULT_MULTIPLIER}. Must be {@code >= 1}. + * @param multiplier the multiplier + * @return the builder + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setMultiplier(double multiplier) { + this.multiplier = multiplier; + return this; + } + + /** + * Sets the maximum value of the back off period in milliseconds. Once the current interval + * reaches this value it stops increasing. The default value is + * {@link #DEFAULT_MAX_INTERVAL_MILLIS}. + * @param maxIntervalMillis maximum interval in miliseconds + * @return the builder + * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setMaxIntervalMillis(int maxIntervalMillis) { + this.maxIntervalMillis = maxIntervalMillis; + return this; + } + + /** + * Sets the maximum elapsed time in milliseconds. The default value is + * {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. Must be {@code > 0}. + * @param maxElapsedTimeMillis maximum elapsed time millis + * @return the builder + * + *

+ * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the + * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning + * {@link BackOff#STOP}. The elapsed time can be reset by calling {@link #reset()}. + *

+ * + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setMaxElapsedTimeMillis(int maxElapsedTimeMillis) { + this.maxElapsedTimeMillis = maxElapsedTimeMillis; + return this; + } + + /** + * Sets the nano clock ({@link NanoClock#SYSTEM} by default). + * @param nanoClock the nano clock + * @return the builder + *

+ * Overriding is only supported for the purpose of calling the super implementation and changing + * the return type, but nothing else. + *

+ */ + public Builder setNanoClock(NanoClock nanoClock) { + if (nanoClock != null) { + this.nanoClock = nanoClock; + } + return this; + } + } +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/HttpClient.java b/net-http-client/src/main/java/org/xbib/net/http/client/HttpClient.java new file mode 100644 index 0000000..f5110b6 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/HttpClient.java @@ -0,0 +1,12 @@ +package org.xbib.net.http.client; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public interface HttpClient extends Closeable { + + CompletableFuture execute(Req request, + Function supplier) throws IOException; +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/HttpRequest.java b/net-http-client/src/main/java/org/xbib/net/http/client/HttpRequest.java new file mode 100644 index 0000000..4a651cf --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/HttpRequest.java @@ -0,0 +1,29 @@ +package org.xbib.net.http.client; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.Request; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +public interface HttpRequest extends Request { + + URL getURL(); + + HttpVersion getVersion(); + + HttpMethod getMethod(); + + HttpHeaders getHeaders(); + + ParameterBuilder getParameters(); + + ByteBuffer getBody(); + + CharBuffer getBodyAsChars(Charset charset); + +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/HttpRequestBuilder.java b/net-http-client/src/main/java/org/xbib/net/http/client/HttpRequestBuilder.java new file mode 100644 index 0000000..7ba3d17 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/HttpRequestBuilder.java @@ -0,0 +1,23 @@ +package org.xbib.net.http.client; + +import java.nio.ByteBuffer; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; + +public interface HttpRequestBuilder { + + HttpRequestBuilder setAddress(HttpAddress httpAddress); + + HttpRequestBuilder setURL(URL url); + + HttpRequestBuilder setRequestPath(String requestPath); + + HttpRequestBuilder setParameterBuilder(ParameterBuilder parameterBuilder); + + HttpRequestBuilder setBody(ByteBuffer byteBuffer); + + HttpRequest build() throws UnmappableCharacterException, MalformedInputException; +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/HttpResponse.java b/net-http-client/src/main/java/org/xbib/net/http/client/HttpResponse.java new file mode 100644 index 0000000..7a3b9fc --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/HttpResponse.java @@ -0,0 +1,34 @@ +package org.xbib.net.http.client; + +import java.io.InputStream; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.CookieBox; + +public interface HttpResponse { + + SocketAddress getLocalAddress(); + + SocketAddress getRemoteAddress(); + + HttpAddress getAddress(); + + HttpResponseStatus getStatus(); + + HttpHeaders getHeaders(); + + CookieBox getCookies(); + + ByteBuffer getBody(); + + CharBuffer getBodyAsChars(Charset charset); + + InputStream getBodyAsStream(); + + void release(); +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/HttpResponseBuilder.java b/net-http-client/src/main/java/org/xbib/net/http/client/HttpResponseBuilder.java new file mode 100644 index 0000000..b9f8546 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/HttpResponseBuilder.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.client; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.cookie.CookieBox; + +public interface HttpResponseBuilder { + + HttpResponseBuilder setHttpAddress(HttpAddress httpAddress); + + HttpResponseBuilder setCookieBox(CookieBox cookieBox); +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/ResponseListener.java b/net-http-client/src/main/java/org/xbib/net/http/client/ResponseListener.java new file mode 100644 index 0000000..8adf91b --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/ResponseListener.java @@ -0,0 +1,7 @@ +package org.xbib.net.http.client; + +@FunctionalInterface +public interface ResponseListener { + + void onResponse(R response); +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/TimeoutListener.java b/net-http-client/src/main/java/org/xbib/net/http/client/TimeoutListener.java new file mode 100644 index 0000000..99f0b3b --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/TimeoutListener.java @@ -0,0 +1,7 @@ +package org.xbib.net.http.client; + +@FunctionalInterface +public interface TimeoutListener { + + void onTimeout(HttpRequest httpRequest); +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieDecoder.java b/net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieDecoder.java new file mode 100644 index 0000000..836cb40 --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieDecoder.java @@ -0,0 +1,255 @@ +package org.xbib.net.http.client.cookie; + +import java.time.Instant; +import java.util.Locale; +import java.util.Objects; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieHeaderNames; +import org.xbib.net.http.cookie.DefaultCookie; +import org.xbib.net.http.cookie.SameSite; +import org.xbib.net.util.DateTimeUtil; + +/** + * A RFC6265 compliant cookie decoder to be used client side. + * + * It will store the way the raw value was wrapped in {@link Cookie#setWrap(boolean)} so it can be + * eventually sent back to the Origin server as is. + * + */ +public final class CookieDecoder extends org.xbib.net.http.cookie.CookieDecoder { + + /** + * Strict encoder that validates that name and value chars are in the valid scope defined in RFC6265. + */ + public static final CookieDecoder STRICT = new CookieDecoder(true); + + /** + * Lax instance that doesn't validate name and value. + */ + public static final CookieDecoder LAX = new CookieDecoder(false); + + private CookieDecoder(boolean strict) { + super(strict); + } + + /** + * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}. + * + * @param header header + * @return the decoded {@link Cookie} + */ + public Cookie decode(String header) { + final int headerLen = Objects.requireNonNull(header, "header").length(); + if (headerLen == 0) { + throw new IllegalArgumentException("header length is 0"); + } + CookieBuilder cookieBuilder = null; + int i = 0; + while (i < headerLen) { + while (i < headerLen) { + // Skip spaces and separators. + char c = header.charAt(i); + if (c == ',') { + // Having multiple cookies in a single Set-Cookie header is + // deprecated, modern browsers only parse the first one + break; + } else { + switch (c) { + case '\t': + case '\n': + case 0x0b: + case '\f': + case '\r': + case ' ': + case ';': + i++; + continue; + default: + break; + } + } + break; + } + int nameBegin = i; + int nameEnd = 0; + int valueBegin = 0; + int valueEnd = 0; + while (i < headerLen) { + char curChar = header.charAt(i); + if (curChar == ';') { + nameEnd = i; + valueBegin = valueEnd = -1; + break; + } else if (curChar == '=') { + nameEnd = i; + i++; + if (i == headerLen) { + valueBegin = valueEnd = 0; + break; + } + valueBegin = i; + int semiPos = header.indexOf(';', i); + valueEnd = i = semiPos > 0 ? semiPos : headerLen; + break; + } else { + i++; + } + if (i == headerLen) { + nameEnd = headerLen; + valueBegin = valueEnd = -1; + break; + } + } + if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') { + valueEnd--; + } + if (nameEnd >= nameBegin && valueEnd >= valueBegin) { + if (cookieBuilder == null) { + DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd); + if (cookie == null) { + return null; + } + cookieBuilder = new CookieBuilder(cookie, header); + } else { + cookieBuilder.appendAttribute(nameBegin, nameEnd, valueBegin, valueEnd); + } + } + } + if (cookieBuilder == null) { + throw new IllegalArgumentException("no cookie found"); + } + return cookieBuilder.cookie(); + } + + private static class CookieBuilder { + + private final String header; + + private final DefaultCookie cookie; + + private String domain; + + private String path; + + private long maxAge = Long.MIN_VALUE; + + private int expiresStart; + + private int expiresEnd; + + private boolean secure; + + private boolean httpOnly; + + private SameSite sameSite = SameSite.STRICT; + + CookieBuilder(DefaultCookie cookie, String header) { + this.cookie = cookie; + this.header = header; + } + + Cookie cookie() { + cookie.setDomain(domain); + cookie.setPath(path); + cookie.setMaxAge(mergeMaxAgeAndExpires()); + cookie.setSecure(secure); + cookie.setHttpOnly(httpOnly); + cookie.setSameSite(sameSite); + return cookie; + } + + /** + * Parse and store a key-value pair. First one is considered to be the + * cookie name/value. Unknown attribute names are silently discarded. + * + * @param keyStart + * where the key starts in the header + * @param keyEnd + * where the key ends in the header + * @param valueStart + * where the value starts in the header + * @param valueEnd + * where the value ends in the header + */ + void appendAttribute(int keyStart, int keyEnd, int valueStart, int valueEnd) { + int length = keyEnd - keyStart; + if (length == 4) { + parse4(keyStart, valueStart, valueEnd); + } else if (length == 6) { + parse6(keyStart, valueStart, valueEnd); + } else if (length == 7) { + parse7(keyStart, valueStart, valueEnd); + } else if (length == 8) { + parse8(keyStart, valueStart, valueEnd); + } + } + + private void parse4(int nameStart, int valueStart, int valueEnd) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.PATH, 0, 4)) { + path = computeValue(valueStart, valueEnd); + } + } + + private void parse6(int nameStart, int valueStart, int valueEnd) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.DOMAIN, 0, 5)) { + domain = computeValue(valueStart, valueEnd); + } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SECURE, 0, 5)) { + secure = true; + } + } + + private void setMaxAge(String value) { + try { + maxAge = Math.max(Long.parseLong(value), 0L); + } catch (NumberFormatException e1) { + // ignore failure to parse -> treat as session cookie + } + } + + private long mergeMaxAgeAndExpires() { + if (maxAge != Long.MIN_VALUE) { + return maxAge; + } else if (isValueDefined(expiresStart, expiresEnd)) { + Instant expiresDate = DateTimeUtil.parseDate(header, expiresStart, expiresEnd); + if (expiresDate != null) { + Instant now = Instant.now(); + long maxAgeMillis = expiresDate.toEpochMilli() - now.toEpochMilli(); + return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0); + } + } + return Long.MIN_VALUE; + } + + private void parse7(int nameStart, int valueStart, int valueEnd) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) { + expiresStart = valueStart; + expiresEnd = valueEnd; + } else if (header.regionMatches(true, nameStart, CookieHeaderNames.MAX_AGE, 0, 7)) { + setMaxAge(computeValue(valueStart, valueEnd)); + } + } + + private void parse8(int nameStart, int valueStart, int valueEnd) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) { + httpOnly = true; + } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) { + String string = computeValue(valueStart, valueEnd); + if (string != null) { + setSameSite(SameSite.valueOf(string.toUpperCase(Locale.ROOT))); + } + } + } + + private static boolean isValueDefined(int valueStart, int valueEnd) { + return valueStart != -1 && valueStart != valueEnd; + } + + private String computeValue(int valueStart, int valueEnd) { + return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null; + } + + private void setSameSite(SameSite value) { + sameSite = value; + } + } +} diff --git a/net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieEncoder.java b/net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieEncoder.java new file mode 100644 index 0000000..edd0fcf --- /dev/null +++ b/net-http-client/src/main/java/org/xbib/net/http/client/cookie/CookieEncoder.java @@ -0,0 +1,199 @@ +package org.xbib.net.http.client.cookie; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieUtil; +import org.xbib.net.http.cookie.DefaultCookie; + +/** + * A RFC6265 compliant cookie encoder to be used client side, so + * only name=value pairs are sent. + * + * Note that multiple cookies are supposed to be sent at once in a single "Cookie" header. + * + *
+ * // Example
+ * HttpRequest req = ...
+ * res.setHeader("Cookie", {@link CookieEncoder}.encode("JSESSIONID", "1234"))
+ * 
+ * + */ +public final class CookieEncoder extends org.xbib.net.http.cookie.CookieEncoder { + + /** + * Strict encoder that validates that name and value chars are in the valid scope and (for methods that accept + * multiple cookies) sorts cookies into order of decreasing path length, as specified in RFC6265. + */ + public static final CookieEncoder STRICT = new CookieEncoder(true); + + /** + * Lax instance that doesn't validate name and value, and (for methods that accept multiple cookies) keeps + * cookies in the order in which they were given. + */ + public static final CookieEncoder LAX = new CookieEncoder(false); + + private CookieEncoder(boolean strict) { + super(strict); + } + + /** + * Encodes the specified cookie into a Cookie header value. + * + * @param name + * the cookie name + * @param value + * the cookie value + * @return a Rfc6265 style Cookie header value + */ + public String encode(String name, String value) { + return encode(new DefaultCookie(name, value)); + } + + /** + * Encodes the specified cookie into a Cookie header value. + * + * @param cookie the specified cookie + * @return a Rfc6265 style Cookie header value + */ + public String encode(Cookie cookie) { + StringBuilder buf = new StringBuilder(); + encode(buf, Objects.requireNonNull(cookie, "cookie")); + return CookieUtil.stripTrailingSeparator(buf); + } + + /** + * Sort cookies into decreasing order of path length, breaking ties by sorting into increasing chronological + * order of creation time, as recommended by RFC 6265. + */ + private static final Comparator COOKIE_COMPARATOR = (c1, c2) -> { + String path1 = c1.path(); + String path2 = c2.path(); + // Cookies with unspecified path default to the path of the request. We don't + // know the request path here, but we assume that the length of an unspecified + // path is longer than any specified path (i.e. pathless cookies come first), + // because setting cookies with a path longer than the request path is of + // limited use. + int len1 = path1 == null ? Integer.MAX_VALUE : path1.length(); + int len2 = path2 == null ? Integer.MAX_VALUE : path2.length(); + int diff = len2 - len1; + if (diff != 0) { + return diff; + } + // Rely on Java's sort stability to retain creation order in cases where + // cookies have same path length. + return -1; + }; + + /** + * Encodes the specified cookies into a single Cookie header value. + * + * @param cookies + * some cookies + * @return a Rfc6265 style Cookie header value, null if no cookies are passed. + */ + public String encode(Cookie... cookies) { + if (Objects.requireNonNull(cookies, "cookies").length == 0) { + return null; + } + StringBuilder buf = new StringBuilder(); + if (strict) { + if (cookies.length == 1) { + encode(buf, cookies[0]); + } else { + Cookie[] cookiesSorted = Arrays.copyOf(cookies, cookies.length); + Arrays.sort(cookiesSorted, COOKIE_COMPARATOR); + for (Cookie c : cookiesSorted) { + encode(buf, c); + } + } + } else { + for (Cookie c : cookies) { + encode(buf, c); + } + } + return CookieUtil.stripTrailingSeparatorOrNull(buf); + } + + /** + * Encodes the specified cookies into a single Cookie header value. + * + * @param cookies + * some cookies + * @return a Rfc6265 style Cookie header value, null if no cookies are passed. + */ + public String encode(Collection cookies) { + if (Objects.requireNonNull(cookies, "cookies").isEmpty()) { + return null; + } + StringBuilder buf = new StringBuilder(); + if (strict) { + if (cookies.size() == 1) { + encode(buf, cookies.iterator().next()); + } else { + Cookie[] cookiesSorted = cookies.toArray(new Cookie[cookies.size()]); + Arrays.sort(cookiesSorted, COOKIE_COMPARATOR); + for (Cookie c : cookiesSorted) { + encode(buf, c); + } + } + } else { + for (Cookie c : cookies) { + encode(buf, c); + } + } + return CookieUtil.stripTrailingSeparatorOrNull(buf); + } + + /** + * Encodes the specified cookies into a single Cookie header value. + * + * @param cookies some cookies + * @return a Rfc6265 style Cookie header value, null if no cookies are passed. + */ + public String encode(Iterable cookies) { + Iterator cookiesIt = Objects.requireNonNull(cookies, "cookies").iterator(); + if (!cookiesIt.hasNext()) { + return null; + } + StringBuilder buf = new StringBuilder(); + if (strict) { + Cookie firstCookie = cookiesIt.next(); + if (!cookiesIt.hasNext()) { + encode(buf, firstCookie); + } else { + List cookiesList = new ArrayList<>(); + cookiesList.add(firstCookie); + while (cookiesIt.hasNext()) { + cookiesList.add(cookiesIt.next()); + } + Cookie[] cookiesSorted = cookiesList.toArray(new Cookie[0]); + Arrays.sort(cookiesSorted, COOKIE_COMPARATOR); + for (Cookie c : cookiesSorted) { + encode(buf, c); + } + } + } else { + while (cookiesIt.hasNext()) { + encode(buf, cookiesIt.next()); + } + } + return CookieUtil.stripTrailingSeparatorOrNull(buf); + } + + public void encode(StringBuilder buf, Cookie c) { + final String name = c.name(); + final String value = c.value() != null ? c.value() : ""; + validateCookie(name, value); + if (c.wrap()) { + CookieUtil.addQuoted(buf, name, value); + } else { + CookieUtil.add(buf, name, value); + } + } +} diff --git a/net-http-client/src/test/java/org/xbib/net/http/client/ClientCookieDecoderTest.java b/net-http-client/src/test/java/org/xbib/net/http/client/ClientCookieDecoderTest.java new file mode 100644 index 0000000..f6540d4 --- /dev/null +++ b/net-http-client/src/test/java/org/xbib/net/http/client/ClientCookieDecoderTest.java @@ -0,0 +1,274 @@ +package org.xbib.net.http.client; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.cookie.CookieDecoder; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.SameSite; +import org.xbib.net.util.DateTimeUtil; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CookieDecoderTest { + + @Test + void testDecodingSingleCookieV0() { + long millis = System.currentTimeMillis() + 50000; + String cookieString = "myCookie=myValue;expires=" + + DateTimeUtil.formatRfc1123(millis) + + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;"; + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + assertNotEquals(Long.MIN_VALUE, cookie.maxAge()); + assertTrue(cookie.maxAge() >= 40 && cookie.maxAge() <= 60); + } + + @Test + void testDecodingSingleCookieV0ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=0;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingSingleCookieV1() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere" + + ";secure;comment=this is a comment;version=1;"; + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingSingleCookieV1ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=1;" + + "commentURL=http://aurl.com;port='80,8080';discard;"; + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingSingleCookieV2() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = CookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingComplexCookie() { + String c1 = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=\"http://aurl.com\";port='80,8080';discard;"; + Cookie cookie = CookieDecoder.STRICT.decode(c1); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingQuotedCookie() { + Collection sources = new ArrayList<>(); + sources.add("a=\"\","); + sources.add("b=\"1\","); + Collection cookies = new ArrayList<>(); + for (String source : sources) { + cookies.add(CookieDecoder.STRICT.decode(source)); + } + Iterator it = cookies.iterator(); + Cookie c; + c = it.next(); + assertEquals("a", c.name()); + assertEquals("", c.value()); + c = it.next(); + assertEquals("b", c.name()); + assertEquals("1", c.value()); + assertFalse(it.hasNext()); + } + + @Test + void testDecodingGoogleAnalyticsCookie() { + String source = "ARPT=LWUKQPSWRTUN04CKKJI; " + + "kw-2E343B92-B097-442c-BFA5-BE371E0325A2=unfinished furniture; " + + "__utma=48461872.1094088325.1258140131.1258140131.1258140131.1; " + + "__utmb=48461872.13.10.1258140131; __utmc=48461872; " + + "__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" + + "utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance,/clearance,/32/dept.html"; + Cookie cookie = CookieDecoder.STRICT.decode(source); + assertNotNull(cookie); + assertEquals("ARPT", cookie.name()); + assertEquals("LWUKQPSWRTUN04CKKJI", cookie.value()); + } + + @Test + void testDecodingLongDates() { + ZonedDateTime zonedDateTime = ZonedDateTime.of(2100,12,31,23,59,59,0, ZoneId.of("UTC")); + long expectedMaxAge = ((zonedDateTime.toEpochSecond() * 1000L) - System.currentTimeMillis()) / 1000; + String source = "Format=EU; expires=Fri, 31-Dec-2100 23:59:59 GMT; path=/"; + Cookie cookie = CookieDecoder.STRICT.decode(source); + assertNotNull(cookie); + assertTrue(Math.abs(expectedMaxAge - cookie.maxAge()) < 2); + } + + @Test + void testDecodingValueWithCommaFails() { + String source = "UserCookie=timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE;" + + " expires=Sat, 01-Dec-2012 10:53:31 GMT; path=/"; + Cookie cookie = CookieDecoder.STRICT.decode(source); + assertNull(cookie); + } + + @Test + void testDecodingWeirdNames1() { + String src = "path=; expires=Mon, 01-Jan-1990 00:00:00 GMT; path=/; domain=.www.google.com"; + Cookie cookie = CookieDecoder.STRICT.decode(src); + assertNotNull(cookie); + assertEquals("path", cookie.name()); + assertEquals("", cookie.value()); + assertEquals("/", cookie.path()); + } + + @Test + void testDecodingWeirdNames2() { + String src = "HTTPOnly="; + Cookie cookie = CookieDecoder.STRICT.decode(src); + assertNotNull(cookie); + assertEquals("HTTPOnly", cookie.name()); + assertEquals("", cookie.value()); + } + + @Test + void testDecodingValuesWithCommasAndEqualsFails() { + assertNull(CookieDecoder.STRICT.decode( "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E")); + } + + @Test + void testDecodingLongValue() { + String longValue = "b___$Q__$ha______" + + "%=J^wI__3iD____$=HbQW__3iF____#=J^wI__3iH____%=J^wI__3iM____%=J^wI__3iS____" + + "#=J^wI__3iU____%=J^wI__3iZ____#=J^wI__3i]____%=J^wI__3ig____%=J^wI__3ij____" + + "%=J^wI__3ik____#=J^wI__3il____$=HbQW__3in____%=J^wI__3ip____$=HbQW__3iq____" + + "$=HbQW__3it____%=J^wI__3ix____#=J^wI__3j_____$=HbQW__3j%____$=HbQW__3j'____" + + "%=J^wI__3j(____%=J^wI__9mJ____'=KqtH__=SE__M____" + + "'=KqtH__s1X____$=MMyc__s1_____#=MN#O__ypn____'=KqtH__ypr____'=KqtH_#%h_____" + + "%=KqtH_#%o_____'=KqtH_#)H6______'=KqtH_#]9R____$=H/Lt_#]I6____#=KqtH_#]Z#____%=KqtH_#^*N____" + + "#=KqtH_#^:m____#=KqtH_#_*_____%=J^wI_#`-7____#=KqtH_#`T>____'=KqtH_#`T?____" + + "'=KqtH_#`TA____'=KqtH_#`TB____'=KqtH_#`TG____'=KqtH_#`TP____#=KqtH_#`U_____" + + "'=KqtH_#`U/____'=KqtH_#`U0____#=KqtH_#`U9____'=KqtH_#aEQ____%=KqtH_#b<)____" + + "'=KqtH_#c9-____%=KqtH_#dxC____%=KqtH_#dxE____%=KqtH_#ev$____'=KqtH_#fBi____" + + "#=KqtH_#fBj____'=KqtH_#fG)____'=KqtH_#fG+____'=KqtH_#g*B____'=KqtH_$>hD____+=J^x0_$?lW____'=KqtH_$?ll____'=KqtH_$?lm____" + + "%=KqtH_$?mi____'=KqtH_$?mx____'=KqtH_$D7]____#=J_#p_$D@T____#=J_#p_$V + CookieEncoder.STRICT.encode(new DefaultCookie("myCookie", "foo;bar"))); + } +} diff --git a/net-http-netty-boringssl/build.gradle b/net-http-netty-boringssl/build.gradle new file mode 100644 index 0000000..42b89ee --- /dev/null +++ b/net-http-netty-boringssl/build.gradle @@ -0,0 +1,8 @@ +dependencies { + api project(':net-http-server-netty-secure') + api project(':net-http-client-netty-secure') + runtimeOnly(variantOf(libs.netty.boringssl) { classifier('linux-x86_64') }) + runtimeOnly(variantOf(libs.netty.boringssl) { classifier('linux-aarch_64') }) + runtimeOnly(variantOf(libs.netty.boringssl) { classifier('osx-x86_64') }) + runtimeOnly(variantOf(libs.netty.boringssl) { classifier('osx-aarch_64') }) +} diff --git a/net-http-netty-boringssl/src/main/java/module-info.java b/net-http-netty-boringssl/src/main/java/module-info.java new file mode 100644 index 0000000..df3f821 --- /dev/null +++ b/net-http-netty-boringssl/src/main/java/module-info.java @@ -0,0 +1,17 @@ +import org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; +import org.xbib.net.http.netty.boringssl.BoringSSLClientSecureSocketProvider; +import org.xbib.net.http.netty.boringssl.BoringSSLServerSecureSocketProvider; + +module org.xbib.net.http.netty.boringssl { + exports org.xbib.net.http.netty.boringssl; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.xbib.net.http.server.netty.secure; + requires org.xbib.net.http.client.netty.secure; + requires io.netty.handler; + requires io.netty.codec.http2; + provides ClientSecureSocketProvider with BoringSSLClientSecureSocketProvider; + provides ServerSecureSocketProvider with BoringSSLServerSecureSocketProvider; +} diff --git a/net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLClientSecureSocketProvider.java b/net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLClientSecureSocketProvider.java new file mode 100644 index 0000000..6f7adb5 --- /dev/null +++ b/net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLClientSecureSocketProvider.java @@ -0,0 +1,63 @@ +package org.xbib.net.http.netty.boringssl; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import java.security.Provider; +import java.util.List; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; + +public class BoringSSLClientSecureSocketProvider implements ClientSecureSocketProvider { + + public BoringSSLClientSecureSocketProvider() { + } + + @Override + public String name() { + return "BORINGSSL"; + } + + @Override + public Provider securityProvider(HttpAddress httpAddress) { + return null; + } + + @Override + public SslProvider sslProvider(HttpAddress httpAddress) { + return SslProvider.OPENSSL; + } + + @Override + public List ciphers(HttpAddress httpAddress) { + if (httpAddress.getVersion().majorVersion() == 2) { + return Http2SecurityUtil.CIPHERS; + } + return List.of( + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ); + } + + @Override + public CipherSuiteFilter cipherSuiteFilter(HttpAddress httpAddress) { + return SupportedCipherSuiteFilter.INSTANCE; + } + + @Override + public String[] protocols(HttpAddress httpAddress) { + // kill SSLv2Hello, disable TLS 1.3 on old OpenSSL + return OpenSsl.isAvailable() && OpenSsl.version() <= 0x10101009L ? + new String[] { "TLSv1.2" } : + new String[] { "TLSv1.3", "TLSv1.2" }; + } +} diff --git a/net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLServerSecureSocketProvider.java b/net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLServerSecureSocketProvider.java new file mode 100644 index 0000000..749ac3c --- /dev/null +++ b/net-http-netty-boringssl/src/main/java/org/xbib/net/http/netty/boringssl/BoringSSLServerSecureSocketProvider.java @@ -0,0 +1,61 @@ +package org.xbib.net.http.netty.boringssl; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import java.security.Provider; +import java.util.List; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider; + +public class BoringSSLServerSecureSocketProvider implements ServerSecureSocketProvider { + + public BoringSSLServerSecureSocketProvider() { + } + + @Override + public String name() { + return "BORINGSSL"; + } + + @Override + public Provider securityProvider(HttpAddress httpAddress) { + return null; + } + + @Override + public SslProvider sslProvider(HttpAddress httpAddress) { + return SslProvider.OPENSSL; + } + + @Override + public List ciphers(HttpAddress httpAddress) { + if (httpAddress.getVersion().majorVersion() == 2) { + return Http2SecurityUtil.CIPHERS; + } + return List.of( + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256"); + } + + @Override + public CipherSuiteFilter cipherSuiteFilter(HttpAddress httpAddress) { + return SupportedCipherSuiteFilter.INSTANCE; + } + + @Override + public String[] protocols(HttpAddress httpAddress) { + return OpenSsl.isAvailable() && OpenSsl.version() <= 0x10101009L ? + new String[] { "TLSv1.2" } : + new String[] { "TLSv1.3", "TLSv1.2" }; + } +} diff --git a/net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider b/net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider new file mode 100644 index 0000000..824b301 --- /dev/null +++ b/net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.boringssl.BoringSSLClientSecureSocketProvider \ No newline at end of file diff --git a/net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.netty.client.secure.ServerSecureSocketProvider b/net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.netty.client.secure.ServerSecureSocketProvider new file mode 100644 index 0000000..453d611 --- /dev/null +++ b/net-http-netty-boringssl/src/main/resources/META-INF/services/org.xbib.net.http.netty.client.secure.ServerSecureSocketProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.boringssl.BoringSSLServerSecureSocketProvider \ No newline at end of file diff --git a/net-http-netty-conscrypt/build.gradle b/net-http-netty-conscrypt/build.gradle new file mode 100644 index 0000000..0712a6a --- /dev/null +++ b/net-http-netty-conscrypt/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':net-http-server-netty-secure') + api project(':net-http-client-netty-secure') + api libs.conscrypt +} diff --git a/net-http-netty-conscrypt/src/main/java/module-info.java b/net-http-netty-conscrypt/src/main/java/module-info.java new file mode 100644 index 0000000..e4b5340 --- /dev/null +++ b/net-http-netty-conscrypt/src/main/java/module-info.java @@ -0,0 +1,15 @@ +import org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; +import org.xbib.net.http.netty.conscrypt.ConscryptClientSecureSocketProvider; +import org.xbib.net.http.netty.conscrypt.ConscryptServerSecureSocketProvider; + +module org.xbib.net.http.netty.conscrypt { + exports org.xbib.net.http.netty.conscrypt; + requires org.xbib.net.http.client.netty; + requires org.xbib.net.http.client.netty.secure; + requires org.xbib.net.http.server.netty; + requires org.xbib.net.http.server.netty.secure; + requires io.netty.handler; + provides ClientSecureSocketProvider with ConscryptClientSecureSocketProvider; + provides ServerSecureSocketProvider with ConscryptServerSecureSocketProvider; +} diff --git a/net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptClientSecureSocketProvider.java b/net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptClientSecureSocketProvider.java new file mode 100644 index 0000000..05a65e5 --- /dev/null +++ b/net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptClientSecureSocketProvider.java @@ -0,0 +1,57 @@ +package org.xbib.net.http.netty.conscrypt; + +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.List; +import javax.net.ssl.SSLSocketFactory; +import org.conscrypt.Conscrypt; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider; + +public class ConscryptClientSecureSocketProvider implements ClientSecureSocketProvider { + + static { + Security.insertProviderAt(Conscrypt.newProviderBuilder() + .provideTrustManager(true) + .build(), 1); + } + + public ConscryptClientSecureSocketProvider() { + } + + @Override + public String name() { + return "CONSCRYPT"; + } + + @Override + public Provider securityProvider(HttpAddress address) { + return Conscrypt.newProviderBuilder() + .provideTrustManager(true) + .build(); + } + + @Override + public SslProvider sslProvider(HttpAddress address) { + return SslProvider.JDK; + } + + @Override + public List ciphers(HttpAddress address) { + return Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + } + + @Override + public CipherSuiteFilter cipherSuiteFilter(HttpAddress address) { + return SupportedCipherSuiteFilter.INSTANCE; + } + + @Override + public String[] protocols(HttpAddress address) { + return new String[] { "TLSv1.2" }; + } +} diff --git a/net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptServerSecureSocketProvider.java b/net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptServerSecureSocketProvider.java new file mode 100644 index 0000000..abef7ce --- /dev/null +++ b/net-http-netty-conscrypt/src/main/java/org/xbib/net/http/netty/conscrypt/ConscryptServerSecureSocketProvider.java @@ -0,0 +1,57 @@ +package org.xbib.net.http.netty.conscrypt; + +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.List; +import javax.net.ssl.SSLSocketFactory; +import org.conscrypt.Conscrypt; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider; + +public class ConscryptServerSecureSocketProvider implements ServerSecureSocketProvider { + + static { + Security.insertProviderAt(Conscrypt.newProviderBuilder() + .provideTrustManager(true) + .build(), 1); + } + + public ConscryptServerSecureSocketProvider() { + } + + @Override + public String name() { + return "CONSCRYPT"; + } + + @Override + public Provider securityProvider(HttpAddress httpAddress) { + return Conscrypt.newProviderBuilder() + .provideTrustManager(true) + .build(); + } + + @Override + public SslProvider sslProvider(HttpAddress httpAddress) { + return SslProvider.JDK; + } + + @Override + public List ciphers(HttpAddress httpAddress) { + return Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + } + + @Override + public CipherSuiteFilter cipherSuiteFilter(HttpAddress httpAddress) { + return SupportedCipherSuiteFilter.INSTANCE; + } + + @Override + public String[] protocols(HttpAddress httpAddress) { + return new String[] { "TLSv1.3", "TLSv1.2" }; + } +} diff --git a/net-http-netty-conscrypt/src/main/resources/META-INF/services/java.security.Provider b/net-http-netty-conscrypt/src/main/resources/META-INF/services/java.security.Provider new file mode 100644 index 0000000..b5f1a30 --- /dev/null +++ b/net-http-netty-conscrypt/src/main/resources/META-INF/services/java.security.Provider @@ -0,0 +1 @@ +org.conscrypt.OpenSSLProvider \ No newline at end of file diff --git a/net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider b/net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider new file mode 100644 index 0000000..882dbee --- /dev/null +++ b/net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.secure.ClientSecureSocketProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.conscrypt.ConscryptClientSecureSocketProvider \ No newline at end of file diff --git a/net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider b/net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider new file mode 100644 index 0000000..eeed46f --- /dev/null +++ b/net-http-netty-conscrypt/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.conscrypt.ConscryptServerSecureSocketProvider \ No newline at end of file diff --git a/net-http-netty-conscrypt/src/test/java/org/xbib/net/http/netty/conscrypt/Http1Test.java b/net-http-netty-conscrypt/src/test/java/org/xbib/net/http/netty/conscrypt/Http1Test.java new file mode 100644 index 0000000..c6dfdd5 --- /dev/null +++ b/net-http-netty-conscrypt/src/test/java/org/xbib/net/http/netty/conscrypt/Http1Test.java @@ -0,0 +1,38 @@ +package org.xbib.net.http.netty.conscrypt; + +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; + +public class Http1Test { + + private static final Logger logger = Logger.getLogger(Http1Test.class.getName()); + + @Test + void testGoogleConscrypt() throws Exception { + + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setSecureSocketProviderName("CONSCRYPT") + .setDebug(true); + + // java.security.cert.CertificateException: Unknown authType: GENERIC + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL("https://www.google.de/") + .setResponseListener(resp -> logger.log(Level.INFO, + "got response: " + resp.getHeaders() + + resp.getBodyAsChars(StandardCharsets.UTF_8) + + " status=" + resp.getStatus())) + .build(); + logger.log(Level.INFO, "request = " + request); + client.execute(request).get().close(); + } + } +} diff --git a/net-http-netty-conscrypt/src/test/resources/logging.properties b/net-http-netty-conscrypt/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-netty-conscrypt/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-netty-epoll/build.gradle b/net-http-netty-epoll/build.gradle new file mode 100644 index 0000000..e302e3e --- /dev/null +++ b/net-http-netty-epoll/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':net-http-server-netty') + api project(':net-http-client-netty') + api(variantOf(libs.netty.epoll) { classifier('linux-x86_64') }) +} diff --git a/net-http-netty-epoll/src/main/java/module-info.java b/net-http-netty-epoll/src/main/java/module-info.java new file mode 100644 index 0000000..c2d5832 --- /dev/null +++ b/net-http-netty-epoll/src/main/java/module-info.java @@ -0,0 +1,14 @@ +import org.xbib.net.http.server.netty.ServerTransportProvider; +import org.xbib.net.http.client.netty.ClientTransportProvider; +import org.xbib.net.http.netty.epoll.EpollClientTransportProvider; +import org.xbib.net.http.netty.epoll.EpollServerTransportProvider; + +module org.xbib.net.http.netty.epoll { + exports org.xbib.net.http.netty.epoll; + requires org.xbib.net.http.client.netty; + requires org.xbib.net.http.server.netty; + requires io.netty.transport; + requires io.netty.transport.classes.epoll; + provides ClientTransportProvider with EpollClientTransportProvider; + provides ServerTransportProvider with EpollServerTransportProvider; +} diff --git a/net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollClientTransportProvider.java b/net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollClientTransportProvider.java new file mode 100644 index 0000000..ccb5c97 --- /dev/null +++ b/net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollClientTransportProvider.java @@ -0,0 +1,25 @@ +package org.xbib.net.http.netty.epoll; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.socket.SocketChannel; +import java.util.concurrent.ThreadFactory; +import org.xbib.net.http.client.netty.ClientTransportProvider; + +public class EpollClientTransportProvider implements ClientTransportProvider { + + public EpollClientTransportProvider() { + } + + @Override + public EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + return Epoll.isAvailable() ? new EpollEventLoopGroup(nThreads, threadFactory) : null; + } + + @Override + public Class createSocketChannelClass() { + return Epoll.isAvailable() ? EpollSocketChannel.class : null; + } +} diff --git a/net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollServerTransportProvider.java b/net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollServerTransportProvider.java new file mode 100644 index 0000000..19c7f4c --- /dev/null +++ b/net-http-netty-epoll/src/main/java/org/xbib/net/http/netty/epoll/EpollServerTransportProvider.java @@ -0,0 +1,26 @@ +package org.xbib.net.http.netty.epoll; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.socket.ServerSocketChannel; +import org.xbib.net.http.server.netty.ServerTransportProvider; + +import java.util.concurrent.ThreadFactory; + +public class EpollServerTransportProvider implements ServerTransportProvider { + + public EpollServerTransportProvider() { + } + + @Override + public EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + return Epoll.isAvailable() ? new EpollEventLoopGroup(nThreads, threadFactory) : null; + } + + @Override + public Class createServerSocketChannelClass() { + return Epoll.isAvailable() ? EpollServerSocketChannel.class : null; + } +} diff --git a/net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider b/net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider new file mode 100644 index 0000000..fa29031 --- /dev/null +++ b/net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.epoll.EpollClientTransportProvider \ No newline at end of file diff --git a/net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider b/net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider new file mode 100644 index 0000000..6bee834 --- /dev/null +++ b/net-http-netty-epoll/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.epoll.EpollServerTransportProvider \ No newline at end of file diff --git a/net-http-netty-kqueue/build.gradle b/net-http-netty-kqueue/build.gradle new file mode 100644 index 0000000..3a465d3 --- /dev/null +++ b/net-http-netty-kqueue/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':net-http-server-netty') + api project(':net-http-client-netty') + api(variantOf(libs.netty.kqueue) { classifier('osx-x86_64') }) +} diff --git a/net-http-netty-kqueue/src/main/java/module-info.java b/net-http-netty-kqueue/src/main/java/module-info.java new file mode 100644 index 0000000..007ee92 --- /dev/null +++ b/net-http-netty-kqueue/src/main/java/module-info.java @@ -0,0 +1,14 @@ +import org.xbib.net.http.server.netty.ServerTransportProvider; +import org.xbib.net.http.client.netty.ClientTransportProvider; +import org.xbib.net.http.netty.kqueue.KqueueClientTransportProvider; +import org.xbib.net.http.netty.kqueue.KqueueServerTransportProvider; + +module org.xbib.net.http.netty.kqueue { + exports org.xbib.net.http.netty.kqueue; + requires org.xbib.net.http.client.netty; + requires org.xbib.net.http.server.netty; + requires io.netty.transport; + requires io.netty.transport.classes.kqueue; + provides ClientTransportProvider with KqueueClientTransportProvider; + provides ServerTransportProvider with KqueueServerTransportProvider; +} diff --git a/net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueClientTransportProvider.java b/net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueClientTransportProvider.java new file mode 100644 index 0000000..84af743 --- /dev/null +++ b/net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueClientTransportProvider.java @@ -0,0 +1,25 @@ +package org.xbib.net.http.netty.kqueue; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueSocketChannel; +import io.netty.channel.socket.SocketChannel; +import java.util.concurrent.ThreadFactory; +import org.xbib.net.http.client.netty.ClientTransportProvider; + +public class KqueueClientTransportProvider implements ClientTransportProvider { + + public KqueueClientTransportProvider() { + } + + @Override + public EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + return KQueue.isAvailable() ? new KQueueEventLoopGroup(nThreads, threadFactory) : null; + } + + @Override + public Class createSocketChannelClass() { + return KQueue.isAvailable() ? KQueueSocketChannel.class : null; + } +} diff --git a/net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueServerTransportProvider.java b/net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueServerTransportProvider.java new file mode 100644 index 0000000..8706416 --- /dev/null +++ b/net-http-netty-kqueue/src/main/java/org/xbib/net/http/netty/kqueue/KqueueServerTransportProvider.java @@ -0,0 +1,25 @@ +package org.xbib.net.http.netty.kqueue; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.socket.ServerSocketChannel; +import org.xbib.net.http.server.netty.ServerTransportProvider; +import java.util.concurrent.ThreadFactory; + +public class KqueueServerTransportProvider implements ServerTransportProvider { + + public KqueueServerTransportProvider() { + } + + @Override + public EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + return KQueue.isAvailable() ? new KQueueEventLoopGroup(nThreads, threadFactory) : null; + } + + @Override + public Class createServerSocketChannelClass() { + return KQueue.isAvailable() ? KQueueServerSocketChannel.class : null; + } +} diff --git a/net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider b/net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider new file mode 100644 index 0000000..7ef580b --- /dev/null +++ b/net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.client.netty.ClientTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.kqueue.KqueueClientTransportProvider \ No newline at end of file diff --git a/net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider b/net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider new file mode 100644 index 0000000..f85dab3 --- /dev/null +++ b/net-http-netty-kqueue/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.netty.kqueue.KqueueServerTransportProvider \ No newline at end of file diff --git a/net-http-server-application-config/build.gradle b/net-http-server-application-config/build.gradle new file mode 100644 index 0000000..03cbd3f --- /dev/null +++ b/net-http-server-application-config/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':net-http-server') + api libs.config +} diff --git a/net-http-server-application-config/src/main/java/module-info.java b/net-http-server-application-config/src/main/java/module-info.java new file mode 100644 index 0000000..46df99e --- /dev/null +++ b/net-http-server-application-config/src/main/java/module-info.java @@ -0,0 +1,12 @@ +import org.xbib.net.http.server.ApplicationModule; +import org.xbib.net.http.server.application.config.ConfigApplicationModule; + +module org.xbib.net.http.server.application.config { + uses org.xbib.config.ConfigLogger; + exports org.xbib.net.http.server.application.config; + requires org.xbib.net; + requires org.xbib.net.http.server; + requires org.xbib.config; + requires java.logging; + provides ApplicationModule with ConfigApplicationModule; +} diff --git a/net-http-server-application-config/src/main/java/org/xbib/net/http/server/application/config/ConfigApplicationModule.java b/net-http-server-application-config/src/main/java/org/xbib/net/http/server/application/config/ConfigApplicationModule.java new file mode 100644 index 0000000..10455d4 --- /dev/null +++ b/net-http-server-application-config/src/main/java/org/xbib/net/http/server/application/config/ConfigApplicationModule.java @@ -0,0 +1,70 @@ +package org.xbib.net.http.server.application.config; + +import java.util.Optional; +import java.util.ServiceLoader; +import org.xbib.config.ConfigLoader; +import org.xbib.config.ConfigLogger; +import org.xbib.config.ConfigParams; +import org.xbib.config.SystemConfigLogger; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.BaseApplicationModule; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.HttpService; +import org.xbib.settings.Settings; + +public class ConfigApplicationModule extends BaseApplicationModule { + + private static final ConfigLogger bootLogger; + + static { + // early loading of boot logger during static initialization block + ServiceLoader serviceLoader = ServiceLoader.load(ConfigLogger.class); + Optional optionalBootLogger = serviceLoader.findFirst(); + bootLogger = optionalBootLogger.orElse(new SystemConfigLogger()); + } + + private ConfigParams configParams; + + private ConfigLoader configLoader; + + private Settings settings; + + public ConfigApplicationModule() { + } + + @Override + public String getName() { + return "config"; + } + + @Override + public void onOpen(Application application) throws Exception { + String profile = System.getProperty("application.profile"); + if (profile == null) { + profile = "developer"; + } + String[] args = profile.split(";"); + this.configParams = new ConfigParams() + .withArgs(args) + .withDirectoryName("application") + .withFileNamesWithoutSuffix(args[0]) + .withSystemEnvironment() + .withSystemProperties(); + this.configLoader = ConfigLoader.getInstance() + .withLogger(bootLogger); + this.settings = configLoader.load(configParams); + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService) { + httpServerContext.attributes().put("configparams", configParams); + httpServerContext.attributes().put("configloader", configLoader); + httpServerContext.attributes().put("settings", settings); + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService, HttpRequest httpRequest) { + // do nothing + } +} diff --git a/net-http-server-application-database/build.gradle b/net-http-server-application-database/build.gradle new file mode 100644 index 0000000..e454b87 --- /dev/null +++ b/net-http-server-application-database/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':net-http-server') + api libs.datastructures.json.tiny + api libs.jdbc.query +} diff --git a/net-http-server-application-database/src/main/java/module-info.java b/net-http-server-application-database/src/main/java/module-info.java new file mode 100644 index 0000000..8f5f36f --- /dev/null +++ b/net-http-server-application-database/src/main/java/module-info.java @@ -0,0 +1,12 @@ +import org.xbib.net.http.server.ApplicationModule; +import org.xbib.net.http.server.application.database.DatabaseApplicationModule; + +module org.xbib.net.http.server.application.database { + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.xbib.jdbc.query; + requires org.xbib.datastructures.tiny; + requires java.logging; + provides ApplicationModule with DatabaseApplicationModule; +} diff --git a/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/BaseDatabase.java b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/BaseDatabase.java new file mode 100644 index 0000000..a34ee32 --- /dev/null +++ b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/BaseDatabase.java @@ -0,0 +1,283 @@ +package org.xbib.net.http.server.application.database; + +import org.xbib.jdbc.query.DatabaseException; +import org.xbib.jdbc.query.Rows; +import org.xbib.jdbc.query.SqlInsert; +import org.xbib.jdbc.query.SqlSelect; +import org.xbib.jdbc.query.SqlUpdate; + +import java.math.BigDecimal; +import java.sql.ResultSetMetaData; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class BaseDatabase implements Database { + + private static final Logger logger = Logger.getLogger(BaseDatabase.class.getName()); + + private final org.xbib.jdbc.query.Database db; + + private final int fetchSize; + + private final int timeoutSeconds; + + public BaseDatabase(org.xbib.jdbc.query.Database db, int fetchSize, int timeoutSeconds) throws Exception { + this.db = db; + this.fetchSize = fetchSize; + this.timeoutSeconds = timeoutSeconds; + } + + @Override + public Table getPagedRows(Table table) { + if (table == null) { + return table; + } + Map params = table.getParams() != null ? + new HashMap<>(table.getParams()) : new HashMap<>(); + if (table.getOffset() != null && table.getSize() != null) { + params.put("offset", table.getOffset()); + params.put("limit", table.getSize()); + } + String where = table.getWhereClause() != null ? table.getWhereClause() : ""; + String groupby = table.getGroupByClause() != null ? table.getGroupByClause() : ""; + String orderby = !table.getSort().isEmpty() ? "order by " + table.getSort() : ""; + String statement = table.getStatement() + " " + where + " " + groupby + " " + orderby; + if (table.getOffset() != null && table.getSize() != null) { + statement = statement + " offset :offset rows fetch next :limit rows only"; + } + return getUnlimitedRows(statement, params); + } + + @Override + public long getRowsCount(Table table) { + Map params = new HashMap<>(table.getParams()); + String statement = table.getStatement() + " " + table.getWhereClause(); + return countRows(statement, params); + } + + @Override + public long countRows(String statement, Map params) { + String countStatament = "select count(*) as \"cnt\" from (" + statement + ")"; + Table table = getSingleRow(countStatament, params); + if (!table.isEmpty()) { + BigDecimal bigDecimal = table.getValue(0,"cnt"); + return bigDecimal.longValue(); + } else { + return -1L; + } + } + + @Override + public Table getSingleRow(String statement, Map params) { + return getLimitedRows(statement, params, 1, fetchSize, timeoutSeconds); + } + + @Override + public Table getUnlimitedRows(String statement, Map params) { + return getLimitedRows(statement, params, 0, fetchSize, timeoutSeconds); + } + + @Override + public Table getLimitedRows(String statement, Map params, + int limit, int fetchSize, int timeoutSeconds) { + SqlSelect sql = db.toSelect(statement).fetchSize(fetchSize).withTimeoutSeconds(timeoutSeconds); + selectParams(sql, params); + Table table = new Table(); + sql.query(rows -> { + ResultSetMetaData md = rows.getMetadata(); + List columnNames = new ArrayList<>(); + List classNames = new ArrayList<>(); + for (int i = 1; i <= md.getColumnCount(); i++) { + columnNames.add(md.getColumnName(i).toLowerCase(Locale.ROOT)); + classNames.add(md.getColumnClassName(i)); + } + table.add(columnNames); + table.add(classNames); + int i = 0; + while (rows.next() && (limit <= 0 || i++ < limit)) { + table.add(getRow(rows, classNames)); + } + table.setTotal(rows.rowCount()); + return true; + }); + return table; + } + + @Override + public void insert(String statement, Map params) { + SqlInsert sql = db.toInsert(statement); + insertParams(sql, params); + sql.insert(1); + } + + @Override + public void insert(String statement, List> params) { + SqlInsert sqlInsert = db.toInsert(statement); + for (Map param : params) { + insertParams(sqlInsert, param); + sqlInsert.batch(); + } + sqlInsert.insertBatch(); + } + + @Override + public void update(String statement, Map params) { + SqlUpdate sql = db.toUpdate(statement); + updateParams(sql, params); + sql.update(); + } + + @Override + public void update(String statement, List> params) { + SqlUpdate sqlUpdate = db.toUpdate(statement); + for (Map param : params) { + updateParams(sqlUpdate, param); + sqlUpdate.update(); + } + } + + @Override + public void upsert(String insertStatement, String updateStatement, Map params) { + // try insert then update if error + try { + SqlInsert sql = db.toInsert(insertStatement); + insertParams(sql, params); + sql.insert(1); + } catch (Exception e) { + logger.log(Level.WARNING, e.getMessage(), e); + SqlUpdate sql = db.toUpdate(updateStatement); + updateParams(sql, params); + sql.update(); + } + } + + @Override + public void delete(String statement, Map params) { + SqlUpdate sql = db.toDelete(statement); + updateParams(sql, params); + sql.update(); + } + + @Override + public void delete(String statement, List> params) { + SqlUpdate sqlUpdate = db.toDelete(statement); + for (Map param : params) { + updateParams(sqlUpdate, param); + sqlUpdate.update(); + } + } + + private void selectParams(SqlSelect sql, Map params) { + if (params == null) { + return; + } + params.forEach((k, v) -> { + if (v instanceof String) { + sql.argString(k, (String) v); + } else if (v instanceof Integer) { + sql.argInteger(k, (Integer) v); + } else if (v instanceof Long) { + sql.argLong(k, (Long) v); + } else if (v instanceof Boolean) { + sql.argBoolean(k, (Boolean) v); + } else if (v instanceof LocalDate) { + sql.argLocalDate(k, (LocalDate) v); + } else if (v instanceof LocalDateTime) { + sql.argDate(k, (LocalDateTime) v); + } else { + throw new DatabaseException("unknown type for param: " + (v != null ? v.getClass() : "null")); + } + }); + } + + private void insertParams(SqlInsert sql, Map params) { + if (params == null) { + return; + } + params.forEach((k, v) -> { + if (v instanceof String) { + sql.argString(k, (String) v); + } else if (v instanceof Integer) { + sql.argInteger(k, (Integer) v); + } else if (v instanceof Long) { + sql.argLong(k, (Long) v); + } else if (v instanceof Boolean) { + sql.argBoolean(k, (Boolean) v); + } else if (v instanceof LocalDate) { + sql.argLocalDate(k, (LocalDate) v); + } else if (v instanceof LocalDateTime) { + sql.argDate(k, (LocalDateTime) v); + } else { + throw new DatabaseException("unknown type for param: " + (v != null ? v.getClass() : "null")); + } + }); + } + + private void updateParams(SqlUpdate sql, Map params) { + if (params == null) { + return; + } + params.forEach((k, v) -> { + if (v instanceof String) { + sql.argString(k, (String) v); + } else if (v instanceof Integer) { + sql.argInteger(k, (Integer) v); + } else if (v instanceof Long) { + sql.argLong(k, (Long) v); + } else if (v instanceof Boolean) { + sql.argBoolean(k, (Boolean) v); + } else if (v instanceof LocalDate) { + sql.argLocalDate(k, (LocalDate) v); + } else if (v instanceof LocalDateTime) { + sql.argDate(k, (LocalDateTime) v); + } else { + throw new DatabaseException("unknown type for param: " + (v != null ? v.getClass() : "null")); + } + }); + } + + private List getRow(Rows rows, List classNames) { + List row = new ArrayList<>(); + for (int i = 0; i < classNames.size(); i++) { + String className = classNames.get(i).toString(); + switch (className) { + case "java.lang.String": + row.add(rows.getStringOrEmpty(i + 1)); + break; + case "java.lang.Integer": + row.add(rows.getIntegerOrNull(i + 1)); + break; + case "java.lang.Long": + row.add(rows.getLongOrNull(i + 1)); + break; + case "java.lang.Boolean": + row.add(rows.getBooleanOrFalse(i + 1)); + break; + case "java.sql.Clob": + case "oracle.jdbc.OracleClob": + row.add(rows.getClobStringOrEmpty(i + 1)); + break; + case "java.sql.Date": + row.add(rows.getLocalDateOrNull(i + 1)); + break; + case "java.sql.Timestamp": + case "oracle.sql.TIMESTAMP": + row.add(rows.getLocalDateTimeOrNull(i + 1)); + break; + case "java.math.BigDecimal": + row.add(rows.getBigDecimalOrNull(i + 1)); + break; + default: + throw new DatabaseException("unexpected column class name: " + className); + } + } + return row; + } +} diff --git a/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Database.java b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Database.java new file mode 100644 index 0000000..3363e13 --- /dev/null +++ b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Database.java @@ -0,0 +1,37 @@ +package org.xbib.net.http.server.application.database; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface Database extends AutoCloseable { + + Table getPagedRows(Table table); + + long getRowsCount(Table table); + + long countRows(String statement, Map params); + + Table getSingleRow(String statement, Map params); + + Table getUnlimitedRows(String statement, Map params); + + Table getLimitedRows(String statement, Map params, + int limit, int fetchSize, int timeoutSeconds); + + void insert(String statement, Map params); + + void insert(String statement, List> params); + + void update(String statement, Map params); + + void update(String statement, List> params); + + void upsert(String insertStatement, String updateStatement, Map params); + + void delete(String statement, Map params); + + void delete(String statement, List> params); + + void close() throws IOException; +} diff --git a/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/DatabaseApplicationModule.java b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/DatabaseApplicationModule.java new file mode 100644 index 0000000..1285b0f --- /dev/null +++ b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/DatabaseApplicationModule.java @@ -0,0 +1,103 @@ +package org.xbib.net.http.server.application.database; + +import org.xbib.jdbc.connection.pool.PoolConfig; +import org.xbib.jdbc.connection.pool.PoolDataSource; +import org.xbib.jdbc.query.DatabaseProvider; +import org.xbib.jdbc.query.Flavor; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.BaseApplicationModule; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.HttpService; + +import javax.sql.DataSource; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DatabaseApplicationModule extends BaseApplicationModule { + + private static final Logger logger = Logger.getLogger(DatabaseApplicationModule.class.getName()); + + private DataSource dataSource; + + private DatabaseProvider databaseProvider; + + public DatabaseApplicationModule() { + } + + @Override + public String getName() { + return "database"; + } + + @Override + public void onOpen(Application application) throws Exception { + this.dataSource = createDataSource(); + String flavor = System.getProperty("database.flavor"); + this.databaseProvider = flavor != null ? + DatabaseProvider.builder(dataSource, Flavor.valueOf(flavor)).build() : null; + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService) { + if (dataSource != null) { + httpServerContext.attributes().put("datasource", dataSource); + } + if (databaseProvider != null) { + httpServerContext.attributes().put("databaseprovider", databaseProvider); + } + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService, HttpRequest httpRequest) { + // nothing + } + + private DataSource createDataSource() throws Exception { + Properties properties = new Properties(); + System.getProperties().forEach((key, value) -> { + if (key.toString().startsWith("database.") && !key.toString().equals("database.flavor")) { + properties.setProperty(key.toString().substring(9), value.toString()); + } + }); + if (!properties.containsKey("url")) { + throw new IllegalArgumentException(" no database.url in system properties given"); + } + if (!properties.containsKey("user")) { + logger.log(Level.WARNING, "no database.user in system properties given"); + } + PoolConfig config = new PoolConfig(properties); + config.setPoolName("net-http-database"); + config.setMaximumPoolSize(getAsInt(properties, "poolsize", 4)); + config.setMaxLifetime(getAsLong(properties, "maxlifetime", 600L * 1000L)); // 10 minutes + config.setConnectionTimeout(getAsLong(properties, "timeout", 15L * 1000L)); // 15 seconds + config.setHousekeepingPeriodMs(getAsLong(properties, "housekeeping", 600L * 1000L)); // 10 minutes + config.setAutoCommit(getAsBoolean(properties, "autocommit", true)); + return new PoolDataSource(config); + } + + private int getAsInt(Properties properties, String key, int defaultValue) { + if (properties.containsKey(key)) { + return Integer.parseInt(properties.getProperty(key)); + } else { + return defaultValue; + } + } + + private long getAsLong(Properties properties, String key, long defaultValue) { + if (properties.containsKey(key)) { + return Long.parseLong(properties.getProperty(key)); + } else { + return defaultValue; + } + } + + private boolean getAsBoolean(Properties properties, String key, boolean defaultValue) { + if (properties.containsKey(key)) { + return Boolean.parseBoolean(properties.getProperty(key)); + } else { + return defaultValue; + } + } +} diff --git a/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Table.java b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Table.java new file mode 100644 index 0000000..2f9a9b7 --- /dev/null +++ b/net-http-server-application-database/src/main/java/org/xbib/net/http/server/application/database/Table.java @@ -0,0 +1,203 @@ +package org.xbib.net.http.server.application.database; + +import org.xbib.datastructures.tiny.TinyMap; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@SuppressWarnings("serial") +public class Table extends ArrayList> implements List> { + + private String statement; + + private Map params; + + private String search; + + private LocalDateTime from; + + private LocalDateTime to; + + private Integer offset; + + private Integer size; + + private List where; + + private String whereClause; + + private String groupByClause; + + private final List> sort = new ArrayList<>(); + + private long total; + + public void setStatement(String statement) { + this.statement = statement; + } + + public String getStatement() { + return statement; + } + + public void setParams(Map params) { + this.params = params; + } + + public Map getParams() { + return params; + } + + public void setSearch(String search) { + this.search = search; + } + + public String getSearch() { + return search; + } + + public void setFromDate(LocalDateTime from) { + this.from = from; + } + + public LocalDateTime getFromDate() { + return from; + } + + public void setToDate(LocalDateTime to) { + this.to = to; + } + + public LocalDateTime getToDate() { + return to; + } + + public void setOffset(Integer offset) { + this.offset = offset; + } + + public Integer getOffset() { + return offset; + } + + public void setSize(Integer size) { + this.size = size; + } + + public Integer getSize() { + return size; + } + + public void setWhere(List where) { + this.where = where; + } + + public List getWhere() { + return where; + } + + public void setWhereClause(String whereClause) { + this.whereClause = whereClause; + } + + public String getWhereClause() { + return whereClause; + } + + public void setGroupByClause(String groupByClause) { + this.groupByClause = groupByClause; + } + + public String getGroupByClause() { + return groupByClause; + } + + public void addSort(String sort) { + addSort(sort, true); + } + + public void addSort(String sort, Boolean asc) { + this.sort.add(Map.entry(sort, asc)); + } + + public String getSort() { + return sort.stream().map(e -> "\"" + e.getKey() + "\"" + " " + (e.getValue() ? "asc" : "desc")) + .collect(Collectors.joining(",")); + } + + public List getColumnNames() { + return get(0).stream().map(Object::toString).collect(Collectors.toList()); + } + + public int getColumn(String columnName) { + return get(0).indexOf(columnName); + } + + public List getColumnClassNames() { + return get(1).stream().map(Object::toString).collect(Collectors.toList()); + } + + public String getColumnName(int i) { + return (String) get(0).get(i); + } + + public String getColumnClassName(int i) { + return (String) get(1).get(i); + } + + public int getRowCount() { + return size() - 2; + } + + public int getColumnCount() { + return get(0).size(); + } + + public List getRow(int i) { + return get(i + 2); + } + + public Map getRowAsMap(int i) { + TinyMap.Builder map = TinyMap.builder(); + List row = getRow(i); + for (int c = 0; c < getColumnCount(); c++) { + map.put(getColumnName(c), row.get(c)); + } + return map.build(); + } + + public Object getObject(int row, int col) { + return get(row + 2).get(col); + } + + public Object getObject(int row, String columnName) { + int col = getColumn(columnName); + return col >= 0 ? get(row + 2).get(col) : null; + } + + @SuppressWarnings("unchecked") + public T getValue(int row, int col) { + return (T) getObject(row, col); + } + + @SuppressWarnings("unchecked") + public T getValue(int row, String columnName) { + return (T) getObject(row, columnName); + } + + public void setTotal(long total) { + this.total = total; + } + + public long getTotal() { + return total; + } + + @Override + public boolean isEmpty() { + return super.isEmpty() || size() <= 2; + } +} diff --git a/net-http-server-application-web/build.gradle b/net-http-server-application-web/build.gradle new file mode 100644 index 0000000..e0cfffe --- /dev/null +++ b/net-http-server-application-web/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'application' + +dependencies { + api project(':net-http-server-netty-secure') + api project(':net-http-server-application-config') + api project(':net-http-template-groovy') + api libs.jdbc.query + implementation libs.settings.datastructures.json + implementation libs.settings.datastructures.yaml + implementation libs.webjars.bootstrap + implementation libs.webjars.jquery + implementation libs.webjars.fontawesome + runtimeOnly libs.net.bouncycastle + runtimeOnly libs.oracle +} + +application { + mainClass.set('org.xbib.net.http.server.application.web.Bootstrap') + applicationDefaultJvmArgs = [ + '-Dfile.encoding=UTF-8', + '-Duser.language=de', + '-Duser.country=DE', + '-Djava.awt.headless=true', + '-Djava.util.logging.config.file=src/main/resources/logging.properties', + '-Dapplication.home=src/main/application', + '-Dapplication.profile=dev', + '-Dapplication.name=web', + '-Ddatabase.url=jdbc:derby:sample', + '-Ddatabase.flavor=derby', + '-Ddatabase.user=derby', + '-Ddatabase.password=derby' + ] +} diff --git a/net-http-server-application-web/src/main/application/400.gtpl b/net-http-server-application-web/src/main/application/400.gtpl new file mode 100644 index 0000000..cbb8bae --- /dev/null +++ b/net-http-server-application-web/src/main/application/400.gtpl @@ -0,0 +1,3 @@ +html { + h1 "${_message}" +} diff --git a/net-http-server-application-web/src/main/application/403.gtpl b/net-http-server-application-web/src/main/application/403.gtpl new file mode 100644 index 0000000..cbb8bae --- /dev/null +++ b/net-http-server-application-web/src/main/application/403.gtpl @@ -0,0 +1,3 @@ +html { + h1 "${_message}" +} diff --git a/net-http-server-application-web/src/main/application/404.gtpl b/net-http-server-application-web/src/main/application/404.gtpl new file mode 100644 index 0000000..cbb8bae --- /dev/null +++ b/net-http-server-application-web/src/main/application/404.gtpl @@ -0,0 +1,3 @@ +html { + h1 "${_message}" +} diff --git a/net-http-server-application-web/src/main/application/500.gtpl b/net-http-server-application-web/src/main/application/500.gtpl new file mode 100644 index 0000000..c76c047 --- /dev/null +++ b/net-http-server-application-web/src/main/application/500.gtpl @@ -0,0 +1,29 @@ +yieldUnescaped '' +html(lang:'en') { + head { + title('500 - Server error') + meta(charset: 'utf-8') + meta(name: 'viewport', content: 'width=device-width, initial-scale=1.0') + link(rel: 'stylesheet', href: bootstrapCss()) + } + body { + div(class: 'container') { + h1('Server error') + p(class: 'exception') { + yield "Exception ${stringOf { _throwable } }" + } + p(class: 'exceptionMessage') { + yield "Exception message ${stringOf { _message } }" + } + pre { + code(class: 'trace') { + StringWriter s = new StringWriter() + if (_throwable) { + org.codehaus.groovy.runtime.StackTraceUtils.printSanitizedStackTrace(_throwable, new PrintWriter(s)) + } + yield "${s.toString()}" + } + } + } + } +} diff --git a/net-http-server-application-web/src/main/application/demo/auth/form/index.gtpl b/net-http-server-application-web/src/main/application/demo/auth/form/index.gtpl new file mode 100644 index 0000000..dee9849 --- /dev/null +++ b/net-http-server-application-web/src/main/application/demo/auth/form/index.gtpl @@ -0,0 +1,29 @@ +yieldUnescaped '' +html(lang: 'en') { + head { + title 'Login Page' + } + body { + if (userprofile.isLoggedIn()) { + h2 "Logged in as ${userprofile.uid}!" + } else { + h2 'Hello, please log in:' + br + br + form(action: '/demo/auth/form/success.gtpl', method: 'post') { + p { + strong 'Please Enter Your User Name: ' + } + input(type: 'text', name: 'j_username', size: 25) + p {} + p {} + input(type: 'password', name: 'j_password', size: 15) + p {} + p {} + input(type: 'submit', value: 'Submit') + input(type: 'reset', value: 'Reset') + } + p "Original path: ${originalPath}" + } + } +} diff --git a/net-http-server-application-web/src/main/application/demo/auth/form/success.gtpl b/net-http-server-application-web/src/main/application/demo/auth/form/success.gtpl new file mode 100644 index 0000000..f55fa3c --- /dev/null +++ b/net-http-server-application-web/src/main/application/demo/auth/form/success.gtpl @@ -0,0 +1,12 @@ +yieldUnescaped '' +html(lang: 'en') { + head { + title 'Login OK' + } + body { + h1 'Welcome' + div { + yield "userprofile=${userprofile}" + } + } +} diff --git a/net-http-server-application-web/src/main/application/index.gtpl b/net-http-server-application-web/src/main/application/index.gtpl new file mode 100644 index 0000000..ac23c62 --- /dev/null +++ b/net-http-server-application-web/src/main/application/index.gtpl @@ -0,0 +1,5 @@ +html { + body { + p "Hello Jörg" + } +} \ No newline at end of file diff --git a/net-http-server-application-web/src/main/java/module-info.java b/net-http-server-application-web/src/main/java/module-info.java new file mode 100644 index 0000000..4b8634d --- /dev/null +++ b/net-http-server-application-web/src/main/java/module-info.java @@ -0,0 +1,16 @@ +module org.xbib.net.http.server.application.web { + uses org.xbib.config.ConfigLogger; + exports org.xbib.net.http.server.application.web; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.xbib.net.http.server.netty; + requires org.xbib.net.http.server.netty.secure; + requires org.xbib.net.http.template.groovy; + requires org.xbib.datastructures.tiny; + requires org.xbib.datastructures.json.tiny; + requires org.xbib.jdbc.query; + requires org.xbib.net.mime; + requires org.xbib.config; + requires java.logging; +} diff --git a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/Bootstrap.java b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/Bootstrap.java new file mode 100644 index 0000000..e16183c --- /dev/null +++ b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/Bootstrap.java @@ -0,0 +1,199 @@ +package org.xbib.net.http.server.application.web; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import org.xbib.config.ConfigLoader; +import org.xbib.config.ConfigLogger; +import org.xbib.config.ConfigParams; +import org.xbib.config.SystemConfigLogger; +import org.xbib.net.NetworkClass; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpSecurityDomain; +import org.xbib.net.http.server.HttpSecurityDomain; +import org.xbib.net.http.server.HttpService; +import org.xbib.net.http.server.auth.BasicAuthenticationHandler; +import org.xbib.net.http.server.auth.FormAuthenticationHandler; +import org.xbib.net.http.server.ldap.LdapContextFactory; +import org.xbib.net.http.server.ldap.LdapGroupMapping; +import org.xbib.net.http.server.ldap.LdapRealm; +import org.xbib.net.http.server.ldap.LdapUserMapping; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.buffer.NettyDataBufferFactory; +import org.xbib.net.http.server.netty.secure.HttpsAddress; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.resource.ClassLoaderResourceHandler; +import org.xbib.net.http.template.groovy.GroovyInternalServerErrorHandler; +import org.xbib.net.http.template.groovy.GroovyHttpStatusHandler; +import org.xbib.net.http.template.groovy.GroovyTemplateResourceHandler; +import org.xbib.net.http.template.groovy.GroovyTemplateService; +import org.xbib.net.mime.stream.Hex; +import org.xbib.settings.Settings; + +public final class Bootstrap { + + private static final ConfigLogger bootLogger; + + static { + // early loading of boot logger during static initialization block + ServiceLoader serviceLoader = ServiceLoader.load(ConfigLogger.class); + Optional optionalBootLogger = serviceLoader.findFirst(); + bootLogger = optionalBootLogger.orElse(new SystemConfigLogger()); + } + + private Bootstrap() { + } + + public static void main(String[] args) throws Exception { + String profile = args.length > 0 ? args[0] : System.getProperty("application.profile"); + ConfigParams configParams; + ConfigLoader configLoader; + Settings settings; + configParams = new ConfigParams() + .withArgs(args) + .withDirectoryName("application") + .withFileNamesWithoutSuffix(profile) + .withSystemEnvironment() + .withSystemProperties(); + configLoader = ConfigLoader.getInstance() + .withLogger(bootLogger); + settings = configLoader.load(configParams); + int rc = 1; + try { + rc = runApplication(settings); + } catch (Throwable t) { + t.printStackTrace(); + } finally { + // always hard-exit the JVM, maybe there are threads hanging + System.exit(rc); + } + } + + private static int runApplication(Settings settings) throws Exception { + HttpsAddress httpsAddress = HttpsAddress.builder() + .setSecure(true) + .setVersion(HttpVersion.HTTP_2_0) + .setHost("localhost") + .setPort(8443) + .setSelfCert("localhost") + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("WebApplication", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.SITE); + serverConfig.setDebug(true); + + Map contextFactories = new HashMap<>(); + LdapContextFactory contextFactory = new LdapContextFactory("simple", + "com.sun.jndi.ldap.LdapCtxFactory", + null, + "ldap://localhost:1389", + false, + null, + null, + "follow" + ); + contextFactories.put("default", contextFactory); + Map userMappings = new HashMap<>(); + LdapUserMapping userMapping = new LdapUserMapping("ou=People,dc=example.org", + "(&(objectclass=posixAccount)(uid:caseExactMatch:={0}))", + "uid", + "cn" + ); + userMappings.put("default", userMapping); + Map groupMappings = new HashMap<>(); + LdapGroupMapping groupMapping = new LdapGroupMapping("ou=group,dc=example.org", + "cn", + "(&(objectclass=posixGroup)(memberUid:caseExactMatch:={0}))", + new String[] { "uid" } + ); + groupMappings.put("default", groupMapping); + LdapRealm ldapRealm = new LdapRealm("Web Application Realm", contextFactories, userMappings, groupMappings); + + BasicAuthenticationHandler basicAuthenticationHandler = + new BasicAuthenticationHandler(ldapRealm); + FormAuthenticationHandler formAuthenticationHandler = + new FormAuthenticationHandler("j_username", "j_password", "j_remember", + "demo/auth/form/index.gtpl", ldapRealm); + + HttpSecurityDomain securityDomain = BaseHttpSecurityDomain.builder() + .setSecurityRealm(ldapRealm) + .setHandlers(formAuthenticationHandler) + .build(); + + HttpService httpService = BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain: " + + " SNI host = " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + + " SSL session = " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + ctx.done(); + }) + .build(); + + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(WebApplication.builder() + .setSettings(settings) + .setSecret("1088e6b7ad58d64d09961e1357bf95544447051c6ad1332cd626e3a33bb5786b") + .setRouter(BaseHttpRouter.builder() + .setHandler(400, new GroovyHttpStatusHandler(HttpResponseStatus.BAD_REQUEST, "Bad request", "400.gtpl")) + .setHandler(401, new GroovyHttpStatusHandler(HttpResponseStatus.UNAUTHORIZED, "Unauthorized", "401.gtpl")) + .setHandler(403, new GroovyHttpStatusHandler(HttpResponseStatus.FORBIDDEN, "Forbidden", "403.gtpl")) + .setHandler(404, new GroovyHttpStatusHandler(HttpResponseStatus.NOT_FOUND, "Not found", "404.gtpl")) + .setHandler(500, new GroovyInternalServerErrorHandler("500.gtpl")) + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, "image/x-icon") + .write(NettyDataBufferFactory.getInstance().wrap(Hex.fromHex(hexFavIcon))) + .build(); + ctx.done(); + }) + .build()) + .addService(BaseHttpService.builder() + .setPath("/webjars/**") + .setHandler(new ClassLoaderResourceHandler(Bootstrap.class.getClassLoader(), "META-INF/resources/")) + .build()) + .addService(httpService) + .addService(GroovyTemplateService.builder() + .setTemplateName("index.gtpl") + .setSecurityDomain(securityDomain) + .setPath("glob:**") + .setHandler(new GroovyTemplateResourceHandler()) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + server.loop(); + } + return 0; + } + + + private static final String hexFavIcon = + "00000100010010100000010020006804000016000000280000001000000020000000010020000000000040040000130b0000130b0000000000000000000000000000000000000000000000000000000000005140322f62524a536050475f5140322f5140320c000000000000000000000000000000000000000000000000000000000000000000000000fffffe40b1a9a5c76f605bff6f605bff6f605bff6f605bff6b5b55f38f7d71ff978473bf877465100000000000000000000000000000000000000000fffffe8ff2eeeafff2f0eeffb7b0adff786a65ff6f605bff6f605bff6f605bff6f605bff72625af77f6d60bf00000000000000000000000000000000fffffe8fd9ccc0fffbfaf9fffbfaf8fffbfaf8fff6f5f5ff9c928eff6f605bff786a65ff6f605bff6f605bff716159ff6d5d4f9f0000000000000000fffffe40e7e0d8ffcfbfb2ffc4b2a0ffcebdafffccbbabffe1d6ceffe7ded7ffd2cdccff786a65ff6f605bff6f605bff6c5c54ff6a5a4ffb6050425000000000faf9f6afd3c5b8ffd4c6baffcfc1b2ffd5c7bbffcab9a9ffe7ded7fff2f0efff786a65ff6f605bff6f605bff6f605bff6c5c53ff6b5b53fb5d4d3fdf00000000e9e2dbffd8ccc0ffdcd1c5ffdcd1c6ffd4c7b9ffdacec3ffffffffffd2cdccff6f605bff6f605bff6f605bff6f605bff6a5a4fff6b5b52ff5b4b3eef53443660ece7e0ffdbd0c5ffe0d6ccffe9e2d9ffe3dad1fff1ece8ffffffffffc0b9b7ff6f605bff6f605bff6f605bff6f605bff685749ff6a5a4fff5e4e43cf5142348fefe9e3ffe0d7ccffe9e1d9ffeae3dbffe3dbd0fff4f0ecffffffffffdbd7d6ff6f605bff6f605bff6f605bff6d5e56ff675648ff6a5a4fff5e4f44af514233bfefeae4ffe8e0d7ffebe5ddffede8dfffebe5dcfff2eee8ffffffffffffffffff817470ff6f605bff6f605aff69594dff685749ff6b5b52ff5c4d429f5142338ff9f7f4afeae3dbffe7e1d6ffece7ddffefebe3fff1ece7ffffffffffffffffffdbd7d6ff6f605bff6a5a4fff68574bff6a5a50ff6b5b54f7554638574f403150fffffe40f1ede7ffece7dfffebe5dcffeae3dbfffbf9f7fffffffffffffffffff8f5f3ff83756dff69584dff69584cff6b5b52ff675750af000000004e3f304000000000fcfbf99fede7e0ffe4dbd1fff6f3effffffffffff9f8f6ffe1d7ceffdbcfc5ff9b8a7cff68574aff6a594eff6b5b54e751403218000000004e3f30100000000000000000fbfaf89fece6dfffeee9e3ffe4dbd2ffd8cbbfffd8ccc0ffcdbcadff6f5e52ff6a5a50ff6b5b54e75d4d4328000000000000000000000000000000000000000000000000fffffe60f6f4f0cff1ece7ffe2d9d0ffdbcec3ffccc1b7ff6a5a52f7675750af51403218000000000000000000000000000000000000000000000000000000000000000000000000fffffe10fffffe40fffffe40fffffe205140320c000000000000000000000000000000000000000000000000f83f9407e0070000c0079807800300000001603f0001603f0000603f0000603f0000e13e0000e23e0000e23e0002e23e8002e23ec007e33ee00fe33ef83fe33"; +} diff --git a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java new file mode 100644 index 0000000..bf77c51 --- /dev/null +++ b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java @@ -0,0 +1,59 @@ +package org.xbib.net.http.server.application.web; + +import java.nio.file.Paths; +import java.time.Duration; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.http.server.session.IncomingSessionHandler; +import org.xbib.net.http.server.session.OutgoingSessionHandler; +import org.xbib.net.http.server.session.Session; +import org.xbib.net.http.server.session.file.FileJsonSessionCodec; + +public class WebApplication extends BaseApplication { + + protected WebApplication(WebApplicationBuilder builder) { + super(builder); + } + + public static WebApplicationBuilder builder() { + try { + return new WebApplicationBuilder(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + protected Codec buildSessionCodec(HttpServerContext httpServerContext) { + return new FileJsonSessionCodec(this, 1024, Duration.ofDays(1), + Paths.get("/var/tmp/session")); + } + + protected HttpHandler buildIncomingSessionHandler(HttpServerContext httpServerContext) { + @SuppressWarnings("unchecked") + Codec sessionCodec = httpServerContext.attributes().get(Codec.class, "sessioncodec"); + return new IncomingSessionHandler( + getSecret(), + "HmacSHA1", + "SESSION", + sessionCodec, + getStaticFileSuffixes(), + "user_id", + "e_user_id"); + } + + protected OutgoingSessionHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) { + @SuppressWarnings("unchecked") + Codec sessionCodec = httpServerContext.attributes().get(Codec.class, "sessioncodec"); + return new OutgoingSessionHandler( + getSecret(), + "HmacSHA1", + "SESSION", + Duration.ofDays(1), + sessionCodec, + getStaticFileSuffixes(), + "user_id", + "e_user_id"); + } +} diff --git a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplicationBuilder.java b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplicationBuilder.java new file mode 100644 index 0000000..3c1a476 --- /dev/null +++ b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplicationBuilder.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.server.application.web; + +import org.xbib.net.http.server.BaseApplicationBuilder; +import org.xbib.net.http.server.Application; + +import org.xbib.settings.Settings; + +public class WebApplicationBuilder extends BaseApplicationBuilder { + + protected String profile; + + protected String name; + + protected Settings settings; + + protected WebApplicationBuilder() throws Exception { + super(); + this.profile = System.getProperty("application.profile"); + this.name = System.getProperty("application.name"); + } + + public WebApplicationBuilder setProfile(String profile) { + this.profile = profile; + return this; + } + + public WebApplicationBuilder setName(String name) { + this.name = name; + return this; + } + + public WebApplicationBuilder setSettings(Settings settings) { + this.settings = settings; + return this; + } + + @Override + public Application build() { + WebApplication webApplication = new WebApplication(this); + setupApplication(webApplication); + return webApplication; + } +} diff --git a/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.ApplicationModule b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.ApplicationModule new file mode 100644 index 0000000..b31326c --- /dev/null +++ b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.ApplicationModule @@ -0,0 +1,3 @@ +org.xbib.net.http.server.application.config.ConfigApplicationModule +org.xbib.net.http.template.groovy.GroovyTemplateApplicationModule +org.xbib.net.http.server.application.database.DatabaseApplicationModule diff --git a/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer new file mode 100644 index 0000000..f079b71 --- /dev/null +++ b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer @@ -0,0 +1,4 @@ +org.xbib.net.http.server.netty.http1.Http1ChannelInitializer +org.xbib.net.http.server.netty.http2.Http2ChannelInitializer +org.xbib.net.http.server.netty.secure.http1.Https1ChannelInitializer +org.xbib.net.http.server.netty.secure.http2.Https2ChannelInitializer diff --git a/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider new file mode 100644 index 0000000..e031bc6 --- /dev/null +++ b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.server.netty.NioServerTransportProvider diff --git a/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider new file mode 100644 index 0000000..dfa32bc --- /dev/null +++ b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider @@ -0,0 +1 @@ +org.xbib.net.security.DefaultCertificateProvider diff --git a/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.settings.SettingsLoader b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.settings.SettingsLoader new file mode 100644 index 0000000..287219c --- /dev/null +++ b/net-http-server-application-web/src/main/resources/META-INF/services/org.xbib.settings.SettingsLoader @@ -0,0 +1,2 @@ +org.xbib.settings.datastructures.json.JsonSettingsLoader +org.xbib.settings.datastructures.yaml.YamlSettingsLoader diff --git a/net-http-server-application-web/src/main/resources/logging.properties b/net-http-server-application-web/src/main/resources/logging.properties new file mode 100644 index 0000000..cf55f8f --- /dev/null +++ b/net-http-server-application-web/src/main/resources/logging.properties @@ -0,0 +1,8 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +jdk.event.security.level=INFO +javax.management.mbeanserver.level=INFO +javax.management.misc.level=INFO diff --git a/net-http-server-application-web/src/test/resources/logging.properties b/net-http-server-application-web/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-server-application-web/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-server-netty-secure/build.gradle b/net-http-server-netty-secure/build.gradle new file mode 100644 index 0000000..63d350f --- /dev/null +++ b/net-http-server-netty-secure/build.gradle @@ -0,0 +1,13 @@ +dependencies { + api project(':net-http-server-netty') + api libs.net.security + testImplementation project(':net-http-client-netty-secure') + testImplementation project(':net-http-netty-boringssl') + testImplementation libs.net.bouncycastle +} + +test { + systemProperty 'application.name', 'test' + systemProperty 'application.home', 'src/test/resources' + systemProperty 'application.profile', 'test' +} diff --git a/net-http-server-netty-secure/src/main/java/module-info.java b/net-http-server-netty-secure/src/main/java/module-info.java new file mode 100644 index 0000000..bbc795e --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/module-info.java @@ -0,0 +1,24 @@ +import org.xbib.net.http.server.netty.secure.JdkServerSecureSocketProvider; +import org.xbib.net.http.server.netty.secure.ServerSecureSocketProvider; +import org.xbib.net.security.CertificateProvider; + +module org.xbib.net.http.server.netty.secure { + exports org.xbib.net.http.server.netty.secure.http1; + exports org.xbib.net.http.server.netty.secure.http2; + exports org.xbib.net.http.server.netty.secure; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.xbib.net.http.server.netty; + requires org.xbib.net.security; + requires io.netty.buffer; + requires io.netty.codec.http; + requires io.netty.codec.http2; + requires io.netty.common; + requires io.netty.handler; + requires io.netty.transport; + requires java.logging; + uses CertificateProvider; + uses ServerSecureSocketProvider; + provides ServerSecureSocketProvider with JdkServerSecureSocketProvider; +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsAddress.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsAddress.java new file mode 100644 index 0000000..4480f8e --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsAddress.java @@ -0,0 +1,382 @@ +package org.xbib.net.http.server.netty.secure; + +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import org.xbib.net.security.CertificateProvider; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.security.CertificateReader; +import org.xbib.net.security.util.DistinguishedNameParser; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HttpsAddress extends HttpAddress { + + private static final Logger logger = Logger.getLogger(HttpsAddress.class.getName()); + + private final SslContext sslContext; + + public HttpsAddress(String host, Integer port, HttpVersion version, + boolean secure, Set hostNames, SslContext sslContext) { + super(host, port, version, secure, hostNames); + this.sslContext = sslContext; + } + + public static Builder builder() { + return new Builder().setSecure(true); + } + + public static HttpsAddress https1(String host) throws KeyStoreException, SSLException { + return builder() + .setVersion(HttpVersion.HTTP_1_1) + .setHost(host) + .setPort(443) + .build(); + } + + public static HttpAddress https1(String host, int port) throws KeyStoreException, SSLException { + return builder() + .setVersion(HttpVersion.HTTP_1_1) + .setHost(host) + .setPort(port) + .build(); + } + + public static HttpAddress https2(String host) throws KeyStoreException, SSLException { + return builder() + .setVersion(HttpVersion.HTTP_2_0) + .setHost(host) + .setPort(443) + .build(); + } + + public static HttpAddress https2(String host, int port) throws KeyStoreException, SSLException { + return builder() + .setVersion(HttpVersion.HTTP_2_0) + .setHost(host) + .setPort(port) + .build(); + } + + public SslContext getSslContext() { + return sslContext; + } + + public static class Builder { + + private static TrustManagerFactory TRUST_MANAGER_FACTORY; + + private static final Iterable DEFAULT_OPENSSL_CIPHERS = Arrays.asList( + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ); + + private static final Iterable DEFAULT_JDK_CIPHERS = + Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + + static { + try { + TRUST_MANAGER_FACTORY = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (Exception e) { + TRUST_MANAGER_FACTORY = null; + } + } + + private String host; + + private int port = -1; + + private boolean isSecure = true; + + private HttpVersion httpVersion = HttpVersion.HTTP_1_1; + + private TrustManagerFactory trustManagerFactory; + + private KeyStore trustManagerKeyStore; + + private Provider sslContextProvider; + + private SslProvider sslProvider; + + private Iterable ciphers; + + private CipherSuiteFilter cipherSuiteFilter; + + private Collection certChain; + + private PrivateKey privateKey; + + private ApplicationProtocolConfig applicationProtocolConfig; + + private long sesseionCacheSize = 0L; + + private long sessionTimeout = 0L; + + private ClientAuth clientAuth = ClientAuth.NONE; + + private boolean enableOcsp; + + private Set hostNames; + + private Builder() { + this.trustManagerFactory = TRUST_MANAGER_FACTORY; + this.sslProvider = OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK; + this.ciphers = OpenSsl.isAvailable() ? DEFAULT_OPENSSL_CIPHERS : DEFAULT_JDK_CIPHERS; + this.cipherSuiteFilter = SupportedCipherSuiteFilter.INSTANCE; + } + + public Builder setHost(String host) { + this.host = host; + return this; + } + + public Builder setPort(int port) { + this.port = port; + return this; + } + + public Builder setSecure(boolean secure) { + this.isSecure = secure; + return this; + } + + public Builder setVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public Builder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + public Builder setTrustManagerKeyStore(KeyStore trustManagerKeyStore) { + this.trustManagerKeyStore = trustManagerKeyStore; + return this; + } + + public Builder setSslContextProvider(Provider sslContextProvider) { + this.sslContextProvider = sslContextProvider; + return this; + } + + public Builder setSslProvider(SslProvider sslProvider) { + this.sslProvider = sslProvider; + return this; + } + + public Builder setCiphers(Iterable ciphers) { + this.ciphers = ciphers; + return this; + } + + public Builder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { + this.cipherSuiteFilter = cipherSuiteFilter; + return this; + } + + public Builder setJdkSslProvider() { + setSslProvider(SslProvider.JDK); + setCiphers(DEFAULT_JDK_CIPHERS); + return this; + } + + public Builder setOpenSSLSslProvider() { + setSslProvider(SslProvider.OPENSSL); + setCiphers(DEFAULT_OPENSSL_CIPHERS); + return this; + } + + public Builder setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + return this; + } + + public Builder setCertChain(Collection chain) { + Objects.requireNonNull(chain); + this.certChain = chain; + return this; + } + + public Builder setCertChain(InputStream keyInputStream, String password, InputStream chain) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + ServiceLoader certificateProviders = ServiceLoader.load(CertificateProvider.class); + boolean found = false; + for (CertificateProvider provider : certificateProviders) { + try { + Map.Entry> entry = + provider.provide(keyInputStream, password, chain); + if (entry != null) { + setPrivateKey(entry.getKey()); + setCertChain(entry.getValue()); + found = true; + break; + } + } catch (CertificateException | IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + if (!found) { + throw new CertificateException("no certificate found"); + } + // automatic adding of certificate DNS names for automatic domain name match setup + List certificates = CertificateReader.orderCertificateChain(certChain); + hostNames = getServerNames(certificates.get(0)); + return this; + } + + public Builder setSelfCert(String fullQualifiedDomainName) throws CertificateException { + ServiceLoader certificateProviders = ServiceLoader.load(CertificateProvider.class); + boolean found = false; + for (CertificateProvider provider : certificateProviders) { + try { + Map.Entry> entry = + provider.provideSelfSigned(fullQualifiedDomainName); + setPrivateKey(entry.getKey()); + setCertChain(entry.getValue()); + found = true; + } catch (CertificateException | IOException e) { + // ignore + } + } + if (!found) { + throw new CertificateException("no self-signed certificate found"); + } + return this; + } + + public Builder setApplicationProtocolConfig(ApplicationProtocolConfig applicationProtocolConfig) { + this.applicationProtocolConfig = applicationProtocolConfig; + return this; + } + + public Builder setSessionCacheSize(long sessionCacheSize) { + this.sesseionCacheSize = sessionCacheSize; + return this; + } + + public Builder setSessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + + /** + * NONE, OPTIONAL, REQUIRE. + * @param clientAuth the client auth mode + * @return this builder + */ + public Builder setClientAuth(ClientAuth clientAuth) { + this.clientAuth = clientAuth; + return this; + } + + public Builder enableOcsp(boolean enableOcsp) { + this.enableOcsp = enableOcsp; + return this; + } + + public HttpsAddress build() throws KeyStoreException, SSLException { + Objects.requireNonNull(host); + Objects.requireNonNull(httpVersion); + Objects.requireNonNull(privateKey); + Objects.requireNonNull(certChain); + if (certChain.isEmpty()) { + throw new IllegalArgumentException("cert chain must not be empty"); + } + Objects.requireNonNull(sslProvider); + Objects.requireNonNull(ciphers); + Objects.requireNonNull(cipherSuiteFilter); + // trustManagerKeyStore may be null, this will be used to init() for default behavior + trustManagerFactory.init(trustManagerKeyStore); + SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(privateKey, certChain) + .trustManager(trustManagerFactory) + .sslProvider(sslProvider) + .ciphers(ciphers, cipherSuiteFilter); + if (sslContextProvider != null) { + sslContextBuilder.sslContextProvider(sslContextProvider); + } + if (applicationProtocolConfig == null) { + if (httpVersion.equals(HttpVersion.HTTP_2_0)) { + // OpenSSL does not support FATAL_ALERT behaviour + applicationProtocolConfig = new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1); + } + if (httpVersion.equals(HttpVersion.HTTP_1_1)) { + applicationProtocolConfig = new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_1_1); + } + } + sslContextBuilder.applicationProtocolConfig(applicationProtocolConfig); + sslContextBuilder.sessionCacheSize(sesseionCacheSize); + sslContextBuilder.sessionTimeout(sessionTimeout); + sslContextBuilder.clientAuth(clientAuth); + sslContextBuilder.enableOcsp(enableOcsp); + SslContext sslContext = sslContextBuilder.build(); + logger.log(Level.FINE, "SSL context up: " + sslContext.getClass().getName() + + " negotiating for protocols = " + sslContext.applicationProtocolNegotiator().protocols() + + " session cache = " + sslContext.sessionCacheSize() + + " session timeout = " + sslContext.sessionTimeout() + + " cipher suite = " + sslContext.cipherSuites() + ); + return new HttpsAddress(host, port, httpVersion, isSecure, hostNames, sslContext); + } + } + + private static Set getServerNames(X509Certificate certificate) throws CertificateParsingException { + Set set = new LinkedHashSet<>(); + set.add(new DistinguishedNameParser(certificate.getSubjectX500Principal()).findMostSpecific("CN")); + Collection> altNames = certificate.getSubjectAlternativeNames(); + if (altNames != null) { + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + if (type == 2) { // Type = DNS + String string = altName.get(1).toString(); + set.add(string); + } + } + } + return set; + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequest.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequest.java new file mode 100644 index 0000000..6db0f5d --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequest.java @@ -0,0 +1,27 @@ +package org.xbib.net.http.server.netty.secure; + +import org.xbib.net.http.server.netty.HttpRequest; + +import javax.net.ssl.SSLSession; + +public class HttpsRequest extends HttpRequest { + + private final HttpsRequestBuilder builder; + + protected HttpsRequest(HttpsRequestBuilder builder) { + super(builder); + this.builder = builder; + } + + public static HttpsRequestBuilder builder() { + return new HttpsRequestBuilder(); + } + + public SSLSession getSSLSession() { + return builder.sslSession; + } + + public String getSNIHost() { + return builder.sniHost; + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequestBuilder.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequestBuilder.java new file mode 100644 index 0000000..b76162f --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/HttpsRequestBuilder.java @@ -0,0 +1,86 @@ +package org.xbib.net.http.server.netty.secure; + +import io.netty.handler.codec.http.FullHttpRequest; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpRequestBuilder; + +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; + +public class HttpsRequestBuilder extends HttpRequestBuilder { + + protected SSLSession sslSession; + + protected String sniHost; + + protected HttpsRequestBuilder() { + } + + public HttpsRequestBuilder setFullHttpRequest(FullHttpRequest fullHttpRequest) { + super.setFullHttpRequest(fullHttpRequest); + return this; + } + + @Override + public HttpsRequestBuilder setAddress(HttpAddress httpAddress) { + super.setAddress(httpAddress); + return this; + } + + @Override + public HttpsRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + super.setLocalAddress(localAddress); + return this; + } + + @Override + public HttpsRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + super.setRemoteAddress(remoteAddress); + return this; + } + + @Override + public HttpsRequestBuilder setBaseURL(URL baseURL) { + super.setBaseURL(baseURL); + return this; + } + + @Override + public HttpsRequestBuilder setBaseURL(HttpAddress httpAddress, String uri, String hostAndPort) { + super.setBaseURL(httpAddress, uri, hostAndPort); + return this; + } + + @Override + public HttpsRequestBuilder setSequenceId(Integer sequenceId) { + super.setSequenceId(sequenceId); + return this; + } + + @Override + public HttpsRequestBuilder setStreamId(Integer streamId) { + super.setStreamId(streamId); + return this; + } + + @Override + public HttpsRequestBuilder setRequestId(Long requestId) { + super.setRequestId(requestId); + return this; + } + + public HttpsRequestBuilder setSNIHost(String host) { + this.sniHost = host; + return this; + } + + public HttpsRequestBuilder setSSLSession(SSLSession sslSession) { + this.sslSession = sslSession; + return this; + } + + public HttpsRequest build() { + return new HttpsRequest(this); + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/JdkServerSecureSocketProvider.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/JdkServerSecureSocketProvider.java new file mode 100644 index 0000000..59d1b9f --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/JdkServerSecureSocketProvider.java @@ -0,0 +1,45 @@ +package org.xbib.net.http.server.netty.secure; + +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import java.security.Provider; +import java.util.Arrays; +import javax.net.ssl.SSLSocketFactory; +import org.xbib.net.http.HttpAddress; + +public class JdkServerSecureSocketProvider implements ServerSecureSocketProvider { + + public JdkServerSecureSocketProvider() { + } + + @Override + public String name() { + return "JDK"; + } + + @Override + public Provider securityProvider(HttpAddress httpAddress) { + return null; + } + + @Override + public SslProvider sslProvider(HttpAddress httpAddress) { + return SslProvider.JDK; + } + + @Override + public Iterable ciphers(HttpAddress httpAddress) { + return Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + } + + @Override + public CipherSuiteFilter cipherSuiteFilter(HttpAddress httpAddress) { + return SupportedCipherSuiteFilter.INSTANCE; + } + + @Override + public String[] protocols(HttpAddress httpAddress) { + return new String[] { "TLSv1.3", "TLSv1.2" }; + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/NettyHttpsServerConfig.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/NettyHttpsServerConfig.java new file mode 100644 index 0000000..0acdd0d --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/NettyHttpsServerConfig.java @@ -0,0 +1,172 @@ +package org.xbib.net.http.server.netty.secure; + +import io.netty.handler.ssl.SslContext; +import io.netty.util.AttributeKey; +import io.netty.util.DomainWildcardMappingBuilder; +import io.netty.util.Mapping; +import java.security.AlgorithmConstraints; +import java.util.Optional; +import java.util.ServiceLoader; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.HttpDomain; + +import java.util.Collection; + +public class NettyHttpsServerConfig extends NettyHttpServerConfig { + + public static final AttributeKey ATTRIBUTE_KEY_SNI_HANDLER = AttributeKey.valueOf("_sni_handler"); + + private static final String[] DEFAULT_PROTOCOLS = new String[] { "TLSv1.2", "TLSv1.3"}; // prefer TLSv1.2 + + private static ServerSecureSocketProvider serverSecureSocketProvider; + + static { + ServiceLoader serviceLoader = ServiceLoader.load(ServerSecureSocketProvider.class); + Optional optional = serviceLoader.findFirst(); + serverSecureSocketProvider = optional.orElse(new JdkServerSecureSocketProvider()); + } + + private Mapping domainNameMapping; + + private String[] protocols; + + private String[] cipherSuites; + + private boolean isUseCipherSuiteOrdered; + + private boolean isRetransmissionEnabled; + + private int maximumPacketSize; + + private AlgorithmConstraints algorithmConstraints; + + private boolean needsClientAuth; + + private boolean wantsClientAuth; + + public NettyHttpsServerConfig() { + this.isRetransmissionEnabled = true; + this.maximumPacketSize = 0; + this.isUseCipherSuiteOrdered = true; + this.algorithmConstraints = null; + this.needsClientAuth = false; + this.wantsClientAuth = false; + } + + public static void setServerSecureSocketProvider(ServerSecureSocketProvider serverSecureSocketProvider) { + NettyHttpsServerConfig.serverSecureSocketProvider = serverSecureSocketProvider; + } + + public ServerSecureSocketProvider getServerSecureSocketProvider() { + return serverSecureSocketProvider; + } + + public Mapping getDomainNameMapping(Collection domains) { + if (domainNameMapping == null) { + buildMapping(domains); + } + return domainNameMapping; + } + + public void setProtocols(String[] protocols) { + this.protocols = protocols; + } + + public String[] getProtocols(HttpAddress httpAddress) { + if (protocols == null) { + protocols = serverSecureSocketProvider.protocols(httpAddress); + } + if (protocols == null) { + protocols = DEFAULT_PROTOCOLS; + } + return protocols; + } + + public void setCipherSuites(String[] cipherSuites) { + this.cipherSuites = cipherSuites; + } + + public String[] getCipherSuites(HttpAddress httpAddress) { + return cipherSuites; + } + + public void setUseCipherSuiteOrdered(boolean useCipherSuiteOrdered) { + isUseCipherSuiteOrdered = useCipherSuiteOrdered; + } + + public boolean isUseCipherSuiteOrdered() { + return isUseCipherSuiteOrdered; + } + + public void setMaximumPacketSize(int maximumPacketSize) { + this.maximumPacketSize = maximumPacketSize; + } + + public int getMaximumPacketSize() { + return maximumPacketSize; + } + + public void setRetransmissionEnabled(boolean retransmissionEnabled) { + isRetransmissionEnabled = retransmissionEnabled; + } + + public boolean isRetransmissionEnabled() { + return isRetransmissionEnabled; + } + + public void setAlgorithmConstraints(AlgorithmConstraints algorithmConstraints) { + this.algorithmConstraints = algorithmConstraints; + } + + public AlgorithmConstraints getAlgorithmConstraints() { + return algorithmConstraints; + } + + public void setNeedsClientAuth(boolean needsClientAuth) { + this.needsClientAuth = needsClientAuth; + } + + public boolean isNeedsClientAuth() { + return needsClientAuth; + } + + public void setWantsClientAuth(boolean wantsClientAuth) { + this.wantsClientAuth = wantsClientAuth; + } + + public boolean isWantsClientAuth() { + return wantsClientAuth; + } + + private void buildMapping(Collection domains) { + if (domains.isEmpty()) { + throw new IllegalStateException("no domains found for domain name mapping"); + } + // use first domain as default domain for SSL context + SslContext defaultContext = getSslContextFrom(domains.iterator().next()); + DomainWildcardMappingBuilder mappingBuilder = new DomainWildcardMappingBuilder<>(defaultContext); + for (HttpDomain httpDomain : domains) { + SslContext sslContext = getSslContextFrom(httpDomain); + HttpAddress httpAddress = httpDomain.getAddress(); + if (httpAddress.getHostNames() != null) { + for (String name : httpAddress.getHostNames()) { + mappingBuilder.add(name + ":" + httpAddress.getPort(), sslContext); + } + } + for (String name : httpDomain.getNames()) { + mappingBuilder.add(name, sslContext); + } + } + domainNameMapping = mappingBuilder.build(); + } + + public SslContext getSslContextFrom(HttpDomain httpDomain) { + HttpAddress httpAddress = httpDomain.getAddress(); + if (httpAddress instanceof HttpsAddress) { + HttpsAddress httpsAddress = (HttpsAddress) httpAddress; + return httpsAddress.getSslContext(); + } + throw new IllegalStateException("no secure http, no SslContext configured for domain " + httpDomain); + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerNameIndicationHandler.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerNameIndicationHandler.java new file mode 100644 index 0000000..edcc951 --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerNameIndicationHandler.java @@ -0,0 +1,64 @@ +package org.xbib.net.http.server.netty.secure; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.SniHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.Mapping; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; + +public class ServerNameIndicationHandler extends SniHandler { + + private static final Logger logger = Logger.getLogger(ServerNameIndicationHandler.class.getName()); + + private final NettyHttpsServerConfig serverConfig; + + private final HttpAddress httpAddress; + + private SslHandler sslHandler; + + public ServerNameIndicationHandler(NettyHttpsServerConfig serverConfig, + HttpAddress httpAddress, + Mapping mapping) { + super(mapping); + this.serverConfig = serverConfig; + this.httpAddress = httpAddress; + } + + public SslHandler getSslHandler() { + return sslHandler; + } + + @Override + protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) { + sslHandler = createSslHandler(context, allocator); + return sslHandler; + } + + private SslHandler createSslHandler(SslContext sslContext, ByteBufAllocator allocator) { + SslHandler sslHandler = sslContext.newHandler(allocator); + SSLEngine engine = sslHandler.engine(); + SSLParameters params = engine.getSSLParameters(); + params.setEndpointIdentificationAlgorithm("HTTPS"); + params.setEnableRetransmissions(serverConfig.isRetransmissionEnabled()); + params.setCipherSuites(serverConfig.getCipherSuites(httpAddress)); + params.setMaximumPacketSize(serverConfig.getMaximumPacketSize()); + params.setUseCipherSuitesOrder(serverConfig.isUseCipherSuiteOrdered()); + params.setAlgorithmConstraints(serverConfig.getAlgorithmConstraints()); + params.setNeedClientAuth(serverConfig.isNeedsClientAuth()); + params.setNeedClientAuth(serverConfig.isWantsClientAuth()); + engine.setSSLParameters(params); + String[] protocols = serverConfig.getProtocols(httpAddress); + logger.log(Level.FINER, () -> "enabled TLS protocols in SSL engine = " + Arrays.asList(protocols)); + engine.setEnabledProtocols(protocols); + logger.log(Level.FINER, () -> "enabled application protocol negotiator protocols = " + + sslContext.applicationProtocolNegotiator().protocols()); + return sslHandler; + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerSecureSocketProvider.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerSecureSocketProvider.java new file mode 100644 index 0000000..839bfb6 --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/ServerSecureSocketProvider.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.server.netty.secure; + +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; +import java.security.Provider; +import org.xbib.net.http.HttpAddress; + +public interface ServerSecureSocketProvider { + + String name(); + + Provider securityProvider(HttpAddress httpAddress); + + SslProvider sslProvider(HttpAddress httpAddress); + + Iterable ciphers(HttpAddress httpAddress); + + CipherSuiteFilter cipherSuiteFilter(HttpAddress httpAddress); + + String[] protocols(HttpAddress httpAddress); +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1ChannelInitializer.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1ChannelInitializer.java new file mode 100644 index 0000000..148a3da --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1ChannelInitializer.java @@ -0,0 +1,117 @@ +package org.xbib.net.http.server.netty.secure.http1; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.util.AsciiString; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpChannelInitializer; +import org.xbib.net.http.server.netty.NettyCustomizer; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.netty.http1.HttpPipeliningHandler; +import org.xbib.net.http.server.netty.IdleTimeoutHandler; +import org.xbib.net.http.server.netty.TrafficLoggingHandler; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.netty.secure.ServerNameIndicationHandler; +import org.xbib.net.http.server.netty.secure.http2.Https2ChildChannelInitializer; + +public class Https1ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Https1ChannelInitializer.class.getName()); + + public Https1ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress address) { + return org.xbib.net.http.HttpVersion.HTTP_1_1.equals(address.getVersion()) && address.isSecure(); + } + + @Override + public void init(Channel channel, NettyHttpServer nettyHttpServer, NettyCustomizer customizer) { + final HttpAddress httpAddress = channel.attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + final NettyHttpsServerConfig nettyHttpsServerConfig = (NettyHttpsServerConfig) nettyHttpServer.getNettyHttpServerConfig(); + final ServerNameIndicationHandler serverNameIndicationHandler = + new ServerNameIndicationHandler(nettyHttpsServerConfig, httpAddress, + nettyHttpsServerConfig.getDomainNameMapping(nettyHttpServer.getApplication().getDomains())); + channel.attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_SNI_HANDLER).set(serverNameIndicationHandler); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("server-sni", serverNameIndicationHandler); + HttpServerCodec httpServerCodec = new HttpServerCodec(nettyHttpsServerConfig.getMaxInitialLineLength(), + nettyHttpsServerConfig.getMaxHeadersSize(), nettyHttpsServerConfig.getMaxChunkSize()); + pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); + pipeline.addLast("server-codec", httpServerCodec); + if (nettyHttpsServerConfig.isCompressionEnabled()) { + pipeline.addLast("server-compressor", new HttpContentCompressor()); + } + if (nettyHttpsServerConfig.isDecompressionEnabled()) { + pipeline.addLast("server-decompressor", new HttpContentDecompressor()); + } + HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(nettyHttpsServerConfig.getMaxContentLength()); + httpObjectAggregator.setMaxCumulationBufferComponents(nettyHttpsServerConfig.getMaxCompositeBufferComponents()); + pipeline.addLast("server-aggregator", httpObjectAggregator); + if (nettyHttpsServerConfig.isPipeliningEnabled()) { + pipeline.addLast("server-pipelining", new HttpPipeliningHandler(nettyHttpsServerConfig.getPipeliningCapacity())); + } + pipeline.addLast("server-messages", new Https1Handler(nettyHttpServer)); + pipeline.addLast("server-idle-timeout", new IdleTimeoutHandler(nettyHttpsServerConfig.getTimeoutMillis())); + if (nettyHttpsServerConfig.isDebug()) { + pipeline.addLast("server-logging", new TrafficLoggingHandler(LogLevel.DEBUG)); + } + if (customizer != null) { + customizer.afterChannelInitialized(channel); + } + if (nettyHttpsServerConfig.isDebug()) { + logger.log(Level.FINE, "HTTP/1.1 secure server channel initialized: address=" + channel.localAddress() + + " pipeline=" + pipeline.names()); + } + } + + /** + * This upgrade handler ensures to upgrade to HTTPS 2.0 via prior knowledge. + * @param nettyHttpServer the netty server + * @param httpAddress the HTTP address + * @param serverNameIndicationHandler the SNI handler + * @return the CleartextHttp2ServerUpgradeHandler + */ + protected CleartextHttp2ServerUpgradeHandler createUpgradeHandler(NettyHttpServer nettyHttpServer, + HttpAddress httpAddress, + ServerNameIndicationHandler serverNameIndicationHandler) { + NettyHttpServerConfig nettyHttpServerConfig = nettyHttpServer.getNettyHttpServerConfig(); + Https2ChildChannelInitializer childHandler = + new Https2ChildChannelInitializer(nettyHttpServer, httpAddress, serverNameIndicationHandler); + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(childHandler) + .initialSettings(Http2Settings.defaultSettings()); + if (nettyHttpServerConfig.isDebug()) { + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); + } + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.build(); + HttpServerCodec serverCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(serverCodec, protocol -> { + logger.log(Level.INFO, "upgrade handler protocol = " + protocol); + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(multiplexCodec); + } else { + return null; + } + }); + return new CleartextHttp2ServerUpgradeHandler(serverCodec, upgradeHandler, multiplexCodec); + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1Handler.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1Handler.java new file mode 100644 index 0000000..6f2b266 --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http1/Https1Handler.java @@ -0,0 +1,104 @@ +package org.xbib.net.http.server.netty.secure.http1; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import java.net.InetSocketAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.netty.handler.codec.http.HttpResponseStatus; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpResponse; +import org.xbib.net.http.server.netty.HttpResponseBuilder; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.http1.HttpPipelinedRequest; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.HttpsRequestBuilder; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.netty.secure.ServerNameIndicationHandler; + +@ChannelHandler.Sharable +public class Https1Handler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Https1Handler.class.getName()); + + private final NettyHttpServer nettyHttpServer; + + public Https1Handler(NettyHttpServer nettyHttpServer) { + this.nettyHttpServer = nettyHttpServer; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpPipelinedRequest) { + HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg; + try { + if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) httpPipelinedRequest.getRequest(); + requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId()); + } + } finally { + httpPipelinedRequest.release(); + } + } else if (msg instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) msg; + try { + requestReceived(ctx, fullHttpRequest, 0); + } finally { + fullHttpRequest.release(); + } + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + ctx.close(); + } + + protected void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) { + HttpAddress httpAddress = ctx.channel().attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + try { + HttpResponseBuilder serverResponseBuilder = HttpResponse.builder() + .setChannelHandlerContext(ctx); + if (nettyHttpServer.getNettyHttpServerConfig().isPipeliningEnabled()) { + serverResponseBuilder.setSequenceId(sequenceId); + } + // host header present? RFC2616#14.23: missing Host header gets 400 + HttpsRequestBuilder serverRequestBuilder = HttpsRequest.builder() + .setLocalAddress((InetSocketAddress) ctx.channel().localAddress()) + .setRemoteAddress((InetSocketAddress) ctx.channel().remoteAddress()) + .setSequenceId(sequenceId) + .setFullHttpRequest(fullHttpRequest) + .setBaseURL(httpAddress, + fullHttpRequest.uri(), + fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + serverResponseBuilder.shouldClose("close".equalsIgnoreCase(fullHttpRequest.headers().get(HttpHeaderNames.CONNECTION))); + // find SSL session, we have to look up the SSL handler in the SNI handler + ServerNameIndicationHandler serverNameIndicationHandler = + ctx.channel().attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_SNI_HANDLER).get(); + if (serverNameIndicationHandler != null) { + serverRequestBuilder.setSNIHost(serverNameIndicationHandler.hostname()); + serverRequestBuilder.setSSLSession(serverNameIndicationHandler.getSslHandler().engine().getSession()); + } + nettyHttpServer.getApplication().dispatch(serverRequestBuilder, serverResponseBuilder); + } catch (Exception e) { + logger.log(Level.SEVERE, "bad request: " + e.getMessage(), e); + DefaultFullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.valueOf(httpAddress.getVersion().text()), + HttpResponseStatus.BAD_REQUEST); + ctx.writeAndFlush(fullHttpResponse); + ctx.close(); + } + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChannelInitializer.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChannelInitializer.java new file mode 100644 index 0000000..75c71e9 --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChannelInitializer.java @@ -0,0 +1,120 @@ +package org.xbib.net.http.server.netty.secure.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.logging.LogLevel; +import io.netty.util.AsciiString; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpChannelInitializer; +import org.xbib.net.http.server.netty.IdleTimeoutHandler; +import org.xbib.net.http.server.netty.NettyCustomizer; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.netty.TrafficLoggingHandler; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.netty.secure.ServerNameIndicationHandler; + +public class Https2ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Https2ChannelInitializer.class.getName()); + + public Https2ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress address) { + return org.xbib.net.http.HttpVersion.HTTP_2_0.equals(address.getVersion()) && address.isSecure(); + } + + @Override + public void init(Channel channel, NettyHttpServer nettyHttpServer, NettyCustomizer customizer) { + final HttpAddress httpAddress = channel.attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + final NettyHttpsServerConfig nettyHttpsServerConfig = (NettyHttpsServerConfig) nettyHttpServer.getNettyHttpServerConfig(); + final ServerNameIndicationHandler serverNameIndicationHandler = + new ServerNameIndicationHandler(nettyHttpsServerConfig, httpAddress, + nettyHttpsServerConfig.getDomainNameMapping(nettyHttpServer.getApplication().getDomains())); + channel.attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_SNI_HANDLER).set(serverNameIndicationHandler); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("server-sni", serverNameIndicationHandler); + if (nettyHttpsServerConfig.isDebug()) { + pipeline.addLast("server-logger", new TrafficLoggingHandler(LogLevel.DEBUG)); + } + pipeline.addLast("server-upgrade", createUpgradeHandler(nettyHttpServer, httpAddress, serverNameIndicationHandler)); + // handler for HTTP1 + pipeline.addLast("server-object-aggregator", new HttpObjectAggregator(nettyHttpsServerConfig.getMaxContentLength())); + pipeline.addLast("server-requests", new Https2Handler(nettyHttpServer)); + pipeline.addLast("server-messages", new Https2Messages()); + pipeline.addLast("server-idle-timeout", new IdleTimeoutHandler(nettyHttpsServerConfig.getTimeoutMillis())); + if (customizer != null) { + customizer.afterChannelInitialized(channel); + } + if (nettyHttpsServerConfig.isDebug()) { + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "HTTP/2 secure server channel initialized: address=" + + channel.localAddress() + " pipeline=" + channel.pipeline().names()); + } + } + } + + protected CleartextHttp2ServerUpgradeHandler createUpgradeHandler(NettyHttpServer nettyHttpServer, + HttpAddress httpAddress, + ServerNameIndicationHandler serverNameIndicationHandler) { + NettyHttpServerConfig nettyHttpServerConfig = nettyHttpServer.getNettyHttpServerConfig(); + Https2ChildChannelInitializer childHandler = + new Https2ChildChannelInitializer(nettyHttpServer, httpAddress, serverNameIndicationHandler); + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(childHandler) + .initialSettings(Http2Settings.defaultSettings()); + if (nettyHttpServerConfig.isDebug()) { + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); + } + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.build(); + HttpServerCodec serverCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(serverCodec, protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(multiplexCodec); + } else { + return null; + } + }, nettyHttpServerConfig.getMaxContentLength()); + return new CleartextHttp2ServerUpgradeHandler(serverCodec, upgradeHandler, multiplexCodec); + } + + protected CleartextHttp2ServerUpgradeHandler createNewUpgradeHandler(NettyHttpServer nettyHttpServer, + HttpAddress httpAddress, + ServerNameIndicationHandler serverNameIndicationHandler) { + NettyHttpServerConfig nettyHttpServerConfig = nettyHttpServer.getNettyHttpServerConfig(); + Https2ChildChannelInitializer childHandler = + new Https2ChildChannelInitializer(nettyHttpServer, httpAddress, serverNameIndicationHandler); + Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forServer() + .frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")) + .initialSettings(Http2Settings.defaultSettings()) + .validateHeaders(true) + .build(); + Http2MultiplexHandler multiplexHandler = new Http2MultiplexHandler(childHandler); + HttpServerCodec serverCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(serverCodec, protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(frameCodec, multiplexHandler); + } else { + return null; + } + }, nettyHttpServerConfig.getMaxContentLength()); + return new CleartextHttp2ServerUpgradeHandler(serverCodec, upgradeHandler, childHandler); + } + +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChildChannelInitializer.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChildChannelInitializer.java new file mode 100644 index 0000000..b62a325 --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2ChildChannelInitializer.java @@ -0,0 +1,52 @@ +package org.xbib.net.http.server.netty.secure.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.netty.handler.stream.ChunkedWriteHandler; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.IdleTimeoutHandler; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.netty.secure.ServerNameIndicationHandler; + +public class Https2ChildChannelInitializer extends ChannelInitializer { + + private final NettyHttpServer server; + + private final HttpAddress httpAddress; + + private final ServerNameIndicationHandler serverNameIndicationHandler; + + public Https2ChildChannelInitializer(NettyHttpServer server, + HttpAddress httpAddress, + ServerNameIndicationHandler serverNameIndicationHandler) { + this.server = server; + this.httpAddress = httpAddress; + this.serverNameIndicationHandler = serverNameIndicationHandler; + } + + @Override + protected void initChannel(Channel channel) { + NettyHttpsServerConfig nettyHttpsServerConfig = (NettyHttpsServerConfig) server.getNettyHttpServerConfig(); + channel.attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).set(httpAddress); + channel.attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_SNI_HANDLER).set(serverNameIndicationHandler); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("server-frame-converter", new Http2StreamFrameToHttpObjectCodec(true)); + if (nettyHttpsServerConfig.isCompressionEnabled()) { + pipeline.addLast("server-compressor", new HttpContentCompressor()); + } + if (nettyHttpsServerConfig.isDecompressionEnabled()) { + pipeline.addLast("server-decompressor", new HttpContentDecompressor()); + } + pipeline.addLast("server-object-aggregator", new HttpObjectAggregator(nettyHttpsServerConfig.getMaxContentLength())); + pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); + pipeline.addLast("server-request", new Https2Handler(server)); + pipeline.addLast("server-messages", new Https2Messages()); + pipeline.addLast("server-idle-timeout", new IdleTimeoutHandler(nettyHttpsServerConfig.getTimeoutMillis())); + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Handler.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Handler.java new file mode 100644 index 0000000..abbed28 --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Handler.java @@ -0,0 +1,83 @@ +package org.xbib.net.http.server.netty.secure.http2; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.HttpConversionUtil; +import java.net.InetSocketAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.netty.HttpResponse; +import org.xbib.net.http.server.netty.HttpResponseBuilder; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.http2.Http2Handler; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.HttpsRequestBuilder; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.netty.secure.ServerNameIndicationHandler; + +@ChannelHandler.Sharable +public class Https2Handler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Http2Handler.class.getName()); + + private final NettyHttpServer nettyHttpServer; + + public Https2Handler(NettyHttpServer nettyHttpServer) { + this.nettyHttpServer = nettyHttpServer; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) msg; + HttpAddress httpAddress = ctx.channel().attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + try { + Integer streamId = fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + HttpResponseBuilder httpsResponseBuilder = HttpResponse.builder() + .setChannelHandlerContext(ctx) + .setVersion(HttpVersion.HTTP_2_0); + if (streamId != null) { + httpsResponseBuilder.setStreamId(streamId + 1); + } + HttpsRequestBuilder httpsRequestBuilder = HttpsRequest.builder() + .setFullHttpRequest(fullHttpRequest) + .setBaseURL(httpAddress, + fullHttpRequest.uri(), + fullHttpRequest.headers().get(HttpHeaderNames.HOST)) + .setLocalAddress((InetSocketAddress) ctx.channel().localAddress()) + .setRemoteAddress((InetSocketAddress) ctx.channel().remoteAddress()) + .setStreamId(streamId); + if ("PRI".equals(fullHttpRequest.method().name())) { + nettyHttpServer.getApplication().dispatch(httpsRequestBuilder, httpsResponseBuilder, HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED); + return; + } + httpsResponseBuilder.shouldClose("close".equalsIgnoreCase(fullHttpRequest.headers().get(HttpHeaderNames.CONNECTION))); + ServerNameIndicationHandler serverNameIndicationHandler = + ctx.channel().attr(NettyHttpsServerConfig.ATTRIBUTE_KEY_SNI_HANDLER).get(); + if (serverNameIndicationHandler != null) { + httpsRequestBuilder.setSNIHost(serverNameIndicationHandler.hostname()); + httpsRequestBuilder.setSSLSession(serverNameIndicationHandler.getSslHandler().engine().getSession()); + } + nettyHttpServer.getApplication().dispatch(httpsRequestBuilder, httpsResponseBuilder); + } catch (Exception e) { + logger.log(Level.SEVERE, "bad request: " + e.getMessage(), e); + DefaultFullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.valueOf(httpAddress.getVersion().text()), + io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST); + ctx.writeAndFlush(fullHttpResponse); + ctx.close(); + } finally { + fullHttpRequest.release(); + } + } else { + super.channelRead(ctx, msg); + } + } +} diff --git a/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Messages.java b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Messages.java new file mode 100644 index 0000000..4616a5c --- /dev/null +++ b/net-http-server-netty-secure/src/main/java/org/xbib/net/http/server/netty/secure/http2/Https2Messages.java @@ -0,0 +1,36 @@ +package org.xbib.net.http.server.netty.secure.http2; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ChannelHandler.Sharable +public class Https2Messages extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Https2Messages.class.getName()); + + public Https2Messages() { + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof DefaultHttp2SettingsFrame) { + DefaultHttp2SettingsFrame http2SettingsFrame = (DefaultHttp2SettingsFrame) msg; + logger.log(Level.FINER, "settings received, ignoring " + http2SettingsFrame); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + ctx.close(); + } +} diff --git a/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerMultiRequestLoadTest.java b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerMultiRequestLoadTest.java new file mode 100644 index 0000000..6cc6055 --- /dev/null +++ b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerMultiRequestLoadTest.java @@ -0,0 +1,107 @@ +package org.xbib.net.http.server.netty.secure.test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.util.ResourceLeakDetector; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.secure.HttpsAddress; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NettyHttps2ServerMultiRequestLoadTest { + + private static final Logger logger = Logger.getLogger(NettyHttps2ServerMultiRequestLoadTest.class.getName()); + + @Test + public void testHttps2Load() throws Exception { + // client HTTP 2.0, server HTTP 2.0 + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setSecure(true) + .setVersion(HttpVersion.HTTP_2_0) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain " + + " SNI host " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + " " + + " SSL peer host " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + " " + + " base URL = " + ctx.request().getBaseURL() + " " + + ctx.httpRequest().getParameter() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .trustInsecure(); + int requests = 32; + AtomicInteger count = new AtomicInteger(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + for (int i = 0; i < requests; i++) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setVersion(HttpVersion.HTTP_2_0) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTP/2): " + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + count.incrementAndGet(); + }) + .build(); + client.execute(request); // wiithout get(), this works because of HTTP/2 client + } + } + logger.log(Level.INFO, "count = " + count.get()); + assertEquals(requests, count.get()); + } + } +} diff --git a/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerTest.java b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerTest.java new file mode 100644 index 0000000..7cdce68 --- /dev/null +++ b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttps2ServerTest.java @@ -0,0 +1,106 @@ +package org.xbib.net.http.server.netty.secure.test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.util.ResourceLeakDetector; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.secure.HttpsAddress; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NettyHttps2ServerTest { + + private static final Logger logger = Logger.getLogger(NettyHttps2ServerTest.class.getName()); + + @Test + public void testHttps2() throws Exception { + // client HTTP 2.0 + server HTTP 2.0 (no upgrade) + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setSecure(true) + .setVersion(HttpVersion.HTTP_2_0) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + serverConfig.setDebug(true); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain " + + " SNI host " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + " " + + " SSL peer host " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + " " + + " base URL = " + ctx.request().getBaseURL() + " " + + ctx.httpRequest().getParameter() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + AtomicBoolean received = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .trustInsecure() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setVersion(HttpVersion.HTTP_2_0) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTP/2):" + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + received.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } + } +} diff --git a/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerMultiRequestLoadTest.java b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerMultiRequestLoadTest.java new file mode 100644 index 0000000..c667a62 --- /dev/null +++ b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerMultiRequestLoadTest.java @@ -0,0 +1,197 @@ +package org.xbib.net.http.server.netty.secure.test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.util.ResourceLeakDetector; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.secure.HttpsAddress; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NettyHttpsServerMultiRequestLoadTest { + + private static final Logger logger = Logger.getLogger(NettyHttpsServerMultiRequestLoadTest.class.getName()); + + @Test + public void testHttps1Load() throws Exception { + // client HTTP 1.1, server HTTP 1.1 (no upgrade) + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setVersion(HttpVersion.HTTP_1_1) + .setSecure(true) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + logger.log(Level.INFO, "executing /secure"); + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain: " + + " SNI host = " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + + " SSL peer host = " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setSecureProtocolName(new String[]{ "TLSv1.2" } ) + .trustInsecure(); + int requests = 1024; + AtomicInteger count = new AtomicInteger(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + for (int i = 0; i < requests; i++) { + HttpRequest request = HttpRequest.get() + .setVersion(HttpVersion.HTTP_1_1) + .setURL(url) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTPS 1.1): " + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + count.incrementAndGet(); + }) + .setExceptionListener(e -> { + logger.log(Level.SEVERE, e.getMessage(), e); + }) + .setTimeoutListener(listener -> { + logger.log(Level.SEVERE, "timeout"); + }, 5000L) + .build(); + client.execute(request).get(); + } + } + logger.log(Level.INFO, "count = " + count.get()); + assertEquals(requests, count.get()); + } + } + + @Test + public void testHttps1LoadUpgrade() throws Exception { + // client HTTP 1.1, server HTTP 2.0 (upgrade) - use this with browsers + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setSecure(true) + .setVersion(HttpVersion.HTTP_2_0) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain: " + + " SNI host = " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + + " SSL session = " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .trustInsecure(); + int requests = 1024; + AtomicInteger count = new AtomicInteger(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + for (int i = 0; i < requests; i++) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setVersion(HttpVersion.HTTP_1_1) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTPS 1.1 client -> HTTPS 2.0) server: " + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + count.incrementAndGet(); + }) + .build(); + client.execute(request).get(); + } + } + logger.log(Level.INFO, "count = " + count.get()); + assertEquals(requests, count.get()); + } + } +} diff --git a/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerTest.java b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerTest.java new file mode 100644 index 0000000..a659bfd --- /dev/null +++ b/net-http-server-netty-secure/src/test/java/org/xbib/net/http/server/netty/secure/test/NettyHttpsServerTest.java @@ -0,0 +1,264 @@ +package org.xbib.net.http.server.netty.secure.test; + +import io.netty.bootstrap.Bootstrap; + +import io.netty.util.ResourceLeakDetector; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.client.netty.secure.NettyHttpsClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.secure.HttpsAddress; +import org.xbib.net.http.server.netty.secure.HttpsRequest; +import org.xbib.net.http.server.netty.secure.NettyHttpsServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.BaseHttpService; + +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NettyHttpsServerTest { + + private static final Logger logger = Logger.getLogger(NettyHttpsServerTest.class.getName()); + + @Test + public void testHttps1() throws Exception { + // client HTTP 1.1 + server HTTP 1.1 (no upgrade) + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setVersion(HttpVersion.HTTP_1_1) + .setSecure(true) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + serverConfig.setDebug(true); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain: " + + " SNI host = " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + + " SSL peer host = " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + AtomicBoolean received = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .setSecureProtocolName(new String[]{ "TLSv1.2" } ) + .trustInsecure() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setVersion(HttpVersion.HTTP_1_1) + .setURL(url) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTPS 1.1): " + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + received.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } + } + + @Test + public void testHttps1Upgrade() throws Exception { + // client HTTP 1.1, server HTTP 2.0 (upgrade) - use this with browsers + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setSecure(true) + .setVersion(HttpVersion.HTTP_2_0) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", + Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + serverConfig.setDebug(true); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain: " + + " SNI host = " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + + " SSL session = " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + AtomicBoolean received = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .trustInsecure() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setVersion(HttpVersion.HTTP_1_1) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTPS 1.1 client -> HTTPS 2.0) server: " + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + received.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } + } + + @Test + public void nettySecureHttps2UpgradeClientTest() { + // client HTTP 2.0, server HTTP 1.1 (this is not supported, server should return HTTP 1.1 505) + Assertions.assertThrows(IOException.class, () -> { + URL url = URL.from("https://localhost:8443/secure"); + HttpsAddress httpsAddress = HttpsAddress.builder() + .setVersion(HttpVersion.HTTP_1_1) + .setSecure(true) + .setHost(url.getHost()) + .setPort(url.getPort()) + .setSelfCert(url.getHost()) + .build(); + NettyHttpsServerConfig serverConfig = new NettyHttpsServerConfig(); + serverConfig.setServerName("NettySecureHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + serverConfig.setDebug(true); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain " + + " SNI host " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + " " + + " SSL peer host " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + " " + + " base URL = " + ctx.httpRequest().getBaseURL() + " " + + ctx.httpRequest().getParameter() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + AtomicBoolean received = new AtomicBoolean(); + NettyHttpClientConfig config = new NettyHttpsClientConfig() + .trustInsecure() + .setDebug(true); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setVersion(HttpVersion.HTTP_2_0) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response (HTTP/2) " + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + if (resp.getStatus().code() == 505) { + received.set(true); + } + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } + }); + } +} diff --git a/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer b/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer new file mode 100644 index 0000000..f079b71 --- /dev/null +++ b/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer @@ -0,0 +1,4 @@ +org.xbib.net.http.server.netty.http1.Http1ChannelInitializer +org.xbib.net.http.server.netty.http2.Http2ChannelInitializer +org.xbib.net.http.server.netty.secure.http1.Https1ChannelInitializer +org.xbib.net.http.server.netty.secure.http2.Https2ChannelInitializer diff --git a/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider b/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider new file mode 100644 index 0000000..e031bc6 --- /dev/null +++ b/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.server.netty.NioServerTransportProvider diff --git a/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider b/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider new file mode 100644 index 0000000..b28b1d0 --- /dev/null +++ b/net-http-server-netty-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider @@ -0,0 +1,2 @@ +org.xbib.net.security.DefaultCertificateProvider +org.xbib.net.bouncycastle.BouncyCastleCertificateProvider \ No newline at end of file diff --git a/net-http-server-netty-secure/src/test/resources/logging.properties b/net-http-server-netty-secure/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-server-netty-secure/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-server-netty/build.gradle b/net-http-server-netty/build.gradle new file mode 100644 index 0000000..9c572c2 --- /dev/null +++ b/net-http-server-netty/build.gradle @@ -0,0 +1,12 @@ +dependencies { + api project(':net-http-server') + api libs.netty.codec.http2 + testImplementation project(':net-http-client-netty') + testImplementation project(':net-http-template-groovy') +} + +test { + systemProperty 'application.name', 'test' + systemProperty 'application.home', 'src/test/resources' + systemProperty 'application.profile', 'test' +} diff --git a/net-http-server-netty/src/main/java/module-info.java b/net-http-server-netty/src/main/java/module-info.java new file mode 100644 index 0000000..46fd044 --- /dev/null +++ b/net-http-server-netty/src/main/java/module-info.java @@ -0,0 +1,30 @@ +import org.xbib.net.buffer.DataBufferFactory; +import org.xbib.net.http.server.netty.HttpChannelInitializer; +import org.xbib.net.http.server.netty.buffer.NettyDataBufferFactory; +import org.xbib.net.http.server.netty.http1.Http1ChannelInitializer; +import org.xbib.net.http.server.netty.http2.Http2ChannelInitializer; +import org.xbib.net.http.server.netty.NioServerTransportProvider; +import org.xbib.net.http.server.netty.ServerTransportProvider; + +module org.xbib.net.http.server.netty { + exports org.xbib.net.http.server.netty; + exports org.xbib.net.http.server.netty.buffer; + exports org.xbib.net.http.server.netty.http1; + exports org.xbib.net.http.server.netty.http2; + requires transitive org.xbib.net; + requires transitive org.xbib.net.http; + requires transitive org.xbib.net.http.server; + requires io.netty.buffer; + requires io.netty.common; + requires io.netty.transport; + requires io.netty.handler; + requires io.netty.codec; + requires io.netty.codec.http; + requires io.netty.codec.http2; + requires java.logging; + uses HttpChannelInitializer; + provides HttpChannelInitializer with Http1ChannelInitializer, Http2ChannelInitializer; + uses ServerTransportProvider; + provides ServerTransportProvider with NioServerTransportProvider; + provides DataBufferFactory with NettyDataBufferFactory; +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpChannelInitializer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpChannelInitializer.java new file mode 100644 index 0000000..060fb1d --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpChannelInitializer.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.server.netty; + +import io.netty.channel.Channel; +import org.xbib.net.http.HttpAddress; + +public interface HttpChannelInitializer { + + boolean supports(HttpAddress httpAddress); + + void init(Channel channel, NettyHttpServer server, NettyCustomizer customizer); +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java new file mode 100644 index 0000000..3eb0494 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java @@ -0,0 +1,56 @@ +package org.xbib.net.http.server.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import org.xbib.net.Request; +import org.xbib.net.http.server.BaseHttpRequest; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Objects; + +public class HttpRequest extends BaseHttpRequest { + + private final HttpRequestBuilder builder; + + protected HttpRequest(HttpRequestBuilder builder) { + super(builder); + this.builder = builder; + } + + public static HttpRequestBuilder builder() { + return new HttpRequestBuilder(); + } + + @Override + public InputStream getInputStream() { + return new ByteBufInputStream(builder.fullHttpRequest.content()); + } + + @Override + public ByteBuffer getBody() { + return builder.getBody(); + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return builder.getBodyAsChars(charset); + } + + @Override + public R as(Class type) { + Objects.requireNonNull(type); + return type.isInstance(this) ? type.cast(this) : null; + } + + @Override + public String toString() { + return "HttpRequest[request=" + builder.fullHttpRequest + "]"; + } + + public ByteBuf getByteBuf() { + return builder.fullHttpRequest.content(); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java new file mode 100644 index 0000000..6f8143b --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java @@ -0,0 +1,106 @@ +package org.xbib.net.http.server.netty; + +import io.netty.buffer.ByteBufUtil; +import io.netty.handler.codec.http.FullHttpRequest; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.BaseHttpRequestBuilder; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +public class HttpRequestBuilder extends BaseHttpRequestBuilder { + + FullHttpRequest fullHttpRequest; + + ByteBuffer byteBuffer; + + protected HttpRequestBuilder() { + } + + public HttpRequestBuilder setFullHttpRequest(FullHttpRequest fullHttpRequest) { + if (fullHttpRequest != null) { + this.fullHttpRequest = fullHttpRequest; + setVersion(HttpVersion.valueOf(fullHttpRequest.protocolVersion().text())); + setMethod(HttpMethod.valueOf(fullHttpRequest.method().name())); + setRequestURI(fullHttpRequest.uri()); + fullHttpRequest.headers().entries().forEach(e -> addHeader(e.getKey(), e.getValue())); + } + return this; + } + + @Override + public ByteBuffer getBody() { + if (byteBuffer != null) { + return byteBuffer; + } + // read all bytes from request into a JDK ByteBuffer. This might be expensive. + if (fullHttpRequest.content() != null) { + byteBuffer = ByteBuffer.wrap(ByteBufUtil.getBytes(fullHttpRequest.content())); + } + return byteBuffer; + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return fullHttpRequest.content() != null ? + CharBuffer.wrap(fullHttpRequest.content().toString(charset)) : null; + } + + @Override + public HttpRequestBuilder setAddress(HttpAddress httpAddress) { + super.setAddress(httpAddress); + return this; + } + + @Override + public HttpRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + super.setLocalAddress(localAddress); + return this; + } + + @Override + public HttpRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + super.setRemoteAddress(remoteAddress); + return this; + } + + @Override + public HttpRequestBuilder setBaseURL(URL baseURL) { + super.setBaseURL(baseURL); + return this; + } + + @Override + public HttpRequestBuilder setBaseURL(HttpAddress httpAddress, String uri, String hostAndPort) { + super.setBaseURL(httpAddress, uri, hostAndPort); + return this; + } + + @Override + public HttpRequestBuilder setSequenceId(Integer sequenceId) { + super.setSequenceId(sequenceId); + return this; + } + + @Override + public HttpRequestBuilder setStreamId(Integer streamId) { + super.setStreamId(streamId); + return this; + } + + @Override + public HttpRequestBuilder setRequestId(Long requestId) { + super.setRequestId(requestId); + return this; + } + + @Override + public HttpRequest build() { + return new HttpRequest(this); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponse.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponse.java new file mode 100644 index 0000000..cc3c949 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponse.java @@ -0,0 +1,35 @@ +package org.xbib.net.http.server.netty; + +import org.xbib.net.http.server.BaseHttpResponse; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HttpResponse extends BaseHttpResponse { + + private static final Logger logger = Logger.getLogger(HttpResponse.class.getName()); + + private final HttpResponseBuilder builder; + + protected HttpResponse(HttpResponseBuilder builder) { + super(builder); + this.builder = builder; + } + + public static HttpResponseBuilder builder() { + return new HttpResponseBuilder(); + } + + @Override + public void close() throws IOException { + //logger.log(Level.FINE, "close"); + //builder.internalClose(); + } + + @Override + public void flush() throws IOException { + //logger.log(Level.FINE, "flush"); + //builder.internalFlush(); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponseBuilder.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponseBuilder.java new file mode 100644 index 0000000..32a1a8a --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpResponseBuilder.java @@ -0,0 +1,285 @@ +package org.xbib.net.http.server.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.stream.ChunkedNioFile; +import io.netty.handler.stream.ChunkedStream; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.server.netty.buffer.NettyDataBuffer; +import org.xbib.net.http.server.netty.buffer.NettyDataBufferFactory; +import org.xbib.net.http.server.netty.http1.HttpPipelinedResponse; +import org.xbib.net.http.server.BaseHttpResponseBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static io.netty.channel.ChannelFutureListener.CLOSE; + +public class HttpResponseBuilder extends BaseHttpResponseBuilder { + + private static final Logger logger = Logger.getLogger(HttpResponseBuilder.class.getName()); + + private ChannelHandlerContext ctx; + + HttpResponseBuilder() { + super(); + } + + @Override + public void reset() { + super.reset(); + this.dataBufferFactory = NettyDataBufferFactory.getInstance(); + } + + @Override + public HttpResponseBuilder setVersion(org.xbib.net.http.HttpVersion version) { + super.setVersion(version); + return this; + } + + @Override + public HttpResponseBuilder setResponseStatus(org.xbib.net.http.HttpResponseStatus status) { + super.setResponseStatus(status); + return this; + } + + @Override + public HttpResponseBuilder setContentType(String contentType) { + super.setContentType(contentType); + return this; + } + + @Override + public HttpResponseBuilder setCharset(Charset charset) { + super.setCharset(charset); + return this; + } + + @Override + public HttpResponseBuilder setHeader(CharSequence name, String value) { + super.setHeader(name, value); + return this; + } + + @Override + public HttpResponseBuilder setTrailingHeader(CharSequence name, String value) { + super.setTrailingHeader(name, value); + return this; + } + + @Override + public HttpResponseBuilder shouldFlush(boolean shouldFlush) { + super.shouldFlush(shouldFlush); + return this; + } + + @Override + public HttpResponseBuilder shouldClose(boolean shouldClose) { + super.shouldClose(shouldClose); + return this; + } + + @Override + public HttpResponseBuilder setSequenceId(Integer sequenceId) { + super.setSequenceId(sequenceId); + return this; + } + + @Override + public HttpResponseBuilder setStreamId(Integer streamId) { + super.setStreamId(streamId); + return this; + } + + @Override + public HttpResponseBuilder setResponseId(Long responseId) { + super.setResponseId(responseId); + return this; + } + + public HttpResponseBuilder setChannelHandlerContext(ChannelHandlerContext ctx) { + this.ctx = ctx; + return this; + } + + @Override + public HttpResponse build() { + Objects.requireNonNull(ctx); + //if (shouldFlush()) { + // internalFlush(); + //} + if (body != null) { + internalWrite(body); + } else if (charBuffer != null && charset != null) { + internalWrite(charBuffer, charset); + } else if (dataBuffer != null) { + internalWrite(dataBuffer); + } else if (fileChannel != null) { + internalWrite(fileChannel, bufferSize, true); + } else if (inputStream != null) { + internalWrite(inputStream, bufferSize, true); + } + return new HttpResponse(this); + } + + //void internalFlush() { + // logger.log(Level.FINE, "internal flush"); + // internalBufferWrite(Unpooled.buffer(0)); + //} + + //void internalClose() { + // logger.log(Level.FINE, "internal close"); + // ctx.close(); + //} + + private void internalWrite(String body) { + internalWrite(dataBufferFactory.wrap(StandardCharsets.UTF_8.encode(body))); + } + + private void internalWrite(DataBuffer dataBuffer) { + NettyDataBuffer nettyDataBuffer = (NettyDataBuffer) dataBuffer; + internalBufferWrite(nettyDataBuffer.getNativeBuffer()); + } + + private void internalWrite(CharBuffer charBuffer, Charset charset) { + internalBufferWrite(ByteBufUtil.encodeString(ctx.alloc(), charBuffer, charset)); + } + + private void internalBufferWrite(ByteBuf byteBuf) { + internalBufferWrite(byteBuf, byteBuf.readableBytes()); + } + + private void internalBufferWrite(ByteBuf byteBuf, int length) { + super.buildHeaders(length); + HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(status.code()); + HttpHeaders headers = new DefaultHttpHeaders(); + super.headers.entries().forEach(e -> headers.add(e.getKey(), e.getValue())); + // fix headers + if (streamId != null) { + headers.add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), String.valueOf(streamId)); + } + if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { + headers.remove(HttpHeaderNames.TRANSFER_ENCODING); + headers.set(HttpHeaderNames.CONTENT_LENGTH, length); + } + HttpHeaders trailingHeaders = new DefaultHttpHeaders(); + super.trailingHeaders.entries().forEach(e -> trailingHeaders.add(e.getKey(), e.getValue())); + // retain Netty byteBuf because FullHttpResponse will be released in writeAndFlush() + FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.valueOf(version.text()), + responseStatus, byteBuf.retain(), headers, trailingHeaders); + if (!ctx.channel().isWritable()) { + logger.log(Level.WARNING, "we have a problem, the channel " + ctx.channel() + " is not writable"); + return; + } + if (sequenceId != null) { + HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(fullHttpResponse, + ctx.channel().newPromise(), sequenceId); + ctx.write(httpPipelinedResponse); + } else { + ctx.write(fullHttpResponse); + } + ctx.flush(); + } + + private void internalWrite(FileChannel fileChannel, int bufferSize, boolean keepAlive) { + if (!ctx.channel().isWritable()) { + logger.log(Level.WARNING, "we have a problem, the channel " + ctx.channel() + " is not writable"); + } + HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(status.code()); + DefaultHttpResponse rsp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, responseStatus); + ctx.channel().eventLoop().execute(() -> { + ctx.write(rsp); + try { + ctx.write(new ChunkedNioFile(fileChannel, bufferSize)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + ChannelFuture channelFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + if (headers.containsHeader(HttpHeaderNames.CONTENT_LENGTH)) { + if (!keepAlive) { + channelFuture.addListener(CLOSE); + } + } else { + channelFuture.addListener(CLOSE); + } + }); + } + + private void internalWrite(InputStream inputStream, int bufferSize, boolean keepAlive) { + if (!ctx.channel().isWritable()) { + logger.log(Level.WARNING, "channel not writeable: " + ctx.channel()); + return; + } + // TODO we write a single chunk, but we should use chunked output here. + ByteBuf buffer; + int count; + try { + byte[] chunk = new byte[bufferSize]; + count = inputStream.read(chunk, 0, bufferSize); + if (count <= 0) { + return; + } + buffer = Unpooled.wrappedBuffer(chunk, 0, count); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + return; + } + if (count < bufferSize) { + // not chunked, no headers (???) + internalBufferWrite(buffer, count); + } else { + // chunked + super.buildHeaders(0); + HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(status.code()); + HttpHeaders headers = new DefaultHttpHeaders(); + super.headers.entries().forEach(e -> headers.add(e.getKey(), e.getValue())); + if (streamId != null) { + headers.add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), String.valueOf(streamId)); + } + HttpHeaders trailingHeaders = new DefaultHttpHeaders(); + super.trailingHeaders.entries().forEach(e -> trailingHeaders.add(e.getKey(), e.getValue())); + DefaultHttpResponse defaultHttpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, responseStatus); + if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { + headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + } else { + if (keepAlive) { + headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + } + defaultHttpResponse.headers().set(headers); + ctx.write(defaultHttpResponse); + //ctx.write(buffer); ??? + ctx.write(new ChunkedStream(inputStream, bufferSize)); + ChannelFuture channelFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { + if (!keepAlive) { + channelFuture.addListener(CLOSE); + } + } else { + channelFuture.addListener(CLOSE); + } + } + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/IdleTimeoutHandler.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/IdleTimeoutHandler.java new file mode 100644 index 0000000..8d77275 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/IdleTimeoutHandler.java @@ -0,0 +1,32 @@ +package org.xbib.net.http.server.netty; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.IdleStateHandler; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Idle timeout handler. + */ +@ChannelHandler.Sharable +public class IdleTimeoutHandler extends IdleStateHandler { + + private static final Logger logger = Logger.getLogger(IdleTimeoutHandler.class.getName()); + + public IdleTimeoutHandler(int idleTimeoutMillis) { + super(idleTimeoutMillis, idleTimeoutMillis, idleTimeoutMillis, TimeUnit.MILLISECONDS); + } + + @Override + protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) { + if (!evt.isFirst()) { + return; + } + logger.log(Level.FINER, () -> "closing an idle connection " + ctx.channel()); + ctx.close(); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyCustomizer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyCustomizer.java new file mode 100644 index 0000000..56d6fa8 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyCustomizer.java @@ -0,0 +1,52 @@ +package org.xbib.net.http.server.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; + +/** + * Strategy interface to customize netty {@link io.netty.bootstrap.Bootstrap} and {@link io.netty.channel.Channel} via callback + * hooks.
+ * Extending the NettyCustomizer API + *

+ * Contrary to other driver options, the options available in this class should be considered as advanced feature and as such, + * they should only be modified by expert users. A misconfiguration introduced by the means of this API can have unexpected + * results and cause the driver to completely fail to connect. + */ +public interface NettyCustomizer { + + default void afterServerBootstrapInitialized(ServerBootstrap bootstrap) { + } + + /** + * Hook invoked each time the driver creates a new Connection and configures a new instance of Bootstrap for it. This hook + * is called after the driver has applied all {@link java.net.SocketOption}s. This is a good place to add extra + * {@link io.netty.channel.ChannelOption}s to the {@link Bootstrap}. + * + * @param bootstrap must not be {@code null}. + */ + default void afterBootstrapInitialized(Bootstrap bootstrap) { + } + + /** + * Hook invoked each time the driver initializes the channel. This hook is called after the driver has registered all its + * internal channel handlers, and applied the configured options. + * + * @param channel must not be {@code null}. + */ + default void afterChannelInitialized(Channel channel) { + } + + /** + * Hook invoked each time a full HTTP request is received in a Netty handler pipeline. + * Useful to adjust headers in a Netty way. + * + * @param ctx the channel context + * @param fullHttpRequest the full HTTP request + */ + default void afterFullHttpRequestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) { + } + +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServer.java new file mode 100644 index 0000000..85aa8f2 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServer.java @@ -0,0 +1,217 @@ +package org.xbib.net.http.server.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.AttributeKey; +import org.xbib.net.NetworkClass; +import org.xbib.net.NetworkUtils; +import org.xbib.net.SocketConfig; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServer; + +import java.io.IOException; +import java.net.BindException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Netty HTTP server. + */ +public class NettyHttpServer implements HttpServer { + + private static final Logger logger = Logger.getLogger(NettyHttpServer.class.getName()); + + private final NettyHttpServerBuilder builder; + + private final EventLoopGroup parentEventLoopGroup; + + private final EventLoopGroup childEventLoopGroup; + + private final Class socketChannelClass; + + private final HttpChannelInitializer httpChannelInitializer; + + private final ServiceLoader serviceLoader; + + private final Collection channelFutures; + + private final Collection channels; + + NettyHttpServer(NettyHttpServerBuilder builder, + EventLoopGroup parentEventLoopGroup, + EventLoopGroup childEventLoopGroup, + Class socketChannelClass) { + this.builder = builder; + this.parentEventLoopGroup = parentEventLoopGroup; + this.childEventLoopGroup = childEventLoopGroup; + this.socketChannelClass = socketChannelClass; + this.httpChannelInitializer = builder.httpChannelInitializer; + this.serviceLoader = ServiceLoader.load(HttpChannelInitializer.class); + this.channelFutures = new ArrayList<>(); + this.channels = new ArrayList<>(); + logger.log(Level.INFO, "parent event loop group = " + parentEventLoopGroup + + " child event loop group = " + childEventLoopGroup + + " socket channel class = " + socketChannelClass + + " router addresses = " + getApplication().getAddresses()); + } + + public static NettyHttpServerBuilder builder() { + return new NettyHttpServerBuilder(); + } + + public NettyHttpServer getServer() { + return this; + } + + public NettyHttpServerConfig getNettyHttpServerConfig() { + return builder.nettyHttpServerConfig; + } + + @Override + public void bind() throws BindException { + Set httpAddressSet = getApplication().getAddresses(); + logger.log(Level.INFO, "http adresses = " + httpAddressSet); + for (HttpAddress httpAddress : httpAddressSet) { + SocketConfig socketConfig = httpAddress.getSocketConfig(); + ServerBootstrap bootstrap = new ServerBootstrap() + .group(parentEventLoopGroup, childEventLoopGroup) + .channel(socketChannelClass) + .option(ChannelOption.ALLOCATOR, builder.byteBufAllocator) + .option(ChannelOption.SO_REUSEADDR, socketConfig.isReuseAddr()) + .option(ChannelOption.SO_RCVBUF, socketConfig.getTcpReceiveBufferSize()) + .option(ChannelOption.SO_BACKLOG, socketConfig.getBackLogSize()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, socketConfig.getConnectTimeoutMillis()) + .childOption(ChannelOption.ALLOCATOR, builder.byteBufAllocator) + .childOption(ChannelOption.SO_REUSEADDR, socketConfig.isReuseAddr()) + .childOption(ChannelOption.TCP_NODELAY, socketConfig.isTcpNodelay()) + .childOption(ChannelOption.SO_SNDBUF, socketConfig.getTcpSendBufferSize()) + .childOption(ChannelOption.SO_RCVBUF, socketConfig.getTcpReceiveBufferSize()) + .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, socketConfig.getConnectTimeoutMillis()) + .childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + AttributeKey key = AttributeKey.valueOf("_address"); + ch.attr(key).set(httpAddress); + createChannelInitializer(httpAddress).init(ch, getServer(), builder.nettyCustomizer); + } + }); + if (getNettyHttpServerConfig().isDebug()) { + bootstrap.handler(new LoggingHandler("server-logging", LogLevel.DEBUG)); + } + if (builder.nettyCustomizer != null) { + builder.nettyCustomizer.afterServerBootstrapInitialized(bootstrap); + } + try { + InetSocketAddress inetSocketAddress = httpAddress.getInetSocketAddress(); + NetworkClass configuredNetworkClass = getNettyHttpServerConfig().getNetworkClass(); + NetworkClass detectedNetworkClass = NetworkUtils.getNetworkClass(inetSocketAddress.getAddress()); + if (!NetworkUtils.matchesNetwork(detectedNetworkClass, configuredNetworkClass)) { + throw new IOException("unable to bind to " + httpAddress + " because network class " + + detectedNetworkClass + " is not allowed by configured network class " + configuredNetworkClass); + } + logger.log(Level.INFO, () -> "trying to bind to " + inetSocketAddress); + channelFutures.add(bootstrap.bind(inetSocketAddress)); + } catch (IOException e) { + throw new BindException(e.getMessage()); + } + } + for (ChannelFuture channelFuture : channelFutures) { + try { + channelFuture.channel().closeFuture() + .addListener((ChannelFutureListener) future -> { + future.await(); + logger.log(Level.INFO, "future " + future + " awaited"); + }); + channels.add(channelFuture.sync().channel()); + channelFuture.await(); + logger.log(Level.INFO, () -> channelFuture.channel() + " ready, listening"); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + } + + @Override + public void loop() { + for (Channel channel : channels) { + try { + ChannelFuture channelFuture = channel.closeFuture().sync(); + if (channelFuture.isDone()) { + logger.log(Level.INFO, () -> channel + " close future synced"); + } + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + } + + @Override + public Application getApplication() { + return builder.application; + } + + @Override + public void close() throws IOException { + shutdownGracefully(30L, TimeUnit.SECONDS); + } + + public void shutdownGracefully(long amount, TimeUnit timeUnit) throws IOException { + logger.log(Level.INFO, "server shutting down"); + // shut down child event loop group, then parent event loop group, then channel futures + childEventLoopGroup.shutdownGracefully(1L, amount, timeUnit); + try { + if (!childEventLoopGroup.awaitTermination(amount, timeUnit)) { + logger.log(Level.WARNING, "timeout"); + } + } catch (InterruptedException e) { + logger.log(Level.WARNING, "timeout"); + } + parentEventLoopGroup.shutdownGracefully(1L, amount, timeUnit); + try { + if (!parentEventLoopGroup.awaitTermination(amount, timeUnit)) { + logger.log(Level.WARNING, "timeout"); + } + } catch (InterruptedException e) { + logger.log(Level.WARNING, "timeout"); + } + for (ChannelFuture channelFuture : channelFutures) { + if (channelFuture != null && !channelFuture.isDone()) { + if (channelFuture.channel().isOpen()) { + logger.log(Level.INFO, "closing channel future"); + channelFuture.channel().close(); + } + channelFuture.cancel(true); + } + } + // close application + getApplication().close(); + logger.log(Level.INFO, "server shutdown complete"); + } + + private HttpChannelInitializer createChannelInitializer(HttpAddress address) { + if (httpChannelInitializer != null && httpChannelInitializer.supports(address)) { + return httpChannelInitializer; + } + for (HttpChannelInitializer httpChannelInitializer : serviceLoader) { + if (httpChannelInitializer.supports(address)) { + return httpChannelInitializer; + } + } + throw new IllegalStateException("no channel initializer found for address " + address); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerBuilder.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerBuilder.java new file mode 100644 index 0000000..3190fb0 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerBuilder.java @@ -0,0 +1,152 @@ +package org.xbib.net.http.server.netty; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServerBuilder; +import org.xbib.net.util.NamedThreadFactory; + +import java.util.ServiceLoader; +import java.util.concurrent.ThreadFactory; + +public class NettyHttpServerBuilder implements HttpServerBuilder { + + NettyHttpServerConfig nettyHttpServerConfig; + + Application application; + + ByteBufAllocator byteBufAllocator; + + EventLoopGroup parentEventLoopGroup; + + EventLoopGroup childEventLoopGroup; + + Class socketChannelClass; + + HttpChannelInitializer httpChannelInitializer; + + NettyCustomizer nettyCustomizer; + + String secureSocketProviderName; + + protected NettyHttpServerBuilder() { + this.byteBufAllocator = ByteBufAllocator.DEFAULT; + this.nettyHttpServerConfig = new NettyHttpServerConfig(); + } + + public NettyHttpServerBuilder setHttpServerConfig(NettyHttpServerConfig nettyHttpServerConfig) { + this.nettyHttpServerConfig = nettyHttpServerConfig; + return this; + } + + public NettyHttpServerBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = byteBufAllocator; + return this; + } + + public NettyHttpServerBuilder setParentEventLoopGroup(EventLoopGroup parentEventLoopGroup) { + this.parentEventLoopGroup = parentEventLoopGroup; + return this; + } + + public NettyHttpServerBuilder setChildEventLoopGroup(EventLoopGroup childEventLoopGroup) { + this.childEventLoopGroup = childEventLoopGroup; + return this; + } + + public NettyHttpServerBuilder setChannelClass(Class socketChannelClass) { + this.socketChannelClass = socketChannelClass; + return this; + } + + public NettyHttpServerBuilder setNettyCustomizer(NettyCustomizer nettyCustomizer) { + this.nettyCustomizer = nettyCustomizer; + return this; + } + + public NettyHttpServerBuilder setHttpChannelInitializer(HttpChannelInitializer httpChannelInitializer) { + this.httpChannelInitializer = httpChannelInitializer; + return this; + } + + public NettyHttpServerBuilder setSecureSocketProviderName(String name) { + this.secureSocketProviderName = name; + return this; + } + + public NettyHttpServerBuilder setApplication(Application application) { + this.application = application; + return this; + } + + public NettyHttpServer build() { + return new NettyHttpServer(this, + createParentEventLoopGroup(nettyHttpServerConfig, parentEventLoopGroup), + createChildEventLoopGroup(nettyHttpServerConfig, childEventLoopGroup), + createSocketChannelClass(nettyHttpServerConfig, socketChannelClass)); + } + + private static EventLoopGroup createParentEventLoopGroup(NettyHttpServerConfig httpServerConfig, + EventLoopGroup parentEventLoopGroup) { + if (parentEventLoopGroup != null) { + return parentEventLoopGroup; + } + EventLoopGroup eventLoopGroup = null; + ThreadFactory threadFactory = new NamedThreadFactory("org-xbib-net-http-netty-server-parent"); + ServiceLoader transportProviders = ServiceLoader.load(ServerTransportProvider.class); + for (ServerTransportProvider serverTransportProvider : transportProviders) { + if (httpServerConfig.getTransportProviderName() == null || + httpServerConfig.getTransportProviderName().equals(serverTransportProvider.getClass().getName())) { + eventLoopGroup = serverTransportProvider.createEventLoopGroup(httpServerConfig.getParentThreadCount(), + threadFactory); + } + } + if (eventLoopGroup == null) { + eventLoopGroup = new NioEventLoopGroup(httpServerConfig.getParentThreadCount(), threadFactory); + } + return eventLoopGroup; + } + + private static EventLoopGroup createChildEventLoopGroup(NettyHttpServerConfig httpServerConfig, + EventLoopGroup childEventLoopGroup) { + if (childEventLoopGroup != null) { + return childEventLoopGroup; + } + EventLoopGroup eventLoopGroup = null; + ThreadFactory threadFactory = new NamedThreadFactory("org-xbib-net-http-netty-server-child"); + ServiceLoader transportProviders = ServiceLoader.load(ServerTransportProvider.class); + for (ServerTransportProvider serverTransportProvider : transportProviders) { + if (httpServerConfig.getTransportProviderName() == null || + httpServerConfig.getTransportProviderName().equals(serverTransportProvider.getClass().getName())) { + eventLoopGroup = serverTransportProvider.createEventLoopGroup(httpServerConfig.getChildThreadCount(), + threadFactory); + } + } + if (eventLoopGroup == null) { + eventLoopGroup = new NioEventLoopGroup(httpServerConfig.getChildThreadCount(), threadFactory); + } + return eventLoopGroup; + } + + private static Class createSocketChannelClass(NettyHttpServerConfig httpServerConfig, + Class socketChannelClass) { + if (socketChannelClass != null) { + return socketChannelClass; + } + Class channelClass = null; + ServiceLoader transportProviders = ServiceLoader.load(ServerTransportProvider.class); + for (ServerTransportProvider serverTransportProvider : transportProviders) { + if (httpServerConfig.getTransportProviderName() == null || httpServerConfig.getTransportProviderName().equals(serverTransportProvider.getClass().getName())) { + channelClass = serverTransportProvider.createServerSocketChannelClass(); + break; + } + } + if (channelClass == null) { + channelClass = NioServerSocketChannel.class; + } + return channelClass; + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerConfig.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerConfig.java new file mode 100644 index 0000000..b4de21b --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NettyHttpServerConfig.java @@ -0,0 +1,184 @@ +package org.xbib.net.http.server.netty; + +import io.netty.util.AttributeKey; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.HttpServerConfig; + +public class NettyHttpServerConfig extends HttpServerConfig { + + public static final AttributeKey ATTRIBUTE_KEY_HTTP_ADDRESS = AttributeKey.valueOf("_address"); + + /** + * Enforce the transport class name if many transport providers are given. + * Default is null so no transport provider is enforced, the first one + * will be chosen that is supported. The builtin transport class is the {@link NioServerTransportProvider}. + */ + private String transportProviderName = null; + + /** + * Let Netty decide about parent thread count. + */ + private int parentThreadCount = 0; + + /** + * Let Netty decide about child thread count. + */ + private int childThreadCount = 0; + + /** + * Set HTTP initial line length to 4k. + */ + private int maxInitialLineLength = 4 * 1024; + + /** + * Set HTTP maximum headers size to 8k. + */ + private int maxHeadersSize = 8 * 1024; + + /** + * Set HTTP chunk maximum size to 8k. + */ + private int maxChunkSize = 8 * 1024; + + /** + * Set maximum content length to 256 MB. + */ + private int maxContentLength = 256 * 1024 * 1024; + + /** + * HTTP/1 pipelining. Disabled by default. + */ + private boolean isPipeliningEnabled = false; + + /** + * HTTP/1 pipelining capacity. 1024 is very high, it means + * 1024 requests can be present for a single client. + */ + private int pipeliningCapacity = 1024; + + /** + * This is Netty's default. + */ + private int maxCompositeBufferComponents = 1024; + + /** + * Default for compression. + */ + private boolean enableCompression = true; + + /** + * Default for decompression. + */ + private boolean enableDecompression = true; + + public NettyHttpServerConfig() { + } + + public NettyHttpServerConfig setTransportProviderName(String transportProviderName) { + this.transportProviderName = transportProviderName; + return this; + } + + public String getTransportProviderName() { + return transportProviderName; + } + + public NettyHttpServerConfig setParentThreadCount(int parentThreadCount) { + this.parentThreadCount = parentThreadCount; + return this; + } + + public int getParentThreadCount() { + return parentThreadCount; + } + + public NettyHttpServerConfig setChildThreadCount(int childThreadCount) { + this.childThreadCount = childThreadCount; + return this; + } + + public int getChildThreadCount() { + return childThreadCount; + } + + public NettyHttpServerConfig setMaxInitialLineLength(int maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + return this; + } + public int getMaxInitialLineLength() { + return maxInitialLineLength; + } + + public NettyHttpServerConfig setMaxHeadersSize(int maxHeadersSize) { + this.maxHeadersSize = maxHeadersSize; + return this; + } + + public int getMaxHeadersSize() { + return maxHeadersSize; + } + + public NettyHttpServerConfig setMaxChunkSize(int maxChunkSize) { + this.maxChunkSize = maxChunkSize; + return this; + } + + public int getMaxChunkSize() { + return maxChunkSize; + } + + public NettyHttpServerConfig setMaxContentLength(int maxContentLength) { + this.maxContentLength = maxContentLength; + return this; + } + + public int getMaxContentLength() { + return maxContentLength; + } + + public NettyHttpServerConfig setPipelining(boolean isPipeliningEnabled) { + this.isPipeliningEnabled = isPipeliningEnabled; + return this; + } + + public boolean isPipeliningEnabled() { + return isPipeliningEnabled; + } + + public NettyHttpServerConfig setPipeliningCapacity(int pipeliningCapacity) { + this.pipeliningCapacity = pipeliningCapacity; + return this; + } + + public int getPipeliningCapacity() { + return pipeliningCapacity; + } + + public NettyHttpServerConfig setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + this.maxCompositeBufferComponents = maxCompositeBufferComponents; + return this; + } + + public int getMaxCompositeBufferComponents() { + return maxCompositeBufferComponents; + } + + public NettyHttpServerConfig setCompression(boolean enabled) { + this.enableCompression = enabled; + return this; + } + + public boolean isCompressionEnabled() { + return enableCompression; + } + + public NettyHttpServerConfig setDecompression(boolean enabled) { + this.enableDecompression = enabled; + return this; + } + + public boolean isDecompressionEnabled() { + return enableDecompression; + } + +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NioServerTransportProvider.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NioServerTransportProvider.java new file mode 100644 index 0000000..7f01061 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/NioServerTransportProvider.java @@ -0,0 +1,24 @@ +package org.xbib.net.http.server.netty; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +import java.util.concurrent.ThreadFactory; + +public class NioServerTransportProvider implements ServerTransportProvider { + + public NioServerTransportProvider() { + } + + @Override + public EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + return new NioEventLoopGroup(nThreads, threadFactory); + } + + @Override + public Class createServerSocketChannelClass() { + return NioServerSocketChannel.class; + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/ServerTransportProvider.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/ServerTransportProvider.java new file mode 100644 index 0000000..a8125de --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/ServerTransportProvider.java @@ -0,0 +1,13 @@ +package org.xbib.net.http.server.netty; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import java.util.concurrent.ThreadFactory; + +public interface ServerTransportProvider { + + EventLoopGroup createEventLoopGroup(int nThreads, ThreadFactory threadFactory); + + Class createServerSocketChannelClass(); + +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/TrafficLoggingHandler.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/TrafficLoggingHandler.java new file mode 100644 index 0000000..406323b --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/TrafficLoggingHandler.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.server.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +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. + */ +@ChannelHandler.Sharable +public class TrafficLoggingHandler extends LoggingHandler { + + public TrafficLoggingHandler(LogLevel level) { + super("server", level); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.fireChannelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + ctx.fireChannelUnregistered(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { + ctx.write(msg, promise); + } else { + super.write(ctx, msg, promise); + } + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBuffer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBuffer.java new file mode 100644 index 0000000..84e7860 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBuffer.java @@ -0,0 +1,324 @@ +package org.xbib.net.http.server.netty.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.function.IntPredicate; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.ByteBufUtil; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.buffer.PooledDataBuffer; + +/** + * Implementation of the {@code DataBuffer} interface that wraps a Netty + * {@link ByteBuf}. Typically constructed with {@link NettyDataBufferFactory}. + */ +public class NettyDataBuffer implements PooledDataBuffer { + + private final ByteBuf byteBuf; + + private final NettyDataBufferFactory dataBufferFactory; + + + /** + * Create a new {@code NettyDataBuffer} based on the given {@code ByteBuff}. + * @param byteBuf the buffer to base this buffer on + */ + NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferFactory dataBufferFactory) { + Objects.requireNonNull(byteBuf, "ByteBuf must not be null"); + Objects.requireNonNull(dataBufferFactory, "NettyDataBufferFactory must not be null"); + this.byteBuf = byteBuf; + this.dataBufferFactory = dataBufferFactory; + } + + + /** + * Directly exposes the native {@code ByteBuf} that this buffer is based on. + * @return the wrapped byte buffer + */ + public ByteBuf getNativeBuffer() { + return this.byteBuf; + } + + @Override + public NettyDataBufferFactory factory() { + return this.dataBufferFactory; + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + Objects.requireNonNull(predicate, "IntPredicate must not be null"); + if (fromIndex < 0) { + fromIndex = 0; + } + else if (fromIndex >= this.byteBuf.writerIndex()) { + return -1; + } + int length = this.byteBuf.writerIndex() - fromIndex; + return this.byteBuf.forEachByte(fromIndex, length, predicate.negate()::test); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + Objects.requireNonNull(predicate, "IntPredicate must not be null"); + if (fromIndex < 0) { + return -1; + } + fromIndex = Math.min(fromIndex, this.byteBuf.writerIndex() - 1); + return this.byteBuf.forEachByteDesc(0, fromIndex + 1, predicate.negate()::test); + } + + @Override + public int readableByteCount() { + return this.byteBuf.readableBytes(); + } + + @Override + public int writableByteCount() { + return this.byteBuf.writableBytes(); + } + + @Override + public int readPosition() { + return this.byteBuf.readerIndex(); + } + + @Override + public NettyDataBuffer readPosition(int readPosition) { + this.byteBuf.readerIndex(readPosition); + return this; + } + + @Override + public int writePosition() { + return this.byteBuf.writerIndex(); + } + + @Override + public NettyDataBuffer writePosition(int writePosition) { + this.byteBuf.writerIndex(writePosition); + return this; + } + + @Override + public byte getByte(int index) { + return this.byteBuf.getByte(index); + } + + @Override + public int capacity() { + return this.byteBuf.capacity(); + } + + @Override + public NettyDataBuffer capacity(int capacity) { + this.byteBuf.capacity(capacity); + return this; + } + + @Override + public DataBuffer ensureCapacity(int capacity) { + this.byteBuf.ensureWritable(capacity); + return this; + } + + @Override + public byte read() { + return this.byteBuf.readByte(); + } + + @Override + public NettyDataBuffer read(byte[] destination) { + this.byteBuf.readBytes(destination); + return this; + } + + @Override + public NettyDataBuffer read(byte[] destination, int offset, int length) { + this.byteBuf.readBytes(destination, offset, length); + return this; + } + + @Override + public NettyDataBuffer write(byte b) { + this.byteBuf.writeByte(b); + return this; + } + + @Override + public NettyDataBuffer write(byte[] source) { + this.byteBuf.writeBytes(source); + return this; + } + + @Override + public NettyDataBuffer write(byte[] source, int offset, int length) { + this.byteBuf.writeBytes(source, offset, length); + return this; + } + + @Override + public NettyDataBuffer write(DataBuffer... buffers) { + if (buffers != null && buffers.length > 0) { + if (hasNettyDataBuffers(buffers)) { + ByteBuf[] nativeBuffers = new ByteBuf[buffers.length]; + for (int i = 0; i < buffers.length; i++) { + nativeBuffers[i] = ((NettyDataBuffer) buffers[i]).getNativeBuffer(); + } + write(nativeBuffers); + } + else { + ByteBuffer[] byteBuffers = new ByteBuffer[buffers.length]; + for (int i = 0; i < buffers.length; i++) { + byteBuffers[i] = buffers[i].asByteBuffer(); + + } + write(byteBuffers); + } + } + return this; + } + + private static boolean hasNettyDataBuffers(DataBuffer[] buffers) { + for (DataBuffer buffer : buffers) { + if (!(buffer instanceof NettyDataBuffer)) { + return false; + } + } + return true; + } + + @Override + public NettyDataBuffer write(ByteBuffer... buffers) { + if (buffers != null && buffers.length > 0) { + for (ByteBuffer buffer : buffers) { + this.byteBuf.writeBytes(buffer); + } + } + return this; + } + + /** + * Writes one or more Netty {@link ByteBuf ByteBufs} to this buffer, + * starting at the current writing position. + * @param byteBufs the buffers to write into this buffer + * @return this buffer + */ + public NettyDataBuffer write(ByteBuf... byteBufs) { + if (byteBufs != null && byteBufs.length > 0) { + for (ByteBuf byteBuf : byteBufs) { + this.byteBuf.writeBytes(byteBuf); + } + } + return this; + } + + @Override + public DataBuffer write(CharSequence charSequence, Charset charset) { + Objects.requireNonNull(charSequence, "CharSequence must not be null"); + Objects.requireNonNull(charset, "Charset must not be null"); + if (StandardCharsets.UTF_8.equals(charset)) { + ByteBufUtil.writeUtf8(this.byteBuf, charSequence); + } + else if (StandardCharsets.US_ASCII.equals(charset)) { + ByteBufUtil.writeAscii(this.byteBuf, charSequence); + } + else { + return PooledDataBuffer.super.write(charSequence, charset); + } + return this; + } + + @Override + public NettyDataBuffer slice(int index, int length) { + ByteBuf slice = this.byteBuf.slice(index, length); + return new NettyDataBuffer(slice, this.dataBufferFactory); + } + + @Override + public NettyDataBuffer retainedSlice(int index, int length) { + ByteBuf slice = this.byteBuf.retainedSlice(index, length); + return new NettyDataBuffer(slice, this.dataBufferFactory); + } + + @Override + public ByteBuffer asByteBuffer() { + return this.byteBuf.nioBuffer(); + } + + @Override + public ByteBuffer asByteBuffer(int index, int length) { + return this.byteBuf.nioBuffer(index, length); + } + + @Override + public InputStream asInputStream() { + return new ByteBufInputStream(this.byteBuf); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return new ByteBufInputStream(this.byteBuf, releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return new ByteBufOutputStream(this.byteBuf); + } + + @Override + public String toString(Charset charset) { + Objects.requireNonNull(charset, "Charset must not be null"); + return this.byteBuf.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + Objects.requireNonNull(charset, "Charset must not be null"); + return this.byteBuf.toString(index, length, charset); + } + + @Override + public boolean isAllocated() { + return this.byteBuf.refCnt() > 0; + } + + @Override + public PooledDataBuffer retain() { + return new NettyDataBuffer(this.byteBuf.retain(), this.dataBufferFactory); + } + + @Override + public PooledDataBuffer touch(Object hint) { + this.byteBuf.touch(hint); + return this; + } + + @Override + public boolean release() { + return this.byteBuf.release(); + } + + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof NettyDataBuffer && + this.byteBuf.equals(((NettyDataBuffer) other).byteBuf))); + } + + @Override + public int hashCode() { + return this.byteBuf.hashCode(); + } + + @Override + public String toString() { + return this.byteBuf.toString(); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBufferFactory.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBufferFactory.java new file mode 100644 index 0000000..01370a8 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/buffer/NettyDataBufferFactory.java @@ -0,0 +1,128 @@ +package org.xbib.net.http.server.netty.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import java.util.Objects; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.buffer.DataBufferFactory; + +/** + * Implementation of the {@code DataBufferFactory} interface based on a + * Netty {@link ByteBufAllocator}. + */ +public class NettyDataBufferFactory implements DataBufferFactory { + + private static final NettyDataBufferFactory INSTANCE = new NettyDataBufferFactory(); + + private final ByteBufAllocator byteBufAllocator; + + public NettyDataBufferFactory() { + this(ByteBufAllocator.DEFAULT); + } + + /** + * Create a new {@code NettyDataBufferFactory} based on the given factory. + * @param byteBufAllocator the factory to use + * @see io.netty.buffer.PooledByteBufAllocator + * @see io.netty.buffer.UnpooledByteBufAllocator + */ + public NettyDataBufferFactory(ByteBufAllocator byteBufAllocator) { + Objects.requireNonNull(byteBufAllocator, "ByteBufAllocator must not be null"); + this.byteBufAllocator = byteBufAllocator; + } + + public static NettyDataBufferFactory getInstance() { + return INSTANCE; + } + + /** + * Return the {@code ByteBufAllocator} used by this factory. + */ + public ByteBufAllocator getByteBufAllocator() { + return this.byteBufAllocator; + } + + @Override + public NettyDataBuffer allocateBuffer() { + ByteBuf byteBuf = this.byteBufAllocator.buffer(); + return new NettyDataBuffer(byteBuf, this); + } + + @Override + public NettyDataBuffer allocateBuffer(int initialCapacity) { + ByteBuf byteBuf = this.byteBufAllocator.buffer(initialCapacity); + return new NettyDataBuffer(byteBuf, this); + } + + @Override + public NettyDataBuffer wrap(ByteBuffer byteBuffer) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer); + return new NettyDataBuffer(byteBuf, this); + } + + @Override + public DataBuffer wrap(byte[] bytes) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); + return new NettyDataBuffer(byteBuf, this); + } + + /** + * Wrap the given Netty {@link ByteBuf} in a {@code NettyDataBuffer}. + * @param byteBuf the Netty byte buffer to wrap + * @return the wrapped buffer + */ + public NettyDataBuffer wrap(ByteBuf byteBuf) { + byteBuf.touch(); + return new NettyDataBuffer(byteBuf, this); + } + + /** + *

This implementation uses Netty's {@link CompositeByteBuf}. + */ + @Override + public DataBuffer join(List dataBuffers) { + if (dataBuffers == null || dataBuffers.isEmpty()) { + throw new IllegalArgumentException("DataBuffer List must not be empty"); + } + int bufferCount = dataBuffers.size(); + if (bufferCount == 1) { + return dataBuffers.get(0); + } + CompositeByteBuf composite = this.byteBufAllocator.compositeBuffer(bufferCount); + for (DataBuffer dataBuffer : dataBuffers) { + if (!(dataBuffer instanceof NettyDataBuffer)) { + throw new IllegalArgumentException("dataBuffer"); + } + composite.addComponent(true, ((NettyDataBuffer) dataBuffer).getNativeBuffer()); + } + return new NettyDataBuffer(composite, this); + } + + /** + * Return the given Netty {@link DataBuffer} as a {@link ByteBuf}. + *

Returns the {@linkplain NettyDataBuffer#getNativeBuffer() native buffer} + * if {@code buffer} is a {@link NettyDataBuffer}; returns + * {@link Unpooled#wrappedBuffer(ByteBuffer)} otherwise. + * @param buffer the {@code DataBuffer} to return a {@code ByteBuf} for + * @return the netty {@code ByteBuf} + */ + public static ByteBuf toByteBuf(DataBuffer buffer) { + if (buffer instanceof NettyDataBuffer) { + NettyDataBuffer nettyDataBuffer = (NettyDataBuffer) buffer; + return nettyDataBuffer.getNativeBuffer(); + } + else { + return Unpooled.wrappedBuffer(buffer.asByteBuffer()); + } + } + + @Override + public String toString() { + return "NettyDataBufferFactory (" + this.byteBufAllocator + ")"; + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1ChannelInitializer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1ChannelInitializer.java new file mode 100644 index 0000000..0ff5d55 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1ChannelInitializer.java @@ -0,0 +1,69 @@ +package org.xbib.net.http.server.netty.http1; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.stream.ChunkedWriteHandler; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpChannelInitializer; +import org.xbib.net.http.server.netty.IdleTimeoutHandler; +import org.xbib.net.http.server.netty.NettyCustomizer; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.netty.TrafficLoggingHandler; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Insecure HTTP 1.1 server channel initializer. This channel initializer can not upgrade to HTTP 2.0. + */ +public class Http1ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Http1ChannelInitializer.class.getName()); + + public Http1ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress address) { + return org.xbib.net.http.HttpVersion.HTTP_1_1.equals(address.getVersion()) && !address.isSecure(); + } + + @Override + public void init(Channel channel, NettyHttpServer server, NettyCustomizer customizer) { + NettyHttpServerConfig nettyHttpServerConfig = server.getNettyHttpServerConfig(); + ChannelPipeline pipeline = channel.pipeline(); + if (nettyHttpServerConfig.isDebug()) { + pipeline.addLast("server-logging", new TrafficLoggingHandler(LogLevel.DEBUG)); + } + //pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); + pipeline.addLast("server-codec", new HttpServerCodec(nettyHttpServerConfig.getMaxInitialLineLength(), + nettyHttpServerConfig.getMaxHeadersSize(), nettyHttpServerConfig.getMaxChunkSize())); + if (nettyHttpServerConfig.isCompressionEnabled()) { + pipeline.addLast("server-compressor", new HttpContentCompressor()); + } + if (nettyHttpServerConfig.isDecompressionEnabled()) { + pipeline.addLast("server-decompressor", new HttpContentDecompressor()); + } + HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(nettyHttpServerConfig.getMaxContentLength()); + httpObjectAggregator.setMaxCumulationBufferComponents(nettyHttpServerConfig.getMaxCompositeBufferComponents()); + pipeline.addLast("server-aggregator", httpObjectAggregator); + if (nettyHttpServerConfig.isPipeliningEnabled()) { + pipeline.addLast("server-pipelining", new HttpPipeliningHandler(nettyHttpServerConfig.getPipeliningCapacity())); + } + pipeline.addLast("server-handler", new Http1Handler(server)); + pipeline.addLast("server-idle-timeout", new IdleTimeoutHandler(nettyHttpServerConfig.getTimeoutMillis())); + if (customizer != null) { + customizer.afterChannelInitialized(channel); + } + if (nettyHttpServerConfig.isDebug() && logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "HTTP/1.1 server channel initialized: address=" + channel.localAddress() + + " pipeline=" + channel.pipeline().names()); + } + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1Handler.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1Handler.java new file mode 100644 index 0000000..6ba90ef --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/Http1Handler.java @@ -0,0 +1,103 @@ +package org.xbib.net.http.server.netty.http1; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpUtil; +import java.net.InetSocketAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpRequest; +import org.xbib.net.http.server.netty.HttpRequestBuilder; +import org.xbib.net.http.server.netty.HttpResponse; +import org.xbib.net.http.server.netty.HttpResponseBuilder; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; + +@ChannelHandler.Sharable +class Http1Handler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Http1Handler.class.getName()); + + private final NettyHttpServer nettyHttpServer; + + public Http1Handler(NettyHttpServer nettyHttpServer) { + this.nettyHttpServer = nettyHttpServer; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpPipelinedRequest) { + HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg; + try { + if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) httpPipelinedRequest.getRequest(); + requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId()); + } + } finally { + httpPipelinedRequest.release(); + } + } else if (msg instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) msg; + try { + if (HttpUtil.is100ContinueExpected(fullHttpRequest)) { + DefaultFullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.CONTINUE); + ctx.writeAndFlush(fullHttpResponse); + return; + } + requestReceived(ctx, fullHttpRequest, 0); + } finally { + fullHttpRequest.release(); + } + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + ctx.close(); + } + + protected void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) { + HttpAddress httpAddress = ctx.channel().attr(NettyHttpServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + try { + HttpResponseBuilder serverResponseBuilder = HttpResponse.builder() + .setChannelHandlerContext(ctx); + if (nettyHttpServer.getNettyHttpServerConfig().isPipeliningEnabled()) { + serverResponseBuilder.setSequenceId(sequenceId); + } + serverResponseBuilder.shouldClose("close".equalsIgnoreCase(fullHttpRequest.headers().get(HttpHeaderNames.CONNECTION))); + // the base URL construction may fail with exception. In hat case, we return a built-in 400 Bad Request. + HttpRequestBuilder serverRequestBuilder = HttpRequest.builder() + .setFullHttpRequest(fullHttpRequest) + .setBaseURL(httpAddress, + fullHttpRequest.uri(), + fullHttpRequest.headers().get(HttpHeaderNames.HOST)) + .setLocalAddress((InetSocketAddress) ctx.channel().localAddress()) + .setRemoteAddress((InetSocketAddress) ctx.channel().remoteAddress()) + .setSequenceId(sequenceId); + nettyHttpServer.getApplication().dispatch(serverRequestBuilder, serverResponseBuilder); + } catch (Exception e) { + logger.log(Level.SEVERE, "bad request: " + e.getMessage(), e); + DefaultFullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST); + ctx.writeAndFlush(fullHttpResponse); + ctx.close(); + } + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedRequest.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedRequest.java new file mode 100644 index 0000000..68a3afc --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedRequest.java @@ -0,0 +1,63 @@ +package org.xbib.net.http.server.netty.http1; + +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.ReferenceCounted; + +public class HttpPipelinedRequest implements ReferenceCounted { + + private final LastHttpContent request; + + private final int sequenceId; + + public HttpPipelinedRequest(LastHttpContent request, int sequenceId) { + this.request = request; + this.sequenceId = sequenceId; + } + + public LastHttpContent getRequest() { + return request; + } + + public int getSequenceId() { + return sequenceId; + } + + @Override + public int refCnt() { + return request.refCnt(); + } + + @Override + public ReferenceCounted retain() { + request.retain(); + return this; + } + + @Override + public ReferenceCounted retain(int increment) { + request.retain(increment); + return this; + } + + @Override + public ReferenceCounted touch() { + request.touch(); + return this; + } + + @Override + public ReferenceCounted touch(Object hint) { + request.touch(hint); + return this; + } + + @Override + public boolean release() { + return request.release(); + } + + @Override + public boolean release(int decrement) { + return request.release(decrement); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedResponse.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedResponse.java new file mode 100644 index 0000000..6f44dd2 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipelinedResponse.java @@ -0,0 +1,77 @@ +package org.xbib.net.http.server.netty.http1; + +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.util.ReferenceCounted; + +public class HttpPipelinedResponse implements ReferenceCounted, Comparable { + + private final FullHttpResponse response; + + private final ChannelPromise promise; + + private final int sequenceId; + + public HttpPipelinedResponse(FullHttpResponse response, ChannelPromise promise, int sequenceId) { + this.response = response; + this.promise = promise; + this.sequenceId = sequenceId; + } + + public int getSequenceId() { + return sequenceId; + } + + public HttpResponse getResponse() { + return response; + } + + public ChannelPromise getPromise() { + return promise; + } + + @Override + public int compareTo(HttpPipelinedResponse other) { + return Integer.compare(this.sequenceId, other.sequenceId); + } + + @Override + public int refCnt() { + return response.refCnt(); + } + + @Override + public ReferenceCounted retain() { + response.retain(); + return this; + } + + @Override + public ReferenceCounted retain(int increment) { + response.retain(increment); + return this; + } + + @Override + public ReferenceCounted touch() { + response.touch(); + return this; + } + + @Override + public ReferenceCounted touch(Object hint) { + response.touch(hint); + return this; + } + + @Override + public boolean release() { + return response.release(); + } + + @Override + public boolean release(int decrement) { + return response.release(decrement); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipeliningHandler.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipeliningHandler.java new file mode 100644 index 0000000..9a0eb2e --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http1/HttpPipeliningHandler.java @@ -0,0 +1,119 @@ +package org.xbib.net.http.server.netty.http1; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.LastHttpContent; + +import java.nio.channels.ClosedChannelException; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implements HTTP pipelining ordering, ensuring that responses are completely served in the same order as their + * corresponding requests. + * + * Based on https://github.com/typesafehub/netty-http-pipelining + * which uses Netty3. + * + * WARNING: this only works if there are no pipeline interuptions, for example by exceptions that force connection close. + * In that case, the responses will be generated but can not be written. It looks like no message reaches the network. + * This could be a bug. + */ +public class HttpPipeliningHandler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(HttpPipeliningHandler.class.getName()); + + private final int pipelineCapacity; + + private final Lock lock; + + private final Queue httpPipelinedResponses; + + private static final AtomicInteger sequenceIdCounter = new AtomicInteger(0); + + private static final AtomicInteger writtenRequests = new AtomicInteger(0); + + /** + * @param pipelineCapacity the maximum number of channel events that will be retained prior to aborting the channel + * connection. This is required as events cannot queue up indefinitely; we would run out of + * memory if this was the case. + */ + public HttpPipeliningHandler(int pipelineCapacity) { + + this.pipelineCapacity = pipelineCapacity; + this.lock = new ReentrantLock(); + this.httpPipelinedResponses = new PriorityQueue<>(1); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof LastHttpContent) { + ctx.fireChannelRead(new HttpPipelinedRequest((LastHttpContent) msg, sequenceIdCounter.getAndIncrement())); + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof HttpPipelinedResponse) { + boolean channelShouldClose = false; + lock.lock(); + try { + if (httpPipelinedResponses.size() < pipelineCapacity) { + HttpPipelinedResponse httpPipelinedResponse = (HttpPipelinedResponse) msg; + httpPipelinedResponses.add(httpPipelinedResponse); + while (!httpPipelinedResponses.isEmpty()) { + HttpPipelinedResponse queuedPipelinedResponse = httpPipelinedResponses.peek(); + if (queuedPipelinedResponse.getSequenceId() != writtenRequests.get()) { + break; + } + httpPipelinedResponses.remove(); + super.write(ctx, queuedPipelinedResponse.getResponse(), queuedPipelinedResponse.getPromise()); + writtenRequests.getAndIncrement(); + } + } else { + logger.log(Level.WARNING, "pipeline capacity exceeded, closing channel"); + channelShouldClose = true; + } + } finally { + lock.unlock(); + } + if (channelShouldClose) { + ctx.close(); + } + } else { + super.write(ctx, msg, promise); + } + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + if (!httpPipelinedResponses.isEmpty()) { + ClosedChannelException closedChannelException = new ClosedChannelException(); + HttpPipelinedResponse pipelinedResponse; + while ((pipelinedResponse = httpPipelinedResponses.poll()) != null) { + try { + pipelinedResponse.release(); + pipelinedResponse.getPromise().setFailure(closedChannelException); + } catch (Exception e) { + logger.log(Level.SEVERE, "unexpected error while releasing pipelined http responses", e); + } + } + } + ctx.close(promise); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + String message = cause.getMessage() == null ? "null" : cause.getMessage(); + logger.log(Level.WARNING, message, cause); + ctx.close(); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChannelInitializer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChannelInitializer.java new file mode 100644 index 0000000..50e8a52 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChannelInitializer.java @@ -0,0 +1,109 @@ +package org.xbib.net.http.server.netty.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.util.AsciiString; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.HttpChannelInitializer; +import org.xbib.net.http.server.netty.IdleTimeoutHandler; +import org.xbib.net.http.server.netty.NettyCustomizer; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.netty.TrafficLoggingHandler; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Insecure HTTP/2 server channel initializer. + * + * This initializer uses a "h2c" cleartext channel, which is not supported by most of the web browsers. + */ +public class Http2ChannelInitializer implements HttpChannelInitializer { + + private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); + + public Http2ChannelInitializer() { + } + + @Override + public boolean supports(HttpAddress address) { + return org.xbib.net.http.HttpVersion.HTTP_2_0.equals(address.getVersion()) && !address.isSecure(); + } + + @Override + public void init(Channel channel, NettyHttpServer nettyHttpServer, NettyCustomizer customizer) { + final HttpAddress httpAddress = channel.attr(NettyHttpServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + final NettyHttpServerConfig nettyHttpServerConfig = nettyHttpServer.getNettyHttpServerConfig(); + ChannelPipeline pipeline = channel.pipeline(); + if (nettyHttpServerConfig.isDebug()) { + pipeline.addLast("server-logging", new TrafficLoggingHandler(LogLevel.DEBUG)); + } + pipeline.addLast("server-upgrade", createUpgradeHandler(nettyHttpServer, httpAddress)); + pipeline.addLast("server-frame-converter", + new Http2StreamFrameToHttpObjectCodec(true)); + if (nettyHttpServerConfig.isCompressionEnabled()) { + pipeline.addLast("server-compressor", new HttpContentCompressor()); + } + if (nettyHttpServerConfig.isDecompressionEnabled()) { + pipeline.addLast("server-decompressor", new HttpContentDecompressor()); + } + pipeline.addLast("server-object-aggregator", + new HttpObjectAggregator(nettyHttpServerConfig.getMaxContentLength())); + pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); + pipeline.addLast("server-request", new Http2Handler(nettyHttpServer)); + pipeline.addLast("server-messages", new Http2Messages()); + pipeline.addLast("server-idle-timeout", new IdleTimeoutHandler(nettyHttpServerConfig.getTimeoutMillis())); + if (customizer != null) { + customizer.afterChannelInitialized(channel); + } + if (nettyHttpServerConfig.isDebug() && logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "HTTP/2 cleartext server channel initialized: address = " + + channel.localAddress() + " pipeline = " + pipeline.names()); + } + } + + /** + * This upgrade handler ensures to upgrade to HTTP 2.0 via prior knowledge. + * @param nettyHttpServer the netty server + * @param httpAddress the HTTP address + * @return the CleartextHttp2ServerUpgradeHandler + */ + protected CleartextHttp2ServerUpgradeHandler createUpgradeHandler(NettyHttpServer nettyHttpServer, + HttpAddress httpAddress) { + NettyHttpServerConfig nettyHttpServerConfig = nettyHttpServer.getNettyHttpServerConfig(); + Http2ChildChannelInitializer childHandler = + new Http2ChildChannelInitializer(nettyHttpServer, httpAddress); + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(childHandler) + .initialSettings(Http2Settings.defaultSettings()); + if (nettyHttpServerConfig.isDebug()) { + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); + } + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.build(); + HttpServerCodec serverCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(serverCodec, protocol -> { + logger.log(Level.INFO, "upgrade handler protocol = " + protocol); + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(multiplexCodec); + } else { + return null; + } + }); + return new CleartextHttp2ServerUpgradeHandler(serverCodec, upgradeHandler, multiplexCodec); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChildChannelInitializer.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChildChannelInitializer.java new file mode 100644 index 0000000..9c8ca5d --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2ChildChannelInitializer.java @@ -0,0 +1,48 @@ +package org.xbib.net.http.server.netty.http2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.netty.handler.stream.ChunkedWriteHandler; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.netty.IdleTimeoutHandler; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; + +public class Http2ChildChannelInitializer extends ChannelInitializer { + + private final NettyHttpServer nettyHttpServer; + + private final HttpAddress httpAddress; + + public Http2ChildChannelInitializer(NettyHttpServer nettyHttpServer, + HttpAddress httpAddress) { + this.nettyHttpServer = nettyHttpServer; + this.httpAddress = httpAddress; + } + + @Override + protected void initChannel(Channel channel) { + NettyHttpServerConfig nettyHttpServerConfig = nettyHttpServer.getNettyHttpServerConfig(); + channel.attr(NettyHttpServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).set(httpAddress); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("server-frame-converter", + new Http2StreamFrameToHttpObjectCodec(true)); + if (nettyHttpServerConfig.isCompressionEnabled()) { + pipeline.addLast("server-compressor", new HttpContentCompressor()); + } + if (nettyHttpServerConfig.isDecompressionEnabled()) { + pipeline.addLast("server-decompressor", new HttpContentDecompressor()); + } + pipeline.addLast("server-object-aggregator", + new HttpObjectAggregator(nettyHttpServerConfig.getMaxContentLength())); + pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); + pipeline.addLast("server-request", new Http2Handler(nettyHttpServer)); + pipeline.addLast("server-messages", new Http2Messages()); + pipeline.addLast("server-idle-timeout", new IdleTimeoutHandler(nettyHttpServerConfig.getTimeoutMillis())); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Handler.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Handler.java new file mode 100644 index 0000000..0999577 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Handler.java @@ -0,0 +1,82 @@ +package org.xbib.net.http.server.netty.http2; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.HttpConversionUtil; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.netty.HttpRequest; +import org.xbib.net.http.server.netty.HttpRequestBuilder; +import org.xbib.net.http.server.netty.HttpResponse; +import org.xbib.net.http.server.netty.HttpResponseBuilder; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; + +@ChannelHandler.Sharable +public class Http2Handler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Http2Handler.class.getName()); + + private final NettyHttpServer nettyHttpServer; + + public Http2Handler(NettyHttpServer nettyHttpServer) { + this.nettyHttpServer = nettyHttpServer; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object object) throws IOException { + if (object instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) object; + HttpAddress httpAddress = ctx.channel().attr(NettyHttpServerConfig.ATTRIBUTE_KEY_HTTP_ADDRESS).get(); + try { + Integer streamId = fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + HttpResponseBuilder httpResponseBuilder = HttpResponse.builder() + .setChannelHandlerContext(ctx) + .setVersion(HttpVersion.HTTP_2_0); + httpResponseBuilder.shouldClose("close".equalsIgnoreCase(fullHttpRequest.headers().get(HttpHeaderNames.CONNECTION))); + if (streamId != null) { + httpResponseBuilder.setStreamId(streamId + 1); + } + HttpRequestBuilder serverRequestBuilder = HttpRequest.builder() + .setFullHttpRequest(fullHttpRequest) + .setBaseURL(httpAddress, + fullHttpRequest.uri(), + fullHttpRequest.headers().get(HttpHeaderNames.HOST)) + .setLocalAddress((InetSocketAddress) ctx.channel().localAddress()) + .setRemoteAddress((InetSocketAddress) ctx.channel().remoteAddress()) + .setStreamId(streamId); + nettyHttpServer.getApplication().dispatch(serverRequestBuilder, httpResponseBuilder); + } catch (Exception e) { + logger.log(Level.SEVERE, "bad request:" + e.getMessage(), e); + DefaultFullHttpResponse fullHttpResponse = + new DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.valueOf(httpAddress.getVersion().text()), + HttpResponseStatus.BAD_REQUEST); + ctx.writeAndFlush(fullHttpResponse).addListener(ChannelFutureListener.CLOSE); + } finally { + fullHttpRequest.release(); + } + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + ctx.close(); + } +} diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Messages.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Messages.java new file mode 100644 index 0000000..01104a8 --- /dev/null +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/http2/Http2Messages.java @@ -0,0 +1,46 @@ +package org.xbib.net.http.server.netty.http2; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ChannelHandler.Sharable +public class Http2Messages extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(Http2Messages.class.getName()); + + public Http2Messages() { + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof DefaultHttp2SettingsFrame) { + DefaultHttp2SettingsFrame http2SettingsFrame = (DefaultHttp2SettingsFrame) msg; + logger.log(Level.FINER, "settings received, ignoring"); + } else if (msg instanceof DefaultHttpRequest) { + // somehow we got a HTTP 1.1 request, send "version not supported" to HTTP 1.1 client + DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED); + ctx.writeAndFlush(response); + ctx.close(); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.SEVERE, cause.getMessage(), cause); + ctx.close(); + } +} diff --git a/net-http-server-netty/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory b/net-http-server-netty/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory new file mode 100644 index 0000000..5178efc --- /dev/null +++ b/net-http-server-netty/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory @@ -0,0 +1 @@ +org.xbib.net.http.server.netty.buffer.NettyDataBufferFactory \ No newline at end of file diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerMultiRequestLoadTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerMultiRequestLoadTest.java new file mode 100644 index 0000000..eff123e --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerMultiRequestLoadTest.java @@ -0,0 +1,97 @@ +package org.xbib.net.http.netty.test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.util.ResourceLeakDetector; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NettyHttp2ServerMultiRequestLoadTest { + + private static final Logger logger = Logger.getLogger(NettyHttp2ServerMultiRequestLoadTest.class.getName()); + + @Test + public void testHttp2Load() throws Exception { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("http://localhost:8008/domain"); + HttpAddress httpAddress = HttpAddress.http2(url); + NettyHttpServerConfig serverConfig = new NettyHttpServerConfig(); + serverConfig.setServerName("NettyHttp2CleartextServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress) + .addService(BaseHttpService.builder() + .setPath("/domain") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain: " + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpClientConfig(); + int requests = 1024; + AtomicInteger count = new AtomicInteger(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + for (int i = 0; i < requests; i++) { + HttpRequest request = HttpRequest.get() + .setVersion(HttpVersion.HTTP_2_0) + .setURL(url) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response " + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + count.incrementAndGet(); + }) + .setExceptionListener(e -> { + logger.log(Level.SEVERE, e.getMessage(), e); + }) + .setTimeoutListener(listener -> { + logger.log(Level.SEVERE, "timeout"); + }, 5000L) + .build(); + client.execute(request).get(); // we need a get here + } + } + logger.log(Level.INFO, "count = " + count.get()); + assertEquals(requests, count.get()); + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerTest.java new file mode 100644 index 0000000..64b8c98 --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttp2ServerTest.java @@ -0,0 +1,87 @@ +package org.xbib.net.http.netty.test; + +import io.netty.bootstrap.Bootstrap; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NettyHttp2ServerTest { + + private static final Logger logger = Logger.getLogger(NettyHttp2ServerTest.class.getName()); + + @Test + public void testHttp2() throws Exception { + // note that h2c in cleartext is very uncommon, browser do not support this. + URL url = URL.from("http://localhost:8008/domain"); + HttpAddress httpAddress1 = HttpAddress.http2(url); + NettyHttpServerConfig nettyHttpServerConfig = new NettyHttpServerConfig(); + nettyHttpServerConfig.setServerName("NettyHttp2ClearTextServer", + Bootstrap.class.getPackage().getImplementationVersion()); + nettyHttpServerConfig.setNetworkClass(NetworkClass.ANY); + nettyHttpServerConfig.setDebug(true); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(nettyHttpServerConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress1) + .addService(BaseHttpService.builder() + .setPath("/domain") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain " + + ctx.httpRequest().getParameter() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpClientConfig() + .setDebug(true); + AtomicBoolean received = new AtomicBoolean(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setVersion(HttpVersion.HTTP_2_0) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got HTTP/2 response: " + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + received.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerFailureTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerFailureTest.java new file mode 100644 index 0000000..4fba98a --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerFailureTest.java @@ -0,0 +1,112 @@ +package org.xbib.net.http.netty.test; + +import io.netty.bootstrap.Bootstrap; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NettyHttpServerFailureTest { + + private static final Logger logger = Logger.getLogger(NettyHttpServerTest.class.getName()); + + @Test + public void testBadRequest() throws Exception { + URL url = URL.from("http://localhost:8008/domain/"); + HttpAddress httpAddress1 = HttpAddress.http1(url); + NettyHttpServerConfig nettyHttpServerConfig = new NettyHttpServerConfig(); + nettyHttpServerConfig.setServerName("NettyHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + nettyHttpServerConfig.setNetworkClass(NetworkClass.LOCAL); + nettyHttpServerConfig.setDebug(true); + nettyHttpServerConfig.setPipelining(false); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(nettyHttpServerConfig) + .setApplication(BaseApplication.builder() + .setHome(Paths.get(".")) + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress1) + .addService(BaseHttpService.builder() + .setPath("/domain") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain" + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress() + + " attributes = " + ctx.attributes() + ); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + + // bad request + + Socket socket = new Socket(InetAddress.getByName(url.getHost()), url.getPort()); + PrintWriter pw = new PrintWriter(socket.getOutputStream()); + pw.println("GET /::{} HTTP/1.1"); + pw.println("Host: " + url.getHost() + ":" + url.getPort()); + pw.println(""); + pw.flush(); + BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String string; + logger.log(Level.INFO, "enter read loop"); // should print bad request + while ((string = br.readLine()) != null) { + logger.log(Level.INFO, string); // should print bad request + } + br.close(); + + // good request + + NettyHttpClientConfig config = new NettyHttpClientConfig() + .setDebug(true); + AtomicInteger count = new AtomicInteger(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response:" + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + count.incrementAndGet(); + }) + .build(); + client.execute(request).get().close(); + } + assertEquals(1L, count.get()); + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerMultiRequestLoadTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerMultiRequestLoadTest.java new file mode 100644 index 0000000..a59ee3c --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerMultiRequestLoadTest.java @@ -0,0 +1,97 @@ +package org.xbib.net.http.netty.test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.util.ResourceLeakDetector; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; +import org.xbib.net.http.server.route.BaseHttpRouter; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NettyHttpServerMultiRequestLoadTest { + + private static final Logger logger = Logger.getLogger(NettyHttpServerMultiRequestLoadTest.class.getName()); + + @Test + public void loadTestHttp1() throws Exception { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); + URL url = URL.from("http://localhost:8008/domain"); + HttpAddress httpAddress = HttpAddress.http1(url); + NettyHttpServerConfig serverConfig = new NettyHttpServerConfig(); + serverConfig.setServerName("NettyHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress) + .addService(BaseHttpService.builder() + .setPath("/domain") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain: " + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " attributes = " + ctx.attributes() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpClientConfig(); + int requests = 1024; + AtomicInteger count = new AtomicInteger(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + for (int i = 0; i < requests; i++) { + HttpRequest request = HttpRequest.get() + .setVersion(HttpVersion.HTTP_1_1) + .setURL(url) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response " + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + count.incrementAndGet(); + }) + .setExceptionListener(e -> { + logger.log(Level.SEVERE, e.getMessage(), e); + }) + .setTimeoutListener(listener -> { + logger.log(Level.SEVERE, "timeout"); + }, 5000L) + .build(); + client.execute(request).get(); + } + } + logger.log(Level.INFO, "count = " + count.get()); + assertEquals(requests, count.get()); + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerTest.java new file mode 100644 index 0000000..014a71a --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/NettyHttpServerTest.java @@ -0,0 +1,87 @@ +package org.xbib.net.http.netty.test; + +import io.netty.bootstrap.Bootstrap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.client.netty.HttpRequest; +import org.xbib.net.http.client.netty.NettyHttpClient; +import org.xbib.net.http.client.netty.NettyHttpClientConfig; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.netty.NettyHttpServer; +import org.xbib.net.http.server.netty.NettyHttpServerConfig; + +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NettyHttpServerTest { + + private static final Logger logger = Logger.getLogger(NettyHttpServerTest.class.getName()); + + @Test + public void testHttp() throws Exception { + URL url = URL.from("http://localhost:8008/domain/"); + HttpAddress httpAddress1 = HttpAddress.http1(url); + NettyHttpServerConfig nettyHttpServerConfig = new NettyHttpServerConfig(); + nettyHttpServerConfig.setServerName("NettyHttpServer", Bootstrap.class.getPackage().getImplementationVersion()); + nettyHttpServerConfig.setNetworkClass(NetworkClass.LOCAL); + nettyHttpServerConfig.setDebug(true); + nettyHttpServerConfig.setPipelining(false); + try (NettyHttpServer server = NettyHttpServer.builder() + .setHttpServerConfig(nettyHttpServerConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress1) + .addService(BaseHttpService.builder() + .setPath("/domain") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain" + + " parameter = " + ctx.httpRequest().getParameter().allToString() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress() + + " attributes = " + ctx.attributes() + ); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + NettyHttpClientConfig config = new NettyHttpClientConfig() + .setDebug(true); + AtomicBoolean received = new AtomicBoolean(); + try (NettyHttpClient client = NettyHttpClient.builder() + .setConfig(config) + .build()) { + HttpRequest request = HttpRequest.get() + .setURL(url) + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response:" + + " status = " + resp.getStatus() + + " header = " + resp.getHeaders() + + " body = " + resp.getBodyAsChars(StandardCharsets.UTF_8)); + received.set(true); + }) + .build(); + client.execute(request).get().close(); + } + assertTrue(received.get()); + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/pipelining/HttpPipeliningHandlerTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/pipelining/HttpPipeliningHandlerTest.java new file mode 100644 index 0000000..efe11ec --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/pipelining/HttpPipeliningHandlerTest.java @@ -0,0 +1,265 @@ +package org.xbib.net.http.netty.test.pipelining; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.QueryStringDecoder; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.xbib.net.http.server.netty.http1.HttpPipelinedRequest; +import org.xbib.net.http.server.netty.http1.HttpPipelinedResponse; +import org.xbib.net.http.server.netty.http1.HttpPipeliningHandler; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class HttpPipeliningHandlerTest { + + private static final Random random = new SecureRandom(); + + private static final Logger logger = Logger.getLogger(HttpPipeliningHandlerTest.class.getName()); + + private static final Map waitingRequests = new ConcurrentHashMap<>(); + + @AfterAll + void closeResources() { + for (String url : waitingRequests.keySet()) { + finishRequest(url); + } + } + + @Order(1) + @Test + void testThatPipeliningWorksWithFastSerializedRequests() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(Executors.newCachedThreadPool()); + int numberOfRequests = randomIntBetween(random, 2, 128); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new HttpPipeliningHandler(numberOfRequests), + handler); + for (int i = 0; i < 5; i++) { + embeddedChannel.writeInbound(createHttpRequest("/" + i)); + } + for (String url : waitingRequests.keySet()) { + finishRequest(url); + } + handler.shutdownExecutorService(); + for (int i = 0; i < 5; i++) { + assertReadHttpMessageHasContent(embeddedChannel, String.valueOf(i)); + } + assertTrue(embeddedChannel.isOpen()); + } + + @Order(2) + @Test + void testThatPipeliningWorksWhenSlowRequestsInDifferentOrder() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(Executors.newCachedThreadPool()); + int numberOfRequests = randomIntBetween(random, 2, 128); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new HttpPipeliningHandler(numberOfRequests), + handler); + for (int i = 0; i < 5; i++) { + embeddedChannel.writeInbound(createHttpRequest("/" + i)); + } + List urls = new ArrayList<>(waitingRequests.keySet()); + Collections.shuffle(urls); + for (String url : urls) { + finishRequest(url); + } + handler.shutdownExecutorService(); + for (int i = 0; i < 5; i++) { + assertReadHttpMessageHasContent(embeddedChannel, String.valueOf(i)); + } + assertTrue(embeddedChannel.isOpen()); + } + + @Order(3) + @Test + void testThatPipeliningWorksWithChunkedRequests() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(Executors.newCachedThreadPool()); + int numberOfRequests = randomIntBetween(random, 2, 128); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new AggregateUrisAndHeadersHandler(), + new HttpPipeliningHandler(numberOfRequests), handler); + DefaultHttpRequest httpRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/0"); + embeddedChannel.writeInbound(httpRequest); + embeddedChannel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT); + httpRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/1"); + embeddedChannel.writeInbound(httpRequest); + embeddedChannel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT); + finishRequest("1"); + finishRequest("0"); + handler.shutdownExecutorService(); + for (int i = 0; i < 2; i++) { + assertReadHttpMessageHasContent(embeddedChannel, String.valueOf(i)); + } + assertTrue(embeddedChannel.isOpen()); + } + + @Order(4) + @Test + void testThatPipeliningClosesConnectionWithTooManyEvents() { + int numberOfRequests = randomIntBetween(random, 2, 128); + assertThrows(RejectedExecutionException.class, () -> { + WorkEmulatorHandler handler = new WorkEmulatorHandler(Executors.newCachedThreadPool()); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new HttpPipeliningHandler(numberOfRequests), + handler); + embeddedChannel.writeInbound(createHttpRequest("/0")); + embeddedChannel.writeInbound(createHttpRequest("/1")); + embeddedChannel.writeInbound(createHttpRequest("/2")); + embeddedChannel.writeInbound(createHttpRequest("/3")); + finishRequest("1"); + finishRequest("2"); + finishRequest("3"); + finishRequest("0"); + handler.shutdownExecutorService(); + embeddedChannel.writeInbound(createHttpRequest("/")); + }); + } + + private void assertReadHttpMessageHasContent(EmbeddedChannel embeddedChannel, String expectedContent) { + FullHttpResponse response = (FullHttpResponse) embeddedChannel.outboundMessages().poll(); + assertNotNull(response); + assertNotNull(response.content()); + String data = new String(ByteBufUtil.getBytes(response.content()), StandardCharsets.UTF_8); + assertEquals(expectedContent, data); + } + + private void finishRequest(String url) { + waitingRequests.get(url).countDown(); + } + + private FullHttpRequest createHttpRequest(String uri) { + return new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + } + + private static class AggregateUrisAndHeadersHandler extends SimpleChannelInboundHandler { + + static final Queue STRINGS = new LinkedTransferQueue<>(); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) { + STRINGS.add(request.uri()); + } + } + + private static class WorkEmulatorHandler extends SimpleChannelInboundHandler { + + private final ExecutorService executorService; + + WorkEmulatorHandler(ExecutorService executorService) { + this.executorService = executorService; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest pipelinedRequest) { + QueryStringDecoder decoder; + if (pipelinedRequest.getRequest() instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) pipelinedRequest.getRequest(); + decoder = new QueryStringDecoder(fullHttpRequest.uri()); + } else { + decoder = new QueryStringDecoder(AggregateUrisAndHeadersHandler.STRINGS.poll()); + } + String uri = decoder.path().replace("/", ""); + ByteBuf content = Unpooled.copiedBuffer(uri, StandardCharsets.UTF_8); + DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, content); + httpResponse.headers().add(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); + CountDownLatch latch = new CountDownLatch(1); + waitingRequests.put(uri, latch); + executorService.submit(() -> { + try { + latch.await(2, TimeUnit.SECONDS); + HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(httpResponse, + ctx.channel().newPromise(), pipelinedRequest.getSequenceId()); + ctx.writeAndFlush(httpPipelinedResponse); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + + void shutdownExecutorService() { + if (!executorService.isShutdown()) { + executorService.shutdown(); + try { + executorService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + } + } + + /** + * A random integer between min (inclusive) and max (inclusive). + */ + public static int randomIntBetween(Random r, int min, int max) { + assert max >= min : "max must be >= min: " + min + ", " + max; + long range = (long) max - (long) min; + if (range < Integer.MAX_VALUE) { + return min + r.nextInt(1 + (int) range); + } else { + return toIntExact(min + nextLong(r, 1 + range)); + } + } + + public static long nextLong(Random rnd, long n) { + if (n <= 0) { + throw new IllegalArgumentException("n <= 0: " + n); + } + + long value = rnd.nextLong(); + long range = n - 1; + if ((n & range) == 0L) { + value &= range; + } else { + for (long u = value >>> 1; u + range - (value = u % n) < 0L;) { + u = rnd.nextLong() >>> 1; + } + } + return value; + } + + private static int toIntExact(long value) { + if (value > Integer.MAX_VALUE) { + throw new ArithmeticException("Overflow: " + value); + } else { + return (int) value; + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttp2ClientServerTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttp2ClientServerTest.java new file mode 100644 index 0000000..ddd9310 --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttp2ClientServerTest.java @@ -0,0 +1,220 @@ +package org.xbib.net.http.netty.test.simple; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2Settings; +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 java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.fail; + +class SimpleHttp2ClientServerTest { + + private static final Logger clientLogger = Logger.getLogger("client"); + private static final Logger serverLogger = Logger.getLogger("server"); + + private static final LogLevel logLevel = LogLevel.DEBUG; + private static final Level level = Level.FINE; + + private static final Http2FrameLogger serverFrameLogger = new Http2FrameLogger(logLevel, "server"); + private static final Http2FrameLogger clientFrameLogger = new Http2FrameLogger(logLevel, "client"); + + private CompletableFuture settingsPrefaceFuture; + + private CompletableFuture completableFuture; + + @Test + void testHttp2() throws Exception { + final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + settingsPrefaceFuture = new CompletableFuture<>(); + completableFuture = new CompletableFuture<>(); + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + try { + Http2Connection http2ServerConnection = new DefaultHttp2Connection(true); + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .handler(serverFrameLogger) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("server-traffic", new TrafficLoggingHandler("server-traffic", logLevel)) + .addLast("server-connection-handler", new HttpToHttp2ConnectionHandlerBuilder() + .initialSettings(Http2Settings.defaultSettings()) + .connection(http2ServerConnection) + .frameListener(new InboundHttp2ToHttpAdapterBuilder(http2ServerConnection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build()) + .frameLogger(serverFrameLogger) + .build()) + .addLast("server-handler", new ServerHandler()); + } + }); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(level, "server up, channel = " + serverChannel); + + Http2Connection http2ClientConnection = new DefaultHttp2Connection(false); + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("client-traffic", new TrafficLoggingHandler("client-traffic", logLevel)) + .addLast("client-connection-handler", new HttpToHttp2ConnectionHandlerBuilder() + .initialSettings(Http2Settings.defaultSettings()) + .connection(http2ClientConnection) + .frameListener(new InboundHttp2ToHttpAdapterBuilder(http2ClientConnection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build()) + .frameLogger(clientFrameLogger) + .build()) + .addLast("client-handler", new ClientHandler()); + } + }); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(level, "client connected, channel = " + clientChannel); + + settingsPrefaceFuture.get(5L, TimeUnit.SECONDS); + if (!settingsPrefaceFuture.isDone()) { + throw new RuntimeException("no settings and preface written, unable to continue"); + } else { + clientLogger.log(level, "settings and preface written, let's start"); + } + + clientLogger.log(Level.INFO, "start"); + + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, + "http://localhost:8008/foobar/0/0"); + request.headers().add(HttpHeaderNames.HOST, inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + clientChannel.writeAndFlush(request); + + clientLogger.log(level, "waiting"); + completableFuture.get(30, TimeUnit.SECONDS); + if (completableFuture.isDone()) { + clientLogger.log(Level.INFO, "success"); + } else { + fail(); + } + + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientLogger.log(level, "client shutdown"); + serverLogger.log(level, "server shutdown"); + } + } + + class ClientHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + clientLogger.log(level, "got event on client " + evt); + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + settingsPrefaceFuture.complete(ctx); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + clientLogger.log(level, "msg received on client " + msg + " class=" + msg.getClass()); + if (msg instanceof FullHttpResponse) { + completableFuture.complete(true); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class ServerHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + serverLogger.log(level, "got event on server " + evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + serverLogger.log(level, "msg received on server " + msg + " class=" + msg.getClass()); + if (msg instanceof FullHttpRequest) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + serverLogger.log(Level.INFO, "writing server response: " + response); + ctx.writeAndFlush(response); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class TrafficLoggingHandler extends LoggingHandler { + + TrafficLoggingHandler(String name, LogLevel level) { + super(name, level); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.fireChannelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + ctx.fireChannelUnregistered(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { + ctx.write(msg, promise); + } else { + super.write(ctx, msg, promise); + } + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerFailureTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerFailureTest.java new file mode 100644 index 0000000..14d7ceb --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerFailureTest.java @@ -0,0 +1,255 @@ +package org.xbib.net.http.netty.test.simple; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.fail; + +class SimpleHttpClientServerFailureTest { + + private static final Logger clientLogger = Logger.getLogger("client"); + + private static final Logger serverLogger = Logger.getLogger("server"); + + private static final LogLevel logLevel = LogLevel.DEBUG; + + private static final Level level = Level.FINE; + + @Test + void testHttp() throws Exception { + InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + CompletableFuture completableFuture = new CompletableFuture<>(); + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("server-traffic", new TrafficLoggingHandler("server-traffic", logLevel)) + .addLast("server-codec", new HttpServerCodec()) + .addLast("server-http-aggregator", new HttpObjectAggregator(256 * 1024)) + .addLast("server-handler", new ServerHandler()); + } + }); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(level, "server up, channel = " + serverChannel); + + // bad request + + Socket socket = new Socket(InetAddress.getByName(inetSocketAddress.getHostString()), inetSocketAddress.getPort()); + PrintWriter pw = new PrintWriter(socket.getOutputStream()); + pw.println("FOOBAR /::{} HTTP/1.1"); + pw.println("Host: " + inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort()); + pw.println(""); + pw.flush(); + BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String t; + while ((t = br.readLine()) != null) { + clientLogger.log(Level.INFO, t); + } + br.close(); + + // good request + + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("client-traffic", new TrafficLoggingHandler("client-traffic", logLevel)) + .addLast("client-codec", new HttpClientCodec()) + .addLast("client-http-aggregator", new HttpObjectAggregator(256 * 1024)) + .addLast("client-handler", new ClientHandler(completableFuture)); + } + }); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(level, "client connected, channel = " + clientChannel); + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, + "/", Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8))); + request.headers().add(HttpHeaderNames.HOST, inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + clientChannel.writeAndFlush(request); + clientLogger.log(level, "waiting"); + completableFuture.get(30, TimeUnit.SECONDS); + if (completableFuture.isDone()) { + clientLogger.log(Level.INFO, "success"); + } else { + fail(); + } + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientLogger.log(level, "client shutdown"); + serverLogger.log(level, "server shutdown"); + } + } + + private static class ClientHandler extends ChannelDuplexHandler { + + private final CompletableFuture completableFuture; + + ClientHandler(CompletableFuture completableFuture) { + this.completableFuture = completableFuture; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof FullHttpResponse) { + FullHttpResponse httpResponse = (FullHttpResponse) msg; + clientLogger.log(level, "msg received on client " + httpResponse + ", completing future"); + if (httpResponse.status().equals(HttpResponseStatus.OK)) { + completableFuture.complete(true); + } else { + completableFuture.complete(false); + } + } else { + clientLogger.log(level, "unknown msg received on client " + msg + " class=" + msg.getClass() ); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + clientLogger.log(level, "read complete"); + ctx.flush(); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + clientLogger.log(level, "got event on client " + evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + private static class ServerHandler extends ChannelDuplexHandler { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + serverLogger.log(level, "msg received on server " + msg + " class=" + msg.getClass()); + if (msg instanceof FullHttpRequest) { + FullHttpRequest httpRequest = (FullHttpRequest) msg; + HttpMethod httpMethod = httpRequest.method(); + serverLogger.log(Level.INFO, "got method: " + httpMethod); + String hostAndPort = httpRequest.headers().get(HttpHeaderNames.HOST); + try { + URI uri = URI.create("http://" + hostAndPort + httpRequest.uri()); + serverLogger.log(Level.INFO, "got URI: " + uri); + if (httpMethod.equals(HttpMethod.GET)) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + serverLogger.log(Level.INFO, "writing OK response: " + response + " channel writable = " + ctx.channel().isWritable()); + ctx.write(response); + } else { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST); + serverLogger.log(Level.INFO, "writing BAD REQUEST response: " + response + " channel writable = " + ctx.channel().isWritable()); + ctx.write(response); + } + } catch (Exception e) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST); + serverLogger.log(Level.INFO, "writing BAD REQUEST response: " + response + " channel writable = " + ctx.channel().isWritable()); + ctx.write(response); + } + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // important to both flush and close the context + serverLogger.log(Level.INFO, "flush and close"); + ctx.flush(); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + serverLogger.log(level, "got event on server " + evt); + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + ctx.close(); + } + } + + private static class TrafficLoggingHandler extends LoggingHandler { + + TrafficLoggingHandler(String name, LogLevel level) { + super(name, level); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.fireChannelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + ctx.fireChannelUnregistered(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { + ctx.write(msg, promise); + } else { + super.write(ctx, msg, promise); + } + } + } +} diff --git a/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerTest.java b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerTest.java new file mode 100644 index 0000000..f9ed680 --- /dev/null +++ b/net-http-server-netty/src/test/java/org/xbib/net/http/netty/test/simple/SimpleHttpClientServerTest.java @@ -0,0 +1,207 @@ +package org.xbib.net.http.netty.test.simple; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.fail; + +class SimpleHttpClientServerTest { + + private static final Logger clientLogger = Logger.getLogger("client"); + + private static final Logger serverLogger = Logger.getLogger("server"); + + private static final LogLevel logLevel = LogLevel.DEBUG; + + private static final Level level = Level.FINE; + + @Test + void testHttp() throws Exception { + InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + CompletableFuture completableFuture = new CompletableFuture<>(); + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("server-traffic", new TrafficLoggingHandler("server-traffic", logLevel)) + .addLast("server-codec", new HttpServerCodec()) + .addLast("server-http-aggregator", new HttpObjectAggregator(256 * 1024)) + .addLast("server-handler", new ServerHandler()); + } + }); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(level, "server up, channel = " + serverChannel); + + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("client-traffic", new TrafficLoggingHandler("client-traffic", logLevel)) + .addLast("client-codec", new HttpClientCodec()) + .addLast("client-http-aggregator", new HttpObjectAggregator(256 * 1024)) + .addLast("client-handler", new ClientHandler(completableFuture)); + } + }); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(level, "client connected, channel = " + clientChannel); + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, + "/", Unpooled.wrappedBuffer("Hello World".getBytes(StandardCharsets.UTF_8))); + request.headers().add(HttpHeaderNames.HOST, inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + clientChannel.writeAndFlush(request); + clientLogger.log(level, "waiting"); + completableFuture.get(30, TimeUnit.SECONDS); + if (completableFuture.isDone()) { + clientLogger.log(Level.INFO, "success"); + } else { + fail(); + } + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientLogger.log(level, "client shutdown"); + serverLogger.log(level, "server shutdown"); + } + } + + private static class ClientHandler extends ChannelDuplexHandler { + + private final CompletableFuture completableFuture; + + ClientHandler(CompletableFuture completableFuture) { + this.completableFuture = completableFuture; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof FullHttpResponse) { + clientLogger.log(level, "msg received on client " + msg + " class=" + msg.getClass() + " completing future"); + completableFuture.complete(true); + } else { + clientLogger.log(level, "unknown msg received on client " + msg + " class=" + msg.getClass() ); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + clientLogger.log(level, " channel read complete"); + ctx.flush(); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + clientLogger.log(level, "got event on client " + evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + ctx.close(); + } + } + + private static class ServerHandler extends ChannelDuplexHandler { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + serverLogger.log(level, "msg received on server " + msg + " class=" + msg.getClass()); + if (msg instanceof FullHttpRequest) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + serverLogger.log(Level.INFO, "writing OK response: " + response); + ctx.write(response); + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // important to both flush and close the context + ctx.flush(); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + serverLogger.log(level, "got event on server " + evt); + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + ctx.close(); + } + } + + private static class TrafficLoggingHandler extends LoggingHandler { + + TrafficLoggingHandler(String name, LogLevel level) { + super(name, level); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.fireChannelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + ctx.fireChannelUnregistered(); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { + ctx.write(msg, promise); + } else { + super.write(ctx, msg, promise); + } + } + } +} diff --git a/net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer b/net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer new file mode 100644 index 0000000..f53ae78 --- /dev/null +++ b/net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.HttpChannelInitializer @@ -0,0 +1,2 @@ +org.xbib.net.http.server.netty.http1.Http1ChannelInitializer +org.xbib.net.http.server.netty.http2.Http2ChannelInitializer diff --git a/net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider b/net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider new file mode 100644 index 0000000..e031bc6 --- /dev/null +++ b/net-http-server-netty/src/test/resources/META-INF/services/org.xbib.net.http.server.netty.ServerTransportProvider @@ -0,0 +1 @@ +org.xbib.net.http.server.netty.NioServerTransportProvider diff --git a/net-http-server-netty/src/test/resources/logging.properties b/net-http-server-netty/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-server-netty/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-server-nio/build.gradle b/net-http-server-nio/build.gradle new file mode 100644 index 0000000..44c25d3 --- /dev/null +++ b/net-http-server-nio/build.gradle @@ -0,0 +1,9 @@ +dependencies { + api project(':net-http-server') +} + +test { + systemProperty 'application.name', 'test' + systemProperty 'application.home', 'src/test/resources' + systemProperty 'application.profile', 'test' +} diff --git a/net-http-server-nio/src/main/java/module-info.java b/net-http-server-nio/src/main/java/module-info.java new file mode 100644 index 0000000..48417f5 --- /dev/null +++ b/net-http-server-nio/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module org.xbib.net.http.server.nio { + exports org.xbib.net.http.server.nio; + exports org.xbib.net.http.server.nio.demo; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires java.logging; +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequest.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequest.java new file mode 100644 index 0000000..60180a2 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequest.java @@ -0,0 +1,44 @@ +package org.xbib.net.http.server.nio; + +import org.xbib.net.Request; +import org.xbib.net.http.server.BaseHttpRequest; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +public class HttpRequest extends BaseHttpRequest { + + private final HttpRequestBuilder builder; + + protected HttpRequest(HttpRequestBuilder builder) { + super(builder); + this.builder = builder; + } + + @Override + public InputStream getInputStream() { + return builder.getInputStream(); + } + + @Override + public ByteBuffer getBody() { + return builder.getBody(); + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return builder.getBodyAsChars(charset); + } + + @SuppressWarnings("unchecked") + @Override + public R as(Class cl) { + return (R) this; + } + + public static HttpRequestBuilder builder() { + return new HttpRequestBuilder(); + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequestBuilder.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequestBuilder.java new file mode 100644 index 0000000..88aa3b2 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpRequestBuilder.java @@ -0,0 +1,99 @@ +package org.xbib.net.http.server.nio; + +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.BaseHttpRequestBuilder; + +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +public class HttpRequestBuilder extends BaseHttpRequestBuilder { + + HttpRequestBuilder() { + } + + @Override + public HttpRequestBuilder setVersion(HttpVersion httpVersion) { + super.setVersion(httpVersion); + return this; + } + + @Override + public HttpRequestBuilder setMethod(HttpMethod httpMethod) { + super.setMethod(httpMethod); + return this; + } + + @Override + public HttpRequestBuilder setRequestURI(String requestURI) { + super.setRequestURI(requestURI); + return this; + } + + @Override + public HttpRequestBuilder setHeaders(HttpHeaders httpHeaders) { + super.setHeaders(httpHeaders); + return this; + } + + @Override + public HttpRequestBuilder setBody(ByteBuffer byteBuffer) { + super.setBody(byteBuffer); + return this; + } + + @Override + public HttpRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + super.setLocalAddress(localAddress); + return this; + } + + @Override + public HttpRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + super.setRemoteAddress(remoteAddress); + return this; + } + + @Override + public HttpRequestBuilder setBaseURL(HttpAddress httpAddress, String uri, String hostAndPort) { + super.setBaseURL(httpAddress, uri, hostAndPort); + return this; + } + + @Override + public HttpRequestBuilder setBaseURL(URL baseURL) { + super.setBaseURL(baseURL); + return this; + } + + @Override + public HttpRequestBuilder setSequenceId(Integer sequenceId) { + super.setSequenceId(sequenceId); + return this; + } + + @Override + public HttpRequestBuilder setStreamId(Integer streamId) { + super.setStreamId(streamId); + return this; + } + + @Override + public HttpRequestBuilder setRequestId(Long requestId) { + super.setRequestId(requestId); + return this; + } + + @Override + public HttpRequest build() { + return new HttpRequest(this); + } + + public InputStream getInputStream() { + return null; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponse.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponse.java new file mode 100644 index 0000000..0436984 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponse.java @@ -0,0 +1,39 @@ +package org.xbib.net.http.server.nio; + +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.server.BaseHttpResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; +import java.util.Objects; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class HttpResponse extends BaseHttpResponse { + + private final HttpResponseBuilder builder; + + protected HttpResponse(HttpResponseBuilder builder) { + super(builder); + this.builder = builder; + } + + @Override + public void close() throws IOException { + builder.internalClose(); + } + + @Override + public void flush() throws IOException { + builder.internalFlush(); + } + + public static HttpResponseBuilder builder() { + return new HttpResponseBuilder(); + } + +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponseBuilder.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponseBuilder.java new file mode 100644 index 0000000..c531472 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/HttpResponseBuilder.java @@ -0,0 +1,117 @@ +package org.xbib.net.http.server.nio; + +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.server.BaseHttpResponseBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class HttpResponseBuilder extends BaseHttpResponseBuilder { + + private static final Logger logger = Logger.getLogger(HttpResponseBuilder.class.getName()); + + protected OutputStream outputStream; + + HttpResponseBuilder() { + super(); + } + + public HttpResponseBuilder setOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + return this; + } + + @Override + public HttpResponse build() { + Objects.requireNonNull(outputStream); + try { + if (shouldFlush()) { + internalFlush(); + } + if (body != null) { + internalWrite(body); + } else if (charBuffer != null && charset != null) { + internalWrite(charBuffer, charset); + } else if (dataBuffer != null) { + internalWrite(dataBuffer); + } else if (fileChannel != null) { + internalWrite(fileChannel, bufferSize); + } else if (inputStream != null) { + internalWrite(inputStream, bufferSize); + } + if (shouldClose()) { + internalClose(); + } + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } return new HttpResponse(this); + } + + void internalFlush() throws IOException { + outputStream.flush(); + } + + void internalClose() throws IOException { + outputStream.close(); + } + + void internalWrite(String string) throws IOException { + if (string == null) { + internalFlush(); + } else { + write(dataBufferFactory.wrap(StandardCharsets.UTF_8.encode(string))); + } + } + + void internalWrite(CharBuffer charBuffer, Charset charset) throws IOException { + if (charBuffer == null) { + internalFlush(); + } else { + Objects.requireNonNull(charset); + write(dataBufferFactory.wrap(charset.encode(charBuffer))); + } + } + + void internalWrite(DataBuffer dataBuffer) throws IOException { + Objects.requireNonNull(dataBuffer); + try (WritableByteChannel channel = Channels.newChannel(outputStream)) { + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); + int contentLength = byteBuffer.remaining(); + super.buildHeaders(contentLength); + channel.write(US_ASCII.encode(super.wrapHeaders())); + while (byteBuffer.hasRemaining()) { + channel.write(byteBuffer); + } + } + } + + void internalWrite(InputStream inputStream, int bufferSize) throws IOException { + byte[] b = new byte[bufferSize]; + while (inputStream.available() > 0) { + int i = inputStream.read(b); + outputStream.write(b, 0, i); + } + } + + void internalWrite(FileChannel fileChannel, int bufferSize) throws IOException { + try (WritableByteChannel channel = Channels.newChannel(outputStream)) { + long contentLength = fileChannel.size(); + super.buildHeaders(contentLength); + channel.write(US_ASCII.encode(super.wrapHeaders())); + channel.write(fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, contentLength)); + } + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServer.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServer.java new file mode 100644 index 0000000..18b4081 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServer.java @@ -0,0 +1,241 @@ +package org.xbib.net.http.server.nio; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.BindException; +import java.net.InetSocketAddress; +import java.net.StandardSocketOptions; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NioHttpServer implements HttpServer { + + private static final Logger logger = Logger.getLogger(NioHttpServer.class.getName()); + + private final NioHttpServerBuilder builder; + + private final ExecutorService workerPool; + + private final Map serverSockets; + + NioHttpServer(NioHttpServerBuilder builder) { + this.builder = builder; + this.workerPool = Executors.newCachedThreadPool(); + this.serverSockets = new HashMap<>(); + } + + public static NioHttpServerBuilder builder() { + return new NioHttpServerBuilder(); + } + + @Override + public void bind() throws BindException { + for (HttpAddress httpAddress : getApplication().getAddresses()) { + try { + logger.log(Level.INFO, () -> "trying to bind to " + httpAddress); + ServerSocketChannel channel = ServerSocketChannel.open(); + if (channel.isOpen()) { + channel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024); + channel.setOption(StandardSocketOptions.SO_REUSEADDR, true); + channel.bind(httpAddress.getInetSocketAddress()); + } + serverSockets.put(httpAddress, channel); + } catch (Exception e) { + throw new BindException(e.getMessage()); + } + } + try { + Map httpAddressMap = new HashMap<>(); + Selector selector = Selector.open(); + for (Map.Entry entry : serverSockets.entrySet()) { + final HttpAddress httpAddress = entry.getKey(); + final ServerSocketChannel channel = entry.getValue(); + channel.configureBlocking(false); + SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT); + httpAddressMap.put(key, httpAddress); + } + while (true) { + int num = selector.select(); + if (num == 0) { + continue; + } + Set selectionKeys = selector.selectedKeys(); + Iterator iterator = selectionKeys.iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + HttpAddress httpAddress = httpAddressMap.get(key); + iterator.remove(); + if (key.isAcceptable()) { + ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ, SelectionKey.OP_WRITE); + } else if (key.isReadable()) { + //workerPool.submit(() -> { + try { + SocketChannel socketChannel = (SocketChannel) key.channel(); + InputStream inputStream = Channels.newInputStream(socketChannel); + OutputStream outputStream = Channels.newOutputStream(socketChannel); + HttpResponseBuilder responseBuilder = createResponse(outputStream); + HttpRequestBuilder requestBuilder = createRequest(inputStream, + httpAddress, + (InetSocketAddress) socketChannel.getLocalAddress(), + (InetSocketAddress) socketChannel.getRemoteAddress()); + handle(requestBuilder, responseBuilder); + socketChannel.close(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + // }); + } else if (key.isWritable()) { + logger.log(Level.WARNING, "nothing to write"); + } + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void loop() throws IOException { + CountDownLatch latch = new CountDownLatch(1); + try { + latch.await(); + } catch (InterruptedException e) { + throw new IOException(); + } + } + + @Override + public Application getApplication() { + return builder.application; + } + + public void handle(HttpRequestBuilder httpRequestBuilder, HttpResponseBuilder httpResponseBuilder) throws IOException { + getApplication().dispatch(httpRequestBuilder, httpResponseBuilder); + } + + @Override + public void close() throws IOException { + for (Map.Entry entry : serverSockets.entrySet()) { + entry.getValue().close(); + } + } + + protected HttpRequestBuilder createRequest(InputStream inputStream, + HttpAddress httpAddress, + InetSocketAddress localAddress, + InetSocketAddress remoteAddress) throws IOException { + String firstLine = readLine(inputStream); + HttpVersion httpVersion = extractVersion(firstLine); + String requestURI = firstLine.split("\\s+", 3)[1]; + HttpMethod httpMethod = extractMethod(firstLine); + HttpHeaders headers = extractHeaders(inputStream); + ByteBuffer byteBuffer = extractBody(inputStream, headers); + return HttpRequest.builder() + .setBaseURL(httpAddress, + requestURI, + headers.get(HttpHeaderNames.HOST)) + .setLocalAddress(localAddress) + .setRemoteAddress(remoteAddress) + .setMethod(httpMethod) + .setVersion(httpVersion) + .setHeaders(headers) + .setBody(byteBuffer); + } + + protected HttpResponseBuilder createResponse(OutputStream outputStream) { + return HttpResponse.builder() + .setOutputStream(outputStream); + } + + private static HttpVersion extractVersion(String headerLine) throws IllegalArgumentException { + Matcher m = Pattern.compile("HTTP/(\\d+)\\.(\\d+)").matcher(headerLine); + if (m.find()) { + if ((Integer.parseInt(m.group(1)) == 1) && (Integer.parseInt(m.group(2)) == 1)) { + return HttpVersion.HTTP_1_1; + } else if ((Integer.parseInt(m.group(1)) == 1) && (Integer.parseInt(m.group(2)) == 0)) { + return HttpVersion.HTTP_1_0; + } else { + throw new IllegalArgumentException("unknown HTTP version: " + headerLine); + } + } else { + throw new IllegalArgumentException("unknown HTTP version: " + headerLine); + } + } + + private static HttpMethod extractMethod(String headerLine) throws IllegalArgumentException { + String method = headerLine.split("\\s+")[0]; + if (method != null) { + return HttpMethod.valueOf(method); + } else { + throw new IllegalArgumentException(); + } + } + + private static HttpHeaders extractHeaders(InputStream inputStream) throws IOException { + HttpHeaders headers = new HttpHeaders(); + String nextLine; + while (!(nextLine = readLine(inputStream)).equals("")) { + String[] values = nextLine.split(":", 2); + headers.add(values[0].toLowerCase(Locale.ROOT), values[1].trim()); + } + return headers; + } + + private static ByteBuffer extractBody(InputStream inputStream, HttpHeaders headers) throws IOException { + String contentLength = headers.get(HttpHeaderNames.CONTENT_LENGTH); + ByteBuffer byteBuffer = null; + if (contentLength != null) { + int size = Integer.parseInt(contentLength); + byte[] data = new byte[size]; + int n = inputStream.read(data, 0, size); + if (n == size) { + byteBuffer = ByteBuffer.wrap(data); + } + } + return byteBuffer; + } + + private static String readLine(InputStream inputStream) throws IOException { + StringBuilder result = new StringBuilder(); + boolean crRead = false; + int n; + while ((n = inputStream.read()) != -1) { + if (n == '\r') { + crRead = true; + } else if (n == '\n' && crRead) { + return result.toString(); + } else { + result.append((char) n); + } + } + return result.toString(); + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServerBuilder.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServerBuilder.java new file mode 100644 index 0000000..63513f9 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/NioHttpServerBuilder.java @@ -0,0 +1,28 @@ +package org.xbib.net.http.server.nio; + +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServerConfig; + +public class NioHttpServerBuilder { + + HttpServerConfig httpServerConfig; + + Application application; + + NioHttpServerBuilder() { + } + + public NioHttpServerBuilder setHttpServerConfig(HttpServerConfig httpServerConfig) { + this.httpServerConfig = httpServerConfig; + return this; + } + + public NioHttpServerBuilder setApplication(Application application) { + this.application = application; + return this; + } + + public NioHttpServer build() { + return new NioHttpServer(this); + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ByteArray.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ByteArray.java new file mode 100644 index 0000000..0b767f4 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ByteArray.java @@ -0,0 +1,88 @@ +package org.xbib.net.http.server.nio.demo; + +import java.util.Arrays; + +public class ByteArray { + + private final double factor = 0.75; + + private byte[] source = new byte[10]; + + private int size = 0; + + private int capacity = 10; + + public ByteArray() { + } + + public static ByteArray from(byte[] b) { + ByteArray byteArray = new ByteArray(); + byteArray.add(b); + return byteArray; + } + + public ByteArray add(byte[] bytes) { + int newBytesSize = bytes.length; + if (shouldGrow(newBytesSize)) { + grow(newBytesSize); + } + System.arraycopy(bytes, 0, source, size, newBytesSize); + size += newBytesSize; + return this; + } + + public int getInt(int index) { + if (index + 4 > size) { + throw new ArrayIndexOutOfBoundsException(); + } + byte first = source[index]; + byte second = source[index + 1]; + byte third = source[index + 2]; + byte forth = source[index + 3]; + return first << 24 | (second & 0xFF) << 16 | (third & 0xFF) << 8 | (forth & 0xFF); + } + + public char getChar(int index) { + if (index >= size) { + throw new ArrayIndexOutOfBoundsException(); + } + return (char) source[index]; + } + + public byte[] getBytes() { + byte[] clone = new byte[size]; + System.arraycopy(source, 0, clone, 0, size); + return clone; + } + + public ByteArray clear() { + source = new byte[10]; + size = 0; + return this; + } + + private boolean shouldGrow(long newBytesSize) { + return size >= capacity * factor || size + newBytesSize >= capacity * factor; + } + + private void grow(int newBytesSize) { + int newTotalSize = size + newBytesSize; + if (newTotalSize >= capacity) { + capacity = (int) (newTotalSize * (1 + factor)); + } else { + capacity *= (1 + factor); + } + source = Arrays.copyOf(source, capacity); + } + + public int size() { + return size; + } + + public void removeFirst(int count) { + byte[] newBytes = new byte[size - count]; + System.arraycopy(source, count, newBytes, 0, size - count); + source = newBytes; + size -= count; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ClientBootstrap.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ClientBootstrap.java new file mode 100644 index 0000000..18039cd --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ClientBootstrap.java @@ -0,0 +1,33 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +public class ClientBootstrap { + + private InetSocketAddress inetSocketAddress; + private EventLoop eventLoop; + private SocketHandlerProvider socketHandlerProvider; + + ClientBootstrap() { + } + + public CompletableFuture connect(InetSocketAddress inetSocketAddress) throws IOException { + if (socketHandlerProvider == null) { + throw new RuntimeException("socketHandlerProvider is null"); + } + this.inetSocketAddress = inetSocketAddress; + SocketChannel socketChannel = SocketChannel.open(inetSocketAddress); + this.eventLoop = new EventLoop(new CountDownLatch(0)); + this.eventLoop.setSocketHandlerProvider(socketHandlerProvider); + this.eventLoop.add(socketChannel); + return this.eventLoop.loop(); + } + + public void setSocketHandlerProvider(SocketHandlerProvider socketHandlerProvider) { + this.socketHandlerProvider = socketHandlerProvider; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoop.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoop.java new file mode 100644 index 0000000..5de07e7 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoop.java @@ -0,0 +1,72 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +public class EventLoop { + + private final Selector selector; + private final CountDownLatch countDownLatch; + private SocketHandlerProvider socketHandlerProvider; + + public EventLoop(CountDownLatch countDownLatch) throws IOException { + this.selector = Selector.open(); + this.countDownLatch = countDownLatch; + } + + public synchronized void add(SocketChannel socketChannel) throws IOException { + this.add(socketChannel, this.socketHandlerProvider); + } + + public void add(SocketChannel socketChannel, SocketHandlerProvider socketHandlerProvider) throws IOException { + SelectionKey key = socketChannel + .configureBlocking(false) + .register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); + String connectionId = UUID.randomUUID().toString(); + SocketContext socketContext = new SocketContext(socketChannel, key, Thread.currentThread(), connectionId, selector); + SocketHandler handler = socketHandlerProvider.provide(socketContext); + key.attach(handler); + handler.onRegistered(); + selector.wakeup(); + } + + public CompletableFuture loop() { + countDownLatch.countDown(); + while (true) { + try { + selector.select(); + Set keys = selector.selectedKeys(); + Iterator iter = keys.iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + SocketHandler handler = (SocketHandler) key.attachment(); + try { + if (key.isReadable()) { + handler.onRead(); + } else if (key.isWritable()) { + handler.onWrite(); + } + } catch (IOException e) { + key.channel().close(); + } + iter.remove(); + } + } catch (Exception e) { + return CompletableFuture.supplyAsync(() -> { + throw new RuntimeException(e); + }); + } + } + } + + public void setSocketHandlerProvider(SocketHandlerProvider socketHandlerProvider) { + this.socketHandlerProvider = socketHandlerProvider; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoopGroup.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoopGroup.java new file mode 100644 index 0000000..4cb6b39 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/EventLoopGroup.java @@ -0,0 +1,52 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EventLoopGroup { + + private final List loops; + + private int position = 0; + + public EventLoopGroup(int size) throws IOException, InterruptedException { + this.loops = new ArrayList<>(); + CountDownLatch countDownLatch = new CountDownLatch(size); + for (int i = 0; i < size; i++) { + EventLoop loop = new EventLoop(countDownLatch); + Thread thread = new Thread(loop::loop); + thread.setName("event-loop-" + i); + thread.start(); + this.loops.add(loop); + } + boolean await = countDownLatch.await(10, TimeUnit.SECONDS); + if (!await) { + throw new RuntimeException("count down latch await timeout"); + } + } + + public void dispatch(SocketChannel socketChannel, SocketHandlerProvider socketHandlerProvider) throws IOException { + if (position >= loops.size()) { + position = 0; + } + EventLoop eventLoop = loops.get(position); + eventLoop.add(socketChannel, socketHandlerProvider); + position++; + } + + public void addEventLoop() throws IOException, InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(1); + EventLoop loop = new EventLoop(countDownLatch); + Thread thread = new Thread(loop::loop); + thread.setName("event-loop-" + loops.size()); + thread.start(); + boolean await = countDownLatch.await(10, TimeUnit.SECONDS); + if (!await) { + throw new RuntimeException("count down latch await timeout"); + } + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpContext.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpContext.java new file mode 100644 index 0000000..b5ccc4e --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpContext.java @@ -0,0 +1,51 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class HttpContext { + private final HttpRequest request; + private final HttpResponse response; + private final SocketContext socketContext; + + public HttpContext(HttpRequest request, HttpResponse response, SocketContext socketContext) { + this.request = request; + this.response = response; + this.socketContext = socketContext; + } + + public HttpContext status(String status) { + response.setStatusCode(status); + response.setReasonPhrase(toReason(status)); + return this; + } + + private String toReason(String status) { + switch (status) { + case "200": + return "OK"; + case "404": + return "Not Found"; + } + return "OK"; + } + + public void json(String s) throws IOException { + response.getHeaders().remove("content-type"); + response.setBody(ByteArray.from(s.getBytes(StandardCharsets.UTF_8))); + response.getHeaders().add("content-type", "application/json"); + byte[] bytes = response.getBytes(); + response.getHeaders().add("content-length", String.valueOf(bytes.length)); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + socketContext.getSocketChannel().write(buffer); + } + + public HttpRequest getRequest() { + return request; + } + + public void close() throws IOException { + socketContext.close(); + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeader.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeader.java new file mode 100644 index 0000000..0a2d4fc --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeader.java @@ -0,0 +1,45 @@ +package org.xbib.net.http.server.nio.demo; + +public class HttpHeader { + private final String key; + private final String value; + + public HttpHeader(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "HttpHeader{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HttpHeader that = (HttpHeader) o; + + if (!key.equals(that.key)) return false; + return value.equals(that.value); + } + + @Override + public int hashCode() { + int result = key.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeaders.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeaders.java new file mode 100644 index 0000000..7875c62 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpHeaders.java @@ -0,0 +1,55 @@ +package org.xbib.net.http.server.nio.demo; + +import java.util.LinkedList; +import java.util.List; + +public class HttpHeaders { + + private final List headers = new LinkedList<>(); + + HttpHeaders() { + } + + public void add(String key, String value) { + headers.add(new HttpHeader(key.toLowerCase(), value)); + } + + public void remove(String key) { + headers.removeIf(it -> it.getKey().equals(key)); + } + + public String get(String key) { + key = key.toLowerCase(); + for (HttpHeader header : headers) { + if (header.getKey().equals(key)) { + return header.getValue(); + } + } + return null; + } + + public boolean containsKey(String key) { + key = key.toLowerCase(); + for (HttpHeader header : headers) { + if (header.getKey().equals(key)) { + return true; + } + } + return false; + } + + public int size() { + return headers.size(); + } + + public List getList() { + return headers; + } + + @Override + public String toString() { + return "HttpHeaders{" + + "headers=" + headers + + '}'; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpMethod.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpMethod.java new file mode 100644 index 0000000..c115bc4 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpMethod.java @@ -0,0 +1,5 @@ +package org.xbib.net.http.server.nio.demo; + +public enum HttpMethod { + GET, POST, PUT, DELETE, OPTIONS, HEAD, CONNECT, TRACE, PATCH +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequest.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequest.java new file mode 100644 index 0000000..882e2e0 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequest.java @@ -0,0 +1,50 @@ +package org.xbib.net.http.server.nio.demo; + +import java.util.Arrays; + +public class HttpRequest { + private final HttpMethod httpMethod; + private final String uri; + private final String version; + private final HttpHeaders httpHeaders; + private final byte[] body; + + public HttpRequest(HttpMethod httpMethod, String uri, String version, HttpHeaders httpHeaders, byte[] body) { + this.httpMethod = httpMethod; + this.uri = uri; + this.version = version; + this.httpHeaders = httpHeaders; + this.body = body; + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public String getUri() { + return uri; + } + + public String getVersion() { + return version; + } + + public HttpHeaders getHttpHeaders() { + return httpHeaders; + } + + public byte[] getBody() { + return body; + } + + @Override + public String toString() { + return "HttpRequest{" + + "httpMethod=" + httpMethod + + ", uri='" + uri + '\'' + + ", version='" + version + '\'' + + ", httpHeaders=" + httpHeaders + + ", body=" + Arrays.toString(body) + + '}'; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestHandler.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestHandler.java new file mode 100644 index 0000000..2c877da --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestHandler.java @@ -0,0 +1,5 @@ +package org.xbib.net.http.server.nio.demo; + +public interface HttpRequestHandler { + void handle(HttpContext ctx); +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestParser.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestParser.java new file mode 100644 index 0000000..7487aad --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpRequestParser.java @@ -0,0 +1,210 @@ +package org.xbib.net.http.server.nio.demo; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class HttpRequestParser { + + private final ByteArray parseBytes = new ByteArray(); + private HttpMethod httpMethod; + private String uri; + private String version; + private HttpHeaders headers; + private byte[] body; + private boolean ready = false; + + public HttpRequestParser() { + } + + public void read(ByteBuffer buffer) { + byte[] remaining = new byte[buffer.remaining()]; + buffer.get(remaining); + parseBytes.add(remaining); + if (httpMethod == null) { + parseMethod(); + } + if (httpMethod != null && uri == null) { + parseUri(); + } + if (httpMethod != null && uri != null && version == null) { + parseVersion(); + } + if (httpMethod != null && uri != null && version != null && headers == null) { + parseHeaders(); + } + if (httpMethod != null && uri != null && version != null && headers != null && headers.containsKey("Content-Length") && body == null) { + parseBody(); + ready = true; + } else if (httpMethod != null && uri != null && version != null && headers != null && !headers.containsKey("Content-Length")) { + ready = true; + } + } + + private void parseBody() { + int bodyLength = Integer.parseInt(headers.get("Content-Length")); + int size = parseBytes.size(); + if (size < bodyLength) { + return; + } + byte[] copyArray = parseBytes.getBytes(); + this.body = new byte[bodyLength]; + System.arraycopy(copyArray, 0, this.body, 0, bodyLength); + parseBytes.removeFirst(bodyLength); + } + + private void parseHeaders() { + int size = parseBytes.size(); + StringBuilder sb = new StringBuilder(); + boolean ready = false; + int i = 0; + for (; i < size; i++) { + if (i + 3 < size + && parseBytes.getChar(i) == '\r' + && parseBytes.getChar(i + 1) == '\n' + && parseBytes.getChar(i + 2) == '\r' + && parseBytes.getChar(i + 3) == '\n') { + ready = true; + break; + } + sb.append(parseBytes.getChar(i)); + } + if (!ready) { + return; + } + String headersStr = sb.toString().trim(); + this.headers = new HttpHeaders(); + Arrays.stream(headersStr.split("\r\n")) + .forEach(headerStr -> { + String[] split = headerStr.split(":"); + String key = split[0]; + String value = split[1].trim(); + this.headers.add(key, value); + }); + parseBytes.removeFirst(i + 4); + } + + private void parseVersion() { + int size = parseBytes.size(); + if (size < 10) { + return; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + char c = parseBytes.getChar(i); + sb.append(c); + } + if (!sb.toString().equals("HTTP/1.1\r\n")) { + return; + } + version = "HTTP/1.1"; + parseBytes.removeFirst(8); + } + + private void parseUri() { + int size = parseBytes.size(); + StringBuilder sb = new StringBuilder(); + boolean ready = false; + for (int i = 0; i < size; i++) { + char c = parseBytes.getChar(i); + if (c == ' ') { + ready = true; + break; + } + sb.append(c); + } + if (!ready) { + return; + } + uri = sb.toString(); + parseBytes.removeFirst(uri.length() + 1); + } + + public boolean isReady() { + return ready; + } + + public HttpRequest getHttpRequest() { + return new HttpRequest(httpMethod, uri, version, headers, body); + } + + private void parseMethod() { + _parseMethod(); + if (httpMethod != null) { + int l = httpMethod.name().length(); + if (parseBytes.getChar(l) == ' ') { + parseBytes.removeFirst(l + 1); + } else { + httpMethod = null; + } + } + } + + private void _parseMethod() { + int size = parseBytes.size(); + if (size < 3) { + return; + } + + char c0 = parseBytes.getChar(0); + char c1 = parseBytes.getChar(1); + char c2 = parseBytes.getChar(2); + + if ((c0 == 'G') && (c1 == 'E') && (c2 == 'T')) { + httpMethod = HttpMethod.GET; + return; + } + + if (size < 4) { + return; + } + + char c3 = parseBytes.getChar(3); + if (c0 == 'P' && c1 == 'O' && c2 == 'S' && c3 == 'T') { + httpMethod = HttpMethod.POST; + return; + } + + if (c0 == 'P' && c1 == 'U' && c2 == 'T') { + httpMethod = HttpMethod.PUT; + return; + } + + if (size < 6) { + return; + } + + char c4 = parseBytes.getChar(4); + char c5 = parseBytes.getChar(5); + if (c0 == 'D' && c1 == 'E' && c2 == 'L' && c3 == 'E' && c4 == 'T' && c5 == 'E') { + httpMethod = HttpMethod.DELETE; + return; + } + + if (c0 == 'H' && c1 == 'E' && c2 == 'A' && c3 == 'D') { + httpMethod = HttpMethod.HEAD; + return; + } + + if (c0 == 'P' && c1 == 'A' && c2 == 'T' && c3 == 'C' && c4 == 'H') { + httpMethod = HttpMethod.PATCH; + } + + if (c0 == 'T' && c1 == 'R' && c2 == 'A' && c3 == 'C' && c4 == 'E') { + httpMethod = HttpMethod.TRACE; + } + + if (size < 7) { + return; + } + + char c6 = parseBytes.getChar(6); + if (c0 == 'O' && c1 == 'P' && c2 == 'T' && c3 == 'I' && c4 == 'O' && c5 == 'N' && c6 == 'S') { + httpMethod = HttpMethod.OPTIONS; + return; + } + + if (c0 == 'C' && c1 == 'O' && c2 == 'N' && c3 == 'N' && c4 == 'E' && c5 == 'C' && c6 == 'T') { + httpMethod = HttpMethod.CONNECT; + } + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpResponse.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpResponse.java new file mode 100644 index 0000000..e669647 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpResponse.java @@ -0,0 +1,83 @@ +package org.xbib.net.http.server.nio.demo; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +public class HttpResponse { + + private final String version = "HTTP/1.1"; + private final HttpHeaders headers = new HttpHeaders(); + private String statusCode = "200"; + private String reasonPhrase = "OK"; + private ByteArray body; + + HttpResponse() { + } + + public void addHeader(String key, String value) { + headers.add(key, value); + } + + public byte[] getBytes() { + headers.remove("content-length"); + if (body != null) { + headers.add("content-length", String.valueOf(body.size())); + } + String h = headers.getList().stream() + .map(it -> it.getKey() + ": " + it.getValue()) + .collect(Collectors.joining("\r\n")) + "\r\n\r\n"; + ByteArray byteArray = new ByteArray(); + byteArray.add((version + " ").getBytes(StandardCharsets.US_ASCII)); + byteArray.add((statusCode + " ").getBytes(StandardCharsets.US_ASCII)); + byteArray.add((reasonPhrase + "\r\n").getBytes(StandardCharsets.US_ASCII)); + byteArray.add(h.getBytes(StandardCharsets.US_ASCII)); + if (body != null) { + byteArray.add(body.getBytes()); + } + byteArray.add("\r\n".getBytes(StandardCharsets.US_ASCII)); + return byteArray.getBytes(); + } + + public String getVersion() { + return version; + } + + public String getStatusCode() { + return statusCode; + } + + public void setStatusCode(String statusCode) { + this.statusCode = statusCode; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + public void setReasonPhrase(String reasonPhrase) { + this.reasonPhrase = reasonPhrase; + } + + public HttpHeaders getHeaders() { + return headers; + } + + public ByteArray getBody() { + return body; + } + + public void setBody(ByteArray body) { + this.body = body; + } + + @Override + public String toString() { + return "HttpResponse{" + + "version='" + version + '\'' + + ", statusCode='" + statusCode + '\'' + + ", reasonPhrase='" + reasonPhrase + '\'' + + ", headers=" + headers + + ", body=" + body + + '}'; + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServer.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServer.java new file mode 100644 index 0000000..ece2d26 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServer.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; + +public class HttpServer { + + HttpServer() { + } + + public static void main(String[] args) throws IOException, InterruptedException { + int port = 8080; + if (args.length == 1) { + port = Integer.parseInt(args[0]); + } + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.provider(socketContext -> new HttpServerHandler(socketContext, new UriHandler())) + .connect(port); + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServerHandler.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServerHandler.java new file mode 100644 index 0000000..439eb33 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/HttpServerHandler.java @@ -0,0 +1,37 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class HttpServerHandler implements SocketHandler { + + private final ByteBuffer buffer = ByteBuffer.allocate(1024); + + private final HttpRequestParser httpRequestParser = new HttpRequestParser(); + + private final SocketContext socketContext; + + private final HttpRequestHandler handler; + + public HttpServerHandler(SocketContext socketContext, HttpRequestHandler handler) { + this.socketContext = socketContext; + this.handler = handler; + } + + @Override + public void onRead() throws IOException { + int i = socketContext.getSocketChannel().read(buffer); + if (i == -1) { + socketContext.getSocketChannel().close(); + return; + } + buffer.flip(); + httpRequestParser.read(buffer); + buffer.clear(); + if (!httpRequestParser.isReady()) { + return; + } + HttpRequest request = httpRequestParser.getHttpRequest(); + handler.handle(new HttpContext(request, new HttpResponse(), socketContext)); + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ServerBootstrap.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ServerBootstrap.java new file mode 100644 index 0000000..1241bdc --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/ServerBootstrap.java @@ -0,0 +1,48 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class ServerBootstrap { + + private SocketHandlerProvider socketHandlerProvider; + + ServerBootstrap() { + } + + public ServerBootstrap provider(SocketHandlerProvider socketHandlerProvider) { + this.socketHandlerProvider = socketHandlerProvider; + return this; + } + + public void connect(int port) throws IOException, InterruptedException { + if (socketHandlerProvider == null) { + throw new IllegalArgumentException("socketHandlerProvider is null"); + } + Selector selector = Selector.open(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + EventLoopGroup eventLoopGroup = new EventLoopGroup(4); + serverSocketChannel.bind(new InetSocketAddress(port)) + .configureBlocking(false) + .register(selector, SelectionKey.OP_ACCEPT); + while (!Thread.interrupted()) { + selector.select(); + Set keys = selector.selectedKeys(); + Iterator iterator = keys.iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + if (key.isAcceptable()) { + SocketChannel socketChannel = serverSocketChannel.accept(); + eventLoopGroup.dispatch(socketChannel, socketHandlerProvider); + } + iterator.remove(); + } + } + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketContext.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketContext.java new file mode 100644 index 0000000..4f84dfa --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketContext.java @@ -0,0 +1,53 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; + +public class SocketContext { + + private final SocketChannel socketChannel; + + private final SelectionKey selectionKey; + + private final Thread thread; + + private final String connectionId; + + private final Selector selector; + + public SocketContext(SocketChannel socketChannel, SelectionKey selectionKey, Thread thread, String connectionId, Selector selector) { + this.socketChannel = socketChannel; + this.selectionKey = selectionKey; + this.thread = thread; + this.connectionId = connectionId; + this.selector = selector; + } + + public void close() throws IOException { + this.socketChannel.close(); + this.thread.interrupt(); + } + + public SocketChannel getSocketChannel() { + return socketChannel; + } + + public SelectionKey getSelectionKey() { + return selectionKey; + } + + public Thread getThread() { + return thread; + } + + public String getConnectionId() { + return connectionId; + } + + public Selector getSelector() { + return selector; + } + +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandler.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandler.java new file mode 100644 index 0000000..13bb882 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandler.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public interface SocketHandler { + default void onRegistered() throws IOException { + } + + default void onRead() throws IOException { + } + + default void onWrite() throws IOException { + } + + default void close() throws IOException { + } + + default void write(ByteBuffer buffer) throws IOException { + } +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandlerProvider.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandlerProvider.java new file mode 100644 index 0000000..dd3d21b --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/SocketHandlerProvider.java @@ -0,0 +1,5 @@ +package org.xbib.net.http.server.nio.demo; + +public interface SocketHandlerProvider { + SocketHandler provide(SocketContext socketContext); +} diff --git a/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/UriHandler.java b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/UriHandler.java new file mode 100644 index 0000000..e7ebe14 --- /dev/null +++ b/net-http-server-nio/src/main/java/org/xbib/net/http/server/nio/demo/UriHandler.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.server.nio.demo; + +import java.io.IOException; + +public class UriHandler implements HttpRequestHandler { + + public UriHandler() { + } + + @Override + public void handle(HttpContext ctx) { + HttpRequest request = ctx.getRequest(); + String uri = request.getUri(); + String body = "{\"uri\":\"" + uri + "\"}"; + try { + ctx.status("200").json(body); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/ByteArrayTest.java b/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/ByteArrayTest.java new file mode 100644 index 0000000..71a6ab6 --- /dev/null +++ b/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/ByteArrayTest.java @@ -0,0 +1,63 @@ +package org.xbib.net.http.nio.test; + +import org.junit.jupiter.api.Test; +import org.xbib.net.http.server.nio.demo.ByteArray; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ByteArrayTest { + @Test + public void should_return_inserted_data_when_insert_some_bytes_and_call_getCopyArray_given_a_ByteArray() { + ByteArray byteArray = new ByteArray(); + byteArray.add(new byte[]{1, 2, 3}) + .add(new byte[]{4, 5, 6}); + byte[] copy = byteArray.getBytes(); + assertArrayEquals(new byte[]{1, 2, 3, 4, 5, 6}, copy); + assertEquals(6, byteArray.size()); + } + + @Test + public void should_clear_data_when_call_clear_given_a_ByteArray() { + ByteArray byteArray = new ByteArray(); + byteArray.add(new byte[]{1, 2, 3}) + .add(new byte[]{4, 5, 6}); + byte[] copy = byteArray.clear().getBytes(); + assertArrayEquals(new byte[]{}, copy); + assertEquals(0, byteArray.size()); + } + + @Test + public void should_return_int_when_call_getInt_given_a_ByteArray_contains_4_bytes() { + ByteArray byteArray = new ByteArray(); + int i1 = byteArray.add(new byte[]{1, 2, 3, 4}).getInt(0); + assertEquals(16909060, i1); + int i2 = byteArray.add(new byte[]{5}).getInt(1); + assertEquals(33752069, i2); + } + + @Test + public void should_throw_ArrayIndexOutOfBoundsException_when_call_getInt_with_param_1_given_a_ByteArray_only_contains_4_bytes() { + ByteArray byteArray = new ByteArray(); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> { + byteArray.add(new byte[]{1, 2, 3, 4}).getInt(1); + }); + } + + @Test + public void should_remove_bytes() { + ByteArray byteArray = new ByteArray(); + byteArray.add(new byte[]{1, 2, 3}); + byteArray.removeFirst(2); + assertEquals(1, byteArray.size()); + } + + @Test + public void should_get_char() { + ByteArray byteArray = new ByteArray(); + byteArray.add(new byte[]{97, 48}); + assertEquals('a', byteArray.getChar(0)); + assertEquals('0', byteArray.getChar(1)); + } +} diff --git a/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/HttpRequestParserTest.java b/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/HttpRequestParserTest.java new file mode 100644 index 0000000..2fb18b7 --- /dev/null +++ b/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/HttpRequestParserTest.java @@ -0,0 +1,109 @@ +package org.xbib.net.http.nio.test; + +import org.junit.jupiter.api.Test; +import org.xbib.net.http.server.nio.demo.HttpHeader; +import org.xbib.net.http.server.nio.demo.HttpHeaders; +import org.xbib.net.http.server.nio.demo.HttpRequest; +import org.xbib.net.http.server.nio.demo.HttpRequestParser; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HttpRequestParserTest { + + @Test + public void should_parse_get_request_success() { + HttpRequestParser parser = new HttpRequestParser(); + String r = "GET / HTTP/1.1\r\n" + + "host: www.example.com\r\n" + + "cache-control: max-age=0\r\n" + + "\r\n"; + ByteBuffer byteBuffer = ByteBuffer.wrap(r.getBytes(StandardCharsets.UTF_8)); + + parser.read(byteBuffer); + assertTrue(parser.isReady()); + + HttpRequest request = parser.getHttpRequest(); + assertEquals("/", request.getUri()); + assertNull(request.getBody()); + assertEquals("HTTP/1.1", request.getVersion()); + HttpHeaders httpHeaders = request.getHttpHeaders(); + assertEquals(2, httpHeaders.size()); + List headerList = httpHeaders.getList(); + + HttpHeader h2 = headerList.get(0); + assertEquals("host", h2.getKey()); + assertEquals("www.example.com", h2.getValue()); + + HttpHeader h1 = headerList.get(1); + assertEquals("cache-control", h1.getKey()); + assertEquals("max-age=0", h1.getValue()); + } + + @Test + public void should_parse_post_request_success() { + HttpRequestParser parser = new HttpRequestParser(); + String r = "POST /user HTTP/1.1\r\n" + + "host: www.example.com\r\n" + + "content-type: application/json\r\n" + + "content-length: 16\r\n" + + "\r\n" + + "{\"name\":\"admin\"}"; + ByteBuffer byteBuffer = ByteBuffer.wrap(r.getBytes(StandardCharsets.UTF_8)); + + parser.read(byteBuffer); + assertTrue(parser.isReady()); + + HttpRequest request = parser.getHttpRequest(); + assertEquals("/user", request.getUri()); + assertArrayEquals("{\"name\":\"admin\"}".getBytes(), request.getBody()); + assertEquals("HTTP/1.1", request.getVersion()); + + HttpHeaders httpHeaders = request.getHttpHeaders(); + assertEquals(3, httpHeaders.size()); + List headerList = httpHeaders.getList(); + + HttpHeader h3 = headerList.get(0); + assertEquals("host", h3.getKey()); + assertEquals("www.example.com", h3.getValue()); + + HttpHeader h2 = headerList.get(1); + assertEquals("content-type", h2.getKey()); + assertEquals("application/json", h2.getValue()); + + HttpHeader h1 = headerList.get(2); + assertEquals("content-length", h1.getKey()); + assertEquals("16", h1.getValue()); + } + + @Test + public void should_parse_delete_request_success() { + HttpRequestParser parser = new HttpRequestParser(); + String r = "DELETE /user/1 HTTP/1.1\r\n" + + "host: www.example.com\r\n" + + "\r\n"; + ByteBuffer byteBuffer = ByteBuffer.wrap(r.getBytes(StandardCharsets.UTF_8)); + + parser.read(byteBuffer); + assertTrue(parser.isReady()); + + HttpRequest request = parser.getHttpRequest(); + assertEquals("/user/1", request.getUri()); + assertNull(request.getBody()); + assertEquals("HTTP/1.1", request.getVersion()); + + HttpHeaders httpHeaders = request.getHttpHeaders(); + assertEquals(1, httpHeaders.size()); + List headerList = httpHeaders.getList(); + + HttpHeader h3 = headerList.get(0); + assertEquals("host", h3.getKey()); + assertEquals("www.example.com", h3.getValue()); + } +} diff --git a/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/NioHttpServerTest.java b/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/NioHttpServerTest.java new file mode 100644 index 0000000..396931d --- /dev/null +++ b/net-http-server-nio/src/test/java/org/xbib/net/http/nio/test/NioHttpServerTest.java @@ -0,0 +1,75 @@ +package org.xbib.net.http.nio.test; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.HttpServerConfig; +import org.xbib.net.http.server.nio.NioHttpServer; + +import java.net.BindException; +import java.nio.charset.StandardCharsets; + +public class NioHttpServerTest { + + @Disabled + @Test + public void nioServerTest() throws Exception { + HttpAddress httpAddress1 = HttpAddress.http1("localhost", 8008); + HttpAddress httpAddress2 = HttpAddress.http1("localhost", 8009); + NioHttpServer server = NioHttpServer.builder() + .setHttpServerConfig(new HttpServerConfig() + .setServerName("NioHttpServer", NioHttpServer.class.getPackage().getImplementationVendor()) + .setNetworkClass(NetworkClass.SITE) + ) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress1) + .addService(BaseHttpService.builder() + .setPath("/domain1") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain1 " + + ctx.httpRequest().getParameter().toString() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress2) + .addService(BaseHttpService.builder() + .setPath("/domain2") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain2 " + + ctx.httpRequest().getParameter().toString() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build(); + try { + server.bind(); + } catch (BindException e) { + throw new RuntimeException(e); + } + } +} diff --git a/net-http-server-simple-secure/build.gradle b/net-http-server-simple-secure/build.gradle new file mode 100644 index 0000000..42ed313 --- /dev/null +++ b/net-http-server-simple-secure/build.gradle @@ -0,0 +1,11 @@ +dependencies { + api project(':net-http-server-simple') + api libs.net.security + testImplementation libs.net.bouncycastle +} + +test { + systemProperty 'application.name', 'test' + systemProperty 'application.home', 'src/test/resources' + systemProperty 'application.profile', 'test' +} diff --git a/net-http-server-simple-secure/src/main/java/module-info.java b/net-http-server-simple-secure/src/main/java/module-info.java new file mode 100644 index 0000000..ad9d9b9 --- /dev/null +++ b/net-http-server-simple-secure/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module org.xbib.net.http.server.simple.secure { + uses org.xbib.net.security.CertificateProvider; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.xbib.net.http.server.simple; + requires org.xbib.net.security; + requires java.logging; +} diff --git a/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsAddress.java b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsAddress.java new file mode 100644 index 0000000..f8f362f --- /dev/null +++ b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsAddress.java @@ -0,0 +1,258 @@ +package org.xbib.net.http.server.simple.secure; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.security.CertificateProvider; +import org.xbib.net.security.CertificateReader; +import org.xbib.net.security.ssl.SSLFactory; +import org.xbib.net.security.util.DistinguishedNameParser; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HttpsAddress extends HttpAddress { + + private static final Logger logger = Logger.getLogger(HttpsAddress.class.getName()); + + private final SSLContext sslContext; + + public HttpsAddress(String host, Integer port, HttpVersion version, + boolean secure, Set hostNames, SSLContext sslContext) { + super(host, port, version, secure, hostNames); + this.sslContext = sslContext; + } + + public static Builder builder() { + return new Builder().setSecure(true); + } + + public static HttpsAddress https1(String host) throws KeyStoreException { + return builder() + .setVersion(HttpVersion.HTTP_1_1) + .setHost(host) + .setPort(443) + .build(); + } + + public static HttpAddress https1(String host, int port) throws KeyStoreException { + return builder() + .setVersion(HttpVersion.HTTP_1_1) + .setHost(host) + .setPort(port) + .build(); + } + + public static HttpAddress https2(String host) throws KeyStoreException { + return builder() + .setVersion(HttpVersion.HTTP_2_0) + .setHost(host) + .setPort(443) + .build(); + } + + public static HttpAddress https2(String host, int port) throws KeyStoreException { + return builder() + .setVersion(HttpVersion.HTTP_2_0) + .setHost(host) + .setPort(port) + .build(); + } + + public SSLContext getSslContext() { + return sslContext; + } + + public static class Builder { + + private static TrustManagerFactory TRUST_MANAGER_FACTORY; + + private static final Iterable DEFAULT_JDK_CIPHERS = + Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + + static { + try { + TRUST_MANAGER_FACTORY = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (Exception e) { + TRUST_MANAGER_FACTORY = null; + } + } + + private String host; + + private int port = -1; + + private boolean isSecure = true; + + private HttpVersion httpVersion = HttpVersion.HTTP_1_1; + + private TrustManagerFactory trustManagerFactory; + + private KeyStore trustManagerKeyStore; + + private Iterable ciphers; + + private Collection certChain; + + private PrivateKey privateKey; + + private String privateKeyPassword; + + private Set hostNames; + + private Builder() { + this.trustManagerFactory = TRUST_MANAGER_FACTORY; + this.ciphers = DEFAULT_JDK_CIPHERS; + } + + public Builder setHost(String host) { + this.host = host; + return this; + } + + public Builder setPort(int port) { + this.port = port; + return this; + } + + public Builder setSecure(boolean secure) { + this.isSecure = secure; + return this; + } + + public Builder setVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public Builder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + public Builder setTrustManagerKeyStore(KeyStore trustManagerKeyStore) { + this.trustManagerKeyStore = trustManagerKeyStore; + return this; + } + + public Builder setCiphers(Iterable ciphers) { + this.ciphers = ciphers; + return this; + } + + public Builder setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + return this; + } + + public Builder setCertChain(Collection chain) { + Objects.requireNonNull(chain); + this.certChain = chain; + return this; + } + + public Builder setCertChain(InputStream keyInputStream, String password, InputStream chain) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + ServiceLoader certificateProviders = ServiceLoader.load(CertificateProvider.class); + boolean found = false; + this.privateKeyPassword = password; + for (CertificateProvider provider : certificateProviders) { + try { + Map.Entry> entry = + provider.provide(keyInputStream, password, chain); + setPrivateKey(entry.getKey()); + setCertChain(entry.getValue()); + found = true; + break; + } catch (CertificateException | IOException e) { + // ignore + } + } + if (!found) { + throw new CertificateException("no certificate found"); + } + // automatic adding of certificate DNS names for automatic domain name match setup + List certificates = CertificateReader.orderCertificateChain(certChain); + hostNames = getServerNames(certificates.get(0)); + return this; + } + + public Builder setSelfCert(String fullQualifiedDomainName) throws CertificateException { + ServiceLoader certificateProviders = ServiceLoader.load(CertificateProvider.class); + boolean found = false; + for (CertificateProvider provider : certificateProviders) { + try { + Map.Entry> entry = + provider.provideSelfSigned(fullQualifiedDomainName); + setPrivateKey(entry.getKey()); + setCertChain(entry.getValue()); + found = true; + } catch (CertificateException | IOException e) { + // ignore + } + } + if (!found) { + throw new CertificateException("no self-signed certificate found"); + } + return this; + } + + public HttpsAddress build() throws KeyStoreException { + Objects.requireNonNull(host); + Objects.requireNonNull(httpVersion); + Objects.requireNonNull(privateKey); + Objects.requireNonNull(certChain); + if (certChain.isEmpty()) { + throw new IllegalArgumentException("cert chain must not be empty"); + } + Objects.requireNonNull(ciphers); + // trustManagerKeyStore may be null, this will be used to init() for default behavior + trustManagerFactory.init(trustManagerKeyStore); + SSLFactory sslFactory = SSLFactory.builder() + .withCiphers(ciphers) + .withIdentityMaterial(privateKey, + privateKeyPassword != null ? privateKeyPassword.toCharArray() : null, + certChain) + .withTrustMaterial(trustManagerFactory) + .build(); + return new HttpsAddress(host, port, httpVersion, isSecure, hostNames, sslFactory.getSslContext()); + } + } + + private static Set getServerNames(X509Certificate certificate) throws CertificateParsingException { + Set set = new LinkedHashSet<>(); + set.add(new DistinguishedNameParser(certificate.getSubjectX500Principal()).findMostSpecific("CN")); + Collection> altNames = certificate.getSubjectAlternativeNames(); + if (altNames != null) { + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + if (type == 2) { // Type = DNS + String string = altName.get(1).toString(); + set.add(string); + } + } + } + return set; + } +} diff --git a/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequest.java b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequest.java new file mode 100644 index 0000000..d8d4b9c --- /dev/null +++ b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequest.java @@ -0,0 +1,27 @@ +package org.xbib.net.http.server.simple.secure; + +import org.xbib.net.http.server.simple.HttpRequest; + +import javax.net.ssl.SSLSession; + +public class HttpsRequest extends HttpRequest { + + private final HttpsRequestBuilder builder; + + protected HttpsRequest(HttpsRequestBuilder builder) { + super(builder); + this.builder = builder; + } + + public static HttpsRequestBuilder builder() { + return new HttpsRequestBuilder(); + } + + public SSLSession getSSLSession() { + return builder.sslSession; + } + + public String getSNIHost() { + return builder.sniHost; + } +} diff --git a/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequestBuilder.java b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequestBuilder.java new file mode 100644 index 0000000..9dbbe5c --- /dev/null +++ b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/HttpsRequestBuilder.java @@ -0,0 +1,79 @@ +package org.xbib.net.http.server.simple.secure; + +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.simple.HttpRequestBuilder; +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; + +public class HttpsRequestBuilder extends HttpRequestBuilder { + + SSLSession sslSession; + + String sniHost; + + protected HttpsRequestBuilder() { + } + + @Override + public HttpsRequestBuilder setAddress(HttpAddress httpAddress) { + super.setAddress(httpAddress); + return this; + } + + @Override + public HttpsRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + super.setLocalAddress(localAddress); + return this; + } + + @Override + public HttpsRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + super.setRemoteAddress(remoteAddress); + return this; + } + + @Override + public HttpsRequestBuilder setBaseURL(URL baseURL) { + super.setBaseURL(baseURL); + return this; + } + + @Override + public HttpsRequestBuilder setBaseURL(HttpAddress httpAddress, String uri, String hostAndPort) { + super.setBaseURL(httpAddress, uri, hostAndPort); + return this; + } + + @Override + public HttpsRequestBuilder setSequenceId(Integer sequenceId) { + super.setSequenceId(sequenceId); + return this; + } + + @Override + public HttpsRequestBuilder setStreamId(Integer streamId) { + super.setStreamId(streamId); + return this; + } + + @Override + public HttpsRequestBuilder setRequestId(Long requestId) { + super.setRequestId(requestId); + return this; + } + + public HttpsRequestBuilder setSNIHost(String host) { + this.sniHost = host; + return this; + } + + public HttpsRequestBuilder setSSLSession(SSLSession sslSession) { + this.sslSession = sslSession; + return this; + } + + public HttpsRequest build() { + return new HttpsRequest(this); + } +} diff --git a/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServer.java b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServer.java new file mode 100644 index 0000000..dcf0c64 --- /dev/null +++ b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServer.java @@ -0,0 +1,60 @@ +package org.xbib.net.http.server.simple.secure; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.simple.HttpRequestBuilder; +import org.xbib.net.http.server.simple.SimpleHttpServer; +import org.xbib.net.http.server.simple.SimpleHttpServerBuilder; + +import javax.net.ServerSocketFactory; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +public class SimpleHttpsServer extends SimpleHttpServer { + + protected SimpleHttpsServer(SimpleHttpServerBuilder builder) { + super(builder); + } + + public static SimpleHttpsServerBuilder builder() { + return new SimpleHttpsServerBuilder(); + } + + @Override + protected HttpRequestBuilder createRequest(InputStream inputStream, + HttpAddress httpAddress, + InetSocketAddress localAddress, + InetSocketAddress remoteAddress) throws IOException { + String firstLine = readLine(inputStream); + HttpVersion httpVersion = extractVersion(firstLine); + String requestURI = firstLine.split("\\s+", 3)[1]; + HttpMethod httpMethod = extractMethod(firstLine); + HttpHeaders headers = extractHeaders(inputStream); + ByteBuffer byteBuffer = extractBody(inputStream, headers); + return HttpsRequest.builder() + .setBaseURL(httpAddress, + requestURI, + headers.get(HttpHeaderNames.HOST)) + .setLocalAddress(localAddress) + .setRemoteAddress(remoteAddress) + .setMethod(httpMethod) + .setVersion(httpVersion) + .setHeaders(headers) + .setBody(byteBuffer); + } + + @Override + protected ServerSocketFactory getServerSocketFactory(HttpAddress httpAddress) { + if (httpAddress instanceof HttpsAddress) { + HttpsAddress httpsAddress = (HttpsAddress) httpAddress; + return httpsAddress.getSslContext().getServerSocketFactory(); + } else { + return ServerSocketFactory.getDefault(); + } + } +} diff --git a/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerBuilder.java b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerBuilder.java new file mode 100644 index 0000000..479484a --- /dev/null +++ b/net-http-server-simple-secure/src/main/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerBuilder.java @@ -0,0 +1,15 @@ +package org.xbib.net.http.server.simple.secure; + +import org.xbib.net.http.server.simple.SimpleHttpServer; +import org.xbib.net.http.server.simple.SimpleHttpServerBuilder; + +public class SimpleHttpsServerBuilder extends SimpleHttpServerBuilder { + + protected SimpleHttpsServerBuilder() { + } + + @Override + public SimpleHttpsServer build() { + return new SimpleHttpsServer(this); + } +} diff --git a/net-http-server-simple-secure/src/test/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerTest.java b/net-http-server-simple-secure/src/test/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerTest.java new file mode 100644 index 0000000..ad71c4f --- /dev/null +++ b/net-http-server-simple-secure/src/test/java/org/xbib/net/http/server/simple/secure/SimpleHttpsServerTest.java @@ -0,0 +1,72 @@ +package org.xbib.net.http.server.simple.secure; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.HttpServerConfig; +import org.xbib.net.http.server.simple.SimpleHttpServer; + +import java.nio.charset.StandardCharsets; + +public class SimpleHttpsServerTest { + + private static final Logger logger = Logger.getLogger(SimpleHttpsServerTest.class.getName()); + + @Test + public void simpleSecureHttpsServerTest() throws Exception { + HttpsAddress httpsAddress = HttpsAddress.builder() + .setSecure(true) + .setHost("localhost") + .setPort(8443) + .setSelfCert("localhost") + .build(); + logger.log(Level.INFO, "SSL context = " + httpsAddress.getSslContext()); + HttpServerConfig serverConfig = new HttpServerConfig(); + serverConfig.setServerName("SimpleSecureHttpServer", SimpleHttpServer.class.getPackage().getImplementationVersion()); + serverConfig.setNetworkClass(NetworkClass.LOOPBACK); + serverConfig.setDebug(true); + try (SimpleHttpServer server = SimpleHttpsServer.builder() + .setHttpServerConfig(serverConfig) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpsAddress) + .addService(BaseHttpService.builder() + .setPath("/favicon.ico") + .setHandler(ctx -> ctx.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build() + .flush()) + .build()) + .addService(BaseHttpService.builder() + .setPath("/secure") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("secure domain: " + + " SNI host = " + ctx.httpRequest().as(HttpsRequest.class).getSNIHost() + + " SSL peer host = " + ctx.httpRequest().as(HttpsRequest.class).getSSLSession() + + " base URL = " + ctx.httpRequest().getBaseURL() + + " parameter = " + ctx.httpRequest().getParameter() + + " local address = " + ctx.httpRequest().getLocalAddress() + + " remote address = " + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build()) { + server.bind(); + } + } +} diff --git a/net-http-server-simple-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider b/net-http-server-simple-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider new file mode 100644 index 0000000..dfa32bc --- /dev/null +++ b/net-http-server-simple-secure/src/test/resources/META-INF/services/org.xbib.net.security.CertificateProvider @@ -0,0 +1 @@ +org.xbib.net.security.DefaultCertificateProvider diff --git a/net-http-server-simple-secure/src/test/resources/logging.properties b/net-http-server-simple-secure/src/test/resources/logging.properties new file mode 100644 index 0000000..d9913d4 --- /dev/null +++ b/net-http-server-simple-secure/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-server-simple/build.gradle b/net-http-server-simple/build.gradle new file mode 100644 index 0000000..44c25d3 --- /dev/null +++ b/net-http-server-simple/build.gradle @@ -0,0 +1,9 @@ +dependencies { + api project(':net-http-server') +} + +test { + systemProperty 'application.name', 'test' + systemProperty 'application.home', 'src/test/resources' + systemProperty 'application.profile', 'test' +} diff --git a/net-http-server-simple/src/main/java/module-info.java b/net-http-server-simple/src/main/java/module-info.java new file mode 100644 index 0000000..e03f0e6 --- /dev/null +++ b/net-http-server-simple/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module org.xbib.net.http.server.simple { + exports org.xbib.net.http.server.simple; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires java.logging; +} diff --git a/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequest.java b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequest.java new file mode 100644 index 0000000..09bdb45 --- /dev/null +++ b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequest.java @@ -0,0 +1,46 @@ +package org.xbib.net.http.server.simple; + +import org.xbib.net.Request; +import org.xbib.net.http.server.BaseHttpRequest; +import org.xbib.net.util.ByteBufferInputStream; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +public class HttpRequest extends BaseHttpRequest { + + private final HttpRequestBuilder builder; + + protected HttpRequest(HttpRequestBuilder builder) { + super(builder); + this.builder = builder; + } + + @Override + public InputStream getInputStream() { + return new ByteBufferInputStream(builder.getBody()); + } + + @Override + public ByteBuffer getBody() { + return builder.getBody(); + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return builder.getBodyAsChars(charset); + } + + @SuppressWarnings("unchecked") + @Override + public R as(Class cl) { + return (R) this; + } + + public static HttpRequestBuilder builder() { + return new HttpRequestBuilder(); + } + +} diff --git a/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequestBuilder.java b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequestBuilder.java new file mode 100644 index 0000000..2ab50d0 --- /dev/null +++ b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpRequestBuilder.java @@ -0,0 +1,100 @@ +package org.xbib.net.http.server.simple; + +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.BaseHttpRequestBuilder; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +public class HttpRequestBuilder extends BaseHttpRequestBuilder { + + protected HttpRequestBuilder() { + } + + @Override + public HttpRequestBuilder setVersion(HttpVersion httpVersion) { + super.setVersion(httpVersion); + return this; + } + + @Override + public HttpRequestBuilder setMethod(HttpMethod httpMethod) { + super.setMethod(httpMethod); + return this; + } + + @Override + public HttpRequestBuilder setRequestURI(String requestURI) { + super.setRequestURI(requestURI); + return this; + } + + @Override + public HttpRequestBuilder setHeaders(HttpHeaders httpHeaders) { + super.setHeaders(httpHeaders); + return this; + } + + @Override + public HttpRequestBuilder addHeader(String name, String value) { + super.addHeader(name, value); + return this; + } + + @Override + public HttpRequestBuilder setBody(ByteBuffer byteBuffer) { + super.setBody(byteBuffer); + return this; + } + + @Override + public HttpRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + super.setLocalAddress(localAddress); + return this; + } + + @Override + public HttpRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + super.setRemoteAddress(remoteAddress); + return this; + } + + @Override + public HttpRequestBuilder setBaseURL(HttpAddress httpAddress, String uri, String hostAndPort) { + super.setBaseURL(httpAddress, uri, hostAndPort); + return this; + } + + @Override + public HttpRequestBuilder setBaseURL(URL baseURL) { + super.setBaseURL(baseURL); + return this; + } + + @Override + public HttpRequestBuilder setSequenceId(Integer sequenceId) { + super.setSequenceId(sequenceId); + return this; + } + + @Override + public HttpRequestBuilder setStreamId(Integer streamId) { + super.setStreamId(streamId); + return this; + } + + @Override + public HttpRequestBuilder setRequestId(Long requestId) { + super.setRequestId(requestId); + return this; + } + + @Override + public HttpRequest build() { + return new HttpRequest(this); + } +} diff --git a/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponse.java b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponse.java new file mode 100644 index 0000000..0e1db3a --- /dev/null +++ b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponse.java @@ -0,0 +1,30 @@ +package org.xbib.net.http.server.simple; + +import org.xbib.net.http.server.BaseHttpResponse; + +import java.io.IOException; + +public class HttpResponse extends BaseHttpResponse { + + private final HttpResponseBuilder builder; + + protected HttpResponse(HttpResponseBuilder builder) { + super(builder); + this.builder = builder; + } + + @Override + public void close() throws IOException { + builder.internalClose(); + } + + @Override + public void flush() throws IOException { + builder.internalFlush(); + } + + public static HttpResponseBuilder builder() { + return new HttpResponseBuilder(); + } + +} diff --git a/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponseBuilder.java b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponseBuilder.java new file mode 100644 index 0000000..580bfe8 --- /dev/null +++ b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/HttpResponseBuilder.java @@ -0,0 +1,121 @@ +package org.xbib.net.http.server.simple; + +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.server.BaseHttpResponseBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class HttpResponseBuilder extends BaseHttpResponseBuilder { + + private static final Logger logger = Logger.getLogger(HttpResponseBuilder.class.getName()); + + protected OutputStream outputStream; + + HttpResponseBuilder() { + super(); + } + + public HttpResponseBuilder setOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + return this; + } + + @Override + public HttpResponse build() { + Objects.requireNonNull(outputStream); + try { + if (shouldFlush()) { + internalFlush(); + } + if (body != null) { + internalWrite(body); + } else if (charBuffer != null && charset != null) { + internalWrite(charBuffer, charset); + } else if (dataBuffer != null) { + internalWrite(dataBuffer); + } else if (fileChannel != null) { + internalWrite(fileChannel, bufferSize); + } else if (inputStream != null) { + internalWrite(inputStream, bufferSize); + } + if (shouldClose()) { + internalClose(); + } + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + return new HttpResponse(this); + } + + void internalFlush() throws IOException { + write(dataBufferFactory.allocateBuffer()); + } + + void internalClose() throws IOException { + outputStream.close(); + } + + void internalWrite(String string) throws IOException { + if (string == null) { + internalFlush(); + } else { + logger.log(Level.INFO, "internal write: " + string); + internalWrite(dataBufferFactory.wrap(StandardCharsets.UTF_8.encode(string))); + } + } + + void internalWrite(CharBuffer charBuffer, Charset charset) throws IOException { + if (charBuffer == null) { + internalFlush(); + } else { + Objects.requireNonNull(charset); + internalWrite(dataBufferFactory.wrap(charset.encode(charBuffer))); + } + } + + void internalWrite(DataBuffer dataBuffer) throws IOException { + Objects.requireNonNull(dataBuffer); + try (WritableByteChannel channel = Channels.newChannel(outputStream)) { + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); + long contentLength = byteBuffer.remaining(); + logger.log(Level.INFO, "length = " + contentLength); + super.buildHeaders(contentLength); + channel.write(US_ASCII.encode(super.wrapHeaders())); + while (byteBuffer.hasRemaining()) { + logger.log(Level.INFO, "channel write byte buffer"); + channel.write(byteBuffer); + } + } + } + + void internalWrite(InputStream inputStream, int bufferSize) throws IOException { + byte[] b = new byte[bufferSize]; + while (inputStream.available() > 0) { + int i = inputStream.read(b); + outputStream.write(b, 0, i); + } + } + + void internalWrite(FileChannel fileChannel, int bufferSize) throws IOException { + try (WritableByteChannel channel = Channels.newChannel(outputStream)) { + long contentLength = fileChannel.size(); + super.buildHeaders(contentLength); + channel.write(US_ASCII.encode(super.wrapHeaders())); + channel.write(fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, contentLength)); + } + } +} diff --git a/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServer.java b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServer.java new file mode 100644 index 0000000..176ef21 --- /dev/null +++ b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServer.java @@ -0,0 +1,252 @@ +package org.xbib.net.http.server.simple; + +import org.xbib.net.NetworkClass; +import org.xbib.net.NetworkUtils; +import org.xbib.net.SocketConfig; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.HttpServer; + +import javax.net.ServerSocketFactory; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.BindException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SimpleHttpServer implements HttpServer { + + private static final Logger logger = Logger.getLogger(SimpleHttpServer.class.getName()); + + private final SimpleHttpServerBuilder builder; + + private final ExecutorService workerPool; + + private final Map serverSockets; + + protected SimpleHttpServer(SimpleHttpServerBuilder builder) { + this.builder = builder; + this.workerPool = Executors.newCachedThreadPool(); + this.serverSockets = new HashMap<>(); + } + + public static SimpleHttpServerBuilder builder() { + return new SimpleHttpServerBuilder(); + } + + @Override + public void bind() throws BindException { + // bind only once per HttpAddress in all domains + for (HttpAddress httpAddress : getApplication().getAddresses()) { + logger.log(Level.INFO, () -> "trying to bind to " + httpAddress); + try { + InetSocketAddress inetSocketAddress = httpAddress.getInetSocketAddress(); + NetworkClass configuredNetworkClass = builder.httpServerConfig.getNetworkClass(); + NetworkClass detectedNetworkClass = NetworkUtils.getNetworkClass(inetSocketAddress.getAddress()); + if (!NetworkUtils.matchesNetwork(detectedNetworkClass, configuredNetworkClass)) { + throw new BindException("unable to bind to " + inetSocketAddress.getAddress() + " because network class " + + detectedNetworkClass + " is not allowed by configured network class " + configuredNetworkClass); + } + SocketConfig socketConfig = httpAddress.getSocketConfig(); + ServerSocket serverSocket = getServerSocketFactory(httpAddress) + .createServerSocket(httpAddress.getPort(), 0, httpAddress.getInetAddress()); + serverSocket.setPerformancePreferences(1, 2, 3); + serverSocket.setReuseAddress(socketConfig.isReuseAddr()); + if (!serverSocket.isBound()) { + serverSocket.bind(inetSocketAddress); + } + if (serverSocket.isBound()) { + serverSockets.put(httpAddress, serverSocket); + logger.log(Level.INFO, () -> "server socket = " + serverSocket + + " domains = " + getApplication().getAddresses() + " bound, listening on " + inetSocketAddress); + } else { + logger.log(Level.WARNING, "server socket " + serverSocket + " not bound, something is wrong"); + } + } catch (IOException e) { + throw new BindException(e.getMessage()); + } + } + if (serverSockets.isEmpty()) { + return; + } + ExecutorService service = Executors.newFixedThreadPool(serverSockets.size()); + for (Map.Entry entry : serverSockets.entrySet()) { + final HttpAddress httpAddress = entry.getKey(); + final ServerSocket serverSocket = entry.getValue(); + service.submit(() -> { + try { + while (!Thread.interrupted()) { + Socket socket = serverSocket.accept(); + SocketConfig socketConfig = httpAddress.getSocketConfig(); + socket.setKeepAlive(socketConfig.isKeepAlive()); + socket.setReuseAddress(socketConfig.isReuseAddr()); + socket.setTcpNoDelay(socketConfig.isTcpNodelay()); + workerPool.submit(() -> { + try { + InputStream inputStream = new BufferedInputStream(socket.getInputStream(), 4096); + OutputStream outputStream = socket.getOutputStream(); + HttpResponseBuilder httpResponseBuilder = createResponse(outputStream); + HttpRequestBuilder httpRequestBuilder = createRequest(inputStream, + httpAddress, + (InetSocketAddress) socket.getLocalSocketAddress(), + (InetSocketAddress) socket.getRemoteSocketAddress()); + getApplication().dispatch(httpRequestBuilder, httpResponseBuilder); + } catch (Throwable t) { + logger.log(Level.SEVERE, t.getMessage(), t); + } finally { + try { + if (!socket.isClosed()) { + socket.close(); + } + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + }); + } + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + }); + } + } + + @Override + public void loop() throws IOException { + CountDownLatch latch = new CountDownLatch(1); + try { + latch.await(); + } catch (InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public Application getApplication() { + return builder.application; + } + + @Override + public void close() throws IOException { + logger.log(Level.INFO, "closing"); + workerPool.shutdown(); + for (Map.Entry entry : serverSockets.entrySet()) { + entry.getValue().close(); + logger.log(Level.INFO, "socket " + entry.getValue() + " closed"); + } + } + + protected ServerSocketFactory getServerSocketFactory(HttpAddress httpAddress) { + return ServerSocketFactory.getDefault(); + } + + protected HttpRequestBuilder createRequest(InputStream inputStream, + HttpAddress httpAddress, + InetSocketAddress localAddress, + InetSocketAddress remoteAddress) throws IOException { + String firstLine = readLine(inputStream); + HttpVersion httpVersion = extractVersion(firstLine); + String requestURI = firstLine.split("\\s+", 3)[1]; + HttpMethod httpMethod = extractMethod(firstLine); + HttpHeaders headers = extractHeaders(inputStream); + ByteBuffer byteBuffer = extractBody(inputStream, headers); + return HttpRequest.builder() + .setBaseURL(httpAddress, + requestURI, + headers.get(HttpHeaderNames.HOST)) + .setLocalAddress(localAddress) + .setRemoteAddress(remoteAddress) + .setMethod(httpMethod) + .setVersion(httpVersion) + .setHeaders(headers) + .setBody(byteBuffer); + } + + protected HttpResponseBuilder createResponse(OutputStream outputStream) { + return HttpResponse.builder() + .setOutputStream(outputStream); + } + + protected static HttpVersion extractVersion(String headerLine) throws IllegalArgumentException { + Matcher m = Pattern.compile("HTTP/(\\d+)\\.(\\d+)").matcher(headerLine); + if (m.find()) { + if ((Integer.parseInt(m.group(1)) == 1) && (Integer.parseInt(m.group(2)) == 1)) { + return HttpVersion.HTTP_1_1; + } else if ((Integer.parseInt(m.group(1)) == 1) && (Integer.parseInt(m.group(2)) == 0)) { + return HttpVersion.HTTP_1_0; + } else { + throw new IllegalArgumentException("unknown HTTP version: " + headerLine); + } + } else { + throw new IllegalArgumentException("unknown HTTP version: " + headerLine); + } + } + + protected static HttpMethod extractMethod(String headerLine) throws IllegalArgumentException { + String method = headerLine.split("\\s+")[0]; + if (method != null) { + return HttpMethod.valueOf(method); + } else { + throw new IllegalArgumentException(); + } + } + + protected static HttpHeaders extractHeaders(InputStream inputStream) throws IOException { + HttpHeaders headers = new HttpHeaders(); + String nextLine; + while (!(nextLine = readLine(inputStream)).equals("")) { + String[] values = nextLine.split(":", 2); + headers.add(values[0].toLowerCase(Locale.ROOT), values[1].trim()); + } + return headers; + } + + protected static ByteBuffer extractBody(InputStream inputStream, HttpHeaders headers) throws IOException { + String contentLength = headers.get(HttpHeaderNames.CONTENT_LENGTH); + ByteBuffer byteBuffer = null; + if (contentLength != null) { + int size = Integer.parseInt(contentLength); + byte[] data = new byte[size]; + int n = inputStream.read(data, 0, size); + if (n == size) { + byteBuffer = ByteBuffer.wrap(data); + } + } + return byteBuffer; + } + + protected static String readLine(InputStream inputStream) throws IOException { + StringBuilder result = new StringBuilder(); + boolean crRead = false; + int n; + while ((n = inputStream.read()) != -1) { + if (n == '\r') { + crRead = true; + } else if (n == '\n' && crRead) { + return result.toString(); + } else { + result.append((char) n); + } + } + return result.toString(); + } + +} diff --git a/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServerBuilder.java b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServerBuilder.java new file mode 100644 index 0000000..0102fca --- /dev/null +++ b/net-http-server-simple/src/main/java/org/xbib/net/http/server/simple/SimpleHttpServerBuilder.java @@ -0,0 +1,28 @@ +package org.xbib.net.http.server.simple; + +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServerConfig; + +public class SimpleHttpServerBuilder { + + HttpServerConfig httpServerConfig; + + Application application; + + protected SimpleHttpServerBuilder() { + } + + public SimpleHttpServerBuilder setHttpServerConfig(HttpServerConfig httpServerConfig) { + this.httpServerConfig = httpServerConfig; + return this; + } + + public SimpleHttpServerBuilder setApplication(Application application) { + this.application = application; + return this; + } + + public SimpleHttpServer build() { + return new SimpleHttpServer(this); + } +} diff --git a/net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/HttpRouterTest.java b/net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/HttpRouterTest.java new file mode 100644 index 0000000..0153606 --- /dev/null +++ b/net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/HttpRouterTest.java @@ -0,0 +1,65 @@ +package org.xbib.net.http.server.simple.test; + +import org.junit.jupiter.api.Test; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.simple.HttpRequest; +import org.xbib.net.http.server.simple.HttpRequestBuilder; +import org.xbib.net.http.server.simple.HttpResponse; +import org.xbib.net.http.server.simple.HttpResponseBuilder; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HttpRouterTest { + + @Test + public void routerTest() throws Exception { + URL baseURL = URL.http().host("localhost").port(8008).build(); + HttpAddress httpAddress = HttpAddress.of(baseURL); + BaseHttpRouter router = BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress) + .addService(BaseHttpService.builder() + .setMethod(HttpMethod.DELETE) + .setPath("/demo") + .setHandler(ctx -> { + Logger.getAnonymousLogger().log(Level.INFO, "got request: " + ctx.request().getRequestURI()); + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write(ctx.request().getRequestURI()); + }) + .build()) + .build()) + .build(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + HttpResponseBuilder httpResponse = HttpResponse.builder() + .setOutputStream(outputStream); + HttpRequestBuilder httpRequest = HttpRequest.builder() + .setBaseURL(baseURL) + .setVersion(HttpVersion.HTTP_1_1) + .setMethod(HttpMethod.DELETE) + .setRequestURI("/demo") + .addHeader(HttpHeaderNames.HOST, httpAddress.hostAndPort()); + router.setApplication(BaseApplication.builder().build()); + router.route(httpRequest, httpResponse); + String string = outputStream.toString(StandardCharsets.UTF_8); + Logger.getAnonymousLogger().log(Level.INFO, "the response string is = " + string); + assertTrue(string.contains("/demo")); + } +} diff --git a/net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/SimpleHttpServerTest.java b/net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/SimpleHttpServerTest.java new file mode 100644 index 0000000..e3acdde --- /dev/null +++ b/net-http-server-simple/src/test/java/org/xbib/net/http/server/simple/test/SimpleHttpServerTest.java @@ -0,0 +1,76 @@ +package org.xbib.net.http.server.simple.test; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.NetworkClass; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.BaseApplication; +import org.xbib.net.http.server.BaseHttpDomain; +import org.xbib.net.http.server.route.BaseHttpRouter; +import org.xbib.net.http.server.BaseHttpService; +import org.xbib.net.http.server.HttpServerConfig; +import org.xbib.net.http.server.resource.FileResourceHandler; +import org.xbib.net.http.server.simple.SimpleHttpServer; + +import java.nio.charset.StandardCharsets; + +public class SimpleHttpServerTest { + + @Disabled + @Test + public void simpleServerTest() throws Exception { + HttpAddress httpAddress1 = HttpAddress.http1("localhost", 8008); + HttpAddress httpAddress2 = HttpAddress.http1("localhost", 8008); + SimpleHttpServer server = SimpleHttpServer.builder() + .setHttpServerConfig(new HttpServerConfig() + .setServerName("SimpleHttpServer", SimpleHttpServer.class.getPackage().getImplementationVendor()) + .setNetworkClass(NetworkClass.SITE) + ) + .setApplication(BaseApplication.builder() + .setRouter(BaseHttpRouter.builder() + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress1) + .addService(BaseHttpService.builder() + .setPath("/domain1") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain1 " + + ctx.httpRequest().getParameter() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .addService(BaseHttpService.builder() + .setPath("/file1/*") + .setHandler(new FileResourceHandler()) + .build()) + .build()) + .addDomain(BaseHttpDomain.builder() + .setHttpAddress(httpAddress2) + .addService(BaseHttpService.builder() + .setPath("/domain2") + .setHandler(ctx -> { + ctx.response() + .setResponseStatus(HttpResponseStatus.OK) + .setHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN) + .setCharset(StandardCharsets.UTF_8); + ctx.write("domain2 " + + ctx.httpRequest().getParameter() + " " + + ctx.httpRequest().getLocalAddress() + " " + + ctx.httpRequest().getRemoteAddress()); + }) + .build()) + .build()) + .build()) + .build()) + .build(); + server.bind(); + server.loop(); + } +} diff --git a/net-http-server-simple/src/test/resources/logging.properties b/net-http-server-simple/src/test/resources/logging.properties new file mode 100644 index 0000000..f8086a4 --- /dev/null +++ b/net-http-server-simple/src/test/resources/logging.properties @@ -0,0 +1,9 @@ +handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.level=ALL +java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.pattern=build/net.log +jdk.event.security.level=INFO diff --git a/net-http-server/build.gradle b/net-http-server/build.gradle new file mode 100644 index 0000000..5929d8a --- /dev/null +++ b/net-http-server/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':net-http') +} diff --git a/net-http-server/src/main/java/module-info.java b/net-http-server/src/main/java/module-info.java new file mode 100644 index 0000000..ccd3996 --- /dev/null +++ b/net-http-server/src/main/java/module-info.java @@ -0,0 +1,29 @@ +import org.xbib.net.http.server.ApplicationModule; + +module org.xbib.net.http.server { + uses ApplicationModule; + exports org.xbib.net.http.server; + exports org.xbib.net.http.server.auth; + exports org.xbib.net.http.server.cookie; + exports org.xbib.net.http.server.decorate; + exports org.xbib.net.http.server.handler; + exports org.xbib.net.http.server.ldap; + exports org.xbib.net.http.server.persist; + exports org.xbib.net.http.server.persist.file; + exports org.xbib.net.http.server.persist.memory; + exports org.xbib.net.http.server.render; + exports org.xbib.net.http.server.resource; + exports org.xbib.net.http.server.resource.negotiate; + exports org.xbib.net.http.server.route; + exports org.xbib.net.http.server.session; + exports org.xbib.net.http.server.session.file; + exports org.xbib.net.http.server.session.memory; + exports org.xbib.net.http.server.validate; + requires org.xbib.net; + requires org.xbib.net.mime; + requires org.xbib.net.http; + requires org.xbib.datastructures.common; + requires java.logging; + requires java.naming; + requires java.sql; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/Application.java b/net-http-server/src/main/java/org/xbib/net/http/server/Application.java new file mode 100644 index 0000000..9c8a4c1 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/Application.java @@ -0,0 +1,52 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.session.SessionListener; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.time.ZoneId; +import java.util.Collection; +import java.util.Locale; +import java.util.Set; + +public interface Application extends SessionListener, Resolver, Closeable { + + Collection getDomains(); + + Set getAddresses(); + + Locale getLocale(); + + ZoneId getZoneId(); + + Path getHome(); + + String getContextPath(); + + Collection getModules(); + + /** + * Dispatch a regular request. + * @param requestBuilder the request + * @param responseBuilder the response + */ + void dispatch(HttpRequestBuilder requestBuilder, + HttpResponseBuilder responseBuilder); + + void dispatch(HttpRequestBuilder requestBuilder, + HttpResponseBuilder responseBuilder, + HttpResponseStatus httpResponseStatus); + + HttpServerContext createContext(HttpDomain domain, + HttpRequestBuilder httpRequestBuilder, + HttpResponseBuilder httpResponseBuilder); + + void onOpen(HttpServerContext httpServerContext); + + void onClose(HttpServerContext httpServerContext); + + void close() throws IOException; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ApplicationBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/ApplicationBuilder.java new file mode 100644 index 0000000..2e4d64c --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ApplicationBuilder.java @@ -0,0 +1,32 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.server.route.HttpRouter; + +import java.nio.file.Path; +import java.time.ZoneId; +import java.util.Locale; + +public interface ApplicationBuilder { + + ApplicationBuilder setThreadCount(int blockingThreadCount); + + ApplicationBuilder setQueueCount(int blockingQueueCount); + + ApplicationBuilder setHome(Path home); + + ApplicationBuilder setContextPath(String contextPath); + + ApplicationBuilder setSecret(String hexSecret); + + ApplicationBuilder setSessionsEnabled(boolean sessionsEnabled); + + ApplicationBuilder setRouter(HttpRouter router); + + ApplicationBuilder setLocale(Locale locale); + + ApplicationBuilder setZoneId(ZoneId zoneId); + + ApplicationBuilder addModule(ApplicationModule module); + + Application build(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ApplicationModule.java b/net-http-server/src/main/java/org/xbib/net/http/server/ApplicationModule.java new file mode 100644 index 0000000..04322a1 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ApplicationModule.java @@ -0,0 +1,24 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.server.session.Session; + +public interface ApplicationModule extends Comparable { + + String getName(); + + void onOpen(Application application) throws Exception; + + void onOpen(Application application, HttpServerContext httpServerContext); + + void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService); + + void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService, HttpRequest httpRequest); + + void onClose(Application application, HttpServerContext httpServerContext); + + void onOpen(Application application, Session session); + + void onClose(Application application, Session session); + + void onClose(Application application); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java new file mode 100644 index 0000000..b79146a --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java @@ -0,0 +1,314 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.server.route.HttpRouter; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.cookie.IncomingCookieHandler; +import org.xbib.net.http.server.cookie.OutgoingCookieHandler; +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.http.server.session.memory.MemoryPropertiesSessionCodec; +import org.xbib.net.http.server.render.HttpResponseRenderer; +import org.xbib.net.http.server.session.IncomingSessionHandler; +import org.xbib.net.http.server.session.OutgoingSessionHandler; +import org.xbib.net.http.server.session.Session; +import org.xbib.net.http.server.util.BlockingThreadPoolExecutor; +import org.xbib.net.http.server.validate.HttpRequestValidator; +import org.xbib.net.util.NamedThreadFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.ZoneId; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BaseApplication implements Application { + + private static final Logger logger = Logger.getLogger(BaseApplication.class.getName()); + + protected BaseApplicationBuilder builder; + + private final BlockingThreadPoolExecutor executor; + + private final HttpRequestValidator httpRequestValidator; + + private final HttpHandler incomingCookieHandler; + + private final HttpHandler outgoingCookieHandler; + + private final HttpResponseRenderer httpResponseRenderer; + + private Codec sessionCodec; + + private HttpHandler incomingSessionHandler; + + private HttpHandler outgoingSessionHandler; + + protected BaseApplication(BaseApplicationBuilder builder) { + this.builder = builder; + this.executor = new BlockingThreadPoolExecutor(builder.blockingThreadCount, builder.blockingQueueCount, + new NamedThreadFactory("org-xbib-net-hhtp-server-application")); + this.executor.setRejectedExecutionHandler((runnable, threadPoolExecutor) -> + logger.log(Level.SEVERE, "rejected " + runnable + " for thread pool executor = " + threadPoolExecutor)); + this.httpRequestValidator = buildRequestValidator(); + this.incomingCookieHandler = buildIncomingCookieHandler(); + this.outgoingCookieHandler = buildOutgoingCookieHandler(); + this.httpResponseRenderer = buildResponseRenderer(); + } + + public static BaseApplicationBuilder builder() { + return new BaseApplicationBuilder(); + } + + @Override + public Locale getLocale() { + return builder.locale; + } + + @Override + public ZoneId getZoneId() { + return builder.zoneId; + } + + public Path getHome() { + return builder.home; + } + + public String getContextPath() { + return builder.contextPath; + } + + public HttpRouter getRouter() { + return builder.router; + } + + public String getSecret() { + return builder.secret; + } + + public Set getStaticFileSuffixes() { + return builder.staticFileSuffixes; + } + + @Override + public Collection getModules() { + return builder.applicationModuleList; + } + + @Override + public Collection getDomains() { + return getRouter().getDomains(); + } + + @Override + public Set getAddresses() { + return getRouter().getDomainsByAddress().keySet(); + } + + @Override + public void dispatch(HttpRequestBuilder requestBuilder, HttpResponseBuilder responseBuilder) { + Future future = executor.submit(() -> getRouter().route(requestBuilder, responseBuilder)); + logger.log(Level.FINE, "dispatching " + future); + } + + @Override + public void dispatch(HttpRequestBuilder httpRequestBuilder, + HttpResponseBuilder httpResponseBuilder, + HttpResponseStatus httpResponseStatus) { + Future future = executor.submit(() -> { + HttpServerContext httpServerContext = createContext(null, httpRequestBuilder, httpResponseBuilder); + httpServerContext.attributes().put("responsebuilder", httpResponseBuilder); + getRouter().routeStatus(httpResponseStatus, httpServerContext); + }); + logger.log(Level.FINE, "dispatching status " + future); + } + + @Override + public HttpServerContext createContext(HttpDomain domain, + HttpRequestBuilder requestBuilder, + HttpResponseBuilder responseBuilder) { + HttpServerContext httpServerContext = new BaseHttpServerContext(this, domain, requestBuilder, responseBuilder); + httpServerContext.attributes().put("requestbuilder", requestBuilder); + httpServerContext.attributes().put("responsebuilder", responseBuilder); + this.sessionCodec = buildSessionCodec(httpServerContext); + if (sessionCodec != null) { + httpServerContext.attributes().put("sessioncodec", sessionCodec); + } + this.incomingSessionHandler = buildIncomingSessionHandler(httpServerContext); + this.outgoingSessionHandler = buildOutgoingSessionHandler(httpServerContext); + return httpServerContext; + } + + protected HttpRequestValidator buildRequestValidator() { + return new HttpRequestValidator(); + } + + protected HttpHandler buildIncomingCookieHandler() { + return new IncomingCookieHandler(); + } + + protected HttpHandler buildOutgoingCookieHandler() { + return new OutgoingCookieHandler(); + } + + protected Codec buildSessionCodec(HttpServerContext httpServerContext) { + return new MemoryPropertiesSessionCodec(this, 1024, Duration.ofDays(1)); + } + + protected HttpHandler buildIncomingSessionHandler(HttpServerContext httpServerContext) { + @SuppressWarnings("unchecked") + Codec sessionCodec = httpServerContext.attributes().get(Codec.class, "sessioncodec"); + return new IncomingSessionHandler( + getSecret(), + "HmacSHA1", + "SESS", + sessionCodec, + getStaticFileSuffixes(), + "user_id", + "e_user_id"); + } + + protected HttpHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) { + @SuppressWarnings("unchecked") + Codec sessionCodec = httpServerContext.attributes().get(Codec.class, "sessioncodec"); + return new OutgoingSessionHandler( + getSecret(), + "HmacSHA1", + "SESS", + Duration.ofDays(1), + sessionCodec, + getStaticFileSuffixes(), + "user_id", + "e_user_id"); + } + + protected HttpResponseRenderer buildResponseRenderer() { + return new HttpResponseRenderer(); + } + + @Override + public void onCreated(Session session) { + logger.log(Level.INFO, "session created = " + session); + builder.applicationModuleList.forEach(module -> module.onOpen(this, session)); + } + + @Override + public void onDestroy(Session session) { + logger.log(Level.INFO, "session destroyed = " + session); + builder.applicationModuleList.forEach(module -> module.onClose(this, session)); + } + + public void onOpen(HttpServerContext httpServerContext) { + try { + if (httpRequestValidator != null) { + httpRequestValidator.handle(httpServerContext); + } + if (incomingCookieHandler != null) { + incomingCookieHandler.handle(httpServerContext); + } + if (builder.sessionsEnabled && incomingSessionHandler != null) { + incomingSessionHandler.handle(httpServerContext); + } + // call modules after request/cookie/session setup + builder.applicationModuleList.forEach(module -> module.onOpen(this, httpServerContext)); + } catch (HttpException e) { + getRouter().routeException(e); + httpServerContext.fail(); + } catch (Throwable t) { + getRouter().routeToErrorHandler(httpServerContext, t); + httpServerContext.fail(); + } + } + + public void onClose(HttpServerContext httpServerContext) { + try { + // call modules before session/cookie setdown + builder.applicationModuleList.forEach(module -> module.onClose(this, httpServerContext)); + if (builder.sessionsEnabled && outgoingSessionHandler != null) { + outgoingSessionHandler.handle(httpServerContext); + } + if (outgoingCookieHandler != null) { + outgoingCookieHandler.handle(httpServerContext); + } + } catch (HttpException e) { + getRouter().routeException(e); + } catch (Throwable t) { + getRouter().routeToErrorHandler(httpServerContext, t); + } finally { + try { + if (httpResponseRenderer != null) { + httpResponseRenderer.handle(httpServerContext); + } + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + } + + @Override + public Path resolve(String string) { + if (string == null) { + return builder.home; + } + try { + Path p = builder.home.resolve(string); + if (Files.exists(p) && Files.isReadable(p)) { + return p; + } else { + logger.log(Level.WARNING, "unable to find path: " + p + " on home " + builder.home); + } + } catch (Exception e) { + // ignore + } + throw new IllegalArgumentException("unable to resolve '" + string + "' on home '" + builder.home + "'"); + } + + @Override + public void close() throws IOException { + logger.log(Level.INFO, "application closing down"); + // stop dispatching and stop dispatched requests + executor.shutdown(); + try { + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + List list = executor.shutdownNow(); + logger.log(Level.WARNING, "unable to stop runnables " + list); + } + } catch (InterruptedException e) { + List list = executor.shutdownNow(); + logger.log(Level.WARNING, "unable to stop runnables " + list); + } + builder.applicationModuleList.forEach(module -> { + logger.log(Level.FINE, "application closing module " + module); + module.onClose(this); + }); + if (outgoingSessionHandler != null && (outgoingSessionHandler instanceof Closeable)) { + logger.log(Level.FINE, "application closing outgoing session handler"); + ((Closeable) outgoingSessionHandler).close(); + } + if (incomingSessionHandler != null && (incomingSessionHandler instanceof Closeable)) { + logger.log(Level.FINE, "application closing incming session handler"); + ((Closeable) incomingSessionHandler).close(); + } + if (sessionCodec != null && sessionCodec instanceof Closeable) { + logger.log(Level.FINE, "application closing session codec"); + ((Closeable) sessionCodec).close(); + } + if (outgoingCookieHandler != null && (outgoingCookieHandler instanceof Closeable)) { + logger.log(Level.FINE, "application closing outgoing cookie handler"); + ((Closeable) outgoingCookieHandler).close(); + } + if (incomingCookieHandler != null && (incomingCookieHandler instanceof Closeable)) { + logger.log(Level.FINE, "application closing incoming cookie handler"); + ((Closeable) incomingCookieHandler).close(); + } + logger.log(Level.INFO, "application closed"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationBuilder.java new file mode 100644 index 0000000..8654ba4 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationBuilder.java @@ -0,0 +1,166 @@ +package org.xbib.net.http.server; + +import org.xbib.datastructures.common.ImmutableSet; +import org.xbib.net.http.server.route.HttpRouter; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BaseApplicationBuilder implements ApplicationBuilder { + + private static final Logger logger = Logger.getLogger(BaseApplicationBuilder.class.getName()); + + protected int blockingThreadCount; + + protected int blockingQueueCount; + + protected Path home; + + protected String contextPath; + + protected String secret; + + protected boolean sessionsEnabled; + + protected HttpRouter router; + + protected Locale locale; + + protected ZoneId zoneId; + + protected List applicationModuleList; + + protected Set staticFileSuffixes; + + protected BaseApplicationBuilder() { + this.blockingThreadCount = Runtime.getRuntime().availableProcessors(); + this.blockingQueueCount = Integer.MAX_VALUE; + this.home = Paths.get(System.getProperties().containsKey("application.home") ? System.getProperty("application.home") : "."); + this.contextPath = "/"; + this.secret = "secret"; + this.sessionsEnabled = true; + this.locale = Locale.getDefault(); + this.zoneId = ZoneId.systemDefault(); + this.applicationModuleList = new ArrayList<>(); + } + + @Override + public BaseApplicationBuilder setThreadCount(int blockingThreadCount) { + this.blockingThreadCount = blockingThreadCount; + return this; + } + + @Override + public BaseApplicationBuilder setQueueCount(int blockingQueueCount) { + this.blockingQueueCount = blockingQueueCount; + return this; + } + + @Override + public BaseApplicationBuilder setHome(Path home) { + this.home = home; + return this; + } + + @Override + public BaseApplicationBuilder setContextPath(String contextPath) { + this.contextPath = contextPath; + return this; + } + + @Override + public BaseApplicationBuilder setSecret(String secret) { + this.secret = secret; + return this; + } + + @Override + public BaseApplicationBuilder setSessionsEnabled(boolean sessionsEnabled) { + this.sessionsEnabled = sessionsEnabled; + return this; + } + + @Override + public BaseApplicationBuilder setRouter(HttpRouter router) { + this.router = router; + return this; + } + + @Override + public ApplicationBuilder setLocale(Locale locale) { + this.locale = locale; + return this; + } + + @Override + public ApplicationBuilder setZoneId(ZoneId zoneId) { + this.zoneId = zoneId; + return this; + } + + @Override + public ApplicationBuilder addModule(ApplicationModule module) { + if (module != null) { + applicationModuleList.add(module); + logger.log(Level.FINE, "module " + module + " added"); + } + return this; + } + + public ApplicationBuilder addStaticSuffixes(String... suffixes) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (String suffix : suffixes) { + builder.add(suffix); + } + this.staticFileSuffixes = builder.build(new String[]{}); + return this; + } + + @Override + public Application build() { + Application application = new BaseApplication(this); + setupApplication(application); + return application; + } + + protected void setupApplication(Application application) { + ServiceLoader serviceLoader = ServiceLoader.load(ApplicationModule.class); + for (ApplicationModule module : serviceLoader) { + applicationModuleList.add(module); + logger.log(Level.FINE, "module " + module + " added"); + } + applicationModuleList.forEach(module -> { + try { + module.onOpen(application); + logger.log(Level.FINE, "module " + module + " opened"); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (router != null) { + router.setApplication(application); + } + if (staticFileSuffixes == null) { + staticFileSuffixes = DEFAULT_SUFFIXES; + } + } + + private static final Set DEFAULT_SUFFIXES = ImmutableSet.builder() + .add("css") + .add("js") + .add("ico") + .add("png") + .add("jpg") + .add("jpeg") + .add("gif") + .add("woff2") + .build(new String[]{}); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationModule.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationModule.java new file mode 100644 index 0000000..4149035 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplicationModule.java @@ -0,0 +1,46 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.server.session.Session; + +public abstract class BaseApplicationModule implements ApplicationModule { + + public BaseApplicationModule() { + } + + @Override + public void onOpen(Application application) throws Exception { + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext) { + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService) { + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService, HttpRequest httpRequest) { + } + + @Override + public void onClose(Application application, HttpServerContext httpServerContext) { + } + + @Override + public void onOpen(Application application, Session session) { + } + + @Override + public void onClose(Application application, Session session) { + } + + @Override + public void onClose(Application application) { + } + + @Override + public int compareTo(ApplicationModule o) { + return getName().compareTo(o.getName()); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseAttributes.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseAttributes.java new file mode 100644 index 0000000..80be175 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseAttributes.java @@ -0,0 +1,24 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Attributes; + +import java.util.LinkedHashMap; + +@SuppressWarnings("serial") +public class BaseAttributes extends LinkedHashMap implements Attributes { + + public BaseAttributes() { + super(); + } + + @SuppressWarnings("unchecked") + @Override + public T get(Class cl, String key) { + return (T) super.get(key); + } + + @SuppressWarnings("unchecked") + public T get(Class cl, String key, T defaultValue) { + return containsKey (key) ? (T) super.get(key) : defaultValue; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomain.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomain.java new file mode 100644 index 0000000..f0cf1d6 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomain.java @@ -0,0 +1,68 @@ +package org.xbib.net.http.server; + +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import org.xbib.net.http.HttpAddress; + +public class BaseHttpDomain implements HttpDomain { + + private static final HttpAddress DEFAULT_ADDRESS = HttpAddress.http1("localhost", 8008); + + private final BaseHttpDomainBuilder builder; + + BaseHttpDomain(BaseHttpDomainBuilder builder) { + this.builder = builder; + } + + public static HttpAddress getDefaultAddress() { + return DEFAULT_ADDRESS; + } + + public static BaseHttpDomainBuilder builder() { + return new BaseHttpDomainBuilder(); + } + + @Override + public Set getNames() { + return builder.names; + } + + @Override + public HttpAddress getAddress() { + return builder.httpAddress; + } + + @Override + public Collection getServices() { + return builder.httpServices; + } + + @Override + public String toString() { + return builder.names + " -> " + builder.httpAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BaseHttpDomain domain = (BaseHttpDomain) o; + return Objects.equals(builder.names, domain.builder.names) && + Objects.equals(builder.httpAddress, domain.builder.httpAddress); + } + + @Override + public int hashCode() { + return Objects.hash(builder.names, builder.httpAddress); + } + + @Override + public int compareTo(HttpDomain o) { + return toString().compareTo(o.toString()); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomainBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomainBuilder.java new file mode 100644 index 0000000..5ed34b3 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpDomainBuilder.java @@ -0,0 +1,51 @@ +package org.xbib.net.http.server; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import org.xbib.net.http.HttpAddress; + +public class BaseHttpDomainBuilder implements HttpDomainBuilder { + + protected final Set names; + + protected HttpAddress httpAddress; + + protected final Collection httpServices; + + BaseHttpDomainBuilder() { + this.names = new LinkedHashSet<>(); + this.httpAddress = BaseHttpDomain.getDefaultAddress(); + this.httpServices = new ArrayList<>(); + } + + @Override + public BaseHttpDomainBuilder addName(String name) { + this.names.add(name); + return this; + } + + @Override + public BaseHttpDomainBuilder setHttpAddress(HttpAddress httpAddress) throws IOException { + this.httpAddress = httpAddress; + names.add(httpAddress.hostAddressAndPort()); + names.add(httpAddress.canonicalHostAndPort()); + return this; + } + + @Override + public BaseHttpDomainBuilder addService(HttpService httpService) { + Objects.requireNonNull(httpService); + this.httpServices.add(httpService); + return this; + } + + @Override + public BaseHttpDomain build() { + Objects.requireNonNull(httpAddress); + return new BaseHttpDomain(this); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequest.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequest.java new file mode 100644 index 0000000..27b7ba4 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequest.java @@ -0,0 +1,97 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Attributes; +import org.xbib.net.Parameter; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +import java.net.InetSocketAddress; + +public abstract class BaseHttpRequest implements HttpRequest { + + protected final BaseHttpRequestBuilder builder; + + private final Attributes attributes; + + protected BaseHttpRequest(BaseHttpRequestBuilder builder) { + this.builder = builder; + this.attributes = new BaseAttributes(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return builder.localAddress; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return builder.remoteAddress; + } + + @Override + public URL getBaseURL() { + return builder.baseURL; + } + + @Override + public URL getServerURL() { + return builder.serverURL; + } + + @Override + public HttpVersion getVersion() { + return builder.getVersion(); + } + + @Override + public HttpMethod getMethod() { + return builder.getMethod(); + } + + @Override + public HttpHeaders getHeaders() { + return builder.getHeaders(); + } + + @Override + public String getRequestURI() { + return builder.getRequestURI(); + } + + @Override + public String getRequestPath() { + return builder.requestPath; + } + + @Override + public Parameter getParameter() { + return builder.parameter; + } + + @Override + public Integer getSequenceId() { + return builder.sequenceId; + } + + @Override + public Integer getStreamId() { + return builder.streamId; + } + + @Override + public Long getRequestId() { + return builder.requestId; + } + + @Override + public HttpServerContext getContext() { + return builder.httpServerContext; + } + + @Override + public Attributes attributes() { + return attributes; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequestBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequestBuilder.java new file mode 100644 index 0000000..a2d71bf --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpRequestBuilder.java @@ -0,0 +1,227 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Parameter; +import org.xbib.net.URL; +import org.xbib.net.URLBuilder; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Objects; + +public abstract class BaseHttpRequestBuilder implements HttpRequestBuilder { + + HttpServerContext httpServerContext; + + HttpAddress httpAddress; + + InetSocketAddress localAddress; + + InetSocketAddress remoteAddress; + + URL serverURL; + + URL baseURL; + + String requestPath; + + Parameter parameter; + + Integer sequenceId; + + Integer streamId; + + Long requestId; + + HttpVersion httpVersion; + + HttpMethod httpMethod; + + HttpHeaders httpHeaders; + + String requestURI; + + ByteBuffer byteBuffer; + + protected BaseHttpRequestBuilder() { + this.httpHeaders = new HttpHeaders(); + } + + @Override + public BaseHttpRequestBuilder setContext(HttpServerContext httpServerContext) { + this.httpServerContext = httpServerContext; + return this; + } + + @Override + public BaseHttpRequestBuilder setVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public HttpVersion getVersion() { + return httpVersion; + } + + @Override + public BaseHttpRequestBuilder setMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + public HttpMethod getMethod() { + return httpMethod; + } + + @Override + public BaseHttpRequestBuilder setRequestURI(String requestURI) { + this.requestURI = requestURI; + return this; + } + + @Override + public String getRequestURI() { + return requestURI; + } + + @Override + public BaseHttpRequestBuilder setHeaders(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } + + @Override + public BaseHttpRequestBuilder addHeader(String key, String value) { + this.httpHeaders.add(key, value); + return this; + } + + public BaseHttpRequestBuilder setBody(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + return this; + } + + public ByteBuffer getBody() { + return byteBuffer; + } + + @Override + public BaseHttpRequestBuilder setBaseURL(URL baseURL) { + this.baseURL = baseURL; + return this; + } + + public BaseHttpRequestBuilder setBaseURL(HttpAddress httpAddress, String uri, String hostAndPort) { + Objects.requireNonNull(httpAddress); + Objects.requireNonNull(uri); + String scheme = httpAddress.isSecure() ? "https" : "http"; + setAddress(httpAddress); + setRequestURI(uri); + String host = stripPort(hostAndPort); + String extractedPort = extractPort(hostAndPort); + Integer port = extractedPort != null ? Integer.parseInt(extractedPort) : httpAddress.getPort(); + this.serverURL = URL.builder() + .scheme(scheme) + .host(host) + .port(port) + .build(); + URLBuilder builder = URL.from(uri).mutator(); + URL url = builder.build(); + if (!url.isAbsolute()) { + this.baseURL = builder + .scheme(scheme) + .host(host) + .port(port) + .build(); + } else { + this.baseURL = url; + } + if (!httpAddress.base().getScheme().equals(url.getScheme())) { + throw new IllegalArgumentException("scheme mismatch in request: " + httpAddress.base().getScheme() + " != " + url.getScheme()); + } + if (url.getPort() != null && !httpAddress.getPort().equals(url.getPort())) { + throw new IllegalArgumentException("port mismatch in request: " + httpAddress.getPort() + " != " + url.getPort()); + } + return this; + } + + public URL getBaseURL() { + return baseURL; + } + + public BaseHttpRequestBuilder setRequestPath(String requestPath) { + this.requestPath = requestPath; + return this; + } + + public String getRequestPath() { + return requestPath; + } + + @Override + public CharBuffer getBodyAsChars(Charset charset) { + return byteBuffer != null ? charset.decode(byteBuffer) : null; + } + + public BaseHttpRequestBuilder setAddress(HttpAddress httpAddress) { + this.httpAddress = httpAddress; + return this; + } + + public BaseHttpRequestBuilder setLocalAddress(InetSocketAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + public BaseHttpRequestBuilder setRemoteAddress(InetSocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + + public BaseHttpRequestBuilder setSequenceId(Integer sequenceId) { + this.sequenceId = sequenceId; + return this; + } + + public BaseHttpRequestBuilder setStreamId(Integer streamId) { + this.streamId = streamId; + return this; + } + + public BaseHttpRequestBuilder setRequestId(Long requestId) { + this.requestId = requestId; + return this; + } + + public BaseHttpRequestBuilder setParameter(Parameter parameter) { + this.parameter = parameter; + return this; + } + + private static String stripPort(String hostMaybePort) { + if (hostMaybePort == null) { + return null; + } + int i = hostMaybePort.lastIndexOf(':'); + return i >= 0 ? hostMaybePort.substring(0, i) : hostMaybePort; + } + + private static String extractPort(String hostMaybePort) { + if (hostMaybePort == null) { + return null; + } + int i = hostMaybePort.lastIndexOf(':'); + return i >= 0 ? hostMaybePort.substring(i + 1) : null; + } + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponse.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponse.java new file mode 100644 index 0000000..49bd46f --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponse.java @@ -0,0 +1,10 @@ +package org.xbib.net.http.server; + +public abstract class BaseHttpResponse implements HttpResponse { + + protected final BaseHttpResponseBuilder builder; + + protected BaseHttpResponse(BaseHttpResponseBuilder builder) { + this.builder = builder; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponseBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponseBuilder.java new file mode 100644 index 0000000..35f0810 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpResponseBuilder.java @@ -0,0 +1,301 @@ +package org.xbib.net.http.server; + +import org.xbib.datastructures.common.Pair; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.buffer.DataBufferFactory; +import org.xbib.net.buffer.DefaultDataBufferFactory; +import org.xbib.net.http.server.cookie.CookieEncoder; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.cookie.Cookie; + +import java.io.InputStream; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class BaseHttpResponseBuilder implements HttpResponseBuilder { + + private static final Logger logger = Logger.getLogger(BaseHttpResponseBuilder.class.getName()); + + private static final String SPACE = " "; + + private static final String COLON = ":"; + + private static final String CRLF = "\r\n"; + + protected DataBufferFactory dataBufferFactory; + + protected HttpHeaders headers; + + protected HttpHeaders trailingHeaders; + + protected HttpVersion version; + + protected HttpResponseStatus status; + + protected HttpServerConfig httpServerConfig; + + protected boolean shouldClose; + + protected boolean shouldFlush; + + protected Integer sequenceId; + + protected Integer streamId; + + protected Long responseId; + + protected String contentType; + + protected Charset charset; + + protected String body; + + protected CharBuffer charBuffer; + + protected DataBuffer dataBuffer; + + protected InputStream inputStream; + + protected FileChannel fileChannel; + + protected int bufferSize; + + protected BaseHttpResponseBuilder() { + reset(); + } + + public void reset() { + this.version = HttpVersion.HTTP_1_1; + this.status = null; + this.headers = new HttpHeaders(); + this.trailingHeaders = new HttpHeaders(); + this.contentType = HttpHeaderValues.APPLICATION_OCTET_STREAM; + this.dataBufferFactory = DefaultDataBufferFactory.getInstance(); + this.shouldClose = false; // tell client we want to keep the connection alive + } + + @Override + public BaseHttpResponseBuilder setDataBufferFactory(DataBufferFactory dataBufferFactory) { + this.dataBufferFactory = dataBufferFactory; + return this; + } + + @Override + public DataBufferFactory getDataBufferFactory() { + return dataBufferFactory; + } + + @Override + public BaseHttpResponseBuilder setVersion(HttpVersion version) { + this.version = version; + return this; + } + + @Override + public BaseHttpResponseBuilder setResponseStatus(HttpResponseStatus status) { + if (this.status != null) { + logger.log(Level.WARNING, "status rejected: " + status + " status is already " + this.status); + return this; + } + this.status = status; + return this; + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public BaseHttpResponseBuilder setHeader(CharSequence name, String value) { + if (HttpHeaderNames.CONTENT_TYPE.equalsIgnoreCase(name.toString())) { + setContentType(value); + } else { + if (!headers.containsHeader(name)) { + headers.set(name, value); + } else { + logger.log(Level.WARNING, "header rejected: " + name + " = " + value); + } + } + return this; + } + + @Override + public BaseHttpResponseBuilder addHeader(CharSequence name, String value) { + if (headers.containsHeader(name)) { + logger.log(Level.WARNING, "duplicate header: " + name + " old value = " + headers.get(name) + " new value = " + value); + } + headers.add(name, value); + return this; + } + + @Override + public BaseHttpResponseBuilder setTrailingHeader(CharSequence name, String value) { + trailingHeaders.set(name, value); + return this; + } + + @Override + public BaseHttpResponseBuilder setContentType(String contentType) { + this.contentType = contentType; + return this; + } + + @Override + public BaseHttpResponseBuilder setCharset(Charset charset) { + this.charset = charset; + return this; + } + + + @Override + public BaseHttpResponseBuilder shouldFlush(boolean shouldFlush) { + this.shouldFlush = shouldFlush; + return this; + } + + @Override + public boolean shouldFlush() { + return shouldFlush; + } + + @Override + public BaseHttpResponseBuilder shouldClose(boolean shouldClose) { + this.shouldClose = shouldClose; + return this; + } + + @Override + public boolean shouldClose() { + return shouldClose; + } + + @Override + public BaseHttpResponseBuilder setSequenceId(Integer sequenceId) { + this.sequenceId = sequenceId; + return this; + } + + @Override + public BaseHttpResponseBuilder setStreamId(Integer streamId) { + this.streamId = streamId; + return this; + } + + @Override + public BaseHttpResponseBuilder setResponseId(Long responseId) { + this.responseId = responseId; + return this; + } + + @Override + public BaseHttpResponseBuilder write(String body) { + if (body != null && this.body == null) { + this.body = body; + } else { + logger.log(Level.WARNING, "cannot write null string"); + } + return this; + } + + @Override + public BaseHttpResponseBuilder write(CharBuffer charBuffer, Charset charset) { + if (charBuffer != null && this.charBuffer == null) { + this.charBuffer = charBuffer; + this.charset = charset; + } else { + logger.log(Level.WARNING, "cannot write CharBuffer"); + } + return this; + } + + @Override + public BaseHttpResponseBuilder write(DataBuffer dataBuffer) { + if (dataBuffer != null && this.dataBuffer == null) { + this.dataBuffer = dataBuffer; + } else { + logger.log(Level.WARNING, "cannot write DataBuffer"); + } + return this; + } + + @Override + public BaseHttpResponseBuilder write(InputStream inputStream, int bufferSize) { + if (inputStream != null && this.inputStream == null) { + this.inputStream = inputStream; + this.bufferSize = bufferSize; + } else { + logger.log(Level.WARNING, "cannot write InputStream"); + } + return this; + } + + @Override + public BaseHttpResponseBuilder write(FileChannel fileChannel, int bufferSize) { + if (fileChannel != null && this.fileChannel == null) { + this.fileChannel = fileChannel; + this.bufferSize = bufferSize; + } else { + logger.log(Level.WARNING, "cannot write FileChannel"); + } + return this; + } + + @Override + public BaseHttpResponseBuilder addCookie(Cookie cookie) { + Objects.requireNonNull(cookie); + headers.add(HttpHeaderNames.SET_COOKIE, CookieEncoder.STRICT.encode(cookie)); + return this; + } + + @Override + public abstract HttpResponse build(); + + public void buildHeaders(long contentLength) { + if (!headers.containsHeader(HttpHeaderNames.CONTENT_TYPE)) { + if (contentType == null) { + contentType = HttpHeaderValues.APPLICATION_OCTET_STREAM; + } + if (!contentType.contains("charset=") && charset != null) { + contentType = contentType + "; charset=" + charset.name(); + } + headers.add(HttpHeaderNames.CONTENT_TYPE, contentType); + } + if (status.code() >= 200 && status.code() != 204) { + if (!headers.containsHeader(HttpHeaderNames.CONTENT_LENGTH)) { + headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(contentLength)); + } + } + if (shouldClose) { + headers.add(HttpHeaderNames.CONNECTION, "close"); + } + if (!headers.containsHeader(HttpHeaderNames.DATE)) { + headers.add(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); + } + if (httpServerConfig != null && httpServerConfig.getServerName() != null) { + headers.add(HttpHeaderNames.SERVER, httpServerConfig.getServerName()); + } + } + + public CharBuffer wrapHeaders() { + StringBuilder sb = new StringBuilder(); + sb.append(version.text()).append(SPACE).append(status.code()).append(SPACE).append(status.reasonPhrase()).append(CRLF); + for (Pair e : headers.entries()) { + sb.append(e.getKey().toLowerCase(Locale.ROOT)).append(COLON).append(SPACE).append(e.getValue()).append(CRLF); + } + sb.append(CRLF); + return CharBuffer.wrap(sb); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomain.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomain.java new file mode 100644 index 0000000..cf81ca8 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomain.java @@ -0,0 +1,26 @@ +package org.xbib.net.http.server; + +import java.util.List; +import org.xbib.net.SecurityRealm; + +public class BaseHttpSecurityDomain implements HttpSecurityDomain { + + private final BaseHttpSecurityDomainBuilder builder; + + BaseHttpSecurityDomain(BaseHttpSecurityDomainBuilder builder) { + this.builder = builder; + } + + public static BaseHttpSecurityDomainBuilder builder() { + return new BaseHttpSecurityDomainBuilder(); + } + + @Override + public SecurityRealm getRealm() { + return builder.securityRealm; + } + + public List getHandlers() { + return builder.handlers; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomainBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomainBuilder.java new file mode 100644 index 0000000..aa0a515 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpSecurityDomainBuilder.java @@ -0,0 +1,30 @@ +package org.xbib.net.http.server; + +import java.util.Arrays; +import java.util.List; +import org.xbib.net.SecurityRealm; + +public class BaseHttpSecurityDomainBuilder { + + SecurityRealm securityRealm; + + List handlers; + + public BaseHttpSecurityDomainBuilder() { + this.handlers = List.of(); + } + + public BaseHttpSecurityDomainBuilder setSecurityRealm(SecurityRealm securityRealm) { + this.securityRealm = securityRealm; + return this; + } + + public BaseHttpSecurityDomainBuilder setHandlers(HttpHandler... handlers) { + this.handlers = Arrays.asList(handlers); + return this; + } + + public BaseHttpSecurityDomain build() { + return new BaseHttpSecurityDomain(this); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java new file mode 100644 index 0000000..ad6caaf --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java @@ -0,0 +1,272 @@ +package org.xbib.net.http.server; + +import java.util.Map; +import org.xbib.net.Attributes; +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.URL; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.HttpHeaderValues; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.cookie.CookieBox; +import org.xbib.net.http.server.route.HttpRouteResolver; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.nio.file.Path; +import java.util.Objects; + +import static org.xbib.net.http.HttpHeaderNames.CONTENT_TYPE; + +public class BaseHttpServerContext implements HttpServerContext { + + private static final String PATH_SEPARATOR = "/"; + + private final Application application; + + private final HttpRequestBuilder httpRequestBuilder; + + private final HttpResponseBuilder httpResponseBuilder; + + private HttpRouteResolver.Result pathResolverResult; + + private String contextPath; + + private URL contextURL; + + private Attributes attributes; + + private HttpRequest httpRequest; + + private boolean done; + + private boolean failed; + + private boolean next; + + public BaseHttpServerContext(Application application, + HttpDomain domain, + HttpRequestBuilder httpRequestBuilder, + HttpResponseBuilder httpResponseBuilder) { + this.application = application; + this.httpRequestBuilder = httpRequestBuilder; + this.httpResponseBuilder = httpResponseBuilder; + this.attributes = new BaseAttributes(); + this.attributes.put("application", application); + this.attributes.put("domain", domain); + this.attributes.put("requestbuilder", httpRequestBuilder); + this.attributes.put("responsebuilder", httpResponseBuilder); + } + + @Override + public HttpRequestBuilder request() { + return httpRequestBuilder; + } + + @Override + public HttpResponseBuilder response() { + return httpResponseBuilder; + } + + @Override + public HttpRequest httpRequest() { + return httpRequest; + } + + @Override + public void setResolverResult(HttpRouteResolver.Result pathResolverResult) { + this.pathResolverResult = pathResolverResult; + this.attributes.put("context", pathResolverResult.getContext()); + this.attributes.put("handler", pathResolverResult.getValue()); + this.attributes.put("pathparams", pathResolverResult.getParameter()); + String contextPath = pathResolverResult.getContext() != null ? + PATH_SEPARATOR + String.join(PATH_SEPARATOR, pathResolverResult.getContext()) : null; + setContextPath(contextPath); + setContextURL(request().getBaseURL().resolve(contextPath != null ? contextPath + "/" : "")); + this.httpRequest = createRequest(httpRequestBuilder); + this.attributes.put("request", httpRequest); + this.next = false; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + @Override + public String getContextPath() { + return contextPath; + } + + public void setContextURL(URL contextURL) { + this.contextURL = contextURL; + } + + @Override + public URL getContextURL() { + return contextURL; + } + + public Path resolve(String path) { + return application.resolve(path); + } + + public void setAttributes(Attributes attributes) { + this.attributes = attributes; + } + + @Override + public Attributes attributes() { + return attributes; + } + + @Override + public void done() { + this.done = true; + } + + @Override + public boolean isDone() { + return done; + } + + @Override + public boolean isFailed() { + return failed; + } + + @Override + public void fail() { + this.failed = true; + } + + public void next() { + this.next = true; + } + + public boolean isNext() { + return next; + } + + @Override + public void write() throws IOException { + httpResponseBuilder.write(""); + } + + @Override + public void write(String string) throws IOException { + httpResponseBuilder.write(string); + } + + @Override + public void write(CharBuffer charBuffer, Charset charset) throws IOException { + httpResponseBuilder.write(charBuffer, charset); + } + + @Override + public void write(DataBuffer dataBuffer) throws IOException { + httpResponseBuilder.write(dataBuffer); + } + + @Override + public void write(InputStream inputStream, int bufferSize) throws IOException { + httpResponseBuilder.write(inputStream, bufferSize); + } + + @Override + public void write(FileChannel fileChannel, int bufferSize) throws IOException { + httpResponseBuilder.write(fileChannel, bufferSize); + } + + protected HttpRequest createRequest(HttpRequestBuilder requestBuilder) { + HttpHeaders headers = requestBuilder.getHeaders(); + String mimeType = headers.get(CONTENT_TYPE); + Charset charset = StandardCharsets.UTF_8; + if (mimeType != null) { + charset = getCharset(mimeType, charset); + } + ParameterBuilder parameterBuilder = Parameter.builder().charset(charset); + // helper URL to collect parameters in request URI + URL url = URL.builder() + .charset(charset, CodingErrorAction.REPLACE) + .path(requestBuilder.getRequestURI()) + .build(); + ParameterBuilder formParameters = Parameter.builder().domain("FORM"); + // https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4 + if (HttpMethod.POST.equals(requestBuilder.getMethod()) && + (mimeType != null && mimeType.contains(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED))) { + Charset htmlCharset = getCharset(mimeType, StandardCharsets.ISO_8859_1); + CharBuffer charBuffer = requestBuilder.getBodyAsChars(htmlCharset); + if (charBuffer != null) { + formParameters.addPercentEncodedBody(charBuffer.toString()); + } + } + CookieBox cookieBox = attributes.get(CookieBox.class, "incomingcookies"); + ParameterBuilder cookieParameters = Parameter.builder().domain("COOKIE"); + if (cookieBox != null) { + cookieBox.forEach(c -> cookieParameters.add(c.name(), c.value())); + } + parameterBuilder.add(url.getQueryParams()); + parameterBuilder.add(formParameters.build()); + parameterBuilder.add(cookieParameters.build()); + parameterBuilder.add(pathResolverResult.getParameter()); + requestBuilder.setParameter(parameterBuilder.build()); + requestBuilder.setContext(this); + return requestBuilder.build(); + } + + private static Charset getCharset(String contentTypeValue, Charset defaultCharset) { + if (contentTypeValue != null) { + CharSequence charsetRaw = getCharsetAsSequence(contentTypeValue); + if (charsetRaw != null) { + if (charsetRaw.length() > 2) { + if (charsetRaw.charAt(0) == '"' && charsetRaw.charAt(charsetRaw.length() - 1) == '"') { + charsetRaw = charsetRaw.subSequence(1, charsetRaw.length() - 1); + } + } + try { + return Charset.forName(charsetRaw.toString()); + } catch (IllegalCharsetNameException | UnsupportedCharsetException ignored) { + // just return the default charset + } + } + } + return defaultCharset; + } + + private static CharSequence getCharsetAsSequence(String contentTypeValue) { + Objects.requireNonNull(contentTypeValue); + int indexOfCharset = contentTypeValue.indexOf("charset="); + if (indexOfCharset == -1) { + return null; + } + int indexOfEncoding = indexOfCharset + "charset=".length(); + if (indexOfEncoding < contentTypeValue.length()) { + CharSequence charsetCandidate = contentTypeValue.subSequence(indexOfEncoding, contentTypeValue.length()); + int indexOfSemicolon = charsetCandidate.toString().indexOf(";"); + if (indexOfSemicolon == -1) { + return charsetCandidate; + } + return charsetCandidate.subSequence(0, indexOfSemicolon); + } + return null; + } + + // user session + + // request attributes + + // locale + + // principal + + // parsed form data, multipart + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpService.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpService.java new file mode 100644 index 0000000..2f88d62 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpService.java @@ -0,0 +1,88 @@ +package org.xbib.net.http.server; + +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.xbib.net.ParameterDefinition; +import org.xbib.net.http.HttpMethod; + +import java.io.IOException; +import java.util.Objects; +import org.xbib.net.http.HttpResponseStatus; + +public class BaseHttpService implements HttpService { + + private static final Logger logger = Logger.getLogger(BaseHttpService.class.getName()); + + private final BaseHttpServiceBuilder builder; + + protected BaseHttpService(BaseHttpServiceBuilder builder) { + this.builder = builder; + } + + public static BaseHttpServiceBuilder builder() { + return new BaseHttpServiceBuilder(); + } + + @Override + public String getPathSpecification() { + return builder.pathSpec; + } + + @Override + public Collection getMethods() { + return builder.methods; + } + + @Override + public Collection getHandlers() { + return builder.handlers; + } + + @Override + public Collection getParameterDefinitions() { + return builder.parameterDefinitions; + } + + @Override + public HttpSecurityDomain getSecurityDomain() { + return builder.securityDomain; + } + + @Override + public void handle(HttpServerContext context) throws IOException { + if (builder.handlers != null) { + for (HttpHandler handler : builder.handlers) { + handler.handle(context); + } + } else { + throw new HttpException("no handler found", context, HttpResponseStatus.NOT_IMPLEMENTED); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BaseHttpService endpoint = (BaseHttpService) o; + return Objects.equals(builder.methods, endpoint.builder.methods) && + Objects.equals(builder.pathSpec, endpoint.builder.pathSpec) && + Objects.equals(builder.handlers, endpoint.builder.handlers); + } + + @Override + public int hashCode() { + return Objects.hash(builder.methods, builder.pathSpec, builder.handlers); + } + + @Override + public String toString() { + return "BaseHttpService[methods=" + builder.methods + ",path=" + builder.pathSpec + ",handler=" + builder.handlers + "]"; + } + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServiceBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServiceBuilder.java new file mode 100644 index 0000000..5fa92ad --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServiceBuilder.java @@ -0,0 +1,69 @@ +package org.xbib.net.http.server; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; + +import org.xbib.net.ParameterDefinition; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.PathNormalizer; + +import java.util.Objects; + +public class BaseHttpServiceBuilder implements HttpServiceBuilder { + + protected Collection methods; + + protected String pathSpec; + + protected Collection handlers; + + protected Collection parameterDefinitions; + + protected HttpSecurityDomain securityDomain; + + protected BaseHttpServiceBuilder() { + this.methods = new HashSet<>(); + methods.add(HttpMethod.GET); + this.pathSpec = "/**"; + this.handlers = null; + this.securityDomain = null; + } + + @Override + public BaseHttpServiceBuilder setMethod(HttpMethod... method) { + this.methods = new LinkedHashSet<>(Arrays.asList(method)); + return this; + } + + public BaseHttpServiceBuilder setPath(String path) { + if (path != null) { + this.pathSpec = PathNormalizer.normalize(path); + } + return this; + } + + @Override + public BaseHttpServiceBuilder setHandler(HttpHandler... handler) { + this.handlers = Arrays.asList(handler); + return this; + } + + @Override + public HttpServiceBuilder setParameterDefinition(ParameterDefinition... parameterDefinition) { + this.parameterDefinitions = Arrays.asList(parameterDefinition); + return this; + } + + @Override + public BaseHttpServiceBuilder setSecurityDomain(HttpSecurityDomain securityDomain) { + this.securityDomain = securityDomain; + return this; + } + + public BaseHttpService build() { + Objects.requireNonNull(handlers); + return new BaseHttpService(this); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpDomain.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpDomain.java new file mode 100644 index 0000000..0830d2d --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpDomain.java @@ -0,0 +1,18 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.HttpAddress; + +import java.util.Collection; +import java.util.Set; + +/** + * The {@code HttpDomain} interface represents a set of domain names attached to an HTTP address. + */ +public interface HttpDomain extends Comparable { + + Set getNames(); + + HttpAddress getAddress(); + + Collection getServices(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpDomainBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpDomainBuilder.java new file mode 100644 index 0000000..038a25b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpDomainBuilder.java @@ -0,0 +1,16 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.HttpAddress; + +import java.io.IOException; + +public interface HttpDomainBuilder { + + HttpDomainBuilder addName(String name); + + HttpDomainBuilder setHttpAddress(HttpAddress httpAddress) throws IOException; + + HttpDomainBuilder addService(HttpService httpService); + + HttpDomain build(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpErrorHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpErrorHandler.java new file mode 100644 index 0000000..994adc3 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpErrorHandler.java @@ -0,0 +1,4 @@ +package org.xbib.net.http.server; + +public interface HttpErrorHandler extends HttpHandler { +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpException.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpException.java new file mode 100644 index 0000000..8609357 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpException.java @@ -0,0 +1,51 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.HttpResponseStatus; +import java.io.IOException; + +@SuppressWarnings("serial") +public class HttpException extends IOException { + + private final HttpServerContext httpServerContext; + + private final HttpResponseStatus httpResponseStatus; + + public HttpException(HttpResponseBuilder httpResponseBuilder, + HttpResponseStatus httpResponseStatus) { + this(httpResponseStatus.codeAsText(), + new BaseHttpServerContext(null, null, null, httpResponseBuilder), + httpResponseStatus); + } + + public HttpException(String message, + HttpServerContext httpServerContext, + HttpResponseStatus httpResponseStatus) { + super(message); + this.httpServerContext = httpServerContext; + this.httpResponseStatus = httpResponseStatus; + } + + public HttpException(String message, Throwable throwable, + HttpServerContext httpServerContext, + HttpResponseStatus httpResponseStatus) { + super(message, throwable); + this.httpServerContext = httpServerContext; + this.httpResponseStatus = httpResponseStatus; + } + + public HttpException(Throwable throwable, + HttpServerContext httpServerContext, + HttpResponseStatus httpResponseStatus) { + super(throwable); + this.httpServerContext = httpServerContext; + this.httpResponseStatus = httpResponseStatus; + } + + public HttpServerContext getHttpServerContext() { + return httpServerContext; + } + + public HttpResponseStatus getResponseStatus() { + return httpResponseStatus; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpHandler.java new file mode 100644 index 0000000..09c33ab --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpHandler.java @@ -0,0 +1,6 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Handler; + +public interface HttpHandler extends Handler { +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpRequest.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpRequest.java new file mode 100644 index 0000000..4d3a983 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpRequest.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Attributes; +import org.xbib.net.Parameter; +import org.xbib.net.Request; +import org.xbib.net.URL; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +import java.io.InputStream; +import java.nio.ByteBuffer; + +public interface HttpRequest extends Request { + + URL getServerURL(); + + HttpServerContext getContext(); + + String getRequestURI(); + + HttpVersion getVersion(); + + HttpMethod getMethod(); + + HttpHeaders getHeaders(); + + Parameter getParameter(); + + String getRequestPath(); + + Integer getSequenceId(); + + Integer getStreamId(); + + Long getRequestId(); + + ByteBuffer getBody(); + + InputStream getInputStream(); + + Attributes attributes(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpRequestBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpRequestBuilder.java new file mode 100644 index 0000000..f016dbf --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpRequestBuilder.java @@ -0,0 +1,48 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Parameter; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpVersion; + +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +public interface HttpRequestBuilder { + + HttpRequestBuilder setAddress(HttpAddress httpAddress); + + HttpRequestBuilder setBaseURL(URL baseURL); + + HttpRequestBuilder setRequestURI(String requestURI); + + HttpRequestBuilder setRequestPath(String requestPath); + + HttpRequestBuilder setParameter(Parameter parameter); + + HttpRequestBuilder setContext(HttpServerContext context); + + HttpRequestBuilder setVersion(HttpVersion version); + + HttpRequestBuilder setMethod(HttpMethod method); + + HttpRequestBuilder setHeaders(HttpHeaders httpHeaders); + + HttpRequestBuilder addHeader(String name, String value); + + URL getBaseURL(); + + HttpMethod getMethod(); + + String getRequestURI(); + + String getRequestPath(); + + HttpHeaders getHeaders(); + + CharBuffer getBodyAsChars(Charset charset); + + HttpRequest build(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpResponse.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpResponse.java new file mode 100644 index 0000000..9a0cac2 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpResponse.java @@ -0,0 +1,10 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Response; + +import java.io.IOException; + +public interface HttpResponse extends Response { + + void close() throws IOException; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpResponseBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpResponseBuilder.java new file mode 100644 index 0000000..cea96cd --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpResponseBuilder.java @@ -0,0 +1,67 @@ +package org.xbib.net.http.server; + +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.buffer.DataBufferFactory; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.HttpVersion; +import org.xbib.net.http.cookie.Cookie; + +import java.io.InputStream; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +public interface HttpResponseBuilder { + + HttpResponseBuilder setDataBufferFactory(DataBufferFactory dataBufferFactory); + + DataBufferFactory getDataBufferFactory(); + + HttpResponseBuilder setVersion(HttpVersion version); + + HttpResponseBuilder setResponseStatus(HttpResponseStatus status); + + HttpResponseBuilder setContentType(String contentType); + + HttpResponseBuilder setCharset(Charset charset); + + HttpHeaders getHeaders(); + + HttpResponseBuilder addHeader(CharSequence name, String value); + + HttpResponseBuilder setHeader(CharSequence name, String value); + + HttpResponseBuilder setTrailingHeader(CharSequence name, String value); + + HttpResponseBuilder addCookie(Cookie cookie); + + HttpResponseBuilder shouldFlush(boolean sholdFlush); + + boolean shouldFlush(); + + HttpResponseBuilder shouldClose(boolean shouldClose); + + boolean shouldClose(); + + HttpResponseBuilder setSequenceId(Integer sequenceId); + + HttpResponseBuilder setStreamId(Integer streamId); + + HttpResponseBuilder setResponseId(Long responseId); + + HttpResponseBuilder write(String body); + + HttpResponseBuilder write(CharBuffer charBuffer, Charset charset); + + HttpResponseBuilder write(DataBuffer dataBuffer); + + HttpResponseBuilder write(InputStream inputStream, int bufferSize); + + HttpResponseBuilder write(FileChannel fileChannel, int bufferSize); + + void reset(); + + HttpResponse build(); + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpSecurityDomain.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpSecurityDomain.java new file mode 100644 index 0000000..50548fa --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpSecurityDomain.java @@ -0,0 +1,9 @@ +package org.xbib.net.http.server; + +import java.util.List; +import org.xbib.net.SecurityDomain; + +public interface HttpSecurityDomain extends SecurityDomain { + + List getHandlers(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpServer.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServer.java new file mode 100644 index 0000000..20b66bb --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServer.java @@ -0,0 +1,14 @@ +package org.xbib.net.http.server; + +import java.io.Closeable; +import java.io.IOException; +import java.net.BindException; + +public interface HttpServer extends Closeable { + + void bind() throws BindException; + + void loop() throws IOException; + + Application getApplication(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerBuilder.java new file mode 100644 index 0000000..bb55999 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerBuilder.java @@ -0,0 +1,6 @@ +package org.xbib.net.http.server; + +public interface HttpServerBuilder { + + HttpServer build(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerConfig.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerConfig.java new file mode 100644 index 0000000..c481cf2 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerConfig.java @@ -0,0 +1,75 @@ +package org.xbib.net.http.server; + +import org.xbib.net.NetworkClass; + +import java.util.Optional; + +public class HttpServerConfig { + + private String serverName = null; + + private NetworkClass networkClass = NetworkClass.LOOPBACK; + + /** + * If frame logging/traffic logging is enabled or not. + */ + private boolean debug = false; + + /** + * Default timeout in milliseconds. + */ + private int timeoutMillis = 30000; + + public HttpServerConfig() { + } + + public HttpServerConfig setServerName(String serverName, String serverVendor) { + if (serverName == null) { + serverName = "HttpServer"; + } + if (serverVendor == null) { + serverVendor = "unknown"; + } + this.serverName = String.format("%s/%s/%s Java/%s/%s/%s OS/%s/%s/%s", + serverName, serverVendor, serverVersion(), + System.getProperty("java.vm.name"), System.getProperty("java.vm.vendor"), System.getProperty("java.vm.version"), + System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version")); + return this; + } + + public String getServerName() { + return serverName; + } + + public HttpServerConfig setNetworkClass(NetworkClass networkClass) { + this.networkClass = networkClass; + return this; + } + + public NetworkClass getNetworkClass() { + return networkClass; + } + + public HttpServerConfig setDebug(boolean debug) { + this.debug = debug; + return this; + } + + public boolean isDebug() { + return debug; + } + + public HttpServerConfig setTimeoutMillis(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; + return this; + } + + public int getTimeoutMillis() { + return timeoutMillis; + } + + private static String serverVersion() { + return Optional.ofNullable(HttpServerConfig.class.getPackage().getImplementationVersion()) + .orElse("unknown"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerContext.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerContext.java new file mode 100644 index 0000000..e1a066d --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServerContext.java @@ -0,0 +1,53 @@ +package org.xbib.net.http.server; + +import org.xbib.net.Attributes; +import org.xbib.net.Context; +import org.xbib.net.URL; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.server.route.HttpRouteResolver; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.file.Path; + +public interface HttpServerContext extends Context { + + void setResolverResult(HttpRouteResolver.Result result); + + Attributes attributes(); + + void done(); + + boolean isDone(); + + void fail(); + + boolean isFailed(); + + void next(); + + boolean isNext(); + + HttpRequest httpRequest(); + + String getContextPath(); + + URL getContextURL(); + + Path resolve(String path); + + void write() throws IOException; + + void write(String string) throws IOException; + + void write(CharBuffer charBuffer, Charset charset) throws IOException; + + void write(DataBuffer dataBuffer) throws IOException; + + void write(InputStream inputStream, int bufferSize) throws IOException; + + void write(FileChannel fileChannel, int bufferSize) throws IOException; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpService.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpService.java new file mode 100644 index 0000000..4149195 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpService.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server; + +import java.util.Collection; + +import org.xbib.net.ParameterDefinition; +import org.xbib.net.http.HttpMethod; + +public interface HttpService extends HttpHandler { + + String getPathSpecification(); + + Collection getMethods(); + + Collection getHandlers(); + + Collection getParameterDefinitions(); + + HttpSecurityDomain getSecurityDomain(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/HttpServiceBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServiceBuilder.java new file mode 100644 index 0000000..9f26226 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/HttpServiceBuilder.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server; + +import org.xbib.net.ParameterDefinition; +import org.xbib.net.http.HttpMethod; + +public interface HttpServiceBuilder { + + HttpServiceBuilder setPath(String path); + + HttpServiceBuilder setMethod(HttpMethod... method); + + HttpServiceBuilder setHandler(HttpHandler... handler); + + HttpServiceBuilder setParameterDefinition(ParameterDefinition... parameterDefinition); + + HttpServiceBuilder setSecurityDomain(HttpSecurityDomain securityDomain); + + HttpService build(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/MissingHostHeaderException.java b/net-http-server/src/main/java/org/xbib/net/http/server/MissingHostHeaderException.java new file mode 100644 index 0000000..8b2f2a2 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/MissingHostHeaderException.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.HttpResponseStatus; + +@SuppressWarnings("serial") +public class MissingHostHeaderException extends HttpException { + + public MissingHostHeaderException(String message, HttpServerContext httpServerContext) { + super(message, httpServerContext, HttpResponseStatus.BAD_REQUEST); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/Resolver.java b/net-http-server/src/main/java/org/xbib/net/http/server/Resolver.java new file mode 100644 index 0000000..ae33390 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/Resolver.java @@ -0,0 +1,6 @@ +package org.xbib.net.http.server; + +public interface Resolver { + + R resolve(String string); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/Service.java b/net-http-server/src/main/java/org/xbib/net/http/server/Service.java new file mode 100644 index 0000000..0004c03 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/Service.java @@ -0,0 +1,56 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.server.decorate.Unwrappable; + +import static java.util.Objects.requireNonNull; + +@FunctionalInterface +public interface Service extends Unwrappable { + + default void serviceAdded(HttpServerConfig cfg) throws Exception { + } + + void serve(HttpServerContext ctx) throws Exception; + + /** + * Unwraps this {@link Service} into the object of the specified {@code type}. + * Use this method instead of an explicit downcast. For example: + *

{@code
+     * HttpService s = new MyService().decorate(LoggingService.newDecorator())
+     *                                .decorate(AuthService.newDecorator());
+     * MyService s1 = s.as(MyService.class);
+     * LoggingService s2 = s.as(LoggingService.class);
+     * AuthService s3 = s.as(AuthService.class);
+     * }
+ * + * @param type the type of the object to return + * @return the object of the specified {@code type} if found, or {@code null} if not found. + * + * @see Unwrappable + */ + @Override + default T as(Class type) { + requireNonNull(type, "type"); + return Unwrappable.super.as(type); + } + + /** + * Unwraps this {@link Service} and returns the object being decorated. + * If this {@link Service} is the innermost object, this method returns itself. + * For example: + *
{@code
+     * HttpService service1 = new MyService();
+     * assert service1.unwrap() == service1;
+     *
+     * HttpService service2 = service1.decorate(LoggingService.newDecorator());
+     * HttpService service3 = service2.decorate(AuthService.newDecorator());
+     * assert service2.unwrap() == service1;
+     * assert service3.unwrap() == service2;
+     * assert service3.unwrap().unwrap() == service1;
+     * }
+ */ + @Override + default Service unwrap() { + return (Service) Unwrappable.super.unwrap(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/UnknownExpectException.java b/net-http-server/src/main/java/org/xbib/net/http/server/UnknownExpectException.java new file mode 100644 index 0000000..da43d77 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/UnknownExpectException.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.server; + +import org.xbib.net.http.HttpResponseStatus; + +@SuppressWarnings("serial") +public class UnknownExpectException extends HttpException { + + public UnknownExpectException(String message, HttpServerContext httpServerContext) { + super(message, httpServerContext, HttpResponseStatus.EXPECTATION_FAILED); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java new file mode 100644 index 0000000..b2dbf3f --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java @@ -0,0 +1,231 @@ +package org.xbib.net.http.server.auth; + +import org.xbib.net.Attributes; +import org.xbib.net.UserProfile; +import org.xbib.net.http.server.BaseAttributes; + +import java.util.ArrayList; +import java.util.List; + +public class BaseUserProfile implements UserProfile { + + private final Attributes attributes; + + private final Attributes effectiveAttributes; + + private final List roles; + + private final List effectiveRoles; + + private final List permissions; + + private final List effectivePermissions; + + private String uid; + + private String euid; + + private String name; + + private boolean isRemembered; + + private boolean isLoggedIn; + + public BaseUserProfile() { + this.attributes = new BaseAttributes(); + this.effectiveAttributes = new BaseAttributes(); + this.roles = new ArrayList<>(); + this.effectiveRoles = new ArrayList<>(); + this.permissions = new ArrayList<>(); + this.effectivePermissions = new ArrayList<>(); + } + + @Override + public boolean isLoggedIn() { + return isLoggedIn; + } + + @Override + public void setUserId(String uid) { + this.uid = uid; + this.isLoggedIn = uid != null; + } + + @Override + public String getUserId() { + return uid; + } + + @Override + public void setEffectiveUserId(String euid) { + this.euid = euid; + } + + @Override + public String getEffectiveUserId() { + return euid; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public void addRole(String role) { + roles.add(role); + } + + @Override + public void addEffectiveRole(String role) { + effectiveRoles.add(role); + } + + @Override + public List getRoles() { + return roles; + } + + @Override + public List getEffectiveRoles() { + return effectiveRoles; + } + + @Override + public boolean hasRole(String role) { + return roles.contains(role); + } + + @Override + public boolean hasEffectiveRole(String role) { + return effectiveRoles.contains(role); + } + + @Override + public boolean hasAccess(String requireAnyRole, String requireAllRoles) { + boolean access = true; + if (!requireAnyRole.isEmpty()) { + String[] expectedRoles = requireAnyRole.split(","); + if (!hasAnyRole(expectedRoles)) { + access = false; + } + } else if (!requireAllRoles.isEmpty()) { + String[] expectedRoles = requireAllRoles.split(","); + if (!hasAllRoles(expectedRoles)) { + access = false; + } + } + return access; + } + + @Override + public boolean hasAnyRole(String[] expectedRoles) { + if (expectedRoles == null || expectedRoles.length == 0) { + return true; + } + for (final String role : expectedRoles) { + if (roles.contains(role)) { + return true; + } + } + return false; + } + + @Override + public boolean hasAnyEffectiveRole(String[] expectedRoles) { + if (expectedRoles == null || expectedRoles.length == 0) { + return true; + } + for (final String role : expectedRoles) { + if (effectiveRoles.contains(role)) { + return true; + } + } + return false; + } + + @Override + public boolean hasAllRoles(String[] expectedRoles) { + if (expectedRoles == null || expectedRoles.length == 0) { + return true; + } + for (String role : expectedRoles) { + if (!roles.contains(role)) { + return false; + } + } + return true; + } + + @Override + public boolean hasAllEffectiveRoles(String[] expectedRoles) { + if (expectedRoles == null || expectedRoles.length == 0) { + return true; + } + for (String role : expectedRoles) { + if (!effectiveRoles.contains(role)) { + return false; + } + } + return true; + } + + @Override + public void addPermission(String permission) { + permissions.add(permission); + } + + @Override + public void removePermission(String permission) { + permissions.remove(permission); + } + + @Override + public void setRemembered(boolean remembered) { + this.isRemembered = remembered; + } + + @Override + public Attributes attributes() { + return attributes; + } + + @Override + public Attributes effectiveAttributes() { + return effectiveAttributes; + } + + @Override + public List getPermissions() { + return permissions; + } + + @Override + public List getEffectivePermissions() { + return effectivePermissions; + } + + @Override + public boolean isRemembered() { + return isRemembered; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("uid=").append(uid) + .append(",roles=").append(roles) + .append(",permissons=").append(permissions) + .append(",attributes=").append(attributes) + .append(",euid=").append(euid) + .append(",eroles=").append(effectiveRoles) + .append(",epermissions=").append(effectivePermissions) + .append(",eattributes=").append(effectiveAttributes); + return sb.toString(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/BasicAuthenticationHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/BasicAuthenticationHandler.java new file mode 100644 index 0000000..093cccc --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/BasicAuthenticationHandler.java @@ -0,0 +1,56 @@ +package org.xbib.net.http.server.auth; + +import org.xbib.net.SecurityRealm; +import org.xbib.net.UserProfile; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.util.Base64; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BasicAuthenticationHandler extends LoginAuthenticationHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(BasicAuthenticationHandler.class.getName()); + + public BasicAuthenticationHandler(SecurityRealm securityRealm) { + super(null, null, securityRealm); + } + + @Override + public void handle(HttpServerContext context) throws IOException { + HttpRequest httpRequest = context.httpRequest(); + UserProfile userProfile = context.attributes().get(UserProfile.class, "userprofile"); + if (userProfile != null && userProfile.getUserId() != null) { + return; + } + String authorization = httpRequest.getHeaders().get(HttpHeaderNames.AUTHORIZATION); + if (authorization != null) { + if (!authorization.startsWith("Basic ")) { + return; + } + byte[] b = Base64.getDecoder().decode(authorization.substring("Basic ".length())); + String[] tokens = new String(b).split(":"); + if (tokens.length != 2) { + return; + } + userProfile = new BaseUserProfile(); + try { + authenticate(userProfile, tokens[0], tokens[1], httpRequest); + context.attributes().put("userprofile", userProfile); + return; + } catch (Exception e) { + logger.log(Level.WARNING, "authentication error"); + } + } else { + logger.log(Level.WARNING, "no authorization header"); + } + logger.log(Level.INFO, "unauthenticated"); + context.response().setResponseStatus(HttpResponseStatus.UNAUTHORIZED) + .setHeader("WWW-Authenticate", "Basic realm=\"" + securityRealm.getName() + "\""); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/FormAuthenticationHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/FormAuthenticationHandler.java new file mode 100644 index 0000000..e9b199b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/FormAuthenticationHandler.java @@ -0,0 +1,84 @@ +package org.xbib.net.http.server.auth; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.Parameter; +import org.xbib.net.SecurityRealm; +import org.xbib.net.URL; +import org.xbib.net.UserProfile; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; + +public class FormAuthenticationHandler extends LoginAuthenticationHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(FormAuthenticationHandler.class.getName()); + + String usernameParameter; + + String passwordParameter; + + String rememberParameter; + + String loginPage; + + public FormAuthenticationHandler(String usernameParameter, + String passwordParameter, + String rememberParameter, + String loginPage, + SecurityRealm securityRealm) { + super(usernameParameter, passwordParameter, securityRealm); + this.usernameParameter = usernameParameter; + this.passwordParameter = passwordParameter; + this.rememberParameter = rememberParameter; + this.loginPage = loginPage; + } + + @Override + public void handle(HttpServerContext context) throws IOException { + if (loginPage == null) { + logger.log(Level.WARNING, "no loginPage configured"); + return; + } + UserProfile userProfile = context.attributes().get(UserProfile.class, "userprofile"); + if (userProfile != null && userProfile.getUserId() != null) { + logger.log(Level.FINE, "user id already set: " + userProfile.getUserId()); + return; + } + // always add an "anonymous" user profile + userProfile = new BaseUserProfile(); + context.attributes().put("userprofile", userProfile); + Parameter parameter = context.httpRequest().getParameter(); + if (!parameter.containsKey("FORM", usernameParameter)) { + logger.log(Level.WARNING, "usernameParameter not set, unable to authenticate"); + prepareFormAuthentication(context); + return; + } + if (!parameter.containsKey("FORM", passwordParameter)) { + logger.log(Level.WARNING, "passwordParameter not set, unable to authenticate"); + prepareFormAuthentication(context); + return; + } + String username = parameter.getAsString("FORM", usernameParameter); + String password = parameter.getAsString("FORM", passwordParameter); + logger.log(Level.FINE, "username and password found, ready for authentication"); + try { + authenticate(userProfile, username, password, context.httpRequest()); + logger.log(Level.FINE, "successful authentication"); + return; + } catch (Exception e) { + logger.log(Level.SEVERE, "authentication error for " + username); + } + prepareFormAuthentication(context); + } + + private void prepareFormAuthentication(HttpServerContext context) { + // this will redirect internally to login page, and back to the original path. + // We need a full path resolve against the server URL. + logger.log(Level.FINE, "templatePath = " + loginPage); + context.attributes().put("templatePath", loginPage); + URL loc = context.getContextURL().resolve(context.httpRequest().getRequestURI()).normalize(); + logger.log(Level.FINE, "context URL = " + context.getContextURL() + " request URI = " + context.httpRequest().getRequestURI() + " loc = " + loc); + context.attributes().put("originalPath", loc.toExternalForm()); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java new file mode 100644 index 0000000..62e1b3b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java @@ -0,0 +1,79 @@ +package org.xbib.net.http.server.auth; + +import java.util.Collection; +import org.xbib.net.Authenticator; +import org.xbib.net.GroupsProvider; +import org.xbib.net.Request; +import org.xbib.net.SecurityRealm; +import org.xbib.net.UserDetails; +import org.xbib.net.UserProfile; +import org.xbib.net.UsersProvider; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LoginAuthenticationHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(LoginAuthenticationHandler.class.getName()); + + private final String userParameterName; + + private final String passwordParameterName; + + protected final SecurityRealm securityRealm; + + public LoginAuthenticationHandler(String userParameterName, + String passwordParameterName, + SecurityRealm securityRealm) { + this.userParameterName = userParameterName; + this.passwordParameterName = passwordParameterName; + this.securityRealm = securityRealm; + } + + @Override + public void handle(HttpServerContext context) throws IOException { + UserProfile userProfile = context.attributes().get(UserProfile.class, "userprofile"); + if (userProfile != null && userProfile.getUserId() != null) { + return; + } + userProfile = new BaseUserProfile(); + try { + authenticate(userProfile, + (String) context.httpRequest().getParameter().get("DEFAULT", userParameterName), + (String) context.httpRequest().getParameter().get("DEFAULT", passwordParameterName), + context.httpRequest()); + context.attributes().put("userprofile", userProfile); + } catch (Exception e) { + logger.log(Level.SEVERE, "authentication error"); + } + } + + protected void authenticate(UserProfile userProfile, String username, String password, Request request) throws Exception { + if (username == null) { + logger.log(Level.FINE, "no username given for check, doing nothing"); + return; + } + if (password == null) { + logger.log(Level.FINE, "no password given for check, doing nothing"); + return; + } + Authenticator auth = securityRealm.getAuthenticator(); + Authenticator.Context authContext = new Authenticator.Context(username, password, request); + if (auth.authenticate(authContext)) { + userProfile.setUserId(authContext.getUsername()); + } + UsersProvider.Context userContext = new UsersProvider.Context(username, null); + UserDetails userDetails = securityRealm.getUsersProvider().getUserDetails(userContext); + userProfile.setEffectiveUserId(userDetails.getEffectiveUserId()); + userProfile.setName(userDetails.getName()); + GroupsProvider.Context groupContext = new GroupsProvider.Context(username, null); + Collection groups = securityRealm.getGroupsProvider().getGroups(groupContext); + for (String group : groups) { + userProfile.addRole(group); + } + logger.log(Level.FINE, "authenticate: userProfile = " + userProfile); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieDecoder.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieDecoder.java new file mode 100644 index 0000000..3a575d0 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieDecoder.java @@ -0,0 +1,128 @@ +package org.xbib.net.http.server.cookie; + +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieHeaderNames; +import org.xbib.net.http.cookie.DefaultCookie; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * A RFC6265 compliant cookie decoder to be used server side. + * + * Only name and value fields are expected, so old fields are not populated (path, domain, etc). + * + * Old RFC2965 cookies are still supported, + * old fields will simply be ignored. + */ +public final class CookieDecoder extends org.xbib.net.http.cookie.CookieDecoder { + + private static final String RFC2965_VERSION = "$Version"; + + private static final String RFC2965_PATH = "$" + CookieHeaderNames.PATH; + + private static final String RFC2965_DOMAIN = "$" + CookieHeaderNames.DOMAIN; + + private static final String RFC2965_PORT = "$Port"; + + /** + * Strict encoder that validates that name and value chars are in the valid scope + * defined in RFC6265 + */ + public static final CookieDecoder STRICT = new CookieDecoder(true); + + /** + * Lax instance that doesn't validate name and value + */ + public static final CookieDecoder LAX = new CookieDecoder(false); + + private CookieDecoder(boolean strict) { + super(strict); + } + + /** + * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}. + * + * @param header header + * @return the decoded {@link Cookie} + */ + public Set decode(String header) { + final int headerLen = Objects.requireNonNull(header, "header").length(); + if (headerLen == 0) { + return Collections.emptySet(); + } + Set cookies = new LinkedHashSet<>(); + int i = 0; + boolean rfc2965Style = false; + if (header.regionMatches(true, 0, RFC2965_VERSION, 0, RFC2965_VERSION.length())) { + // RFC 2965 style cookie, move to after version value + i = header.indexOf(';') + 1; + rfc2965Style = true; + } + while (i < headerLen) { + // Skip spaces and separators. + while (i < headerLen) { + char c = header.charAt(i); + switch (c) { + case '\t': + case '\n': + case 0x0b: + case '\f': + case '\r': + case ' ': + case ',': + case ';': + i++; + continue; + default: + break; + } + break; + } + int nameBegin = i; + int nameEnd = 0; + int valueBegin = 0; + int valueEnd = 0; + while (i < headerLen) { + char curChar = header.charAt(i); + if (curChar == ';') { + nameEnd = i; + valueBegin = valueEnd = -1; + break; + + } else if (curChar == '=') { + nameEnd = i; + i++; + if (i == headerLen) { + break; + } + valueBegin = i; + int semiPos = header.indexOf(';', i); + valueEnd = i = semiPos > 0 ? semiPos : headerLen; + break; + } else { + i++; + } + if (i == headerLen) { + nameEnd = headerLen; + valueBegin = valueEnd = -1; + break; + } + } + if (rfc2965Style && (header.regionMatches(nameBegin, RFC2965_PATH, 0, RFC2965_PATH.length()) || + header.regionMatches(nameBegin, RFC2965_DOMAIN, 0, RFC2965_DOMAIN.length()) || + header.regionMatches(nameBegin, RFC2965_PORT, 0, RFC2965_PORT.length()))) { + continue; + } + if (nameEnd >= nameBegin && valueEnd >= valueBegin) { + DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd); + if (cookie != null) { + cookies.add(cookie); + } + } + } + return cookies; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieEncoder.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieEncoder.java new file mode 100644 index 0000000..510eead --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieEncoder.java @@ -0,0 +1,90 @@ +package org.xbib.net.http.server.cookie; + +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieHeaderNames; +import org.xbib.net.http.cookie.CookieUtil; +import org.xbib.net.util.DateTimeUtil; + +import java.time.Instant; +import java.util.Locale; +import java.util.Objects; + +/** + * A RFC6265 compliant cookie encoder to be used server side, + * so some fields are sent (Version is typically ignored). + * + * As Cookie merges Expires and MaxAge into one single field, only Max-Age field is sent. + * + *
+ * // Example
+ * HttpRequest.Builder req = ...
+ * res.addHeader("Cookie", {@link CookieEncoder}.encode("JSESSIONID", "1234"))
+ * 
+ * + * @see CookieDecoder + */ +public final class CookieEncoder extends org.xbib.net.http.cookie.CookieEncoder { + + /** + * Strict encoder that validates that name and value chars are in the valid scope + * defined in RFC6265, and (for methods that accept multiple cookies) that only + * one cookie is encoded with any given name. (If multiple cookies have the same + * name, the last one is the one that is encoded.) + */ + public static final CookieEncoder STRICT = new CookieEncoder(true); + + /** + * Lax instance that doesn't validate name and value, and that allows multiple + * cookies with the same name. + */ + public static final CookieEncoder LAX = new CookieEncoder(false); + + private CookieEncoder(boolean strict) { + super(strict); + } + + /** + * Encodes the specified cookie into a Set-Cookie header value. + * + * @param cookie the cookie + * @return a single Set-Cookie header value + */ + public String encode(Cookie cookie) { + final String name = Objects.requireNonNull(cookie, "cookie").name(); + final String value = cookie.value() != null ? cookie.value() : ""; + validateCookie(name, value); + StringBuilder stringBuilder = new StringBuilder(); + if (cookie.wrap()) { + CookieUtil.addQuoted(stringBuilder, name, value); + } else { + CookieUtil.add(stringBuilder, name, value); + } + if (cookie.maxAge() != Long.MIN_VALUE) { + CookieUtil.add(stringBuilder, CookieHeaderNames.MAX_AGE, cookie.maxAge()); + Instant expires = Instant.ofEpochMilli(cookie.maxAge() * 1000 + System.currentTimeMillis()); + stringBuilder.append(CookieHeaderNames.EXPIRES); + stringBuilder.append(CookieUtil.EQUALS); + stringBuilder.append(DateTimeUtil.formatRfc1123(expires.toEpochMilli())); + stringBuilder.append(CookieUtil.SEMICOLON); + stringBuilder.append(CookieUtil.SP); + } + if (cookie.path() != null) { + CookieUtil.add(stringBuilder, CookieHeaderNames.PATH, cookie.path()); + } + if (cookie.domain() != null) { + CookieUtil.add(stringBuilder, CookieHeaderNames.DOMAIN, cookie.domain()); + } + if (cookie.isSecure()) { + CookieUtil.add(stringBuilder, CookieHeaderNames.SECURE); + } + if (cookie.isHttpOnly()) { + CookieUtil.add(stringBuilder, CookieHeaderNames.HTTPONLY); + } + if (cookie.sameSite() != null) { + String s = cookie.sameSite().name(); + CookieUtil.add(stringBuilder, CookieHeaderNames.SAMESITE, + s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1).toLowerCase(Locale.ROOT)); + } + return CookieUtil.stripTrailingSeparator(stringBuilder); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureException.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureException.java new file mode 100644 index 0000000..d78e400 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureException.java @@ -0,0 +1,9 @@ +package org.xbib.net.http.server.cookie; + +@SuppressWarnings("serial") +public class CookieSignatureException extends Exception { + + public CookieSignatureException(String message) { + super(message); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureUtil.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureUtil.java new file mode 100644 index 0000000..b45ada0 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/CookieSignatureUtil.java @@ -0,0 +1,44 @@ +package org.xbib.net.http.server.cookie; + +import org.xbib.net.util.JsonUtil; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Map; + +public class CookieSignatureUtil { + + private CookieSignatureUtil() { + } + + public static String hmac(String plainText, String secret, String algo) throws NoSuchAlgorithmException, InvalidKeyException { + return hmac(plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), algo); + } + + public static Map toMap(String string) throws IOException { + return JsonUtil.toMap(new String(Base64.getDecoder().decode(string), StandardCharsets.UTF_8)); + } + + public static String toString(Map map) throws IOException { + return new String(Base64.getEncoder().encode(JsonUtil.toString(map).getBytes(StandardCharsets.UTF_8)), StandardCharsets.ISO_8859_1); + } + + private static String hmac(byte[] plainText, byte[] secret, String algo) throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(algo); + mac.init(new SecretKeySpec(secret, algo)); + return encodeHex(mac.doFinal(plainText)); + } + + private static String encodeHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(Character.forDigit((b & 240) >> 4, 16)).append(Character.forDigit((b & 15), 16)); + } + return sb.toString(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java new file mode 100644 index 0000000..b6dbc25 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java @@ -0,0 +1,37 @@ +package org.xbib.net.http.server.cookie; + +import java.util.logging.Logger; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.cookie.CookieBox; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.util.Collection; + +public class IncomingCookieHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(IncomingCookieHandler.class.getName()); + + public IncomingCookieHandler() { + } + + @Override + public void handle(HttpServerContext context) throws HttpException { + Collection cookieStrings = context.request().getHeaders().getAll(HttpHeaderNames.COOKIE); + if (cookieStrings.isEmpty()) { + return; + } + CookieBox cookieBox = new CookieBox(); + for (String cookieString : cookieStrings) { + if (cookieString == null || cookieString.isEmpty()) { + continue; + } + cookieBox.addAll(CookieDecoder.LAX.decode(cookieString)); + } + if (!cookieBox.isEmpty()) { + context.attributes().put("incomingcookies", cookieBox); + + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/OutgoingCookieHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/OutgoingCookieHandler.java new file mode 100644 index 0000000..e503420 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/OutgoingCookieHandler.java @@ -0,0 +1,29 @@ +package org.xbib.net.http.server.cookie; + +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieBox; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OutgoingCookieHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(OutgoingCookieHandler.class.getName()); + + public OutgoingCookieHandler() { + } + + @Override + public void handle(HttpServerContext context) throws HttpException { + CookieBox cookieBox = context.attributes().get(CookieBox.class, "outgoingcookies"); + if (cookieBox != null) { + for (Cookie cookie : cookieBox) { + context.response().addCookie(cookie); + logger.log(Level.FINEST, "cookie prepared for outgoing = " + cookie); + } + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/decorate/AbstractUnwrappable.java b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/AbstractUnwrappable.java new file mode 100644 index 0000000..bc196bb --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/AbstractUnwrappable.java @@ -0,0 +1,38 @@ +package org.xbib.net.http.server.decorate; + +import java.util.Objects; + +/** + * Skeletal {@link Unwrappable} implementation. + * + * @param the type of the object being decorated + */ +public abstract class AbstractUnwrappable implements Unwrappable { + + private final T delegate; + + /** + * Creates a new decorator with the specified delegate. + */ + protected AbstractUnwrappable(T delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public final U as(Class type) { + final U result = Unwrappable.super.as(type); + return result != null ? result : delegate.as(type); + } + + @Override + public T unwrap() { + return delegate; + } + + @Override + public String toString() { + final String simpleName = getClass().getSimpleName(); + final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; + return name + '(' + delegate + ')'; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingHttpService.java b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingHttpService.java new file mode 100644 index 0000000..7d130af --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingHttpService.java @@ -0,0 +1,58 @@ +package org.xbib.net.http.server.decorate; + +import java.util.Collection; + +import org.xbib.net.ParameterDefinition; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpSecurityDomain; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.HttpService; + +import java.io.IOException; +import java.util.Objects; + +public class DecoratingHttpService implements HttpService { + + private final HttpService delegate; + + private final HttpHandler handler; + + public DecoratingHttpService(HttpService delegate, HttpHandler handler) { + Objects.requireNonNull(delegate); + Objects.requireNonNull(handler); + this.delegate = delegate; + this.handler = handler; + } + + @Override + public void handle(HttpServerContext context) throws IOException { + handler.handle(context); + delegate.handle(context); + } + + @Override + public Collection getMethods() { + return delegate.getMethods(); + } + + @Override + public String getPathSpecification() { + return delegate.getPathSpecification(); + } + + @Override + public Collection getHandlers() { + return delegate.getHandlers(); + } + + @Override + public Collection getParameterDefinitions() { + return delegate.getParameterDefinitions(); + } + + @Override + public HttpSecurityDomain getSecurityDomain() { + return delegate.getSecurityDomain(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingService.java b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingService.java new file mode 100644 index 0000000..98041b1 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/DecoratingService.java @@ -0,0 +1,16 @@ +package org.xbib.net.http.server.decorate; + +import org.xbib.net.http.server.HttpServerConfig; +import org.xbib.net.http.server.Service; + +public abstract class DecoratingService extends AbstractUnwrappable implements Service { + + protected DecoratingService(Service delegate) { + super(delegate); + } + + @Override + public void serviceAdded(HttpServerConfig cfg) throws Exception { + unwrap().serviceAdded(cfg); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/decorate/Unwrappable.java b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/Unwrappable.java new file mode 100644 index 0000000..3c67a0d --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/decorate/Unwrappable.java @@ -0,0 +1,72 @@ +package org.xbib.net.http.server.decorate; + +import static java.util.Objects.requireNonNull; + +/** + * Provides a way to unwrap an object in decorator pattern, similar to down-casting in an inheritance pattern. + */ +public interface Unwrappable { + /** + * Unwraps this object into the object of the specified {@code type}. + * Use this method instead of an explicit downcast. For example: + *
{@code
+     * class Foo {}
+     *
+     * class Bar extends AbstractWrapper {
+     *     Bar(T delegate) {
+     *         super(delegate);
+     *     }
+     * }
+     *
+     * class Qux extends AbstractWrapper {
+     *     Qux(T delegate) {
+     *         super(delegate);
+     *     }
+     * }
+     *
+     * Qux qux = new Qux(new Bar(new Foo()));
+     * Foo foo = qux.as(Foo.class);
+     * Bar bar = qux.as(Bar.class);
+     * }
+ * + * @param type the type of the object to return + * @return the object of the specified {@code type} if found, or {@code null} if not found. + */ + default T as(Class type) { + requireNonNull(type, "type"); + return type.isInstance(this) ? type.cast(this) : null; + } + + /** + * Unwraps this object and returns the object being decorated. If this {@link Unwrappable} is the innermost + * object, this method returns itself. For example: + *
{@code
+     * class Foo implements Unwrappable {}
+     *
+     * class Bar extends AbstractUnwrappable {
+     *     Bar(T delegate) {
+     *         super(delegate);
+     *     }
+     * }
+     *
+     * class Qux extends AbstractUnwrappable {
+     *     Qux(T delegate) {
+     *         super(delegate);
+     *     }
+     * }
+     *
+     * Foo foo = new Foo();
+     * assert foo.unwrap() == foo;
+     *
+     * Bar bar = new Bar<>(foo);
+     * assert bar.unwrap() == foo;
+     *
+     * Qux> qux = new Qux<>(bar);
+     * assert qux.unwrap() == bar;
+     * assert qux.unwrap().unwrap() == foo;
+     * }
+ */ + default Unwrappable unwrap() { + return this; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/BadRequestHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/BadRequestHandler.java new file mode 100644 index 0000000..80df661 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/BadRequestHandler.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server.handler; + +import java.io.IOException; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +public class BadRequestHandler implements HttpErrorHandler { + + public BadRequestHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + context.response() + .setResponseStatus(HttpResponseStatus.BAD_REQUEST) + .write("Bad request"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/ForbiddenHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/ForbiddenHandler.java new file mode 100644 index 0000000..6b9ed72 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/ForbiddenHandler.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server.handler; + +import java.io.IOException; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +public class ForbiddenHandler implements HttpErrorHandler { + + public ForbiddenHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + context.response() + .setResponseStatus(HttpResponseStatus.FORBIDDEN) + .write("Forbidden"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/InternalServerErrorHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/InternalServerErrorHandler.java new file mode 100644 index 0000000..8680cc3 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/InternalServerErrorHandler.java @@ -0,0 +1,39 @@ +package org.xbib.net.http.server.handler; + +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class InternalServerErrorHandler implements HttpErrorHandler { + + private static final Logger logger = Logger.getLogger(InternalServerErrorHandler.class.getName()); + + public InternalServerErrorHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + Throwable throwable = context.attributes().get(Throwable.class, "_throwable"); + if (throwable != null) { + logger.log(Level.SEVERE, throwable.getMessage(), throwable); + } + HttpResponseStatus status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + String message; + if (throwable instanceof HttpException) { + HttpException httpException = (HttpException) throwable; + status = httpException.getResponseStatus(); + message = httpException.getMessage(); + } else { + message = throwable != null ? throwable.getMessage() : ""; + } + context.response() + .setResponseStatus(status) + .setContentType("text/plain;charset=utf-8") + .write(message); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/NotFoundHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/NotFoundHandler.java new file mode 100644 index 0000000..766b700 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/NotFoundHandler.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.server.handler; + +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; + +public class NotFoundHandler implements HttpErrorHandler { + + public NotFoundHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + context.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .setContentType("text/plain;charset=utf-8") + .write("Not found"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/NotImplementedHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/NotImplementedHandler.java new file mode 100644 index 0000000..9b55eef --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/NotImplementedHandler.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.server.handler; + +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; + +public class NotImplementedHandler implements HttpErrorHandler { + + public NotImplementedHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + context.response() + .setResponseStatus(HttpResponseStatus.NOT_IMPLEMENTED) + .setContentType("text/plain;charset=utf-8") + .write("Not implemented"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/UnauthorizedHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/UnauthorizedHandler.java new file mode 100644 index 0000000..123b62a --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/UnauthorizedHandler.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server.handler; + +import java.io.IOException; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +public class UnauthorizedHandler implements HttpErrorHandler { + + public UnauthorizedHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + context.response() + .setResponseStatus(HttpResponseStatus.UNAUTHORIZED) + .write("Unauthorized"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/handler/VersionNotSupportedHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/handler/VersionNotSupportedHandler.java new file mode 100644 index 0000000..3b04b06 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/handler/VersionNotSupportedHandler.java @@ -0,0 +1,20 @@ +package org.xbib.net.http.server.handler; + +import java.io.IOException; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +public class VersionNotSupportedHandler implements HttpErrorHandler { + + public VersionNotSupportedHandler() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + context.response() + .setResponseStatus(HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED) + .setContentType("text/plain;charset=utf-8") + .write("HTTP version not supported"); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/CallbackHandlerImpl.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/CallbackHandlerImpl.java new file mode 100644 index 0000000..50e31c2 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/CallbackHandlerImpl.java @@ -0,0 +1,35 @@ +package org.xbib.net.http.server.ldap; + +import java.io.IOException; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class CallbackHandlerImpl implements CallbackHandler { + + private final String name; + + private final String password; + + public CallbackHandlerImpl(String name, String password) { + this.name = name; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException, IOException { + for (Callback callBack : callbacks) { + if (callBack instanceof NameCallback) { + NameCallback nameCallback = (NameCallback) callBack; + nameCallback.setName(name); + } else if (callBack instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback) callBack; + passwordCallback.setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callBack, "Callback not supported"); + } + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/Krb5LoginConfiguration.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/Krb5LoginConfiguration.java new file mode 100644 index 0000000..781b7f2 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/Krb5LoginConfiguration.java @@ -0,0 +1,29 @@ +package org.xbib.net.http.server.ldap; + +import java.util.HashMap; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +public class Krb5LoginConfiguration extends Configuration { + + private static final AppConfigurationEntry[] CONFIG_LIST = new AppConfigurationEntry[1]; + + static { + String loginModule = "com.sun.security.auth.module.Krb5LoginModule"; + AppConfigurationEntry.LoginModuleControlFlag flag = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; + CONFIG_LIST[0] = new AppConfigurationEntry(loginModule, flag, new HashMap<>()); + } + + public Krb5LoginConfiguration() { + super(); + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String applicationName) { + return CONFIG_LIST.clone(); + } + + @Override + public void refresh() { + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapAuthenticator.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapAuthenticator.java new file mode 100644 index 0000000..a37e61b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapAuthenticator.java @@ -0,0 +1,117 @@ +package org.xbib.net.http.server.ldap; + +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchResult; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import org.xbib.net.Authenticator; + +public class LdapAuthenticator extends Authenticator { + + private static final Logger logger = Logger.getLogger(LdapAuthenticator.class.getName()); + + private final Map contextFactories; + + private final Map userMappings; + + public LdapAuthenticator(Map contextFactories, + Map userMappings) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + } + + @Override + public boolean authenticate(Context context) { + return authenticate(context.getUsername(), context.getPassword()); + } + + /** + * Authenticate the user against LDAP servers until first success. + * @param login The login to use. + * @param password The password to use. + * @return false if specified user cannot be authenticated with specified password on any LDAP server + */ + public boolean authenticate(String login, String password) { + for (String ldapKey : userMappings.keySet()) { + final String principal; + if (contextFactories.get(ldapKey).isSasl()) { + principal = login; + } else { + final SearchResult result; + try { + result = userMappings.get(ldapKey).createSearch(contextFactories.get(ldapKey), login).findUnique(); + } catch (NamingException e) { + logger.log(Level.FINE, "user " + login + " not found in server " + ldapKey + ": " + e.getMessage()); + continue; + } + if (result == null) { + logger.log(Level.FINE, "user " + login + " not found in " + ldapKey); + continue; + } + principal = result.getNameInNamespace(); + } + boolean passwordValid; + if (contextFactories.get(ldapKey).isGssapi()) { + passwordValid = checkPasswordUsingGssapi(principal, password, ldapKey); + } else { + passwordValid = checkPasswordUsingBind(principal, password, ldapKey); + } + if (passwordValid) { + return true; + } + } + logger.log(Level.FINE, "user not found: " + login); + return false; + } + + private boolean checkPasswordUsingBind(String principal, String password, String ldapKey) { + if (password.isEmpty()) { + logger.log(Level.FINE, "password is blank"); + return false; + } + InitialDirContext context = null; + try { + context = contextFactories.get(ldapKey).createUserContext(principal, password); + return true; + } catch (NamingException e) { + logger.log(Level.FINE, "password not valid for user " + principal + " in server " + ldapKey + ": " + e.getMessage()); + return false; + } finally { + closeQuietly(context); + } + } + + private boolean checkPasswordUsingGssapi(String principal, String password, String ldapKey) { + Configuration.setConfiguration(new Krb5LoginConfiguration()); + LoginContext lc; + try { + lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, password)); + lc.login(); + } catch (LoginException e) { + logger.log(Level.FINE, "password not valid for " + principal + " in server " + ldapKey + ": " + e.getMessage()); + return false; + } + try { + lc.logout(); + } catch (LoginException e) { + logger.log(Level.WARNING, "logout fails", e); + } + return true; + } + + private static void closeQuietly(javax.naming.Context context) { + if (context == null) { + return; + } + try { + context.close(); + } catch (NamingException e) { + logger.log(Level.WARNING, "NamingException thrown while closing context", e); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapContextFactory.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapContextFactory.java new file mode 100644 index 0000000..3d8c47b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapContextFactory.java @@ -0,0 +1,210 @@ +package org.xbib.net.http.server.ldap; + +import java.io.IOException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.StartTlsRequest; +import javax.naming.ldap.StartTlsResponse; +import javax.security.auth.Subject; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +public class LdapContextFactory { + + private static final Logger logger = Logger.getLogger(LdapContextFactory.class.getName()); + + public static final String AUTH_METHOD_GSSAPI = "GSSAPI"; + + public static final String AUTH_METHOD_DIGEST_MD5 = "DIGEST-MD5"; + + public static final String AUTH_METHOD_CRAM_MD5 = "CRAM-MD5"; + + public static final String REFERRALS_FOLLOW_MODE = "follow"; + + public static final String REFERRALS_IGNORE_MODE = "ignore"; + + public static final String DEFAULT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; + + /** + * The Sun LDAP property used to enable connection pooling. + * This is used in the default implementation to enable LDAP connection pooling. + */ + private static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool"; + + private static final String SASL_REALM_PROPERTY = "java.naming.security.sasl.realm"; + + private final String providerUrl; + + private final boolean startTLS; + + private final String authentication; + + private final String factory; + + private final String username; + + private final String password; + + private final String realm; + + private final String referral; + + public LdapContextFactory(String authentication, + String factory, + String realm, + String providerUrl, + boolean startTLS, + String username, + String password, + String referral) { + this.authentication = authentication; + this.factory = factory; + this.realm = realm; + this.providerUrl = providerUrl; + this.startTLS = startTLS; + this.username = username; + this.password = password; + this.referral = referral; + } + + public InitialDirContext createBindContext() throws NamingException { + if (isGssapi()) { + return createInitialDirContextUsingGssapi(username, password); + } else { + return createInitialDirContext(username, password, true); + } + } + + public String getProviderUrl() { + return providerUrl; + } + + public String getReferral() { + return referral; + } + + public InitialDirContext createUserContext(String principal, String credentials) throws NamingException { + return createInitialDirContext(principal, credentials, false); + } + + private InitialDirContext createInitialDirContext(String principal, String credentials, boolean pooling) throws NamingException { + final InitialLdapContext ctx; + if (startTLS) { + Properties env = new Properties(); + env.put(Context.INITIAL_CONTEXT_FACTORY, factory); + env.put(Context.PROVIDER_URL, providerUrl); + env.put(Context.REFERRAL, referral); + logger.log(Level.FINE, "new initial LDAP context: " + env); + ctx = new InitialLdapContext(env, null); + // http://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html + StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest()); + try { + tls.negotiate(); + } catch (IOException e) { + NamingException ex = new NamingException("StartTLS failed"); + ex.initCause(e); + throw ex; + } + ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, authentication); + if (principal != null) { + ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal); + } + if (credentials != null) { + ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + } + ctx.reconnect(null); + } else { + Properties env = getEnvironment(principal, credentials, pooling); + logger.log(Level.FINE, "new initial LDAP context: " + env); + ctx = new InitialLdapContext(env, null); + } + return ctx; + } + + private InitialDirContext createInitialDirContextUsingGssapi(String principal, String credentials) throws NamingException { + Configuration.setConfiguration(new Krb5LoginConfiguration()); + InitialDirContext initialDirContext; + try { + LoginContext lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, credentials)); + lc.login(); + initialDirContext = Subject.doAs(lc.getSubject(), (PrivilegedExceptionAction) () -> { + Properties env = new Properties(); + env.put(Context.INITIAL_CONTEXT_FACTORY, factory); + env.put(Context.PROVIDER_URL, providerUrl); + env.put(Context.REFERRAL, referral); + logger.log(Level.FINE, "new initial LDAP context: " + env); + return new InitialLdapContext(env, null); + }); + } catch (LoginException | PrivilegedActionException e) { + NamingException namingException = new NamingException(e.getMessage()); + namingException.initCause(e); + throw namingException; + } + return initialDirContext; + } + + private Properties getEnvironment(String principal, String credentials, boolean pooling) { + Properties env = new Properties(); + env.put(Context.SECURITY_AUTHENTICATION, authentication); + if (realm != null) { + env.put(SASL_REALM_PROPERTY, realm); + } + if (pooling) { + env.put(SUN_CONNECTION_POOLING_PROPERTY, "true"); + } + env.put(Context.INITIAL_CONTEXT_FACTORY, factory); + env.put(Context.PROVIDER_URL, providerUrl); + env.put(Context.REFERRAL, referral); + if (principal != null) { + env.put(Context.SECURITY_PRINCIPAL, principal); + } + if (credentials != null) { + env.put(Context.SECURITY_CREDENTIALS, credentials); + } + return env; + } + + public boolean isSasl() { + return AUTH_METHOD_DIGEST_MD5.equals(authentication) || + AUTH_METHOD_CRAM_MD5.equals(authentication) || + AUTH_METHOD_GSSAPI.equals(authentication); + } + + public boolean isGssapi() { + return AUTH_METHOD_GSSAPI.equals(authentication); + } + + public void testConnection() { + if (username.isBlank() && isSasl()) { + throw new IllegalArgumentException("when using SASL, property bindDn is required"); + } + try { + createBindContext(); + logger.log(Level.INFO, "test LDAP connection on " + providerUrl + ": OK"); + } catch (NamingException e) { + logger.info("test LDAP connection: FAIL"); + throw new LdapException("Unable to open LDAP connection", e); + } + } + + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "url=" + providerUrl + + ", authentication=" + authentication + + ", factory=" + factory + + ", bindDn=" + username + + ", realm=" + realm + + ", referral=" + referral + + "}"; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapException.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapException.java new file mode 100644 index 0000000..cc2c697 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapException.java @@ -0,0 +1,15 @@ +package org.xbib.net.http.server.ldap; + + +@SuppressWarnings("serial") +public class LdapException extends RuntimeException { + + public LdapException(String message) { + super(message); + } + + public LdapException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupMapping.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupMapping.java new file mode 100644 index 0000000..1f99d65 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupMapping.java @@ -0,0 +1,81 @@ +package org.xbib.net.http.server.ldap; + +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.SearchResult; + +public class LdapGroupMapping { + + private static final String DEFAULT_OBJECT_CLASS = "groupOfUniqueNames"; + private static final String DEFAULT_ID_ATTRIBUTE = "cn"; + private static final String DEFAULT_MEMBER_ATTRIBUTE = "uniqueMember"; + private static final String DEFAULT_FILTER = "(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))"; + + private final String baseDn; + + private final String idAttribute; + + private final String filter; + + private final String[] filterArgNames; + + public LdapGroupMapping(String baseDn, + String idAttribute, + String filter, + String[] filterArgNames) { + this.baseDn = baseDn; + this.idAttribute = idAttribute; + this.filter = filter; + this.filterArgNames = filterArgNames; + } + + /** + * Search for this mapping. + */ + public LdapSearch createSearch(LdapContextFactory contextFactory, SearchResult searchResult) { + String[] filterArgs = new String[filterArgNames.length]; + for (int i = 0; i < filterArgs.length; i++) { + String name = filterArgNames[i]; + if ("dn".equals(name)) { + filterArgs[i] = searchResult.getNameInNamespace(); + } else { + filterArgs[i] = getAttributeValue(searchResult, name); + } + } + return new LdapSearch(contextFactory) + .setBaseDn(baseDn) + .setFilter(filter) + .setFilterArgs(filterArgs) + .returns(idAttribute); + } + + private static String getAttributeValue(SearchResult user, String attributeId) { + Attribute attribute = user.getAttributes().get(attributeId); + if (attribute == null) { + return null; + } + try { + return (String) attribute.get(); + } catch (NamingException e) { + throw new IllegalArgumentException(e); + } + } + + public String getIdAttribute() { + return idAttribute; + } + + public String[] getFilterArgNames() { + return filterArgNames; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "baseDn=" + baseDn + + ", idAttribute=" + idAttribute + + ", filter=" + filter + + ", filterArgs=" + filterArgNames + + "}"; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupsProvider.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupsProvider.java new file mode 100644 index 0000000..cf7ee3b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapGroupsProvider.java @@ -0,0 +1,121 @@ +package org.xbib.net.http.server.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchResult; +import org.xbib.net.GroupsProvider; + +public class LdapGroupsProvider extends GroupsProvider { + + private static final Logger logger = Logger.getLogger(LdapGroupsProvider.class.getName()); + + private final Map contextFactories; + + private final Map userMappings; + + private final Map groupMappings; + + public LdapGroupsProvider(Map contextFactories, + Map userMappings, + Map groupMapping) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + this.groupMappings = groupMapping; + } + + @Override + public Collection getGroups(Context context) { + return getGroups(context.getUsername()); + } + + /** + * Get groups, or null if not possible. + * @throws LdapException if unable to retrieve groups + */ + public Collection getGroups(String username) { + if (userMappings == null) { + return null; + } + if (groupMappings == null) { + return null; + } + if (userMappings.isEmpty()) { + throw new LdapException(String.format("Unable to retrieve details for user " + username + ": No user mapping found")); + } + if (groupMappings.isEmpty()) { + throw new LdapException(String.format("Unable to retrieve details for user " + username + ": No group mapping found")); + } + Set groups = new LinkedHashSet<>(); + List exceptions = new ArrayList<>(); + for (String serverKey : userMappings.keySet()) { + if (!groupMappings.containsKey(serverKey)) { + continue; + } + SearchResult searchResult = searchUserGroups(username, exceptions, serverKey); + if (searchResult != null) { + try { + NamingEnumeration result = groupMappings.get(serverKey) + .createSearch(contextFactories.get(serverKey), searchResult).find(); + groups.addAll(mapGroups(serverKey, result)); + break; + } catch (NamingException e) { + logger.log(Level.FINE, e.getMessage(), e); + exceptions.add(new LdapException(String.format("unable to retrieve groups for user %s in %s", username, serverKey), e)); + } + } + } + checkResults(groups, exceptions); + return groups; + } + + private static void checkResults(Set groups, List exceptions) { + if (groups.isEmpty() && !exceptions.isEmpty()) { + throw exceptions.iterator().next(); + } + } + + private void checkPrerequisites(String username) { + } + + private SearchResult searchUserGroups(String username, List exceptions, String serverKey) { + SearchResult searchResult = null; + try { + logger.log(Level.INFO, "requesting groups for user " + username); + searchResult = userMappings.get(serverKey).createSearch(contextFactories.get(serverKey), username) + .returns(groupMappings.get(serverKey).getFilterArgNames()) + .findUnique(); + } catch (NamingException e) { + logger.log(Level.FINE, e.getMessage(), e); + exceptions.add(new LdapException(String.format("unable to retrieve groups for user %s in %s", username, serverKey), e)); + } + return searchResult; + } + + /** + * Map all the groups. + * + * @param serverKey The index we use to choose the correct {@link LdapGroupMapping}. + * @param searchResult The {@link SearchResult} from the search for the user. + * @return A {@link Collection} of groups the user is member of. + * @throws NamingException if name not found + */ + private Collection mapGroups(String serverKey, NamingEnumeration searchResult) throws NamingException { + Set groups = new LinkedHashSet<>(); + while (searchResult.hasMoreElements()) { + SearchResult obj = searchResult.nextElement(); + Attributes attributes = obj.getAttributes(); + String groupId = (String) attributes.get(groupMappings.get(serverKey).getIdAttribute()).get(); + groups.add(groupId); + } + return groups; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapRealm.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapRealm.java new file mode 100644 index 0000000..1983108 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapRealm.java @@ -0,0 +1,59 @@ +package org.xbib.net.http.server.ldap; + +import java.util.Map; +import org.xbib.net.Authenticator; +import org.xbib.net.GroupsProvider; +import org.xbib.net.UsersProvider; +import org.xbib.net.SecurityRealm; + +public class LdapRealm extends SecurityRealm { + + private final String name; + + private final Map contextFactories; + + private final LdapUsersProvider usersProvider; + + private final LdapGroupsProvider groupsProvider; + + private final LdapAuthenticator authenticator; + + public LdapRealm(String name, + Map contextFactories, + Map userMappings, + Map groupMappings) { + this.name = name; + this.contextFactories = contextFactories; + this.usersProvider = new LdapUsersProvider(contextFactories, userMappings); + this.groupsProvider = new LdapGroupsProvider(contextFactories, userMappings, groupMappings); + this.authenticator = new LdapAuthenticator(contextFactories, userMappings); + } + + @Override + public String getName() { + return name; + } + + @Override + public void init() { + for (LdapContextFactory contextFactory : contextFactories.values()) { + contextFactory.testConnection(); + } + } + + @Override + public Authenticator getAuthenticator() { + return authenticator; + } + + @Override + public UsersProvider getUsersProvider() { + return usersProvider; + } + + @Override + public GroupsProvider getGroupsProvider() { + return groupsProvider; + } + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapSearch.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapSearch.java new file mode 100644 index 0000000..800a9d6 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapSearch.java @@ -0,0 +1,174 @@ +package org.xbib.net.http.server.ldap; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.PartialResultException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; + +public class LdapSearch { + + private static final Logger logger = Logger.getLogger(LdapSearch.class.getName()); + + private final LdapContextFactory contextFactory; + + private String baseDn; + + private int scope = SearchControls.SUBTREE_SCOPE; + + private String filter; + + private String[] filterArgs; + + private String[] returningAttributes; + + public LdapSearch(LdapContextFactory contextFactory) { + this.contextFactory = contextFactory; + } + + public LdapSearch setBaseDn(String baseDn) { + this.baseDn = baseDn; + return this; + } + + public LdapSearch setScope(int scope) { + this.scope = scope; + return this; + } + + public LdapSearch setFilter(String filter) { + this.filter = filter; + return this; + } + + public LdapSearch setFilterArgs(String... filterArgs) { + this.filterArgs = filterArgs; + return this; + } + + public LdapSearch returns(String... returningAttributes) { + this.returningAttributes = returningAttributes; + return this; + } + + /** + * Find results. + * @throws NamingException if unable to perform search + */ + public NamingEnumeration find() throws NamingException { + logger.log(Level.FINE, "find: " + this); + NamingEnumeration result; + InitialDirContext context = null; + boolean ok = false; + try { + context = contextFactory.createBindContext(); + SearchControls controls = new SearchControls(); + controls.setSearchScope(scope); + controls.setReturningAttributes(returningAttributes); + result = context.search(baseDn, filter, filterArgs, controls); + logger.log(Level.FINE, "result = " + result + " hasMore = " + result.hasMore()); + ok = true; + } finally { + close(context, ok); + } + return result; + } + + /** + * Find unique. + * @return result, or null if not found + * @throws NamingException if unable to perform search, or non unique result + */ + public SearchResult findUnique() throws NamingException { + logger.log(Level.FINE, "find unique: " + this); + NamingEnumeration result = find(); + if (hasMore(result)) { + SearchResult obj = result.next(); + if (!hasMore(result)) { + logger.log(Level.FINE, "find unique result = " + obj); + return obj; + } + throw new NamingException("Non unique result"); + } + logger.log(Level.FINE, "find unique: no results"); + return null; + } + + private static boolean hasMore(NamingEnumeration result) throws NamingException { + try { + return result.hasMore(); + } catch (PartialResultException e) { + logger.log(Level.FINE, "more result might be forthcoming if the referral is followed", e); + // See http://docs.oracle.com/javase/jndi/tutorial/ldap/referral/jndi.html : + // When the LDAP service provider receives a referral despite your having set Context.REFERRAL to "ignore", it will throw a + // PartialResultException(in the API reference documentation) to indicate that more results might be forthcoming if the referral is + // followed. In this case, the server does not support the Manage Referral control and is supporting referral updates in some other + // way. + return false; + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "baseDn=" + baseDn + + ", scope=" + scopeToString() + + ", filter=" + filter + + ", filterArgs=" + Arrays.toString(filterArgs) + + ", returningAttributes=" + Arrays.toString(returningAttributes) + + "}"; + } + + private String scopeToString() { + switch (scope) { + case SearchControls.ONELEVEL_SCOPE: + return "onelevel"; + case SearchControls.OBJECT_SCOPE: + return "object"; + case SearchControls.SUBTREE_SCOPE: + default: + return "subtree"; + } + } + + /** + *
+     * public void useContextNicely() throws NamingException {
+     *   InitialDirContext context = null;
+     *   boolean threw = true;
+     *   try {
+     *     context = new InitialDirContext();
+     *     // Some code which does something with the Context and may throw a NamingException
+     *     threw = false; // No throwable thrown
+     *   } finally {
+     *     // Close context
+     *     // If an exception occurs, only rethrow it if (threw==false)
+     *     close(context, threw);
+     *   }
+     * }
+     * 
+ * + * @param context the {@code Context} object to be closed, or null, in which case this method does nothing + * @param swallowIOException if true, don't propagate {@code NamingException} thrown by the {@code close} method + * @throws NamingException if {@code swallowIOException} is false and {@code close} throws a {@code NamingException}. + */ + private static void close(Context context, boolean swallowIOException) throws NamingException { + if (context == null) { + return; + } + try { + context.close(); + } catch (NamingException e) { + if (swallowIOException) { + logger.log(Level.WARNING, "NamingException thrown while closing context.", e); + } else { + throw e; + } + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUserMapping.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUserMapping.java new file mode 100644 index 0000000..284af7d --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUserMapping.java @@ -0,0 +1,49 @@ +package org.xbib.net.http.server.ldap; + +public class LdapUserMapping { + + private final String baseDn; + + private final String filter; + + private final String uidAttribute; + + private final String realNameAttribute; + + public LdapUserMapping(String baseDn, + String filter, + String uidAttribute, + String realNameAttribute) { + this.baseDn = baseDn; + this.filter = filter; + this.uidAttribute = uidAttribute; + this.realNameAttribute = realNameAttribute; + } + + public LdapSearch createSearch(LdapContextFactory contextFactory, String username) { + return new LdapSearch(contextFactory) + .setBaseDn(baseDn) + .setFilter(filter) + .setFilterArgs(username) + .returns(uidAttribute, realNameAttribute); + } + + public String getUidAttribute() { + return uidAttribute; + } + + public String getRealNameAttribute() { + return realNameAttribute; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "baseDn=" + baseDn + + ",filter=" + filter + + ",uidAttribute=" + uidAttribute + + ",realNameAttribute=" + realNameAttribute + + "}"; + } + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUsersProvider.java b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUsersProvider.java new file mode 100644 index 0000000..9d23876 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/ldap/LdapUsersProvider.java @@ -0,0 +1,104 @@ +package org.xbib.net.http.server.ldap; + +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchResult; +import org.xbib.net.UsersProvider; +import org.xbib.net.UserDetails; + +import static java.lang.String.format; + +public class LdapUsersProvider extends UsersProvider { + + private static final Logger logger = Logger.getLogger(LdapUsersProvider.class.getName()); + + private final Map contextFactories; + + private final Map userMappings; + + public LdapUsersProvider(Map contextFactories, + Map userMappings) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + } + + @Override + public UserDetails getUserDetails(Context context) { + return getUserDetails(context.getUsername()); + } + + /** + * Get user details. + * + * @return details for specified user, or null if such user doesn't exist + * @throws LdapException if unable to retrieve details + */ + public UserDetails getUserDetails(String username) { + logger.log(Level.FINE, "requesting details for user " + username); + if (userMappings.isEmpty()) { + String errorMessage = format("Unable to retrieve details for user %s: No user mapping found.", username); + logger.log(Level.FINE, errorMessage); + throw new LdapException(errorMessage); + } + UserDetails details = null; + LdapException exception = null; + for (String serverKey : userMappings.keySet()) { + SearchResult searchResult = null; + try { + searchResult = userMappings.get(serverKey).createSearch(contextFactories.get(serverKey), username) + .returns(userMappings.get(serverKey).getRealNameAttribute(), + userMappings.get(serverKey).getUidAttribute()) + .findUnique(); + } catch (NamingException e) { + // just in case if Sonar silently swallowed exception + logger.log(Level.FINE, e.getMessage(), e); + exception = new LdapException("Unable to retrieve details for user " + username + " in " + serverKey, e); + } + if (searchResult != null) { + try { + details = mapUserDetails(serverKey, searchResult); + // if no exceptions occur, we found the user and mapped his details. + break; + } catch (NamingException e) { + // just in case if Sonar silently swallowed exception + logger.log(Level.FINE, e.getMessage(), e); + exception = new LdapException("Unable to retrieve details for user " + username + " in " + serverKey, e); + } + } else { + // user not found + logger.log(Level.FINE, "User " + username + " not found in " + serverKey); + } + } + if (details == null && exception != null) { + // No user found and there is an exception so there is a reason the user could not be found. + throw exception; + } + return details; + } + + /** + * Map the properties from LDAP to the {@link UserDetails}. + * + * @param serverKey the LDAP index so we use the correct {@link LdapUserMapping} + * @return If no exceptions are thrown, a {@link UserDetails} object containing the values from LDAP. + * @throws NamingException In case the communication or mapping to the LDAP server fails. + */ + private UserDetails mapUserDetails(String serverKey, SearchResult searchResult) throws NamingException { + Attributes attributes = searchResult.getAttributes(); + UserDetails details = new UserDetails(); + details.setUserId(getAttributeValue(attributes.get(userMappings.get(serverKey).getUidAttribute()))); + details.setName(getAttributeValue(attributes.get(userMappings.get(serverKey).getRealNameAttribute()))); + return details; + } + + private static String getAttributeValue(Attribute attribute) throws NamingException { + if (attribute == null) { + return ""; + } + return (String) attribute.get(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/AbstractPersistenceStore.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/AbstractPersistenceStore.java new file mode 100644 index 0000000..fd25eaf --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/AbstractPersistenceStore.java @@ -0,0 +1,77 @@ +package org.xbib.net.http.server.persist; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@SuppressWarnings("serial") +public abstract class AbstractPersistenceStore extends LinkedHashMap implements PersistenceStore { + + private final ReentrantReadWriteLock lock; + + final Codec> codec; + + final String storeName; + + public AbstractPersistenceStore(Codec> codec, + String storeName) { + super(); + this.codec = codec; + this.storeName = storeName; + this.lock = new ReentrantReadWriteLock(); + } + + @Override + public Codec> getCodec() { + return codec; + } + + @Override + public void load() throws IOException { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + clear(); + Map map = codec.read(storeName); + if (map != null) { + putAll(map); + } + } finally { + readLock.unlock(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void insertValue(String key, Object value) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + putIfAbsent(key, new ArrayList<>()); + List list = (List) get(key); + list.add(value); + put(key, list); + codec.write(storeName, this); + } finally { + writeLock.unlock(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void removeValue(String key, Object value) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + putIfAbsent(key, new ArrayList<>()); + List list = (List) get(key); + list.remove(value); + put(key, list); + codec.write(storeName, this); + } finally { + writeLock.unlock(); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java new file mode 100644 index 0000000..4f52bb6 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java @@ -0,0 +1,16 @@ +package org.xbib.net.http.server.persist; + +import java.io.IOException; + +public interface Codec { + + D create(String key) throws IOException; + + D read(String key) throws IOException; + + void write(String key, D data) throws IOException; + + void remove(String key) throws IOException; + + void purge(long expiredAfterSeconds) throws IOException; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/PersistenceStore.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/PersistenceStore.java new file mode 100644 index 0000000..ea5c829 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/PersistenceStore.java @@ -0,0 +1,16 @@ +package org.xbib.net.http.server.persist; + +import java.io.IOException; +import java.util.Map; + +public interface PersistenceStore extends Map { + + Codec> getCodec(); + + void load() throws IOException; + + void insertValue(K key, V value) throws IOException; + + void removeValue(K key, V value) throws IOException; + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java new file mode 100644 index 0000000..dc3becf --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java @@ -0,0 +1,75 @@ +package org.xbib.net.http.server.persist.file; + +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.util.JsonUtil; + +public class FileJsonCodec implements Codec> { + + private final ReentrantReadWriteLock lock; + + private final String root; + + public FileJsonCodec(String root) { + this.root = root; + this.lock = new ReentrantReadWriteLock(); + } + + @Override + public Map create(String key) throws IOException { + return JsonUtil.toMap("{}"); + } + + @Override + public Map read(String key) throws IOException { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + Path path = openOrCreate(key); + return Files.exists(path) ? JsonUtil.toMap(Files.readString(path)) : null; + } finally { + readLock.unlock(); + } + } + + @Override + public void write(String key, Map data) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + Path p = openOrCreate(key); + try (Writer writer = Files.newBufferedWriter(p)) { + writer.write(JsonUtil.toString(data)); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void remove(String key) { + try { + Path path = openOrCreate(key); + Files.deleteIfExists(path); + } catch (IOException e) { + // + } + } + + @Override + public void purge(long expiredAfterSeconds) { + // unable to purge + } + + private Path openOrCreate(String key) throws IOException { + Path path = Paths.get(root); + Files.createDirectories(path); + return path.resolve(key); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java new file mode 100644 index 0000000..9698b3a --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.server.persist.file; + +import java.util.Map; +import org.xbib.net.http.server.persist.AbstractPersistenceStore; +import org.xbib.net.http.server.persist.Codec; + +@SuppressWarnings("serial") +public class FileJsonPersistenceStore extends AbstractPersistenceStore { + + public FileJsonPersistenceStore(String name) { + this("/var/tmp/net-http-server-store", name); + } + + public FileJsonPersistenceStore(String root, String storeName) { + this(new FileJsonCodec(root), storeName); + } + + public FileJsonPersistenceStore(Codec> codec, String storeName) { + super(codec, storeName); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java new file mode 100644 index 0000000..01190c6 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java @@ -0,0 +1,97 @@ +package org.xbib.net.http.server.persist.file; + +import org.xbib.net.http.server.persist.Codec; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class FilePropertiesCodec implements Codec> { + + private final ReentrantReadWriteLock lock; + + private final String root; + + public FilePropertiesCodec(String root) { + this.root = root; + this.lock = new ReentrantReadWriteLock(); + } + + @Override + public Map create(String key) throws IOException { + return new LinkedHashMap<>(); + } + + @Override + public Map read(String key) throws IOException { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + Path path = openOrCreate(key); + Properties properties = new Properties(); + if (Files.exists(path)) { + try (Reader reader = Files.newBufferedReader(path)) { + properties.load(reader); + } + } + return toMap(properties); + } finally { + readLock.unlock(); + } + } + + @Override + public void write(String key, Map data) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + Path path = openOrCreate(key); + Properties properties = toProperties(data); + try (Writer writer = Files.newBufferedWriter(path)) { + properties.store(writer, null); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void remove(String key) { + try { + Path path = openOrCreate(key); + Files.deleteIfExists(path); + } catch (IOException e) { + // + } + } + + @Override + public void purge(long expiredAfterSeconds) { + // unable to purge + } + + private Path openOrCreate(String key) throws IOException { + Path path = Paths.get(root); + Files.createDirectories(path); + return path.resolve(key); + } + + private Map toMap(Properties properties) { + Map map = new LinkedHashMap<>(); + properties.forEach((k, v) -> map.put(k.toString(), v)); + return map; + } + + private Properties toProperties(Map map) { + Properties properties = new Properties(); + properties.putAll(map); + return properties; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java new file mode 100644 index 0000000..7287d21 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java @@ -0,0 +1,74 @@ +package org.xbib.net.http.server.persist.memory; + +import org.xbib.net.http.server.persist.Codec; + +import java.io.IOException; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class MemoryPropertiesCodec implements Codec> { + + private final ReentrantReadWriteLock lock; + + private static final Map store = new HashMap<>(); + + public MemoryPropertiesCodec() { + this.lock = new ReentrantReadWriteLock(); + } + + @Override + public Map create(String key) throws IOException { + return new LinkedHashMap<>(); + } + + @Override + public Map read(String key) throws IOException { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + Properties properties = new Properties(); + properties.putAll((Map) store.get(key)); + return toMap(properties); + } finally { + readLock.unlock(); + } + } + + @Override + public void write(String key, Map data) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + Properties properties = toProperties(data); + store.put(key, properties); + } finally { + writeLock.unlock(); + } + } + + @Override + public void remove(String key) { + store.remove(key); + } + + @Override + public void purge(long expiredAfterSeconds) { + // unable to purge + } + + private Map toMap(Properties properties) { + Map map = new LinkedHashMap<>(); + properties.forEach((k, v) -> map.put(k.toString(), v)); + return map; + } + + private Properties toProperties(Map map) { + Properties properties = new Properties(); + properties.putAll(map); + return properties; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealm.java b/net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealm.java new file mode 100644 index 0000000..bb96cff --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealm.java @@ -0,0 +1,4 @@ +package org.xbib.net.http.server.realm; + +public class BaseRealm { +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealmBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealmBuilder.java new file mode 100644 index 0000000..88f383c --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/realm/BaseRealmBuilder.java @@ -0,0 +1,4 @@ +package org.xbib.net.http.server.realm; + +public class BaseRealmBuilder { +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/render/HttpResponseRenderer.java b/net-http-server/src/main/java/org/xbib/net/http/server/render/HttpResponseRenderer.java new file mode 100644 index 0000000..dc89a8d --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/render/HttpResponseRenderer.java @@ -0,0 +1,24 @@ +package org.xbib.net.http.server.render; + +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpResponse; +import org.xbib.net.http.server.HttpResponseBuilder; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; + +public class HttpResponseRenderer implements HttpHandler { + + public HttpResponseRenderer() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + HttpResponseBuilder httpResponseBuilder = context.response(); + // here we do the heavy lifting of rendering all elements for the response + HttpResponse httpResponse = httpResponseBuilder.build(); + if (httpResponseBuilder.shouldClose()) { + httpResponse.close(); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/AbstractResourceHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/AbstractResourceHandler.java new file mode 100644 index 0000000..09f5683 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/AbstractResourceHandler.java @@ -0,0 +1,449 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.Resource; +import org.xbib.net.URL; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.buffer.DataBufferUtil; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpHeaders; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpResponseBuilder; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.util.DateTimeUtil; +import org.xbib.net.mime.MimeTypeService; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.CharBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class AbstractResourceHandler implements HttpHandler { + + protected static final MimeTypeService mimeTypeService = new MimeTypeService(); + + private static final Logger logger = Logger.getLogger(AbstractResourceHandler.class.getName()); + + public AbstractResourceHandler() { + } + + protected abstract Resource createResource(HttpServerContext httpServerContext) throws IOException; + + protected abstract boolean isETagResponseEnabled(); + + protected abstract boolean isCacheResponseEnabled(); + + protected abstract boolean isRangeResponseEnabled(); + + protected abstract int getMaxAgeSeconds(); + + @Override + public void handle(HttpServerContext context) throws IOException { + logger.log(Level.FINE, "handle: before creating resource " + this.getClass().getName()); + Resource resource = createResource(context); + logger.log(Level.FINE, "handle: resource = " + (resource != null ? resource.getClass().getName() : null)); + if (resource == null || !resource.isExists()) { + logger.log(Level.FINER, "resource does not exist: " + resource); + throw new HttpException("resource not found", context, HttpResponseStatus.NOT_FOUND); + } else if (resource.isDirectory()) { + logger.log(Level.FINER, "we have a directory request"); + if (!resource.getResourcePath().isEmpty() && !resource.getResourcePath().endsWith("/")) { + URL url = context.request().getBaseURL(); + String loc = url.resolve(resource.getName() + '/') + .mutator() + .query(url.getQuery()) + .fragment(url.getFragment()) + .build() + .toString(); + logger.log(Level.FINER, "client must add a /, external redirect to = " + loc); + context.response() + .addHeader(HttpHeaderNames.LOCATION, loc) + .setResponseStatus(HttpResponseStatus.TEMPORARY_REDIRECT) + .build().flush(); // flush is important + } else if (resource.isExistsIndexFile()) { + // external redirect to default index file in this directory + logger.log(Level.FINER, "external redirect to default index file in this directory: " + resource.getIndexFileName()); + context.response() + .addHeader(HttpHeaderNames.LOCATION, resource.getIndexFileName()) + .setResponseStatus(HttpResponseStatus.TEMPORARY_REDIRECT) + .build().flush(); // flush is important + } else { + // send forbidden, we do not allow directory access + context.response() + .setResponseStatus(HttpResponseStatus.FORBIDDEN) + .build().flush(); // fluish is important + } + context.done(); + } else { + logger.log(Level.FINE, "handle: generate cacheable resource"); + generateCacheableResource(context, resource); + } + logger.log(Level.FINE, "handle: done"); + } + + private void generateCacheableResource(HttpServerContext context, Resource resource) throws IOException { + // if resource is length of 0, there is nothing to send. Do not send any content, + if (resource.getLength() == 0) { + context.response().build().flush(); + return; + } + HttpHeaders headers = context.request().getHeaders(); + logger.log(Level.FINE, "before generating resource, the response headers are " + context.response().getHeaders()); + String contentType = resource.getMimeType(); + context.response() + .addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + // heuristic for inline disposition + String disposition = "inline"; + if (!contentType.startsWith("text") && !contentType.startsWith("image")) { + String accept = context.request().getHeaders().get(HttpHeaderNames.ACCEPT); + disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment"; + } + if (resource.getBaseName() != null && resource.getSuffix() != null) { + context.response() + .addHeader(HttpHeaderNames.CONTENT_DISPOSITION, + disposition + ";filename=\"" + resource.getBaseName() + '.' + resource.getSuffix() + '"'); + } + long expirationMillis = System.currentTimeMillis() + 1000L * getMaxAgeSeconds(); + if (isCacheResponseEnabled()) { + context.response() + .addHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatRfc1123(expirationMillis)) + .addHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + getMaxAgeSeconds()); + } else { + context.response() + .addHeader(HttpHeaderNames.EXPIRES, "0") + .addHeader(HttpHeaderNames.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + } + boolean sent = false; + if (isETagResponseEnabled()) { + Instant lastModifiedInstant = resource.getLastModified(); + String eTag = Long.toHexString(resource.getResourcePath().hashCode() + lastModifiedInstant.toEpochMilli() + resource.getLength()); + Instant ifUnmodifiedSinceInstant = DateTimeUtil.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE)); + if (ifUnmodifiedSinceInstant != null && + ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { + context.response() + .setResponseStatus(HttpResponseStatus.PRECONDITION_FAILED) + .build().flush(); + return; + } + String ifMatch = headers.get(HttpHeaderNames.IF_MATCH); + if (ifMatch != null && !matches(ifMatch, eTag)) { + context.response() + .setResponseStatus(HttpResponseStatus.PRECONDITION_FAILED) + .build().flush(); + return; + } + String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH); + if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) { + context.response() + .addHeader(HttpHeaderNames.ETAG, eTag) + .addHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatRfc1123(expirationMillis)) + .setResponseStatus(HttpResponseStatus.NOT_MODIFIED) + .build().flush(); + return; + } + Instant ifModifiedSinceInstant = DateTimeUtil.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE)); + if (ifModifiedSinceInstant != null && + ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { + context.response() + .addHeader(HttpHeaderNames.ETAG, eTag) + .addHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatRfc1123(expirationMillis)) + .setResponseStatus(HttpResponseStatus.NOT_MODIFIED) + .build().flush(); + return; + } + context.response() + .addHeader(HttpHeaderNames.ETAG, eTag) + .addHeader(HttpHeaderNames.LAST_MODIFIED, DateTimeUtil.formatRfc1123(lastModifiedInstant)); + if (isRangeResponseEnabled()) { + performRangeResponse(context, resource, contentType, eTag, headers); + sent = true; + } + } + if (!sent) { + long length = resource.getLength(); + if (length > 0L) { + String string = Long.toString(resource.getLength()); + context.response() + .addHeader(HttpHeaderNames.CONTENT_LENGTH, string); + send(resource, HttpResponseStatus.OK, contentType, context, 0L, resource.getLength()); + } else { + send(resource, HttpResponseStatus.OK, contentType, context, 0L, -1L); + } + } + context.done(); + } + + private void performRangeResponse(HttpServerContext context, + Resource resource, + String contentType, String eTag, + HttpHeaders headers) throws IOException { + long length = resource.getLength(); + context.response().addHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes"); + Range full = new Range(0, length - 1, length); + List ranges = new ArrayList<>(); + String range = headers.get(HttpHeaderNames.RANGE); + if (range != null) { + if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { + context.response() + .addHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length) + .setResponseStatus(HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE) + .build(); + return; + } + String ifRange = headers.get(HttpHeaderNames.IF_RANGE); + if (ifRange != null && !ifRange.equals(eTag)) { + try { + Instant ifRangeTime = DateTimeUtil.parseDate(ifRange); + if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) { + ranges.add(full); + } + } catch (IllegalArgumentException ignore) { + ranges.add(full); + } + } + if (ranges.isEmpty()) { + for (String part : range.substring(6).split(",")) { + long start = sublong(part, 0, part.indexOf('-')); + long end = sublong(part, part.indexOf('-') + 1, part.length()); + if (start == -1L) { + start = length - end; + end = length - 1; + } else if (end == -1L || end > length - 1) { + end = length - 1; + } + if (start > end) { + context.response() + .addHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length) + .setResponseStatus(HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE) + .build(); + return; + } + ranges.add(new Range(start, end, length)); + } + } + } + if (ranges.isEmpty() || ranges.get(0) == full) { + context.response() + .addHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total) + .addHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length)); + send(resource, HttpResponseStatus.OK, contentType, context, full.start, full.length); + } else if (ranges.size() == 1) { + Range r = ranges.get(0); + context.response() + .addHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total) + .addHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length)); + send(resource, HttpResponseStatus.PARTIAL_CONTENT, contentType, context, r.start, r.length); + } else { + context.response() + .addHeader(HttpHeaderNames.CONTENT_TYPE, "multipart/byteranges; boundary=MULTIPART_BOUNDARY"); + StringBuilder sb = new StringBuilder(); + for (Range r : ranges) { + try { + DataBuffer dataBuffer = readBuffer(context.response(), resource.getURL(), r.start, r.length); + sb.append('\n') + .append("--MULTIPART_BOUNDARY").append('\n') + .append("content-type: ").append(contentType).append('\n') + .append("content-range: bytes ").append(r.start).append('-').append(r.end).append('/').append(r.total).append('\n') + .append(StandardCharsets.ISO_8859_1.decode(dataBuffer.asByteBuffer())) + .append('\n') + .append("--MULTIPART_BOUNDARY--").append('\n'); + } catch (URISyntaxException | IOException e) { + logger.log(Level.FINEST, e.getMessage(), e); + } + } + context.response() + .setResponseStatus(HttpResponseStatus.OK) + .setContentType(contentType) + .write(CharBuffer.wrap(sb), StandardCharsets.ISO_8859_1); + } + } + + private static boolean matches(String matchHeader, String toMatch) { + String[] matchValues = matchHeader.split("\\s*,\\s*"); + Arrays.sort(matchValues); + return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; + } + + private static long sublong(String value, int beginIndex, int endIndex) { + String substring = value.substring(beginIndex, endIndex); + return substring.length() > 0 ? Long.parseLong(substring) : -1; + } + + protected void send(Resource resource, + HttpResponseStatus httpResponseStatus, + String contentType, + HttpServerContext context, + long offset, long size) throws IOException { + if (resource instanceof HttpServerResource) { + logger.log(Level.FINE, "we have a server resource, render by resource"); + ((HttpServerResource) resource).render(context); + return; + } + URL url = resource.getURL(); + if (url == null) { + logger.log(Level.WARNING, "url is null, generating not found"); + context.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build(); + } else if (context.request().getMethod() == HttpMethod.HEAD) { + context.response() + .setResponseStatus(HttpResponseStatus.OK) + .setContentType(contentType) + .build(); + } else { + if ("file".equals(url.getScheme())) { + Path path = resource.getPath(); + try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(path)) { + send(fileChannel, httpResponseStatus, contentType, context.response(), offset, size); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage() + " path=" + path, e); + context.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build(); + } + } else { + try (InputStream inputStream = url.openStream()) { + send(inputStream, httpResponseStatus, contentType, context.response(), offset, size); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + context.response() + .setResponseStatus(HttpResponseStatus.NOT_FOUND) + .build(); + } + } + } + } + + protected void send(FileChannel fileChannel, + HttpResponseStatus httpResponseStatus, + String contentType, + HttpResponseBuilder responseBuilder, + long offset, long size) throws IOException { + if (fileChannel == null ) { + logger.log(Level.WARNING, "file channel is null, generating not found"); + responseBuilder.setResponseStatus(HttpResponseStatus.NOT_FOUND).build(); + } else { + fileChannel = fileChannel.position(offset); + try (ReadableByteChannel channel = fileChannel) { + responseBuilder.setResponseStatus(httpResponseStatus) + .setContentType(contentType) + .write(DataBufferUtil.readBuffer(responseBuilder.getDataBufferFactory(), channel, size)); + } + } + } + + protected void send(InputStream inputStream, + HttpResponseStatus httpResponseStatus, + String contentType, + HttpResponseBuilder responseBuilder, + long offset, long size) throws IOException { + if (inputStream == null) { + logger.log(Level.WARNING, "inputstream is null, generating not found"); + responseBuilder.setResponseStatus(HttpResponseStatus.NOT_FOUND).build(); + } else { + long n = inputStream.skip(offset); + try (ReadableByteChannel channel = Channels.newChannel(inputStream)) { + responseBuilder + .setResponseStatus(httpResponseStatus) + .setContentType(contentType) + .write(DataBufferUtil.readBuffer(responseBuilder.getDataBufferFactory(), channel, size)); + } + } + } + + private DataBuffer readBuffer(HttpResponseBuilder responseBuilder, URL url, long offset, long size) throws IOException, URISyntaxException { + if ("file".equals(url.getScheme())) { + Path path = Paths.get(url.toURI()); + try (SeekableByteChannel channel = Files.newByteChannel(path)) { + channel.position(offset); + return DataBufferUtil.readBuffer(responseBuilder.getDataBufferFactory(), channel, size); + } + } else { + try (InputStream inputStream = url.openStream()) { + long n = inputStream.skip(offset); + try (ReadableByteChannel channel = Channels.newChannel(inputStream)) { + return DataBufferUtil.readBuffer(responseBuilder.getDataBufferFactory(), channel, size); + } + } + } + } + + protected static String basename(String path) { + return removeSuffix(getFileName(path)); + } + + protected static String suffix(String path) { + return extractSuffix(getFileName(path)); + } + + private static String extractSuffix(String filename) { + if (filename == null) { + return null; + } + int index = indexOfSuffix(filename); + return index == -1 ? null : filename.substring(index + 1); + } + + private static String removeSuffix(String filename) { + if (filename == null) { + return null; + } + int index = indexOfSuffix(filename); + return index == -1 ? filename : filename.substring(0, index); + } + + private static int indexOfSuffix(String filename) { + if (filename == null) { + return -1; + } + int suffixPos = filename.lastIndexOf('.'); + int lastSeparator = filename.lastIndexOf('/'); + return lastSeparator > suffixPos ? -1 : suffixPos; + } + + private static String getFileName(String path) { + if (path == null) { + return null; + } + return path.substring(path.lastIndexOf('/') + 1); + } + + private static boolean accepts(String acceptHeader, String toAccept) { + String[] acceptValues = acceptHeader.split("\\s*([,;])\\s*"); + Arrays.sort(acceptValues); + boolean b1 = Arrays.binarySearch(acceptValues, toAccept) > -1; + boolean b2 = Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1; + boolean b3 = Arrays.binarySearch(acceptValues, "*/*") > -1; + return b1 || b2 || b3; + } + + static class Range { + long start; + long end; + long length; + long total; + + Range(long start, long end, long total) { + this.start = start; + this.end = end; + this.length = end - start + 1; + this.total = total; + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/BaseResource.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/BaseResource.java new file mode 100644 index 0000000..ed63bc6 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/BaseResource.java @@ -0,0 +1,155 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.Resource; +import org.xbib.net.URL; + +import java.nio.file.Path; +import java.time.Instant; + +public class BaseResource implements Resource { + + private Path path; + + private String name; + + private String baseName; + + private String suffix; + + private String resourcePath; + + private URL url; + + private Instant lastModified; + + private long length; + + private boolean isExists; + + private boolean isDirectory; + + private boolean isExistsIndexFile; + + private String mimeType; + + private String indexFileName; + + public BaseResource() { + } + + public void setPath(Path path) { + this.path = path; + } + + public Path getPath() { + return path; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public void setBaseName(String baseName) { + this.baseName = baseName; + } + + @Override + public String getBaseName() { + return baseName; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + @Override + public String getSuffix() { + return suffix; + } + + public void setResourcePath(String resourcePath) { + this.resourcePath = resourcePath; + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + public void setURL(URL url) { + this.url = url; + } + + @Override + public URL getURL() { + return url; + } + + public void setLastModified(Instant lastModified) { + this.lastModified = lastModified; + } + + @Override + public Instant getLastModified() { + return lastModified; + } + + public void setLength(long length) { + this.length = length; + } + + @Override + public long getLength() { + return length; + } + + public void setExists(boolean exists) { + isExists = exists; + } + + @Override + public boolean isExists() { + return isExists; + } + + public void setDirectory(boolean directory) { + isDirectory = directory; + } + + @Override + public boolean isDirectory() { + return isDirectory; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + @Override + public String getMimeType() { + return mimeType; + } + + public void setIndexFileName(String indexFileName) { + this.indexFileName = indexFileName; + } + + @Override + public String getIndexFileName() { + return indexFileName; + } + + public void setExistsIndexFile(boolean isExistsIndexFile) { + this.isExistsIndexFile = isExistsIndexFile; + } + + @Override + public boolean isExistsIndexFile() { + return isExistsIndexFile; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/ClassLoaderResourceHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/ClassLoaderResourceHandler.java new file mode 100644 index 0000000..0ad8aae --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/ClassLoaderResourceHandler.java @@ -0,0 +1,186 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.PathNormalizer; +import org.xbib.net.Resource; +import org.xbib.net.URL; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.net.URLConnection; +import java.nio.file.Path; +import java.time.Instant; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ClassLoaderResourceHandler extends AbstractResourceHandler { + + private static final Logger logger = Logger.getLogger(ClassLoaderResourceHandler.class.getName()); + + private final ClassLoader classLoader; + + private final String prefix; + + public ClassLoaderResourceHandler(ClassLoader classLoader) { + this(classLoader, null); + } + + public ClassLoaderResourceHandler(ClassLoader classLoader, String prefix) { + this.classLoader = classLoader; + this.prefix = prefix; + } + + @Override + protected Resource createResource(HttpServerContext httpServerContext) throws IOException { + return new ClassLoaderResource(httpServerContext); + } + + @Override + protected boolean isETagResponseEnabled() { + return true; + } + + @Override + protected boolean isCacheResponseEnabled() { + return true; + } + + @Override + protected boolean isRangeResponseEnabled() { + return true; + } + + @Override + protected int getMaxAgeSeconds() { + return 24 * 3600; + } + + class ClassLoaderResource implements Resource { + + private final Path path; + + private final String name; + + private final String baseName; + + private final String suffix; + + private final String resourcePath; + + private final Instant lastModified; + + private final long length; + + private final String contentType; + + private URL url; + + ClassLoaderResource(HttpServerContext httpServerContext) throws IOException { + String effectivePath = httpServerContext.request().getRequestPath().substring(1); // httpServerContext.getEffectiveRequestPath(); + this.contentType = mimeTypeService.getContentType(effectivePath); + this.resourcePath = effectivePath.startsWith("/") ? effectivePath.substring(1) : effectivePath; + String path = prefix != null ? (prefix.endsWith("/") ? prefix : prefix + "/") : "/"; + path = resourcePath.startsWith("/") ? path + resourcePath.substring(1) : path + resourcePath; + String normalizedPath = PathNormalizer.normalize(resourcePath); + if (normalizedPath.startsWith("/")) { + normalizedPath = normalizedPath.substring(1); + } + this.path = null; + this.name = normalizedPath; + this.baseName = basename(name); + this.suffix = suffix(name); + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "trying: path=" + path + " classLoader=" + classLoader); + } + java.net.URL url = classLoader.getResource(path); + if (url != null) { + this.url = URL.create(url.toString()); + URLConnection urlConnection = url.openConnection(); + this.lastModified = Instant.ofEpochMilli(urlConnection.getLastModified()); + this.length = urlConnection.getContentLength(); + httpServerContext.done(); + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "success: path=[" + path + + "] -> url=" + url + " lastModified=" + lastModified + "length=" + length); + } + } else { + this.lastModified = Instant.now(); + this.length = 0; + logger.log(Level.WARNING, "fail: resource not found, url=" + url); + } + } + + @Override + public Path getPath() { + return path; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getBaseName() { + return baseName; + } + + @Override + public String getSuffix() { + return suffix; + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public Instant getLastModified() { + return lastModified; + } + + @Override + public long getLength() { + return length; + } + + @Override + public boolean isExists() { + return url != null; + } + + @Override + public boolean isDirectory() { + return resourcePath.isEmpty() || resourcePath.endsWith("/"); + } + + @Override + public String getMimeType() { + return contentType; + } + + @Override + public String getIndexFileName() { + return null; + } + + @Override + public boolean isExistsIndexFile() { + return false; + } + + @Override + public String toString() { + return "[ClassLoaderResource:resourcePath=" + resourcePath + + ",url=" + url + + ",lastmodified=" + lastModified + + ",length=" + length + + ",isDirectory=" + isDirectory() + "]"; + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/FileResourceHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/FileResourceHandler.java new file mode 100644 index 0000000..5462763 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/FileResourceHandler.java @@ -0,0 +1,218 @@ +package org.xbib.net.http.server.resource; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.PathNormalizer; +import org.xbib.net.Resource; +import org.xbib.net.URL; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; + +public class FileResourceHandler extends AbstractResourceHandler { + + private static final Logger logger = Logger.getLogger(FileResourceHandler.class.getName()); + + private final String webRoot; + + private final String indexFileName; + + private final String pathNameOfResource; + + public FileResourceHandler() { + this(null, "index.html", null); + } + + public FileResourceHandler(String webRoot, String indexFileName, String pathNameOfResource) { + this.webRoot = webRoot; + this.indexFileName = indexFileName; + this.pathNameOfResource = pathNameOfResource; + } + + @Override + protected Resource createResource(HttpServerContext httpServerContext) throws IOException { + String pathSpec = httpServerContext.attributes().containsKey("templatePath") ? + (String) httpServerContext.attributes().get("templatePath") : + pathNameOfResource != null ? pathNameOfResource : httpServerContext.httpRequest().getRequestPath(); + if (pathSpec == null || pathSpec.isEmpty()) { + throw new IllegalArgumentException("path must not be null or empty"); + } + Resource resource = null; + if (pathSpec.endsWith("/")) { + if (indexFileName != null) { + resource = new FileResource(httpServerContext, pathSpec + indexFileName); + } + } else { + resource = new FileResource(httpServerContext, pathSpec); + } + return resource; + } + + @Override + protected boolean isETagResponseEnabled() { + return true; + } + + @Override + protected boolean isCacheResponseEnabled() { + return true; + } + + @Override + protected boolean isRangeResponseEnabled() { + return true; + } + + @Override + protected int getMaxAgeSeconds() { + return 24 * 3600; + } + + protected class FileResource implements Resource { + + private final Path path; + + private final String resourcePath; + + private final Instant lastModified; + + private final long length; + + private final URL url; + + private final boolean isDirectory; + + private final boolean isExists; + + private final boolean isExistsIndexFile; + + private final String contentType; + + private final String name; + + private final String baseName; + + private final String suffix; + + protected FileResource(HttpServerContext httpServerContext, String resourcePath) throws IOException { + this.resourcePath = resourcePath; + Application application = httpServerContext.attributes().get(Application.class, "application"); + Path root = application.getHome(); + if (root == null) { + throw new IllegalArgumentException("no home path set for template resource resolving"); + } + logger.log(Level.FINE, "root = " + root); + if (resourcePath.startsWith("file:")) { + this.path = Paths.get(URI.create(resourcePath)); + this.name = path.getFileName().toString(); + } else { + String normalizedPath = PathNormalizer.normalize(resourcePath); + if (normalizedPath.startsWith("/")) { + normalizedPath = normalizedPath.substring(1); + } + this.name = normalizedPath; + this.path = httpServerContext.resolve(webRoot).resolve(normalizedPath); + } + this.contentType = mimeTypeService.getContentType(resourcePath); + this.url = URL.create(path.toUri().toString()); + this.baseName = basename(name); + this.suffix = suffix(name); + this.isExists = Files.exists(path); + this.isDirectory = Files.isDirectory(path); + if (isDirectory && getIndexFileName() != null) { + this.isExistsIndexFile = Files.exists(path.resolve(indexFileName)); + httpServerContext.done(); + } else { + this.isExistsIndexFile = false; + } + if (isExists) { + this.lastModified = Files.getLastModifiedTime(path).toInstant(); + this.length = Files.size(path); + httpServerContext.done(); + } else { + this.lastModified = Instant.now(); + this.length = 0; + } + } + + @Override + public Path getPath() { + return path; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getBaseName() { + return baseName; + } + + @Override + public String getSuffix() { + return suffix; + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public boolean isExists() { + return isExists; + } + + @Override + public boolean isDirectory() { + return isDirectory; + } + + @Override + public String getMimeType() { + return contentType; + } + + @Override + public String getIndexFileName() { + return indexFileName; + } + + @Override + public boolean isExistsIndexFile() { + return isExistsIndexFile; + } + + @Override + public Instant getLastModified() { + return lastModified; + } + + @Override + public long getLength() { + return length; + } + + @Override + public String toString() { + return "[FileResource:resourcePath=" + resourcePath + + ",url=" + url + + ",lastmodified=" + lastModified + + ",length=" + length + + ",isDirectory=" + isDirectory() + "]"; + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java new file mode 100644 index 0000000..65c7316 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java @@ -0,0 +1,161 @@ +package org.xbib.net.http.server.resource; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.URL; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; + +public class HtmlTemplateResource implements HttpServerResource { + + private static final Logger logger = Logger.getLogger(HtmlTemplateResource.class.getName()); + + private final HtmlTemplateResourceHandler templateResourceHandler; + + private final Path path; + + private final String resourcePath; + + private final Instant lastModified; + + private final long length; + + private final URL url; + + private final boolean isExists; + + private final boolean isDirectory; + + private final boolean isExistsIndexFile; + + private final String name; + + private final String baseName; + + private final String suffix; + + protected HtmlTemplateResource(HtmlTemplateResourceHandler templateResourceHandler, + HttpServerContext httpServerContext) throws IOException { + this.templateResourceHandler = templateResourceHandler; + String indexFileName = templateResourceHandler.getIndexFileName(); + Application application = httpServerContext.attributes().get(Application.class, "application"); + Path root = templateResourceHandler.getPrefix(); + root = root != null ? root : application.getHome(); + if (root == null) { + throw new IllegalArgumentException("no home path set for template resource resolving"); + } + logger.log(Level.FINE, "root = " + root); + this.resourcePath = httpServerContext.request().getRequestPath().substring(1); + logger.log(Level.FINE, "resource path = " + resourcePath); + this.path = resourcePath.length() > 0 ? root.resolve(resourcePath) : root; + logger.log(Level.FINE, "path = " + path); + this.url = URL.create(path.toUri().toString()); + logger.log(Level.FINE, "uri = " + url); + this.name = path.getFileName().toString(); + this.baseName = AbstractResourceHandler.basename(name); + this.suffix = AbstractResourceHandler.suffix(name); + this.isExists = Files.exists(path); + this.isDirectory = Files.isDirectory(path); + logger.log(Level.FINE, "exists = " + isExists + " isDirectory = " + isDirectory); + if (isDirectory && getIndexFileName() != null) { + this.isExistsIndexFile = Files.exists(path.resolve(indexFileName)); + httpServerContext.done(); + } else { + this.isExistsIndexFile = false; + } + if (isExists) { + this.lastModified = Files.getLastModifiedTime(path).toInstant(); + httpServerContext.done(); + } else { + this.lastModified = Instant.now(); + } + // length will be computed at rendering time + this.length = -1; + } + + @Override + public Path getPath() { + return path; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getBaseName() { + return baseName; + } + + @Override + public String getSuffix() { + return suffix; + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public boolean isExists() { + return isExists; + } + + @Override + public boolean isDirectory() { + return isDirectory; + } + + @Override + public String getMimeType() { + return "text/html; charset=UTF-8"; + } + + @Override + public String getIndexFileName() { + return templateResourceHandler.getIndexFileName(); + } + + @Override + public boolean isExistsIndexFile() { + return isExistsIndexFile; + } + + @Override + public Instant getLastModified() { + return lastModified; + } + + @Override + public long getLength() { + return length; + } + + @Override + public String toString() { + return "[HtmlTemplateResource:resourcePath=" + resourcePath + + ",path=" + path + + ",url=" + url + + ",lastmodified=" + lastModified + + ",length=" + length + + ",isExists=" + isExists + + ",isDirectory=" + isDirectory() + "]"; + } + + @Override + public void render(HttpServerContext httpServerContext) throws IOException { + // to be overriden + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResourceHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResourceHandler.java new file mode 100644 index 0000000..d292238 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResourceHandler.java @@ -0,0 +1,61 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.Resource; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.nio.file.Path; + +public class HtmlTemplateResourceHandler extends AbstractResourceHandler { + + protected final Path prefix; + + protected final String suffix; + + protected final String indexFileName; + + public HtmlTemplateResourceHandler(Path prefix, + String suffix, + String indexFileName) { + this.prefix = prefix; + this.suffix = suffix; + this.indexFileName = indexFileName; + } + + @Override + protected Resource createResource(HttpServerContext httpServerContext) throws IOException { + return new HtmlTemplateResource(this, httpServerContext); + } + + @Override + protected boolean isETagResponseEnabled() { + return false; + } + + @Override + protected boolean isCacheResponseEnabled() { + return false; + } + + @Override + protected boolean isRangeResponseEnabled() { + return false; + } + + @Override + protected int getMaxAgeSeconds() { + return 0; + } + + public Path getPrefix() { + return prefix; + } + + public String getSuffix() { + return suffix; + } + + public String getIndexFileName() { + return indexFileName; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/HttpServerResource.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HttpServerResource.java new file mode 100644 index 0000000..61dd57c --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HttpServerResource.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.Resource; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; + +public interface HttpServerResource extends Resource { + + void render(HttpServerContext httpServerContext) throws IOException; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/MethodHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/MethodHandler.java new file mode 100644 index 0000000..c830b56 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/MethodHandler.java @@ -0,0 +1,34 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpServerContext; + +import java.lang.reflect.Method; + +public class MethodHandler implements HttpHandler { + + private final Method m; + + private final Object obj; + + public MethodHandler(Method m, Object obj) throws IllegalArgumentException { + this.m = m; + this.obj = obj; + Class[] params = m.getParameterTypes(); + if (params.length != 1 || + !HttpRequest.class.isAssignableFrom(params[0]) || + !Void.class.isAssignableFrom(m.getReturnType())) { + throw new IllegalArgumentException("invalid method signature: " + m); + } + } + + @Override + public void handle(HttpServerContext context) { + try { + m.invoke(obj, context); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/ResourceResolver.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/ResourceResolver.java new file mode 100644 index 0000000..722a9e0 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/ResourceResolver.java @@ -0,0 +1,14 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.Resource; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.util.List; + +public interface ResourceResolver { + + Resource resolveResource(HttpServerContext httpServerContext, + String template, + List indexFiles) throws IOException; +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/WebRootResourceResolver.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/WebRootResourceResolver.java new file mode 100644 index 0000000..c45ab80 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/WebRootResourceResolver.java @@ -0,0 +1,134 @@ +package org.xbib.net.http.server.resource; + +import org.xbib.net.PathNormalizer; +import org.xbib.net.Resource; +import org.xbib.net.URL; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class WebRootResourceResolver implements ResourceResolver { + + private final Path prefix; + + private final String webRoot; + + public WebRootResourceResolver(Path prefix, String webRoot) { + this.prefix = prefix; + this.webRoot = webRoot; + } + + @Override + public Resource resolveResource(HttpServerContext httpServerContext, + String templateName, + List indexFiles) throws IOException { + String pathSpec = httpServerContext.attributes().containsKey("forwardedPath") ? + (String) httpServerContext.attributes().get("forwardedPath") : + templateName != null ? templateName : httpServerContext.httpRequest().getRequestPath(); + if (pathSpec == null || pathSpec.isEmpty()) { + throw new IllegalArgumentException("path must not be null or empty"); + } + Resource resource = null; + if (pathSpec.endsWith("/")) { + if (indexFiles != null) { + for (String indexfile : indexFiles) { + resource = createResource(pathSpec + indexfile); + if (resource.isExists()) { + break; + } + } + } + } else { + resource = createResource(pathSpec); + if (Files.isDirectory(resource.getPath())) { + // we need to move temporarily to the directory, and the browser must know about this. + HttpRequest request = httpServerContext.httpRequest(); + URL url = request.getBaseURL(); //response.server().getPublishURL(request); + String loc = url.resolve(resource.getName() + '/') + .mutator() + .query(request.getBaseURL().getQuery()) + .fragment(request.getBaseURL().getFragment()) + .build() + .toString(); + httpServerContext.response() + .setResponseStatus(HttpResponseStatus.TEMPORARY_REDIRECT) + .setHeader("location", loc); + } + } + return resource; + } + + private Resource createResource(String resourcePath) throws IOException { + Path p; + BaseResource resource = new BaseResource(); + if (resourcePath.startsWith("file:")) { + p = Paths.get(URI.create(resourcePath)); + resource.setName(p.getFileName().toString()); + resource.setBaseName(basename(resource.getName())); + resource.setSuffix(suffix(resource.getName())); + } else { + String normalizedPath = PathNormalizer.normalize(resourcePath); + if (normalizedPath.startsWith("/")) { + normalizedPath = normalizedPath.substring(1); + } + resource.setName(normalizedPath); + resource.setBaseName(basename(normalizedPath)); + resource.setSuffix(suffix(normalizedPath)); + p = prefix.resolve(webRoot).resolve(normalizedPath); + } + resource.setPath(p); + if (Files.isRegularFile(p)) { + resource.setLastModified(Files.getLastModifiedTime(p).toInstant()); + resource.setLength(Files.size(p)); + } + return resource; + } + + private static String basename(String path) { + return removeSuffix(getFileName(path)); + } + + private static String suffix(String path) { + return extractSuffix(getFileName(path)); + } + + private static String extractSuffix(String filename) { + if (filename == null) { + return null; + } + int index = indexOfSuffix(filename); + return index == -1 ? null : filename.substring(index + 1); + } + + private static String removeSuffix(String filename) { + if (filename == null) { + return null; + } + int index = indexOfSuffix(filename); + return index == -1 ? filename : filename.substring(0, index); + } + + private static int indexOfSuffix(String filename) { + if (filename == null) { + return -1; + } + int suffixPos = filename.lastIndexOf('.'); + int lastSeparator = filename.lastIndexOf('/'); + return lastSeparator > suffixPos ? -1 : suffixPos; + } + + private static String getFileName(String path) { + if (path == null) { + return null; + } + return path.substring(path.lastIndexOf('/') + 1); + } + +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/AcceptHeaderOverride.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/AcceptHeaderOverride.java new file mode 100644 index 0000000..9fdc13b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/AcceptHeaderOverride.java @@ -0,0 +1,28 @@ +package org.xbib.net.http.server.resource.negotiate; + +import java.util.regex.Pattern; + +public class AcceptHeaderOverride { + + private final Pattern userAgentPattern; + + private final String original; + + private final String replacement; + + public AcceptHeaderOverride(Pattern userAgentPattern, String original, String replacement) { + this.userAgentPattern = userAgentPattern; + this.original = original; + this.replacement = replacement; + } + + public boolean matches(String acceptHeader, String userAgentHeader) { + boolean b1 = userAgentPattern == null || userAgentPattern.matcher(userAgentHeader).find(); + boolean b2 = original == null || original.equals(acceptHeader); + return b1 && b2; + } + + public String getReplacement() { + return replacement; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/ContentTypeNegotiator.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/ContentTypeNegotiator.java new file mode 100644 index 0000000..009a956 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/ContentTypeNegotiator.java @@ -0,0 +1,129 @@ +package org.xbib.net.http.server.resource.negotiate; + +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.server.HttpRequest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Taken from org.apache.any23.servlet.conneg.ContentTypeNegotiator + */ +public class ContentTypeNegotiator { + + private static final ContentTypeNegotiator contentTypeNegotiator = new ContentTypeNegotiator(); + + private final List variantSpecs = new ArrayList<>(); + + private final Collection userAgentOverrides = new ArrayList<>(); + + private List defaultAcceptRanges = Collections.singletonList(MediaRangeSpec.parseRange("*/*")); + + private ContentTypeNegotiator() { + setDefaultAccept("text/html"); + /* + * Send HTML to clients that indicate they accept everything. + * This is specifically so that cURL sees HTML, and also catches + * various browsers that send "* / *" in some circumstances. + */ + addUserAgentOverride(null, "*/*", "text/html"); + + /* + * MSIE (7.0) sends either \* / *, or * / * with a list of other + * random types, + * but always without q values. That's useless. We will simply send + * HTML to MSIE, no matter what. Boy, do I hate IE. + */ + addUserAgentOverride(Pattern.compile("MSIE"), null, "text/html"); + + addVariant("text/html;q=0.81").addAliasMediaType("text/html;q=0.81").makeDefault(); + addVariant("text/xml;q=0.81").addAliasMediaType("text/xml;q=0.81"); + addVariant("application/json;q=0.80").addAliasMediaType("application/json;q=0.80"); + addVariant("application/xml;q=0.80").addAliasMediaType("application/xml;q=0.80"); + } + + public static String negotiateMediaType(HttpRequest request) { + String userAgent = request.getHeaders().get(HttpHeaderNames.USER_AGENT); + String accept = request.getHeaders().get(HttpHeaderNames.ACCEPT); + return negotiateMediaType(userAgent, accept); + } + + public static String negotiateMediaType(String useragent, String accept) { + MediaRangeSpec mrs = contentTypeNegotiator.getBestMatch(accept, useragent != null ? useragent : ""); + return mrs != null ? mrs.getMediaType() : ""; + } + + public static Locale negotiateLocale(HttpRequest request) { + String languages = request.getHeaders().get(HttpHeaderNames.ACCEPT_LANGUAGE); + return LocaleNegotiator.findLocale(languages); + } + + /** + * Add variant. + * + * @param mediaType the media type + */ + private VariantSpec addVariant(String mediaType) { + VariantSpec variantSpec = new VariantSpec(mediaType); + variantSpecs.add(variantSpec); + return variantSpec; + } + + /** + * Sets an Accept header to be used as the default if a client does + * not send an Accept header, or if the Accept header cannot be parsed. + * Defaults to "*&slash;*" + * @param accept the default accept header + */ + private void setDefaultAccept(String accept) { + this.defaultAcceptRanges = MediaRangeSpec.parseAccept(accept); + } + + /** + * Overrides the Accept header for certain user agents. This can be + * used to implement special-case handling for user agents that send + * faulty Accept headers. + * + * @param userAgentString A pattern to be matched against the User-Agent header, + * null means regardless of User-Agent + * @param originalAcceptHeader Only override the Accept header if the user agent + * sends this header, null means always override + * @param newAcceptHeader The Accept header to be used instead + */ + private void addUserAgentOverride(Pattern userAgentString, + String originalAcceptHeader, + String newAcceptHeader) { + this.userAgentOverrides.add(new AcceptHeaderOverride(userAgentString, originalAcceptHeader, newAcceptHeader)); + } + + /** + * Get best match for an Accept header. + * @param accept the Accept header + * @return the media range spec + */ + private MediaRangeSpec getBestMatch(String accept, String agent) { + String overriddenAccept = accept; + for (AcceptHeaderOverride override : userAgentOverrides) { + if (override.matches(accept, agent)) { + overriddenAccept = override.getReplacement(); + break; + } + } + return new Negotiation(toAcceptRanges(overriddenAccept)).negotiate(variantSpecs); + } + + private List toAcceptRanges(String accept) { + if (accept == null) { + return defaultAcceptRanges; + } + List result = MediaRangeSpec.parseAccept(accept); + if (result.isEmpty()) { + return defaultAcceptRanges; + } + return result; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/LocaleNegotiator.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/LocaleNegotiator.java new file mode 100644 index 0000000..77251da --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/LocaleNegotiator.java @@ -0,0 +1,32 @@ +package org.xbib.net.http.server.resource.negotiate; + +import java.util.Locale; +import java.util.StringTokenizer; + +public class LocaleNegotiator { + + private LocaleNegotiator() { + } + + public static Locale findLocale(String languages) { + Locale locale = null; + if (languages != null) { + StringTokenizer stringTokenizer = new StringTokenizer(languages, ","); + if (stringTokenizer.hasMoreTokens()) { + String s = stringTokenizer.nextToken(); + int pos; + String lang = s; + if ((pos = lang.indexOf(';')) != -1) { + lang = lang.substring(0, pos); + } + lang = lang.trim(); + if ((pos = lang.indexOf('-')) == -1) { + locale = new Locale(lang, Locale.getDefault().getCountry()); + } else { + locale = new Locale(lang.substring(0, pos), lang.substring(pos + 1)); + } + } + } + return locale; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/MediaRangeSpec.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/MediaRangeSpec.java new file mode 100644 index 0000000..ae5e97f --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/MediaRangeSpec.java @@ -0,0 +1,185 @@ +package org.xbib.net.http.server.resource.negotiate; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class MediaRangeSpec { + + private static final Pattern tokenPattern = Pattern.compile("[\\x20-\\x7E&&[^()<>@,;:\\\"/\\[\\]?={} ]]+"); + + private final String type; + + private final String subtype; + + private final Map parameter; + + private final double quality; + + private final String mediaType; + + private MediaRangeSpec(String type, String subtype, Map parameter, Double quality) { + this.type = type; + this.subtype = subtype; + this.parameter = parameter; + this.quality = quality; + this.mediaType = buildMediaType(); + } + + /** + * Parses a media type from a string such as text/html;charset=utf-8;q=0.9 + * @param mediaType mediaType + * @return the media range spec or null + */ + public static MediaRangeSpec parseType(String mediaType) { + MediaRangeSpec m = parseRange(mediaType); + return (m == null || m.isWildcardType() || m.isWildcardSubtype()) ? null : m; + } + + /** + * Parses a media range from a string such as text/*;charset=utf-8;q=0.9. + * Unlike simple media types, media ranges may include wildcards. + * @param mediaRange mediaRange + * @return the media range spec or null + */ + public static MediaRangeSpec parseRange(String mediaRange) { + if (mediaRange.indexOf(';') > -1) { + String[] m = mediaRange.split(";"); + Map params = new LinkedHashMap<>(); + double q = 1.0; + int pos = 1; + while (pos < m.length) { + int i = m[pos].indexOf('='); + if (i > -1) { + String k = m[pos].substring(0, pos - 1).trim(); + String v = m[pos].substring(i + 1).trim(); + params.put(k, v); + if (k.equals("q")) { + try { + q = Double.parseDouble(v); + } catch (NumberFormatException e) { + // ignore + } + } + } + pos++; + } + String[] types = m[0].split("/"); + return types.length > 1 ? new MediaRangeSpec(types[0], types[1], params, q) : null; + } else { + String[] types = mediaRange.split("/"); + return types.length > 1 ? new MediaRangeSpec(types[0], types[1], null, 1.0d) : null; + } + } + + /** + * Parses a HTTP Accept header into a list of MediaRangeSpecs + * @param accept the Accept header + * @return A List of MediaRangeSpecs + */ + public static List parseAccept(String accept) { + List list = new ArrayList<>(); + String[] tokens = accept.split(","); + for (String t : tokens) { + MediaRangeSpec mediaRangeSpec = parseRange(t); + list.add(mediaRangeSpec); + } + return list; + } + + private static String escape(String s) { + return s.replaceAll("[\\\\\"]", "\\\\$0"); + } + + private String buildMediaType() { + StringBuilder result = new StringBuilder(); + result.append(type).append("/").append(subtype); + if (parameter != null) { + for (Map.Entry me : parameter.entrySet()) { + result.append(";").append(me.getKey()).append("="); + String value = me.getValue(); + if (tokenPattern.matcher(value).matches()) { + result.append(value); + } else { + result.append("\"").append(escape(value)).append("\""); + } + } + } + return result.toString(); + } + + public String getType() { + return type; + } + + public String getSubtype() { + return subtype; + } + + public String getMediaType() { + return mediaType; + } + + public Map getParameter() { + return parameter; + } + + public boolean isWildcardType() { + return "*".equals(type); + } + + public boolean isWildcardSubtype() { + return !isWildcardType() && "*".equals(subtype); + } + + public double getQuality() { + return quality; + } + + public int getPrecedence(MediaRangeSpec range) { + if (range.isWildcardType()) { + return 1; + } + if (!range.type.equals(type)) { + return 0; + } + if (range.isWildcardSubtype()) { + return 2; + } + if (!range.subtype.equals(subtype)) { + return 0; + } + if (range.parameter == null || range.parameter.isEmpty()) { + return 3; + } + int result = 3; + for (Map.Entry entry : range.parameter.entrySet()) { + String name = entry.getKey(); + String value = entry.getValue(); + if (!value.equals(parameter.get(name))) { + return 0; + } + result++; + } + return result; + } + + public MediaRangeSpec getBestMatch(List mediaRanges) { + MediaRangeSpec result = null; + int bestPrecedence = 0; + for (MediaRangeSpec range : mediaRanges) { + if (getPrecedence(range) > bestPrecedence) { + bestPrecedence = getPrecedence(range); + result = range; + } + } + return result; + } + + @Override + public String toString() { + return mediaType; + } +} \ No newline at end of file diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/Negotiation.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/Negotiation.java new file mode 100644 index 0000000..547835b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/Negotiation.java @@ -0,0 +1,60 @@ +package org.xbib.net.http.server.resource.negotiate; + +import java.util.List; + +public class Negotiation { + + private final List ranges; + + private MediaRangeSpec bestMatchingVariant; + + private MediaRangeSpec bestDefaultVariant; + + private double bestMatchingQuality; + + private double bestDefaultQuality; + + public Negotiation(List ranges) { + this.ranges = ranges; + this.bestMatchingVariant = null; + this.bestDefaultVariant = null; + this.bestMatchingQuality = 0.0d; + this.bestDefaultQuality = 0.0d; + } + + public MediaRangeSpec negotiate(List variantSpecs) { + for (VariantSpec variant : variantSpecs) { + if (variant.isDefault()) { + evaluateDefaultVariant(variant.getMediaType()); + } + evaluateVariant(variant.getMediaType()); + for (MediaRangeSpec alias : variant.getAliases()) { + evaluateVariantAlias(alias, variant.getMediaType()); + } + + } + return bestMatchingVariant == null ? bestDefaultVariant : bestMatchingVariant; + } + + private void evaluateVariantAlias(MediaRangeSpec variant, MediaRangeSpec isAliasFor) { + if (variant.getBestMatch(ranges) == null) { + return; + } + double q = variant.getBestMatch(ranges).getQuality(); + if (q * variant.getQuality() > bestMatchingQuality) { + bestMatchingVariant = isAliasFor; + bestMatchingQuality = q * variant.getQuality(); + } + } + + private void evaluateVariant(MediaRangeSpec variant) { + evaluateVariantAlias(variant, variant); + } + + private void evaluateDefaultVariant(MediaRangeSpec variant) { + if (variant.getQuality() > bestDefaultQuality) { + bestDefaultVariant = variant; + bestDefaultQuality = 0.00001 * variant.getQuality(); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/VariantSpec.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/VariantSpec.java new file mode 100644 index 0000000..dadf878 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/negotiate/VariantSpec.java @@ -0,0 +1,40 @@ +package org.xbib.net.http.server.resource.negotiate; + +import java.util.ArrayList; +import java.util.List; + +public class VariantSpec { + + private final MediaRangeSpec type; + + private final List aliases; + + private boolean isDefault; + + public VariantSpec(String mediaType) { + type = MediaRangeSpec.parseType(mediaType); + aliases = new ArrayList<>(); + isDefault = false; + } + + public VariantSpec addAliasMediaType(String mediaType) { + aliases.add(MediaRangeSpec.parseType(mediaType)); + return this; + } + + public void makeDefault() { + isDefault = true; + } + + public MediaRangeSpec getMediaType() { + return type; + } + + public boolean isDefault() { + return isDefault; + } + + public List getAliases() { + return aliases; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseDomainsByAddress.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseDomainsByAddress.java new file mode 100644 index 0000000..f8e1845 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseDomainsByAddress.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.server.route; + +import org.xbib.datastructures.common.LinkedHashSetMultiMap; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.HttpDomain; + +public class BaseDomainsByAddress extends LinkedHashSetMultiMap implements DomainsByAddress { + + public BaseDomainsByAddress() { + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java new file mode 100644 index 0000000..db8b57c --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java @@ -0,0 +1,352 @@ +package org.xbib.net.http.server.route; + +import java.util.Collection; +import java.util.Set; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; + +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class BaseHttpRoute implements HttpRoute { + + private final HttpAddress httpAddress; + + private final Collection httpMethods; + + private final String path; + + private final List segments; + + private final String sortKey; + + public BaseHttpRoute(HttpAddress httpAddress, HttpMethod httpMethod, String path) { + this(httpAddress, Set.of(httpMethod), path, false); + } + + public BaseHttpRoute(HttpAddress httpAddress, Collection httpMethods, String path, boolean onlyStrings) { + Objects.requireNonNull(httpAddress, "address"); + Objects.requireNonNull(httpMethods, "methods"); + Objects.requireNonNull(path, "path"); + this.httpAddress = httpAddress; + this.httpMethods = httpMethods; + this.path = path; + this.segments = onlyStrings ? createStringSegments(path): createSegments(path); + this.sortKey = createSortKey(); + } + + @Override + public HttpAddress getHttpAddress() { + return httpAddress; + } + + @Override + public Collection getHttpMethods() { + return httpMethods; + } + + @Override + public String getPath() { + return path; + } + + public List getSegments() { + return segments; + } + + @Override + public boolean matches(ParameterBuilder parameterBuilder, HttpRoute requestedRoute) { + if (!(requestedRoute instanceof BaseHttpRoute)) { + return false; + } + BaseHttpRoute baseHttpRoute = (BaseHttpRoute) requestedRoute; + if (!httpAddress.equals(baseHttpRoute.getHttpAddress())) { + return false; + } + HttpMethod requestedMethod = requestedRoute.getHttpMethods().iterator().next(); + if (!baseHttpRoute.getHttpMethods().contains(requestedMethod)) { + return false; + } + List requestedSegments = baseHttpRoute.getSegments(); + // special case: empty segments match + if (requestedSegments.isEmpty() && segments.isEmpty()) { + return true; + } + // special case: single segment with pattern to match, we must ignore the incoming segments + if (segments.size() == 1 && segments.get(0) instanceof PatternSegment) { + MatchResult matchResult = segments.get(0).match(new StringSegment(requestedRoute.getPath())); + if (matchResult == MatchResult.TRUE) { + return true; + } + } + List matchResults = new ArrayList<>(); + MatchResult matchResult; + if (segments.size() >= requestedSegments.size()) { + int i = 0; + while (i < segments.size()) { + if (i >= requestedSegments.size()) { + // special case: catch_all after prefixes have matched is OK + if (!(segments.get(i) instanceof CatchAllSegment)) { + matchResults.add(MatchResult.FALSE); + } + break; + } + matchResult = segments.get(i).match(requestedSegments.get(i)); + if (matchResult instanceof ValueMatchResult) { + ParameterSegment parameterSegment = (ParameterSegment) segments.get(i); + parameterBuilder.add(parameterSegment.getName(), ((ValueMatchResult) matchResult).getValue()); + } + if (matchResult == MatchResult.ALWAYS) { + break; + } + matchResults.add(matchResult); + i++; + } + } else { + int i = 0; + while (i < requestedSegments.size()) { + if (i >= segments.size()) { + // never match on short patterns + matchResults.add(MatchResult.FALSE); + break; + } + matchResult = requestedSegments.get(i).match(segments.get(i)); + if (matchResult instanceof ValueMatchResult) { + ParameterSegment parameterSegment = (ParameterSegment) segments.get(i); + parameterBuilder.add(parameterSegment.getName(), ((ValueMatchResult) matchResult).getValue()); + } + if (matchResult == MatchResult.ALWAYS) { + break; + } + matchResults.add(matchResult); + i++; + } + } + return matchResults.stream().noneMatch(p -> p == MatchResult.FALSE); + } + + @Override + public String getSortKey() { + return sortKey; + } + + @Override + public String toString() { + return httpAddress + "/" + httpMethods + "/" + segments.stream().map(Object::toString).collect(Collectors.joining("/")); + } + + private static List createSegments(String path) { + if (path.startsWith("glob:")) { + return List.of(new PatternSegment(path)); + } + List list = new ArrayList<>(); + for (String s : path.split("/")) { + if (s.isEmpty()) { + continue; + } + if (isCatchAll(s)) { + list.add(CATCHALL); + } else if (isParameter(s)) { + list.add(new ParameterSegment(s)); + } else if (isPattern(s)) { + list.add(new PatternSegment(s)); + } else { + list.add(new StringSegment(s)); + } + } + return list; + } + + private static List createStringSegments(String path) { + List list = new ArrayList<>(); + for (String s : path.split("/")) { + if (s.isEmpty()) { + continue; + } + list.add(new StringSegment(s)); + } + return list; + } + + private String createSortKey() { + StringBuilder sb = new StringBuilder(); + if (segments.size() == 1 && (segments.get(0) instanceof PatternSegment || segments.get(0) instanceof CatchAllSegment)) { + sb.append("1"); + } else { + sb.append("0"); + } + sb.append(String.format("%03d", path.length())); + return sb.toString(); + } + + private static boolean isCatchAll(String s) { + return "**".equals(s); + } + + private static boolean isParameter(String s) { + return s.startsWith("{") && s.endsWith("}"); + } + + private static boolean isPattern(String s) { + return s.startsWith("glob:"); + } + + public interface RouteSegment { + MatchResult match(RouteSegment segment); + + } + + private static class CatchAllSegment implements RouteSegment { + + @Override + public MatchResult match(RouteSegment segment) { + return MatchResult.ALWAYS; + } + + @Override + public String toString() { + return "CATCH_ALL[**]"; + } + } + + private static class StringSegment implements RouteSegment { + + private final String string; + + StringSegment(String string) { + this.string = string; + } + + @Override + public MatchResult match(RouteSegment segment) { + if (segment instanceof StringSegment) { + return string.equals(((StringSegment) segment).string) ? MatchResult.TRUE : MatchResult.FALSE; + } else if (segment instanceof CatchAllSegment) { + return MatchResult.ALWAYS; + } else if (segment instanceof PatternSegment) { + Path path = Paths.get(string); + return ((PatternSegment) segment).pathMatcher.matches(path) ? MatchResult.TRUE : MatchResult.FALSE; + } else if (segment instanceof ParameterSegment) { + ValueMatchResult matchResult = new ValueMatchResult(); + matchResult.setValue(string); + return matchResult; + } + return MatchResult.FALSE; + } + + @Override + public String toString() { + return "STRING:[" + string + "]"; + } + } + + private static class PatternSegment implements RouteSegment { + + private final PathMatcher pathMatcher; + + PatternSegment(String regex) { + this.pathMatcher = FileSystems.getDefault().getPathMatcher(regex); + } + + @Override + public MatchResult match(RouteSegment segment) { + if (segment instanceof StringSegment) { + Path path = Paths.get(((StringSegment) segment).string); + return pathMatcher.matches(path) ? MatchResult.TRUE : MatchResult.FALSE; + } else if (segment instanceof CatchAllSegment) { + return MatchResult.ALWAYS; + } else if (segment instanceof PatternSegment) { + return MatchResult.FALSE; + } else if (segment instanceof ParameterSegment) { + return MatchResult.FALSE; + } + return MatchResult.FALSE; + } + + @Override + public String toString() { + return "PATTERN:[" + pathMatcher + "]"; + } + } + + private static class ParameterSegment implements RouteSegment { + + private final String name; + + ParameterSegment(String name) { + this.name = name.substring(1, name.length() -1); + } + + public String getName() { + return name; + } + + @Override + public MatchResult match(RouteSegment segment) { + ValueMatchResult matchResult = new ValueMatchResult(); + if (segment instanceof StringSegment) { + matchResult.setValue(((StringSegment) segment).string); + } + return matchResult; + } + + @Override + public String toString() { + return "PARAMETER:{" + name + "}"; + } + } + + public interface MatchResult { + MatchResult TRUE = new TrueMatchResult(); + + MatchResult FALSE = new FalseMatchResult(); + + MatchResult ALWAYS = new AlwaysMatchResult(); + } + + private static class TrueMatchResult implements MatchResult { + @Override + public String toString() { + return "TRUE"; + } + } + + private static class FalseMatchResult implements MatchResult { + @Override + public String toString() { + return "FALSE"; + } + } + + private static class AlwaysMatchResult implements MatchResult { + @Override + public String toString() { + return "ALWAYS"; + } + } + + private static class ValueMatchResult implements MatchResult { + String value; + void setValue(String value) { + this.value = value; + } + + String getValue() { + return value; + } + + @Override + public String toString() { + return "VALUE"; + } + } + + private static final CatchAllSegment CATCHALL = new CatchAllSegment(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java new file mode 100644 index 0000000..3818684 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java @@ -0,0 +1,138 @@ +package org.xbib.net.http.server.route; + +import java.util.Set; +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class BaseHttpRouteResolver implements HttpRouteResolver { + + private final Builder builder; + + private BaseHttpRouteResolver(Builder builder) { + this.builder = builder; + } + + /** + * This naive rsolver walks through all configured routes and tries to match them. + * @param httpRoute the route to match against + * @param listener the listener where the results are going + */ + @Override + public void resolve(HttpRoute httpRoute, ResultListener listener) { + for (Map.Entry entry : builder.routes) { + ParameterBuilder parameterBuilder = Parameter.builder(); + boolean match = entry.getKey().matches(parameterBuilder, httpRoute); + if (match && listener != null) { + List list = Arrays.stream(httpRoute.getPath().replaceFirst(builder.prefix, "").split("/")) + .filter(s -> !s.isEmpty()).collect(Collectors.toList()); + listener.onResult(new Result<>(entry.getValue(), list, parameterBuilder.build())); + } + } + } + + public static Builder builder() { + return new Builder<>(); + } + + public static class Result implements HttpRouteResolver.Result { + + private final T value; + + private final List context; + + private final Parameter parameter; + + Result(T value, List context, Parameter parameter) { + this.value = value; + this.context = context; + this.parameter = parameter; + } + + @Override + public T getValue() { + return value; + } + + @Override + public List getContext() { + return context; + } + + @Override + public Parameter getParameter() { + return parameter; + } + } + + public static class Builder implements HttpRouteResolver.Builder { + + private final RouteComparator comparator; + + private final List> routes; + + private String prefix; + + private boolean sort; + + private Builder() { + this.comparator = new RouteComparator<>(); + this.prefix = ""; + this.routes = new ArrayList<>(); + this.sort = false; + } + + @Override + public HttpRouteResolver.Builder add(HttpAddress httpAddress, HttpMethod httpMethod, String path, T value) { + add(new BaseHttpRoute(httpAddress, Set.of(httpMethod), prefix + path, false), value); + return this; + } + + @Override + public HttpRouteResolver.Builder add(HttpAddress httpAddress, Set httpMethods, String path, T value) { + add(new BaseHttpRoute(httpAddress, httpMethods, prefix + path, false), value); + return this; + } + + @Override + public HttpRouteResolver.Builder add(HttpRoute httpRoute, T value) { + routes.add(Map.entry(httpRoute, value)); + return this; + } + + @Override + public HttpRouteResolver.Builder sort(boolean sort) { + this.sort = sort; + return this; + } + + public HttpRouteResolver.Builder setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + @Override + public BaseHttpRouteResolver build() { + if (sort) { + routes.sort(comparator); + } + return new BaseHttpRouteResolver<>(this); + } + } + + private static class RouteComparator implements Comparator> { + + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return o2.getKey().getSortKey().compareTo(o1.getKey().getSortKey()); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java new file mode 100644 index 0000000..43aacc9 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java @@ -0,0 +1,240 @@ +package org.xbib.net.http.server.route; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Collection; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.TreeSet; + +import org.xbib.datastructures.common.LinkedHashSetMultiMap; +import org.xbib.datastructures.common.MultiMap; +import org.xbib.net.URL; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpDomain; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpRequestBuilder; +import org.xbib.net.http.server.HttpResponseBuilder; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.HttpService; + +import static org.xbib.net.http.HttpResponseStatus.NOT_FOUND; + +public class BaseHttpRouter implements HttpRouter { + + private final Logger logger = Logger.getLogger(BaseHttpRouter.class.getName()); + + private final BaseHttpRouterBuilder builder; + + private final MultiMap domains; + + private final DomainsByAddress domainsByAddress; + + private final HttpRouteResolver httpRouteResolver; + + private Application application; + + protected BaseHttpRouter(BaseHttpRouterBuilder builder) { + this.builder = builder; + HttpRouteResolver.Builder httpRouteResolverBuilder = newHttpRouteResolverBuilder(); + for (HttpDomain domain : builder.domains) { + for (HttpService httpService : domain.getServices()) { + logger.log(Level.FINE, "adding " + domain.getAddress() + " " + httpService.getMethods() + " " + httpService.getPathSpecification() + " " + httpService); + HttpRoute httpRoute = new BaseHttpRoute(domain.getAddress(), httpService.getMethods(), httpService.getPathSpecification(), false); + httpRouteResolverBuilder.add(httpRoute, httpService); + } + } + this.httpRouteResolver = httpRouteResolverBuilder.build(); + this.domains = createDomains(builder.domains); + this.domainsByAddress = createAddresses(builder.domains); + } + + public static BaseHttpRouterBuilder builder() { + return new BaseHttpRouterBuilder(); + } + + public HttpRouteResolver.Builder newHttpRouteResolverBuilder() { + return BaseHttpRouteResolver.builder(); + } + + @Override + public void setApplication(Application application) { + this.application = application; + } + + @Override + public Collection getDomains() { + return builder.domains; + } + + @Override + public DomainsByAddress getDomainsByAddress() { + return domainsByAddress; + } + + @Override + public void route(HttpRequestBuilder requestBuilder, HttpResponseBuilder responseBuilder) { + Objects.requireNonNull(application); + Objects.requireNonNull(requestBuilder); + Objects.requireNonNull(requestBuilder.getRequestURI()); + Objects.requireNonNull(requestBuilder.getBaseURL()); + requestBuilder.setRequestPath(extractPath(requestBuilder.getRequestURI())); + HttpDomain httpDomain = findDomain(requestBuilder.getBaseURL()); + if (httpDomain == null) { + httpDomain = builder.domains.iterator().next(); + } + List> httpRouteResolverResults = new ArrayList<>(); + HttpRoute httpRoute = new BaseHttpRoute(httpDomain.getAddress(), Set.of(requestBuilder.getMethod()), requestBuilder.getRequestPath(), true); + httpRouteResolver.resolve(httpRoute, httpRouteResolverResults::add); + HttpServerContext httpServerContext = application.createContext(httpDomain, requestBuilder, responseBuilder); + route(httpServerContext, httpRouteResolverResults); + } + + protected void route(HttpServerContext httpServerContext, List> httpRouteResolverResults) { + application.onOpen(httpServerContext); + try { + if (httpServerContext.isFailed()) { + return; + } + if (httpRouteResolverResults.isEmpty()) { + logger.log(Level.FINE, "route resolver results is empty, generating a not found message"); + routeStatus(NOT_FOUND, httpServerContext); + return; + } + for (HttpRouteResolver.Result httpRouteResolverResult : httpRouteResolverResults) { + try { + // first: create the final request + httpServerContext.setResolverResult(httpRouteResolverResult); + HttpService httpService = httpRouteResolverResult.getValue(); + application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService, httpServerContext.httpRequest())); + // second: security check, authentication etc. + if (httpService.getSecurityDomain() != null) { + logger.log(Level.FINEST, () -> "handling security domain service " + httpService); + for (HttpHandler httpHandler : httpService.getSecurityDomain().getHandlers()) { + logger.log(Level.FINEST, () -> "handling security domain handler " + httpHandler); + httpHandler.handle(httpServerContext); + } + } + if (httpServerContext.isDone() || httpServerContext.isFailed()) { + break; + } + // accept service and execute service + httpServerContext.attributes().put("service", httpService); + application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService)); + logger.log(Level.FINEST, () -> "handling service " + httpService); + httpService.handle(httpServerContext); + if (httpServerContext.isDone() || httpServerContext.isFailed()) { + break; + } + } catch (HttpException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + routeException(e); + break; + } catch (Throwable t) { + logger.log(Level.SEVERE, t.getMessage(), t); + routeToErrorHandler(httpServerContext, t); + break; + } + } + } finally { + application.onClose(httpServerContext); + } + } + + @Override + public void routeException(HttpException e) { + routeStatus(e.getResponseStatus(), e.getHttpServerContext()); + } + + @Override + public void routeStatus(HttpResponseStatus httpResponseStatus, HttpServerContext httpServerContext) { + try { + HttpHandler httpHandler = getHandler(httpResponseStatus); + httpServerContext.response().reset(); + httpHandler.handle(httpServerContext); + } catch (IOException ioe) { + throw new IllegalStateException("unable to route response status, reason: " + ioe.getMessage(), ioe); + } + } + + @Override + public void routeToErrorHandler(HttpServerContext httpServerContext, Throwable t) { + httpServerContext.attributes().put("_throwable", t); + httpServerContext.fail(); + routeStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, httpServerContext); + } + + private HttpDomain findDomain(URL url) { + NavigableSet httpDomains = new TreeSet<>(); + String hostAndPort = getHostAndPost(url); + if (domains.containsKey(hostAndPort)) { + httpDomains.addAll(domains.get(hostAndPort)); + } + // check if ANY address was used for bind + hostAndPort = "0.0.0.0:" + url.getPort(); + if (domains.containsKey(hostAndPort)) { + httpDomains.addAll(domains.get(hostAndPort)); + } + // check if IPv6 ANY address was used for bind + hostAndPort = ":::" + url.getPort(); + if (domains.containsKey(hostAndPort)) { + httpDomains.addAll(domains.get(hostAndPort)); + } + return httpDomains.isEmpty() ? null: httpDomains.first(); + } + + private HttpHandler getHandler(HttpResponseStatus httpResponseStatus) { + return builder.handlers.containsKey(httpResponseStatus.code()) ? + builder.handlers.get(httpResponseStatus.code()) : builder.handlers.get(500); + } + + private static MultiMap createDomains(Collection domains) { + MultiMap map = new LinkedHashSetMultiMap<>(); + for (HttpDomain domain : domains) { + HttpAddress httpAddress = domain.getAddress(); + if (httpAddress.getHostNames() != null) { + for (String name : httpAddress.getHostNames()) { + map.put(name + ":" + httpAddress.getPort(), domain); + } + } + for (String name : domain.getNames()) { + map.put(name, domain); + } + } + return map; + } + + private static DomainsByAddress createAddresses(Collection domains) { + DomainsByAddress map = new BaseDomainsByAddress(); + for (HttpDomain domain : domains) { + map.put(domain.getAddress(), domain); + } + return map; + } + + private static String extractPath(String uri) { + String path = uri; + int pos = uri.lastIndexOf('#'); + path = pos >= 0 ? path.substring(0, pos) : path; + pos = uri.lastIndexOf('?'); + path = pos >= 0 ? path.substring(0, pos) : path; + return path; + } + + /** + * Returns the host and port in notation "host:port" of the given URL. + * + * @param url the URL + * @return the host and port + */ + private static String getHostAndPost(URL url) { + return url == null ? null : url.getPort() != null && url.getPort() != -1 ? url.getHost() + ":" + url.getPort() : url.getHost(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterBuilder.java new file mode 100644 index 0000000..b7bd1a9 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterBuilder.java @@ -0,0 +1,56 @@ +package org.xbib.net.http.server.route; + +import org.xbib.net.http.server.HttpDomain; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.handler.BadRequestHandler; +import org.xbib.net.http.server.handler.ForbiddenHandler; +import org.xbib.net.http.server.handler.InternalServerErrorHandler; +import org.xbib.net.http.server.handler.NotFoundHandler; +import org.xbib.net.http.server.handler.NotImplementedHandler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.xbib.net.http.server.handler.UnauthorizedHandler; +import org.xbib.net.http.server.handler.VersionNotSupportedHandler; + +public class BaseHttpRouterBuilder implements HttpRouterBuilder { + + protected final Collection domains; + + protected final Map handlers; + + protected BaseHttpRouterBuilder() { + domains = new ArrayList<>(); + handlers = new HashMap<>(); + handlers.put(400, new BadRequestHandler()); + handlers.put(401, new UnauthorizedHandler()); + handlers.put(403, new ForbiddenHandler()); + handlers.put(404, new NotFoundHandler()); + handlers.put(500, new InternalServerErrorHandler()); + handlers.put(501, new NotImplementedHandler()); + handlers.put(505, new VersionNotSupportedHandler()); + } + + @Override + public BaseHttpRouterBuilder setHandler(Integer code, HttpHandler httpHandler) { + handlers.put(code, httpHandler); + return this; + } + + @Override + public BaseHttpRouterBuilder addDomain(HttpDomain domain) { + this.domains.add(domain); + return this; + } + + @Override + public BaseHttpRouter build() { + if (domains.isEmpty()) { + throw new IllegalArgumentException("no domain configured, unable to continue"); + } + return new BaseHttpRouter(this); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/DomainsByAddress.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/DomainsByAddress.java new file mode 100644 index 0000000..586b899 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/DomainsByAddress.java @@ -0,0 +1,8 @@ +package org.xbib.net.http.server.route; + +import org.xbib.datastructures.common.MultiMap; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.server.HttpDomain; + +public interface DomainsByAddress extends MultiMap { +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRoute.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRoute.java new file mode 100644 index 0000000..6c8a942 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRoute.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server.route; + +import java.util.Collection; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; + +public interface HttpRoute { + + HttpAddress getHttpAddress(); + + Collection getHttpMethods(); + + String getPath(); + + boolean matches(ParameterBuilder parameterBuilder, HttpRoute requestedRoute); + + String getSortKey(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouteResolver.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouteResolver.java new file mode 100644 index 0000000..950032f --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouteResolver.java @@ -0,0 +1,43 @@ +package org.xbib.net.http.server.route; + +import java.util.Set; +import org.xbib.net.Parameter; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; + +import java.util.List; + +public interface HttpRouteResolver { + + void resolve(HttpRoute route, ResultListener listener); + + interface Builder { + + Builder add(HttpRoute route, T value); + + Builder add(HttpAddress httpAddress, HttpMethod httpMethod, String path, T value); + + Builder add(HttpAddress httpAddress, Set httpMethods, String path, T value); + + Builder sort(boolean sort); + + HttpRouteResolver build(); + } + + interface Result { + + T getValue(); + + List getContext(); + + Parameter getParameter(); + + } + + @FunctionalInterface + interface ResultListener { + + void onResult(Result result); + + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouter.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouter.java new file mode 100644 index 0000000..bdb8ad5 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouter.java @@ -0,0 +1,28 @@ +package org.xbib.net.http.server.route; + +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpDomain; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpRequestBuilder; +import org.xbib.net.http.server.HttpResponseBuilder; +import org.xbib.net.http.server.HttpServerContext; + +import java.util.Collection; + +public interface HttpRouter { + + Collection getDomains(); + + DomainsByAddress getDomainsByAddress(); + + void setApplication(Application application); + + void route(HttpRequestBuilder requestBuilder, HttpResponseBuilder responseBuilder); + + void routeException(HttpException e); + + void routeStatus(HttpResponseStatus httpResponseStatus, HttpServerContext httpServerContext); + + void routeToErrorHandler(HttpServerContext httpServerContext, Throwable t); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterBuilder.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterBuilder.java new file mode 100644 index 0000000..64bf6c5 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterBuilder.java @@ -0,0 +1,13 @@ +package org.xbib.net.http.server.route; + +import org.xbib.net.http.server.HttpDomain; +import org.xbib.net.http.server.HttpHandler; + +public interface HttpRouterBuilder { + + HttpRouterBuilder setHandler(Integer code, HttpHandler httpHandler); + + HttpRouterBuilder addDomain(HttpDomain domain); + + HttpRouter build(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/BaseSession.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/BaseSession.java new file mode 100644 index 0000000..2d2970b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/BaseSession.java @@ -0,0 +1,222 @@ +package org.xbib.net.http.server.session; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import org.xbib.datastructures.common.LRUCache; + +public class BaseSession implements Session { + + static String CREATED_FIELD = "_created_"; + + static String LAST_MODIFIED_FIELD = "_lastmodified_"; + + static String CACHE_PREFIX = "_cache_"; + + private final SessionListener sessionListener; + + private final String id; + + private final Duration lifetime; + + private final Map map; + + private final int cacheSize; + + private boolean valid; + + public BaseSession(SessionListener sessionListener, + int cacheSize, + String id, + boolean create, + Duration lifetime) { + this.cacheSize = cacheSize; + this.sessionListener = sessionListener; + this.id = id; + this.lifetime = lifetime; + this.map = new LinkedHashMap<>(); + this.valid = !lifetime.isNegative() && !lifetime.isZero(); + Instant now = Instant.now(); + if (create) { + put(CREATED_FIELD, now.toString()); + if (sessionListener != null) { + sessionListener.onCreated(this); + } + } + } + @Override + public String id() { + return id; + } + + @Override + public void invalidate() { + clear(); + valid = false; + if (sessionListener != null) { + sessionListener.onDestroy(this); + } + } + + @Override + public boolean isValid() { + return valid; + } + + @Override + public boolean isExpired() { + String string = (String) get(LAST_MODIFIED_FIELD); + if (string == null) { + string = (String) get(CREATED_FIELD); + } + if (string == null) { + return false; + } + Instant now = Instant.now(); + Instant lastModified = Instant.parse(string); + return Duration.between(lastModified, now).compareTo(lifetime) > 0; + } + + @Override + public boolean hasPayload() { + return !isEmpty() && + !(size() == 1 && containsKey(CREATED_FIELD)) && + !(size() == 2 && containsKey(CREATED_FIELD) && containsKey(LAST_MODIFIED_FIELD)); + } + + @Override + public Duration getAge() { + Instant instant = containsKey(CREATED_FIELD) ? Instant.parse(get(CREATED_FIELD).toString()) : Instant.now(); + return Duration.between(instant, Instant.now()); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Object get(Object key) { + return map.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public Object put(String key, Object value) { + Object v = value; + if (key.startsWith(CACHE_PREFIX)) { + if (value instanceof Map) { + v = newCache((Map) value); + } else if (value == null) { + throw new IllegalArgumentException("null not allowed for session cache: key = " + key); + } else { + throw new IllegalArgumentException("only a map allowed for session cache: key = " + key + " value class = " + value.getClass().getName()); + } + } + return map.put(key, v); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void putAll(Map map) { + if (map == null) { + throw new NullPointerException("unexpected null map for putAll"); + } + this.map.putAll(map); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection values() { + return map.values(); + } + + @Override + public Set> entrySet() { + return map.entrySet(); + } + + @Override + public String toString() { + return map.toString(); + } + + public void setLastModified() { + put(LAST_MODIFIED_FIELD, Instant.now().toString()); + } + + public int getCacheSize() { + return cacheSize; + } + + public void putCache(String cacheName, String token, Map map) { + getCache(cacheName).put(token, map); + } + + @SuppressWarnings("unchecked") + public void putCache(String cacheName, String token, String key, Object value) { + Map cache = (Map) getCache(cacheName).get(token); + if (cache != null) { + cache.put(key, value); + } + } + + @SuppressWarnings("unchecked") + public Object getCache(String cacheName, String token, String key) { + Map cache = (Map) getCache(cacheName).get(token); + return cache != null ? cache.get(key) : null; + } + + @SuppressWarnings("unchecked") + public Map getCache(String cacheName, String token) { + return (Map) getCache(cacheName).getOrDefault(token, new LinkedHashMap<>()); + } + + @SuppressWarnings("unchecked") + public Map getCache(String cacheName) { + map.computeIfAbsent(CACHE_PREFIX + cacheName, s -> newCache()); + return (Map) map.get(CACHE_PREFIX + cacheName); + } + + private Map newCache(Map map) { + Map cache = newCache(); + cache.putAll(map); + return cache; + } + + private Map newCache() { + return new LRUCache<>(cacheSize); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java new file mode 100644 index 0000000..f093bca --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java @@ -0,0 +1,168 @@ +package org.xbib.net.http.server.session; + +import org.xbib.net.PercentDecoder; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieBox; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.auth.BaseUserProfile; +import org.xbib.net.UserProfile; +import org.xbib.net.http.server.cookie.CookieSignatureException; +import org.xbib.net.http.server.cookie.CookieSignatureUtil; +import org.xbib.net.http.server.persist.Codec; + +import java.io.IOException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.MessageFormat; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.util.RandomUtil; + +public class IncomingSessionHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(IncomingSessionHandler.class.getName()); + + private final String sessionSecret; + + private final String sessionCookieAlgorithm; + + private final String sessionCookieName; + + private final Codec sessionCodec; + + private final String sessionUserName; + + private final String sessionEffectiveUserName; + + /** + * These suffixes disable incoming session creation. + */ + private final Set suffixes; + + public IncomingSessionHandler(String sessionSecret, + String sessionCookieAlgorithm, + String sessionCookieName, + Codec sessionCodec, + Set suffixes, + String sessionUserName, + String sessionEffectiveUserName) { + this.sessionSecret = sessionSecret; + this.sessionCookieAlgorithm = sessionCookieAlgorithm; + this.sessionCookieName = sessionCookieName; + this.sessionCodec = sessionCodec; + this.suffixes = suffixes; + this.sessionUserName = sessionUserName; + this.sessionEffectiveUserName = sessionEffectiveUserName; + } + + @Override + public void handle(HttpServerContext context) throws HttpException { + String suffix = SessionUtil.extractExtension(context.request().getRequestPath()); + if (suffix != null && suffixes.contains(suffix)) { + return; + } + Session session = null; + CookieBox cookieBox = context.attributes().get(CookieBox.class, "incomingcookies"); + if (cookieBox != null) { + for (Cookie cookie : cookieBox) { + if (cookie.name().equals(sessionCookieName)) { + if (session == null) { + try { + Map payload = decodeCookie(cookie); + session = toSession(payload); + UserProfile userProfile = newUserProfile(payload, session); + if (userProfile != null) { + context.attributes().put("userprofile", userProfile); + } + } catch (CookieSignatureException e) { + // set exception in context to discard broken cookie later and render exception message + context.attributes().put("_throwable", e); + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new HttpException("unable to create session", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + } else { + logger.log(Level.WARNING, "received extra session cookie, something is wrong, ignoring"); + } + } + } + } + if (session == null) { + try { + session = sessionCodec.create(RandomUtil.randomString(32)); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new HttpException("unable to create session", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + } + context.attributes().put("session", session); + + } + + private Map decodeCookie(Cookie cookie) throws IOException, + NoSuchAlgorithmException, InvalidKeyException, CookieSignatureException { + PercentDecoder percentDecoder = new PercentDecoder(StandardCharsets.ISO_8859_1.newDecoder() + .onMalformedInput(CodingErrorAction.IGNORE) + .onUnmappableCharacter(CodingErrorAction.IGNORE)); + String value = percentDecoder.decode(cookie.value()); + String[] s = value.split(":", 3); + if (s.length != 3) { + throw new IllegalArgumentException("cookie format problem, not 3 groups: " + cookie.value()); + } + String id = s[0]; + String payload = s[1]; + String sig = s[2]; + String mysig = CookieSignatureUtil.hmac(payload, sessionSecret, sessionCookieAlgorithm); + if (!sig.equals(mysig)) { + logger.log(Level.SEVERE, MessageFormat.format("signature in cookie does not match. algo={1} secret={2} payload={3} sig={4} mysig={5}", + sessionCookieAlgorithm, sessionSecret, payload, sig, mysig)); + throw new CookieSignatureException("cookie security problem"); + } + Map map = CookieSignatureUtil.toMap(payload); + return Map.of("id", id, "payload", payload, "map", map); + } + + protected Session toSession(Map map) { + return toSession((String) map.get("id"), map); + } + + protected Session toSession(String id, Map map) { + Session session = null; + try { + session = sessionCodec.read(id); + if (session != null && map != null) { + session.putAll(map); + } + } catch (Exception e) { + logger.log(Level.FINEST, "unable to read session, id = " + id, e); + } + return session; + } + + @SuppressWarnings("unchecked") + protected UserProfile newUserProfile(Map map, Session session) { + UserProfile userProfile = new BaseUserProfile(); + Map m = (Map) map.get("map"); + if (m == null) { + return userProfile; + } + if (m.containsKey(sessionUserName)) { + userProfile.setUserId((String) m.get(sessionUserName)); + } + if (m.containsKey(sessionEffectiveUserName)) { + userProfile.setEffectiveUserId((String) m.get(sessionEffectiveUserName)); + } + if (session != null && userProfile.getUserId() != null) { + session.put("user_id", userProfile.getUserId()); + session.put("e_user_id", userProfile.getUserId()); + } + return userProfile; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java new file mode 100644 index 0000000..67cf94f --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java @@ -0,0 +1,179 @@ +package org.xbib.net.http.server.session; + +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.cookie.Cookie; +import org.xbib.net.http.cookie.CookieBox; +import org.xbib.net.http.cookie.DefaultCookie; +import org.xbib.net.http.cookie.SameSite; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.UserProfile; +import org.xbib.net.http.server.cookie.CookieSignatureException; +import org.xbib.net.http.server.cookie.CookieSignatureUtil; +import org.xbib.net.http.server.persist.Codec; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OutgoingSessionHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(OutgoingSessionHandler.class.getName()); + + private final String sessionSecret; + + private final String sessionCookieAlgorithm; + + private final String sessionCookieName; + + private final Duration sessionDuration; + + private final Codec sessionCodec; + + private final Set suffixes; + + private final String sessionUserName; + + private final String sessionEffectiveUserName; + + public OutgoingSessionHandler(String sessionSecret, + String sessionCookieAlgorithm, + String sessionCookieName, + Duration sessionDuration, + Codec sessionCodec, + Set suffixes, + String sessionUserName, + String sessionEffectiveUserName) { + this.sessionSecret = sessionSecret; + this.sessionCookieAlgorithm = sessionCookieAlgorithm; + this.sessionCookieName = sessionCookieName; + this.sessionDuration = sessionDuration; + this.sessionCodec = sessionCodec; + this.suffixes = suffixes; + this.sessionUserName = sessionUserName; + this.sessionEffectiveUserName = sessionEffectiveUserName; + } + + @Override + public void handle(HttpServerContext context) throws HttpException { + if (context.getContextURL() == null) { + // emergency message + return; + } + String suffix = SessionUtil.extractExtension(context.request().getRequestPath()); + if (suffix != null && suffixes.contains(suffix)) { + logger.log(Level.FINE, "suffix " + suffix + " blocking outgoing session handling"); + return; + } + CookieBox cookieBox = context.attributes().get(CookieBox.class, "outgoingcookies"); + if (cookieBox == null) { + cookieBox = new CookieBox(); + } + Application application = context.attributes().get(Application.class, "application"); + UserProfile userProfile = context.attributes().get(UserProfile.class, "userprofile"); + String host = context.getContextURL().getHost(); + String path = application.getContextPath(); + Throwable throwable = context.attributes().get(Throwable.class, "_throwable"); + if (throwable instanceof CookieSignatureException) { + cookieBox = new CookieBox(); + cookieBox.add(createEmptyCookie(host, path)); + return; + } + Session session = context.attributes().get(Session.class, "session"); + if (session != null) { + try { + if (userProfile != null) { + logger.log(Level.FINE, "user profile present: " + userProfile); + session.put(sessionUserName, userProfile.getUserId()); + session.put(sessionEffectiveUserName, userProfile.getEffectiveUserId()); + } + sessionCodec.write(session.id(), session); + Cookie cookie = encodeCookie(session, host, path); + if (cookie != null) { + cookieBox.add(cookie); + } + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new HttpException("unable to create session cookie", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + } + logger.log(Level.FINER, "outgoing cookies = " + cookieBox); + context.attributes().put("outgoingcookies", cookieBox); + } + + private Cookie encodeCookie(Session session, String host, String path) throws IOException, + NoSuchAlgorithmException, InvalidKeyException { + if (sessionSecret == null) { + logger.log(Level.WARNING, "no secret, no cookie"); + return null; + } + String id = session.id(); + if (!session.isValid()) { + logger.log(Level.WARNING, "session " + id + " has been invalidated, returning empty cookie"); + return createEmptyCookie(host, path); + } + if (session.isExpired()) { + logger.log(Level.WARNING, "session " + id + " is expired, returning empty cookie"); + session.invalidate(); + return createEmptyCookie(host, path); + } + Map map = new HashMap<>(); + map.put(sessionUserName, session.get(sessionUserName)); + map.put(sessionEffectiveUserName, session.get(sessionEffectiveUserName)); + String payload = CookieSignatureUtil.toString(map); + String sig = CookieSignatureUtil.hmac(payload, sessionSecret, sessionCookieAlgorithm); + String cookieValue = String.join(":", id, payload, sig); + PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(StandardCharsets.ISO_8859_1); + DefaultCookie cookie = new DefaultCookie(sessionCookieName, percentEncoder.encode(cookieValue)); + String domain = extractDomain(host); + if (!"localhost".equals(domain)) { + cookie.setDomain('.' + domain); + } + cookie.setPath(path); + cookie.setMaxAge(sessionDuration.toSeconds()); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setSameSite(SameSite.LAX); + return cookie; + } + + private Cookie createEmptyCookie(String host, String path) { + DefaultCookie cookie = new DefaultCookie(sessionCookieName); + String domain = extractDomain(host); + if (!"localhost".equals(domain)) { + cookie.setDomain('.' + domain); + } + cookie.setPath(path); + cookie.setMaxAge(0L); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setSameSite(SameSite.LAX); + logger.log(Level.FINEST, "baked empty cookie"); + return cookie; + } + + private static String extractDomain(String fqdn) { + if ("localhost".equals(fqdn)) { + return fqdn; + } + // strip host name from FQDN + int pos1 = fqdn.indexOf('.'); + int pos2 = fqdn.lastIndexOf('.'); + if (pos1 < pos2) { + // more than one dot, strip host + return fqdn.substring(pos1 + 1); + } + return fqdn; + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/Session.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/Session.java new file mode 100644 index 0000000..7352a75 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/Session.java @@ -0,0 +1,19 @@ +package org.xbib.net.http.server.session; + +import java.time.Duration; +import java.util.Map; + +public interface Session extends Map { + + String id(); + + void invalidate(); + + boolean isValid(); + + boolean isExpired(); + + boolean hasPayload(); + + Duration getAge(); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/SessionListener.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/SessionListener.java new file mode 100644 index 0000000..a9f80c9 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/SessionListener.java @@ -0,0 +1,8 @@ +package org.xbib.net.http.server.session; + +public interface SessionListener { + + void onCreated(Session session); + + void onDestroy(Session session); +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/SessionUtil.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/SessionUtil.java new file mode 100644 index 0000000..42b123d --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/SessionUtil.java @@ -0,0 +1,25 @@ +package org.xbib.net.http.server.session; + +import java.util.Locale; + +public class SessionUtil { + + private SessionUtil() { + } + + public static String extractExtension(String path) { + if (path == null) { + return null; + } + String s = path; + if (s.endsWith(".gz")) { + s = s.substring(0, s.lastIndexOf('.')); + } + int dotIdx = s.lastIndexOf('.'); + int slashIdx = s.lastIndexOf('/'); + if (dotIdx < 0 || slashIdx > dotIdx) { + return null; + } + return s.substring(dotIdx + 1).toLowerCase(Locale.ROOT); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java new file mode 100644 index 0000000..b03ce7e --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java @@ -0,0 +1,127 @@ +package org.xbib.net.http.server.session.file; + +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.http.server.session.BaseSession; +import org.xbib.net.http.server.session.Session; +import org.xbib.net.http.server.session.SessionListener; +import org.xbib.net.util.JsonUtil; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +public class FileJsonSessionCodec implements Codec { + + private static final Logger logger = Logger.getLogger(FileJsonSessionCodec.class.getName()); + + private final ReentrantReadWriteLock lock; + + private final SessionListener sessionListener; + + private final Path path; + + private final int sessionCacheSize; + + private final Duration sessionDuration; + + public FileJsonSessionCodec(SessionListener sessionListener, + int sessionCacheSize, + Duration sessionDuration, + Path path) { + this.sessionListener = sessionListener; + this.path = path; + this.sessionCacheSize = sessionCacheSize; + this.sessionDuration = sessionDuration; + this.lock = new ReentrantReadWriteLock(); + try { + Files.createDirectories(path); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new UncheckedIOException(e); + } + } + + @Override + public Session create(String key) throws IOException { + return new BaseSession(sessionListener, sessionCacheSize, key, true, sessionDuration); + } + + @Override + public Session read(String key) throws IOException { + Session session; + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + session = new BaseSession(sessionListener, sessionCacheSize, key, false, sessionDuration); + Map map = JsonUtil.toMap(Files.readString(path.resolve(percentEncoder.encode(key)))); + session.putAll(map); + return session; + } finally { + readLock.unlock(); + } + } + + @Override + public void write(String key, Session session) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + try (Writer writer = Files.newBufferedWriter(path.resolve(percentEncoder.encode(key)))) { + writer.write(JsonUtil.toString(session)); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void remove(String key) { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + Files.deleteIfExists(path.resolve(percentEncoder.encode(key))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + writeLock.unlock(); + } + } + + @Override + public void purge(long expiredAfterSeconds) { + if (path != null && expiredAfterSeconds > 0L) { + Instant instant = Instant.now(); + try (Stream stream = Files.walk(path)) { + stream.forEach(p -> { + try { + FileTime fileTime = Files.getLastModifiedTime(p); + Duration duration = Duration.between(fileTime.toInstant(), instant); + if (duration.toSeconds() > expiredAfterSeconds) { + Files.delete(p); + } + } catch (IOException e) { + logger.log(Level.WARNING, "i/o error while purge: " + e.getMessage(), e); + } + }); + } catch (IOException e) { + logger.log(Level.WARNING, "i/o error while purge: " + e.getMessage(), e); + } + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java new file mode 100644 index 0000000..560fa77 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java @@ -0,0 +1,181 @@ +package org.xbib.net.http.server.session.jdbc; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javax.sql.DataSource; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.http.server.session.BaseSession; +import org.xbib.net.http.server.session.Session; +import org.xbib.net.http.server.session.SessionListener; +import org.xbib.net.util.JsonUtil; + +public class JdbcSessionCodec implements Codec, Closeable { + + private final SessionListener sessionListener; + + private final int sessionCacheSize; + + private final Duration sessionDuration; + + private final DataSource dataSource; + + private final String readSessionStringStatement; + + private final String writeSessionStringStatement; + + private final String deleteSessionStringStatement; + + private final String purgeSessionStringStatement; + + private final ScheduledExecutorService scheduledExecutorService; + + public JdbcSessionCodec(HttpServerContext httpServerContext, + SessionListener sessionListener, + int sessionCacheSize, + Duration sessionDuration, + DataSource dataSource, + String readSessionStringStatement, + String writeSessionStringStatement, + String deleteSessionStringStatement, + String purgeSessionStringStatement) { + this.sessionListener = sessionListener; + this.sessionCacheSize = sessionCacheSize; + this.sessionDuration = sessionDuration; + this.dataSource = dataSource; + this.readSessionStringStatement = readSessionStringStatement; + this.writeSessionStringStatement = writeSessionStringStatement; + this.deleteSessionStringStatement = deleteSessionStringStatement; + this.purgeSessionStringStatement = purgeSessionStringStatement; + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + this.scheduledExecutorService.schedule(() -> purgeDatabase(sessionDuration.getSeconds()), 0L, TimeUnit.SECONDS); + } + + @Override + public Session create(String key) throws IOException { + return new BaseSession(sessionListener, sessionCacheSize, key, true, sessionDuration); + } + + @Override + public Session read(String key) throws IOException { + Session session; + try { + Map map = JsonUtil.toMap(readString(key)); + if (map != null) { + session = new BaseSession(sessionListener, sessionCacheSize, key, false, sessionDuration); + session.putAll(map); + return session; + } + } catch (SQLException e) { + throw new IOException(e); + } + return null; + } + + @Override + public void write(String key, Session session) throws IOException { + if (session != null) { + try { + writeString(key, JsonUtil.toString(session)); + } catch (SQLException e) { + throw new IOException(e); + } + } + } + + @Override + public void remove(String key) throws IOException { + if (key != null) { + try { + deleteString(key); + } catch (SQLException e) { + throw new IOException(e); + } + } + } + + @Override + public void purge(long expiredAfterSeconds) { + if (expiredAfterSeconds > 0L) { + purgeDatabase(expiredAfterSeconds); + } + } + + private String readString(String key) throws SQLException { + List list = new ArrayList<>(); + Connection connection = dataSource.getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(readSessionStringStatement)) { + preparedStatement.setString(1, key); + ResultSet resultSet = preparedStatement.executeQuery(); + while (resultSet.next()) { + list.add(resultSet.getString(1)); + } + resultSet.close(); + } + return list.size() > 1 ? list.get(0) : null; + } + + private void writeString(String key, String value) throws SQLException { + Connection connection = dataSource.getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(writeSessionStringStatement)) { + preparedStatement.setString(1, key); // key + preparedStatement.setString(2, value); // value + preparedStatement.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now())); // created + preparedStatement.execute(); + } + } + + private void deleteString(String key) throws SQLException { + Connection connection = dataSource.getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(deleteSessionStringStatement)) { + preparedStatement.setString(1, key); + preparedStatement.execute(); + } + } + + private void purgeDatabase(long seconds) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expiredBefore = now.minusSeconds(seconds); + List list = new ArrayList<>(); + try { + Connection connection = dataSource.getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(purgeSessionStringStatement)) { + preparedStatement.setTimestamp(1, Timestamp.valueOf(expiredBefore)); // created + ResultSet resultSet = preparedStatement.executeQuery(); + while (resultSet.next()) { + list.add(resultSet.getString(1)); + } + resultSet.close(); + } + try (PreparedStatement preparedStatement = connection.prepareStatement(deleteSessionStringStatement)) { + for (String key : list) { + if (key != null) { + preparedStatement.setString(1, key); + preparedStatement.execute(); + } + } + } + } catch (SQLException e) { + throw new UncheckedIOException(new IOException(e)); + } + } + + @Override + public void close() throws IOException { + this.scheduledExecutorService.shutdown(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java new file mode 100644 index 0000000..a978655 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java @@ -0,0 +1,96 @@ +package org.xbib.net.http.server.session.memory; + +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.http.server.session.BaseSession; +import org.xbib.net.http.server.session.Session; +import org.xbib.net.http.server.session.SessionListener; + +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class MemoryPropertiesSessionCodec implements Codec { + + private final ReentrantReadWriteLock lock; + + private static final Map store = new HashMap<>(); + + private final SessionListener sessionListener; + + private final int sessionCacheSize; + + private final Duration sessionDuration; + + public MemoryPropertiesSessionCodec(SessionListener sessionListener, + int sessionCacheSize, + Duration sessionDuration) { + this.lock = new ReentrantReadWriteLock(); + this.sessionListener = sessionListener; + this.sessionCacheSize = sessionCacheSize; + this.sessionDuration = sessionDuration; + } + + @Override + public Session create(String key) throws IOException { + return new BaseSession(sessionListener, sessionCacheSize, key, true, sessionDuration); + } + + @Override + public Session read(String key) throws IOException { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + Properties properties = new Properties(); + if (store.containsKey(key)) { + properties.putAll((Map) store.get(key)); + } + return toSession(key, properties); + } finally { + readLock.unlock(); + } + } + + @Override + public void write(String key, Session session) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + Properties properties = toProperties(session); + store.put(key, properties); + } finally { + writeLock.unlock(); + } + } + + private Session toSession(String key, Properties properties) { + Session session = new BaseSession(sessionListener, sessionCacheSize, key, false, sessionDuration); + properties.forEach((k, v) -> session.put(k.toString(), v)); + return session; + } + + private Properties toProperties(Map map) { + Properties properties = new Properties(); + properties.putAll(map); + return properties; + } + + @Override + public void remove(String key) throws IOException { + store.remove(key); + } + + @Override + public void purge(long expiredAfterSeconds) throws IOException { + if (expiredAfterSeconds > 0L) { + for (Map.Entry entry : store.entrySet()) { + Session session = toSession(entry.getKey(), (Properties) entry.getValue()); + if (session.isExpired()) { + remove(entry.getKey()); + } + } + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/util/BlockingThreadPoolExecutor.java b/net-http-server/src/main/java/org/xbib/net/http/server/util/BlockingThreadPoolExecutor.java new file mode 100644 index 0000000..72929f6 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/util/BlockingThreadPoolExecutor.java @@ -0,0 +1,69 @@ +package org.xbib.net.http.server.util; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BlockingThreadPoolExecutor extends ThreadPoolExecutor { + + private final Logger logger = Logger.getLogger(BlockingThreadPoolExecutor.class.getName()); + + public BlockingThreadPoolExecutor(int nThreads, int maxQueue, + ThreadFactory threadFactory) { + this(nThreads, maxQueue, 60L, TimeUnit.SECONDS, threadFactory); + } + + public BlockingThreadPoolExecutor(int nThreads, int maxQueue, + long keepAliveTime, TimeUnit timeUnit, + ThreadFactory threadFactory) { + super(nThreads, nThreads, keepAliveTime, timeUnit, createBlockingQueue(maxQueue), threadFactory); + logger.log(Level.INFO, "blocking threadpool executor up with nThreads = " + nThreads + + " keepALiveTime = " + keepAliveTime + + " time unit = " + timeUnit + + " maxQueue = " + maxQueue + + " thread factory = " + threadFactory); + } + + private static BlockingQueue createBlockingQueue(int max) { + return max == Integer.MAX_VALUE ? new SynchronousQueue<>(true) : new ArrayBlockingQueue<>(max); + } + + /* + * Examine Throwable or Error of a thread after execution just to log them. + */ + @Override + protected void afterExecute(Runnable runnable, Throwable terminationCause) { + super.afterExecute(runnable, terminationCause); + logger.log(Level.FINE, "after dispatching " + runnable); + Throwable throwable = terminationCause; + if (throwable == null && runnable instanceof Future) { + try { + Future future = (Future) runnable; + if (!future.isDone() && !future.isCancelled()) { + logger.log(Level.FINE, "waiting for " + future); + future.get(); + } + } catch (CancellationException ce) { + logger.log(Level.FINE, ce.getMessage(), ce); + throwable = ce; + } catch (ExecutionException ee) { + logger.log(Level.FINE, ee.getMessage(), ee); + throwable = ee.getCause(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + logger.log(Level.FINE, ie.getMessage(), ie); + } + } + if (throwable != null) { + logger.log(Level.SEVERE, throwable.getMessage(), throwable); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/validate/HttpRequestValidator.java b/net-http-server/src/main/java/org/xbib/net/http/server/validate/HttpRequestValidator.java new file mode 100644 index 0000000..aad86cc --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/validate/HttpRequestValidator.java @@ -0,0 +1,30 @@ +package org.xbib.net.http.server.validate; + +import org.xbib.datastructures.common.Pair; +import org.xbib.net.http.server.UnknownExpectException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.HttpHeaderNames; + +public class HttpRequestValidator implements HttpHandler { + + public HttpRequestValidator() { + } + + @Override + public void handle(HttpServerContext context) throws HttpException { + boolean unknownExpect = false; + for (Pair entry : context.request().getHeaders().entries()) { + String name = entry.getKey(); + String value = entry.getValue(); + if (name.equalsIgnoreCase(HttpHeaderNames.EXPECT) && !"100-continue".equalsIgnoreCase(value)) { + unknownExpect = true; + } + } + if (unknownExpect) { + // RFC2616#14.20: if unknown expect, send 417 + throw new UnknownExpectException("unknown expect", context); + } + } +} diff --git a/net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapRealmTest.java b/net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapRealmTest.java new file mode 100644 index 0000000..c5ae046 --- /dev/null +++ b/net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapRealmTest.java @@ -0,0 +1,63 @@ +package org.xbib.net.http.server.ldap; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.Authenticator; +import org.xbib.net.GroupsProvider; +import org.xbib.net.UsersProvider; +import org.xbib.net.UserDetails; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LdapRealmTest { + + + @Disabled + @Test + public void testRealm() { + Map contextFactories = new HashMap<>(); + LdapContextFactory contextFactory = new LdapContextFactory("simple", + "com.sun.jndi.ldap.LdapCtxFactory", + null, + "ldap://localhost:389", + false, + null, + null, + "follow" + ); + contextFactories.put("default", contextFactory); + Map userMappings = new HashMap<>(); + LdapUserMapping userMapping = new LdapUserMapping("ou=People,dc=example,dc=org", + "(&(objectclass=posixAccount)(uid:caseExactMatch:={0}))", + "uid", + "cn" + ); + userMappings.put("default", userMapping); + Map groupMappings = new HashMap<>(); + LdapGroupMapping groupMapping = new LdapGroupMapping("ou=group,dc=example,dc=org", + "cn", + "(&(objectclass=posixGroup)(memberUid:caseExactMatch:={0}))", + new String[] { "uid" } + ); + groupMappings.put("default", groupMapping); + + LdapRealm ldapRealm = new LdapRealm("test", contextFactories, userMappings, groupMappings); + + Authenticator.Context context = new Authenticator.Context("test", "test", null); + boolean result = ldapRealm.getAuthenticator().authenticate(context); + assertTrue(result); + + UsersProvider.Context userContext = new UsersProvider.Context("test", null); + UserDetails userDetails = ldapRealm.getUsersProvider().getUserDetails(userContext); + assertEquals("Test", userDetails.getName()); + assertEquals("test", userDetails.getUserId()); + + GroupsProvider.Context groupContext = new GroupsProvider.Context("test", null); + Collection collection = ldapRealm.getGroupsProvider().getGroups(groupContext); + assertEquals("[test]", collection.toString()); + } +} diff --git a/net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapTest.java b/net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapTest.java new file mode 100644 index 0000000..fd05f78 --- /dev/null +++ b/net-http-server/src/test/java/org/xbib/net/http/server/ldap/LdapTest.java @@ -0,0 +1,39 @@ +package org.xbib.net.http.server.ldap; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LdapTest { + + @Disabled + @Test + public void testLdap() { + Map contextFactories = new HashMap<>(); + LdapContextFactory contextFactory = new LdapContextFactory("simple", + "com.sun.jndi.ldap.LdapCtxFactory", + null, + "ldap://localhost:389", + false, + null, + null, + "follow" + ); + contextFactories.put("default", contextFactory); + + Map userMappings = new HashMap<>(); + LdapUserMapping userMapping = new LdapUserMapping("ou=People.dc=example,dc=org", + "(&(objectclass=posixAccount)(uid:caseExactMatch:={0}))", + "uid", + "cn" + ); + userMappings.put("default", userMapping); + + LdapAuthenticator authenticator = new LdapAuthenticator(contextFactories, userMappings); + boolean result = authenticator.authenticate("test", "test"); + assertTrue(result); + } +} diff --git a/net-http-server/src/test/java/org/xbib/net/http/server/route/base/BaseHttpRouteResolverTest.java b/net-http-server/src/test/java/org/xbib/net/http/server/route/base/BaseHttpRouteResolverTest.java new file mode 100644 index 0000000..4cb5856 --- /dev/null +++ b/net-http-server/src/test/java/org/xbib/net/http/server/route/base/BaseHttpRouteResolverTest.java @@ -0,0 +1,273 @@ +package org.xbib.net.http.server.route.base; + +import org.junit.jupiter.api.Test; +import org.xbib.net.http.HttpAddress; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.server.route.BaseHttpRoute; +import org.xbib.net.http.server.route.BaseHttpRouteResolver; +import org.xbib.net.http.server.route.HttpRoute; +import org.xbib.net.http.server.route.HttpRouteResolver; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BaseHttpRouteResolverTest { + + @Test + public void testEmptyRouteResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, ""); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> atomicInteger.incrementAndGet()); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testNoDefinedRouteResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> atomicInteger.incrementAndGet()); + assertEquals(0, atomicInteger.get()); + } + + @Test + public void testEmptyRouteMatchResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testSingleRouteResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/path", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/path"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testSingleRouteMismatchResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/path1", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/path2"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(0, atomicInteger.get()); + } + + @Test + public void testSingleRouteMismatchTooLongResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/a/b"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(0, atomicInteger.get()); + } + + @Test + public void testSingleRouteCatchAllResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/**", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/path"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testSingleRouteCatchAllLongPathResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/**", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/a/very/long/path"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testSingleRouteJpegResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "glob:*.jpg", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "abc.jpg"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testSingleRouteGlobJpegResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "glob:**.jpg", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/a/picture/abc.jpg"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testSingleRouteParameterResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/{token}", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/abcdef"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + assertEquals("[token=abcdef]", r.getParameter().allToString()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testTwoRouteParameterResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/{token}/{key}", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/abcdef/123456"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + assertEquals("[token=abcdef, key=123456]", r.getParameter().allToString()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testMultiRouteResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/**", 1) + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/b/**", 2) + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/b/c", 3) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/a"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, (int) r.getValue()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testMultiRouteLongestFirstResolver() { + HttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/**", 1) + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/b/c/**", 2) + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/b/c/d/e/f/g", 3) + .sort(true) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/a"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + switch (atomicInteger.get()) { + case 0: + assertEquals(1, (int) r.getValue()); + break; + } + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } + + @Test + public void testMultiRouteLongestFirstWithGlobFirstResolver() { + HttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/**", 4) + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/b/c/**", 3) + .add(HttpAddress.http1("host"), HttpMethod.POST, "/a/b/c/d/e/f/g", 2) + .add(HttpAddress.http1("host"), HttpMethod.POST, "glob:**", 1) + .sort(true) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/a"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + switch (atomicInteger.get()) { + case 0: + assertEquals(1, (int) r.getValue()); + break; + case 1: + assertEquals(4, (int) r.getValue()); + break; + } + atomicInteger.incrementAndGet(); + }); + assertEquals(2, atomicInteger.get()); + } + + @Test + public void testContextRouteResolver() { + BaseHttpRouteResolver.Builder builder = BaseHttpRouteResolver.builder(); + HttpRouteResolver resolver = builder + .setPrefix("/app") + .add(HttpAddress.http1("host"), HttpMethod.POST, "/path", 1) + .build(); + HttpRoute route = new BaseHttpRoute(HttpAddress.http1("host"), HttpMethod.POST, "/app/path"); + AtomicInteger atomicInteger = new AtomicInteger(0); + resolver.resolve(route, r -> { + assertEquals(1, r.getValue()); + assertEquals("[path]", r.getContext().toString()); + atomicInteger.incrementAndGet(); + }); + assertEquals(1, atomicInteger.get()); + } +} diff --git a/net-http-server/src/test/resources/logging.properties b/net-http-server/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-http-server/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-http-template-groovy/build.gradle b/net-http-template-groovy/build.gradle new file mode 100644 index 0000000..b54ed76 --- /dev/null +++ b/net-http-template-groovy/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':net-http-server') + api libs.groovy.templates +} diff --git a/net-http-template-groovy/src/main/java/module-info.java b/net-http-template-groovy/src/main/java/module-info.java new file mode 100644 index 0000000..ce1e8c3 --- /dev/null +++ b/net-http-template-groovy/src/main/java/module-info.java @@ -0,0 +1,13 @@ +import org.xbib.net.http.server.ApplicationModule; +import org.xbib.net.http.template.groovy.GroovyTemplateApplicationModule; + +module org.xbib.net.http.template.groovy { + exports org.xbib.net.http.template.groovy; + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.apache.groovy; + requires org.apache.groovy.templates; + requires java.logging; + provides ApplicationModule with GroovyTemplateApplicationModule; +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java new file mode 100644 index 0000000..aab119f --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java @@ -0,0 +1,239 @@ +package org.xbib.net.http.template.groovy; + +import groovy.text.markup.BaseTemplate; +import groovy.text.markup.MarkupTemplateEngine; +import groovy.text.markup.TemplateConfiguration; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.URL; +import org.xbib.net.URLBuilder; +import org.xbib.net.buffer.DefaultDataBufferFactory; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpResponseBuilder; +import org.xbib.net.http.server.session.Session; +import org.xbib.net.template.URITemplate; +import org.xbib.net.template.vars.Variables; +import org.xbib.net.util.RandomUtil; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Map; + +public abstract class DefaultMarkupTemplate extends BaseTemplate { + + private static final Logger logger = Logger.getLogger(DefaultMarkupTemplate.class.getName()); + + private final Application application; + + private final HttpRequest request; + + private final HttpResponseBuilder responseBuilder; + + private final Session session; + + public DefaultMarkupTemplate(MarkupTemplateEngine templateEngine, + Map model, + Map modelTypes, + TemplateConfiguration configuration) { + super(templateEngine, model, modelTypes, configuration); + this.application = (Application) model.get("application"); + this.request = (HttpRequest) model.get("request"); + this.responseBuilder = (HttpResponseBuilder) model.get("responsebuilder"); + this.session = (Session) model.get("session"); + } + + public void responseStatus(HttpResponseStatus responseStatus) { + responseBuilder.setResponseStatus(responseStatus); + } + + public void contentType(String contentType) { + responseBuilder.setHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + } + + public boolean isContentType(String contentType) { + return request.getHeaders().containsHeader(HttpHeaderNames.CONTENT_TYPE) && contentType != null && + request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE).startsWith(contentType); + } + + public void contentDisposition(String contentDisposition) { + responseBuilder.setHeader(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition); + } + + public void contentLength(int contentLength) { + responseBuilder.setHeader(HttpHeaderNames.CONTENT_LENGTH, Integer.toString(contentLength)); + } + + public void sendPermanentRedirect(String url) { + responseBuilder.setResponseStatus(HttpResponseStatus.MOVED_PERMANENTLY); + responseBuilder.setHeader(HttpHeaderNames.LOCATION, url); + } + + public void sendRedirect(String url) { + responseBuilder.setResponseStatus(HttpResponseStatus.FOUND); + responseBuilder.setHeader(HttpHeaderNames.LOCATION, url); + } + + public void seeOther(String url) { + responseBuilder.setResponseStatus(HttpResponseStatus.SEE_OTHER); + responseBuilder.setHeader(HttpHeaderNames.LOCATION, url); + } + + public void temporaryRedirect(String url) { + responseBuilder.setResponseStatus(HttpResponseStatus.TEMPORARY_REDIRECT); + responseBuilder.setHeader(HttpHeaderNames.LOCATION, url); + } + + public String contextPath(String rel) { + return urlProto(rel, false); + } + + public String url(String rel) { + return urlProto(rel, true); + } + + public String urlProto(String rel, boolean absolute) { + URL url = request.getServerURL().resolve(rel); + logger.log(Level.FINE, "server URL = " + request.getServerURL() + " rel = " + rel + " --> " + url); + return absolute ? url.toExternalForm() : toOrigin(url); + } + + public String encodeUrl(String rel) { + return encodeUrl(rel, true); + } + + public String encodeUrl(String rel, boolean absolute) { + URLBuilder builder = request.getServerURL().resolve(rel).mutator(); + if (session != null) { + if (getModel().containsKey("session.url.enabled") && getModel().containsKey("session.url.parametername")) { + String sessionIdParameterName = (String) getModel().get("session.url.parametername"); + builder.queryParam(sessionIdParameterName, session.id()); + } + } + URL url = builder.build(); + return absolute ? url.toExternalForm() : toOrigin(url); + } + + public static String toOrigin(URL url) { + StringBuilder sb = new StringBuilder(); + if (url.getPath() != null) { + sb.append(url.getPath()); + } + if (url.getQuery() != null) { + sb.append('?').append(url.getQuery()); + } + if (url.getFragment() != null) { + sb.append('#').append(url.getFragment()); + } + if (sb.length() == 0) { + sb.append('/'); + } + return sb.toString(); + } + + public String encodeUriTemplate(String spec, Map vars) { + URITemplate uriTemplate = new URITemplate(spec); + Variables.Builder builder = Variables.builder(); + for (Map.Entry entry : vars.entrySet()) { + builder.add(entry.getKey(), entry.getValue()); + } + return uriTemplate.toURL(builder.build()).toString(); + } + + public void write(String string) { + if (string != null) { + responseBuilder.write(string); + } + } + + public void writeBytes(byte[] bytes) { + responseBuilder.write(DefaultDataBufferFactory.getInstance().wrap(bytes)); + } + + public String randomKey() { + return RandomUtil.randomString(16); + } + + public String fullDateTimeNow() { + DateTimeFormatter formatter = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.FULL) + .withLocale(application.getLocale()) + .withZone(application.getZoneId()); + return ZonedDateTime.now().format(formatter); + } + + public String bootstrapCss() { + return contextPath("webjars/bootstrap/3.4.1/dist/css/bootstrap.min.css"); + } + + public String bootstrapJs() { + return contextPath("webjars/bootstrap/3.4.1/dist/js/bootstrap.min.js"); + } + + public String jqueryJs() { + return contextPath("webjars/jquery/3.5.1/dist/jquery.min.js"); + } + + public String fontawesomeCss() { + return contextPath("webjars/font-awesome/5.14.0/css/all.min.css"); + } + + public String fontawesomeJs() { + return contextPath("webjars/font-awesome/5.14.0/js/all.min.js"); + } + + public String popperJs() { + return contextPath("webjars/popper.js/1.14.4/umd/popper.min.js"); + } + + public String fileinputCss() { + return contextPath("webjars/bootstrap-fileinput/4.4.8/css/fileinput.min.css"); + } + + public String fileinputJs() { + return contextPath("webjars/bootstrap-fileinput/4.4.8/js/fileinput.min.js"); + } + + public String fileinputLocale(String locale) { + return contextPath("webjars/bootstrap-fileinput/4.4.8/js/locales/" + locale + ".js"); + } + + public String fileinputTheme(String theme) { + return contextPath("webjars/bootstrap-fileinput/4.4.8/themes/" + theme + "/theme.min.js"); + } + + public String datatablesCss() { + return contextPath("webjars/datatables/1.10.19/css/jquery.dataTables.min.css"); + } + + public String datatablesJs() { + return contextPath("webjars/datatables/1.10.19/js/jquery.dataTables.min.js"); + } + + public String bootstrapTableCss() { + return contextPath("webjars/bootstrap-table/1.15.4/dist/bootstrap-table.min.css"); + } + + public String bootstrapTableLocale(String locale) { + return contextPath("webjars/bootstrap-table/1.15.4/dist/locale/bootstrap-table-${locale}.min.js"); + } + + public String bootstrapTableJs() { + return contextPath("webjars//bootstrap-table/1.15.4/dist/bootstrap-table.min.js"); + } + + public String bootstrapTableAutoRefreshJs() { + return contextPath("webjars/bootstrap-table/1.15.4/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"); + } + + public String bootstrapHoverDropdownJs() { + return contextPath("webjars/bootstrap-hover-dropdown/2.2.1/bootstrap-hover-dropdown.min.js"); + } + + public String bootstrapValidatorJs() { + return contextPath("webjars/bootstrap-validator/0.11.9/js/validator.js"); + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultTemplateResolver.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultTemplateResolver.java new file mode 100644 index 0000000..69682a7 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultTemplateResolver.java @@ -0,0 +1,53 @@ +package org.xbib.net.http.template.groovy; + +import groovy.text.markup.MarkupTemplateEngine.TemplateResource; +import groovy.text.markup.TemplateConfiguration; +import groovy.text.markup.TemplateResolver; +import org.xbib.net.http.server.Resolver; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DefaultTemplateResolver implements TemplateResolver { + + private static final Logger logger = Logger.getLogger(DefaultTemplateResolver.class.getName()); + + private final Resolver resolver; + + private TemplateConfiguration templateConfiguration; + + private Locale locale; + + public DefaultTemplateResolver(Resolver resolver) { + this.resolver = resolver; + } + + @Override + public void configure(ClassLoader cl, TemplateConfiguration templateConfiguration) { + this.templateConfiguration = templateConfiguration; + } + + @Override + public URL resolveTemplate(String templatePath) throws IOException { + TemplateResource templateResource = TemplateResource.parse(templatePath); + String languageTag = locale != null ? + locale.toLanguageTag().replace("-", "_") : + templateConfiguration.getLocale() != null ? templateConfiguration.getLocale().toLanguageTag().replace("-", "_") : + null; + String templateResourceString = languageTag != null ? + templateResource.withLocale(languageTag).toString() : + templateResource.toString(); + logger.log(Level.FINER, "template resource string = " + templateResourceString + " locale = " + locale + + " templateConfiguration.getLocale() = " + templateConfiguration.getLocale() + " languageTag = " + languageTag); + Path path = resolver.resolve(templateResourceString); + return path.toUri().toURL(); + } + + public void setLocale(Locale locale) { + this.locale = locale; + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java new file mode 100644 index 0000000..2f5b490 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java @@ -0,0 +1,99 @@ +package org.xbib.net.http.template.groovy; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.URL; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.nio.file.Path; + +class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource { + + private static final Logger logger = Logger.getLogger(GroovyHttpResonseStatusTemplateResource.class.getName()); + + private final String indexFileName; + + private final HttpResponseStatus httpResponseStatus; + + private final String message; + + protected GroovyHttpResonseStatusTemplateResource(GroovyTemplateResourceHandler handler, + HttpServerContext httpServerContext, + String indexFileName, + HttpResponseStatus httpResponseStatus, + String message) throws IOException { + super(handler, httpServerContext); + this.indexFileName = indexFileName; + this.httpResponseStatus = httpResponseStatus; + this.message = message; + } + + @Override + public void render(HttpServerContext httpServerContext) throws IOException { + logger.log(Level.FINE, "rendering HTTP status by Groovy"); + httpServerContext.attributes().put("_status", httpResponseStatus); + httpServerContext.attributes().put("_message", message); + httpServerContext.attributes().put("_resource", this); + Application application = httpServerContext.attributes().get(Application.class, "application"); + GroovyMarkupTemplateHandler groovyMarkupTemplateHandler = new GroovyMarkupTemplateHandler(application); + logger.log(Level.FINE, "handle groovyMarkupTemplateHandler"); + groovyMarkupTemplateHandler.handle(httpServerContext); + super.render(httpServerContext); + GroovyTemplateRenderer groovyTemplateRenderer = new GroovyTemplateRenderer(); + logger.log(Level.FINE, "handle groovyTemplateRenderer"); + groovyTemplateRenderer.handle(httpServerContext); + } + + @Override + public Path getPath() { + return null; + } + + @Override + public String getName() { + return "status-resource"; + } + + @Override + public String getBaseName() { + return null; + } + + @Override + public String getSuffix() { + return null; + } + + @Override + public String getResourcePath() { + return null; + } + + @Override + public URL getURL() { + return null; + } + + @Override + public boolean isExists() { + return true; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public String getMimeType() { + return "text/html; charset=UTF-8"; + } + + @Override + public String getIndexFileName() { + return indexFileName; + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpStatusHandler.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpStatusHandler.java new file mode 100644 index 0000000..399151f --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpStatusHandler.java @@ -0,0 +1,35 @@ +package org.xbib.net.http.template.groovy; + +import org.xbib.net.Resource; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpErrorHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; + +public class GroovyHttpStatusHandler extends GroovyTemplateResourceHandler implements HttpErrorHandler { + + private final HttpResponseStatus httpResponseStatus; + + private final String message; + + private final String templateName; + + public GroovyHttpStatusHandler(HttpResponseStatus httpResponseStatus, + String message, + String templateName) { + this.httpResponseStatus = httpResponseStatus; + this.message = message; + this.templateName = templateName; + } + + @Override + protected Resource createResource(HttpServerContext httpServerContext) throws IOException { + return new GroovyHttpResonseStatusTemplateResource(this, httpServerContext, + templateName, httpResponseStatus, message); + } + + public String getTemplateName() { + return templateName; + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyInternalServerErrorHandler.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyInternalServerErrorHandler.java new file mode 100644 index 0000000..9dc08d3 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyInternalServerErrorHandler.java @@ -0,0 +1,40 @@ +package org.xbib.net.http.template.groovy; + +import org.xbib.net.Resource; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class GroovyInternalServerErrorHandler extends GroovyTemplateResourceHandler { + + private static final Logger logger = Logger.getLogger(GroovyInternalServerErrorHandler.class.getName()); + + private final String templateName; + + public GroovyInternalServerErrorHandler(String templateName) { + this.templateName = templateName; + } + + @Override + protected Resource createResource(HttpServerContext httpServerContext) throws IOException { + return new GroovyHttpResonseStatusTemplateResource(this, httpServerContext, templateName, + HttpResponseStatus.INTERNAL_SERVER_ERROR, createMessage(httpServerContext)); + } + + private String createMessage(HttpServerContext context) throws IOException { + Throwable throwable = context.attributes().get(Throwable.class, "_throwable"); + if (throwable != null) { + logger.log(Level.SEVERE, throwable.getMessage(), throwable); + } + if (throwable instanceof HttpException) { + HttpException httpException = (HttpException) throwable; + return httpException.getMessage(); + } else { + return throwable != null ? throwable.getMessage() : ""; + } + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyMarkupTemplateHandler.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyMarkupTemplateHandler.java new file mode 100644 index 0000000..37f4565 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyMarkupTemplateHandler.java @@ -0,0 +1,102 @@ +package org.xbib.net.http.template.groovy; + +import groovy.text.TemplateEngine; +import groovy.text.markup.BaseTemplate; +import groovy.text.markup.MarkupTemplateEngine; +import groovy.text.markup.TemplateConfiguration; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.Resolver; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Locale; + +public class GroovyMarkupTemplateHandler implements HttpHandler { + + private final Resolver resolver; + + private final ClassLoader classLoader; + + private final TemplateConfiguration templateConfiguration; + + private final DefaultTemplateResolver templateResolver; + + private final TemplateEngine templateEngine; + + public GroovyMarkupTemplateHandler(Application application) { + this(application, GroovyMarkupTemplateHandler.class.getClassLoader(), + DefaultMarkupTemplate.class, application.getLocale(), false, false, + " ", false, true, null, + true, System.getProperty("line.separator"), true); + } + + public GroovyMarkupTemplateHandler(Resolver resolver, + ClassLoader classLoader, + Class templateClass, + Locale locale, + boolean autoEscape, + boolean autoIndent, + String autoIndentString, + boolean autoNewLine, + boolean cacheTemplates, + String declarationEncoding, + boolean expandEmptyElements, + String newLineString, + boolean useDoubleQuotes) { + this.resolver = resolver; + this.classLoader = classLoader; + this.templateConfiguration = createConfiguration(templateClass != null ? templateClass : DefaultMarkupTemplate.class, + locale, autoEscape, autoIndent, autoIndentString, autoNewLine, cacheTemplates, declarationEncoding, + expandEmptyElements, newLineString, useDoubleQuotes); + this.templateResolver = createTemplateResolver(); + this.templateEngine = createEngine(); + } + + @Override + public void handle(HttpServerContext context) throws IOException { + DefaultTemplateResolver templateResolver = context.attributes().get(DefaultTemplateResolver.class, "templateresolver"); + if (templateResolver == null) { + context.attributes().put("templateresolver", this.templateResolver); + } + TemplateEngine templateEngine = context.attributes().get(TemplateEngine.class, "templateengine"); + if (templateEngine == null) { + context.attributes().put("templateengine", this.templateEngine); + } + } + + protected TemplateConfiguration createConfiguration(Class templateClass, + Locale locale, + boolean autoEscape, + boolean autoIndent, + String autoIndentString, + boolean autoNewLine, + boolean cacheTemplates, + String declarationEncoding, + boolean expandEmptyElements, + String newLineString, + boolean useDoubleQuotes) { + TemplateConfiguration templateConfiguration = new TemplateConfiguration(); + templateConfiguration.setBaseTemplateClass(templateClass); + templateConfiguration.setLocale(locale); + templateConfiguration.setAutoEscape(autoEscape); + templateConfiguration.setAutoIndent(autoIndent); + templateConfiguration.setAutoIndentString(autoIndentString); + templateConfiguration.setAutoNewLine(autoNewLine); + templateConfiguration.setCacheTemplates(cacheTemplates); + templateConfiguration.setDeclarationEncoding(declarationEncoding); + templateConfiguration.setExpandEmptyElements(expandEmptyElements); + templateConfiguration.setNewLineString(newLineString); + templateConfiguration.setUseDoubleQuotes(useDoubleQuotes); + return templateConfiguration; + } + + protected DefaultTemplateResolver createTemplateResolver() { + return new DefaultTemplateResolver(resolver); + } + + protected TemplateEngine createEngine() { + return new MarkupTemplateEngine(classLoader, templateConfiguration, templateResolver); + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java new file mode 100644 index 0000000..5a316e8 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java @@ -0,0 +1,54 @@ +package org.xbib.net.http.template.groovy; + +import org.xbib.net.http.server.BaseApplicationModule; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.io.UncheckedIOException; +import org.xbib.net.http.server.HttpService; + +public class GroovyTemplateApplicationModule extends BaseApplicationModule { + + private GroovyMarkupTemplateHandler groovyMarkupTemplateHandler; + + private GroovyTemplateRenderer groovyTemplateRenderer; + + public GroovyTemplateApplicationModule() { + } + + @Override + public String getName() { + return "groovy-template"; + } + + @Override + public void onOpen(Application application) { + this.groovyMarkupTemplateHandler = new GroovyMarkupTemplateHandler(application); + this.groovyTemplateRenderer = new GroovyTemplateRenderer(); + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext) { + try { + groovyMarkupTemplateHandler.handle(httpServerContext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void onOpen(Application application, HttpServerContext httpServerContext, HttpService httpService, HttpRequest httpRequest) { + httpServerContext.attributes().put("params", httpRequest.getParameter().asSingleValuedMap()); + } + + @Override + public void onClose(Application application, HttpServerContext httpServerContext) { + try { + groovyTemplateRenderer.handle(httpServerContext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateRenderer.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateRenderer.java new file mode 100644 index 0000000..e1a8cfc --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateRenderer.java @@ -0,0 +1,38 @@ +package org.xbib.net.http.template.groovy; + +import groovy.lang.Writable; +import org.xbib.net.buffer.DataBuffer; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpServerContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +public class GroovyTemplateRenderer implements HttpHandler { + + public GroovyTemplateRenderer() { + } + + @Override + public void handle(HttpServerContext context) throws IOException { + Writable writable = context.attributes().get(Writable.class, "writable"); + if (writable != null) { + DataBuffer dataBuffer = context.response().getDataBufferFactory().allocateBuffer(); + try (OutputStream outputStream = dataBuffer.asOutputStream()) { + Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + writable.writeTo(writer); + } + HttpResponseStatus httpResponseStatus = context.attributes().get(HttpResponseStatus.class, "_status", HttpResponseStatus.OK); + context.response() + .setResponseStatus(httpResponseStatus) + .setHeader("content-length", Integer.toString(dataBuffer.writePosition())) + .setContentType("text/html; charset=" + StandardCharsets.UTF_8.displayName()) + .setHeader("cache-control", "no-cache") + .write(dataBuffer); + } + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java new file mode 100644 index 0000000..5ad1815 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java @@ -0,0 +1,140 @@ +package org.xbib.net.http.template.groovy; + +import groovy.lang.Binding; +import groovy.lang.Writable; +import groovy.text.Template; +import groovy.text.TemplateEngine; +import org.xbib.net.http.HttpHeaderNames; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.Application; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.HttpService; +import org.xbib.net.http.server.resource.negotiate.LocaleNegotiator; +import org.xbib.net.http.server.resource.HtmlTemplateResource; +import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class GroovyTemplateResource extends HtmlTemplateResource { + + private static final Logger logger = Logger.getLogger(GroovyTemplateResource.class.getName()); + + private static final Map templates = new HashMap<>(); + + private static final ReentrantLock lock = new ReentrantLock(); + + protected GroovyTemplateResource(HtmlTemplateResourceHandler templateResourceHandler, + HttpServerContext httpServerContext) throws IOException { + super(templateResourceHandler, httpServerContext); + } + + @Override + public void render(HttpServerContext httpServerContext) throws IOException { + logger.log(Level.FINE, "rendering groovy template, path = " + getPath() + " isExists = " + isExists() + " isDirectory =" + isDirectory() ); + Application application = httpServerContext.attributes().get(Application.class, "application"); + if (application == null) { + logger.log(Level.WARNING, "application is null"); + return; + } + TemplateEngine templateEngine = httpServerContext.attributes().get(TemplateEngine.class, "templateengine"); + if (templateEngine == null) { + logger.log(Level.WARNING, "template engine is null"); + return; + } + // + Path templatePath = getPath(); + logger.log(Level.FINE, "templatePath = " + getPath()); + GroovyHttpResonseStatusTemplateResource resource = httpServerContext.attributes().get(GroovyHttpResonseStatusTemplateResource.class, "_resource"); + if (templatePath == null && isExists() && resource != null) { + logger.log(Level.FINE, "Groovy HTTP status response rendering"); + String indexFileName = resource.getIndexFileName(); + if (indexFileName != null) { + templatePath = application.resolve(indexFileName); + } + if (templatePath == null) { + HttpService service = httpServerContext.attributes().get(HttpService.class, "service"); + GroovyTemplateService groovyTemplateService = (GroovyTemplateService) service; + if (groovyTemplateService.getTemplateName() != null) { + templatePath = application.resolve(groovyTemplateService.getTemplateName()); + logger.log(Level.FINE, "templatePath after application.resolve() = " + templatePath); + } else { + logger.log(Level.FINE, "the GroovyTemplateService does not have a template name set"); + } + } + } + // override if 'templatePath' attribute is set + String overridePath = httpServerContext.attributes().get(String.class, "templatePath"); + if (overridePath != null) { + logger.log(Level.FINE, "found override templatePath = " + overridePath); + templatePath = application.resolve(overridePath); + logger.log(Level.FINE, "found override templatePath, resolved to " + templatePath); + } + if (templatePath == null) { + logger.log(Level.FINE, "templatePath is null, OOTB effort on " + getIndexFileName()); + // OOTB rendering via getIndexFileName(), no getPath(), no getTemplateName() + templatePath = application.resolve(getIndexFileName()); + } + if (isDirectory()) { + logger.log(Level.WARNING, "unable to render a directory, this is forbidden"); + throw new HttpException("forbidden", httpServerContext, HttpResponseStatus.FORBIDDEN); + } + logger.log(Level.FINE, "rendering groovy template " + templatePath); + templates.computeIfAbsent(templatePath, path -> { + try { + return templateEngine.createTemplate(Files.readString(path)); + } catch (ClassNotFoundException | IOException e) { + throw new IllegalArgumentException(e); + } + }); + Template template = templates.get(templatePath); + Logger templateLogger = Logger.getLogger("template." + getName().replace('/', '.')); + Binding binding = new Binding(); + binding.setVariable("variables", binding.getVariables()); + httpServerContext.attributes().forEach(binding::setVariable); + binding.setVariable("logger", templateLogger); + binding.setVariable("log", templateLogger); + DefaultTemplateResolver templateResolver = httpServerContext.attributes().get(DefaultTemplateResolver.class, "templateresolver"); + if (templateResolver != null) { + // handle programmatic locale change plus template making under lock so no other request/response can interrupt us + logger.log(Level.FINER, "application locale for template = " + application.getLocale()); + try { + lock.lock(); + templateResolver.setLocale(application.getLocale()); + String acceptLanguage = httpServerContext.request().getHeaders().get(HttpHeaderNames.ACCEPT_LANGUAGE); + if (acceptLanguage != null) { + Locale negotiatedLocale = LocaleNegotiator.findLocale(acceptLanguage); + if (negotiatedLocale != null) { + logger.log(Level.FINER, "negotiated locale for template = " + negotiatedLocale); + templateResolver.setLocale(negotiatedLocale); + } + } + Writable writable = template.make(binding.getVariables()); + httpServerContext.attributes().put("writable", writable); + httpServerContext.done(); + } catch (Exception e) { + // in case there is not template with negotiated locale + templateResolver.setLocale(application.getLocale()); + Writable writable = template.make(binding.getVariables()); + httpServerContext.attributes().put("writable", writable); + httpServerContext.done(); + } finally { + lock.unlock(); + } + } else { + // for Groovy template engines without a resolver + Writable writable = template.make(binding.getVariables()); + httpServerContext.attributes().put("writable", writable); + httpServerContext.done(); + } + logger.log(Level.FINER, "rendering done: " + httpServerContext.isDone()); + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResourceHandler.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResourceHandler.java new file mode 100644 index 0000000..d745aea --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResourceHandler.java @@ -0,0 +1,24 @@ +package org.xbib.net.http.template.groovy; + +import org.xbib.net.Resource; +import org.xbib.net.http.server.HttpServerContext; +import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; + +import java.io.IOException; +import java.nio.file.Path; + +public class GroovyTemplateResourceHandler extends HtmlTemplateResourceHandler { + + public GroovyTemplateResourceHandler() { + this(null, "gtpl", "index.gtpl"); + } + + public GroovyTemplateResourceHandler(Path prefix, String suffix, String indexFileName) { + super(prefix, suffix, indexFileName); + } + + @Override + protected Resource createResource(HttpServerContext httpServerContext) throws IOException { + return new GroovyTemplateResource(this, httpServerContext); + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateService.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateService.java new file mode 100644 index 0000000..a7f0163 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateService.java @@ -0,0 +1,21 @@ +package org.xbib.net.http.template.groovy; + +import org.xbib.net.http.server.BaseHttpService; + +public class GroovyTemplateService extends BaseHttpService { + + private final GroovyTemplateServiceBuilder builder; + + protected GroovyTemplateService(GroovyTemplateServiceBuilder builder) { + super(builder); + this.builder = builder; + } + + public static GroovyTemplateServiceBuilder builder() { + return new GroovyTemplateServiceBuilder(); + } + + public String getTemplateName() { + return builder.templateName; + } +} diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateServiceBuilder.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateServiceBuilder.java new file mode 100644 index 0000000..d1bc996 --- /dev/null +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateServiceBuilder.java @@ -0,0 +1,85 @@ +package org.xbib.net.http.template.groovy; + +import org.xbib.net.ParameterDefinition; +import org.xbib.net.http.HttpMethod; +import org.xbib.net.http.server.BaseHttpServiceBuilder; + +import java.nio.file.Path; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpSecurityDomain; + +public class GroovyTemplateServiceBuilder extends BaseHttpServiceBuilder { + + protected Path prefix; + + protected String suffix; + + protected String indexFileName; + + protected String templateName; + + public GroovyTemplateServiceBuilder() { + super(); + this.prefix = null; + this.indexFileName = "index.gtpl"; + this.templateName = indexFileName; + } + + @Override + public GroovyTemplateServiceBuilder setPath(String path) { + super.setPath(path); + return this; + } + + @Override + public GroovyTemplateServiceBuilder setMethod(HttpMethod... httpMethod) { + super.setMethod(httpMethod); + return this; + } + + @Override + public GroovyTemplateServiceBuilder setHandler(HttpHandler... handler) { + super.setHandler(handler); + return this; + } + + @Override + public GroovyTemplateServiceBuilder setParameterDefinition(ParameterDefinition... parameterDefinition) { + super.setParameterDefinition(parameterDefinition); + return this; + } + + @Override + public BaseHttpServiceBuilder setSecurityDomain(HttpSecurityDomain securityDomain) { + super.setSecurityDomain(securityDomain); + return this; + } + + public GroovyTemplateServiceBuilder setPrefix(Path prefix) { + this.prefix = prefix; + return this; + } + + public GroovyTemplateServiceBuilder setIndexFileName(String indexFileName) { + this.indexFileName = indexFileName; + return this; + } + + public GroovyTemplateServiceBuilder setSuffix(String suffix) { + this.suffix = suffix; + return this; + } + + public GroovyTemplateServiceBuilder setTemplateName(String templateName) { + this.templateName = templateName; + return this; + } + + public GroovyTemplateService build() { + if (this.handlers == null) { + HttpHandler httpHandler = new GroovyTemplateResourceHandler(prefix, suffix, indexFileName); + setHandler(httpHandler); + } + return new GroovyTemplateService(this); + } +} diff --git a/net-http/build.gradle b/net-http/build.gradle new file mode 100644 index 0000000..e19c4ec --- /dev/null +++ b/net-http/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api libs.net + api libs.net.mime +} diff --git a/net-http/src/main/java/module-info.java b/net-http/src/main/java/module-info.java new file mode 100644 index 0000000..1a3376c --- /dev/null +++ b/net-http/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module org.xbib.net.http { + exports org.xbib.net.http; + exports org.xbib.net.http.cookie; + requires org.xbib.net; + requires org.xbib.net.mime; + requires java.logging; +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpAddress.java b/net-http/src/main/java/org/xbib/net/http/HttpAddress.java new file mode 100644 index 0000000..feb3352 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpAddress.java @@ -0,0 +1,173 @@ +package org.xbib.net.http; + +import org.xbib.net.Address; +import org.xbib.net.NetworkUtils; +import org.xbib.net.SocketConfig; +import org.xbib.net.URL; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Set; + +/** + * An address handle for host, port, HTTP version, secure transport flag of a channel for HTTP. + */ +public class HttpAddress implements Address { + + private final String host; + + private final Integer port; + + private final HttpVersion version; + + private final Boolean secure; + + private final Set hostNames; + + private InetAddress inetAddress; + + private InetSocketAddress inetSocketAddress; + + private SocketConfig socketConfig; + + public static HttpAddress http1(String host) { + return new HttpAddress(host, 80, HttpVersion.HTTP_1_1, false); + } + + public static HttpAddress http1(String host, int port) { + return new HttpAddress(host, port, HttpVersion.HTTP_1_1, false); + } + + public static HttpAddress http2(String host) { + return new HttpAddress(host, 443, HttpVersion.HTTP_2_0, false); + } + + public static HttpAddress http2(String host, int port) { + return new HttpAddress(host, port, HttpVersion.HTTP_2_0, false); + } + + public static HttpAddress http1(URL url) { + return new HttpAddress(url, HttpVersion.HTTP_1_1); + } + + public static HttpAddress http2(URL url) { + return new HttpAddress(url, HttpVersion.HTTP_2_0); + } + + public static HttpAddress of(URL url) { + return new HttpAddress(url, HttpVersion.HTTP_1_1); + } + + public static HttpAddress of(URL url, HttpVersion httpVersion) { + return new HttpAddress(url, httpVersion); + } + + public static HttpAddress of(String host, Integer port, HttpVersion version, boolean secure) { + return new HttpAddress(host, port, version, secure, Set.of()); + } + + public HttpAddress(URL url, HttpVersion version) { + this(url.getHost(), url.getPort(), version, "https".equals(url.getScheme()), Set.of()); + } + + public HttpAddress(String host, Integer port, HttpVersion version, boolean secure) { + this(host, port, version, secure, Set.of()); + } + public HttpAddress(String host, Integer port, HttpVersion version, + boolean secure, Set hostNames) { + this.host = host; + this.port = (port == null || port == -1) ? secure ? 443 : 80 : port; + this.version = version; + this.secure = secure; + this.hostNames = hostNames; + this.socketConfig = new SocketConfig(); + } + + @Override + public String getHost() { + return host; + } + + @Override + public Integer getPort() { + return port; + } + + @Override + public InetAddress getInetAddress() throws IOException { + if (inetAddress == null) { + this.inetAddress = NetworkUtils.resolveInetAddress(host, null); + } + return inetAddress; + } + + @Override + public InetSocketAddress getInetSocketAddress() throws IOException { + if (inetSocketAddress == null) { + InetAddress inetAddress = getInetAddress(); + this.inetSocketAddress = new InetSocketAddress(inetAddress.getHostAddress(), port); + } + return inetSocketAddress; + } + + @Override + public URL base() { + return isSecure() ? + URL.https().host(host).port(port).build() : + URL.http().host(host).port(port).build(); + } + + @Override + public boolean isSecure() { + return secure; + } + + public void setSocketConfig(SocketConfig socketConfig) { + this.socketConfig = socketConfig; + } + + @Override + public SocketConfig getSocketConfig() { + return socketConfig; + } + + public Set getHostNames() { + return hostNames; + } + + public HttpVersion getVersion() { + return version; + } + + public String hostAndPort() { + return host + ":" + port; + } + + public String hostAddressAndPort() throws IOException { + return getInetAddress().getHostAddress() + ":" + port; + } + + public String canonicalHostAndPort() throws IOException { + return getInetAddress().getCanonicalHostName() + ":" + port; + } + + @Override + public String toString() { + return "[" + version + "]" + (secure ? "[SECURE]" : "") + host + ":" + port; + } + + @Override + public boolean equals(Object object) { + return object instanceof HttpAddress && + host.equals(((HttpAddress) object).host) && + (port != null && port.equals(((HttpAddress) object).port)) && + version.equals(((HttpAddress) object).version) && + secure.equals(((HttpAddress) object).secure); + } + + @Override + public int hashCode() { + return host.hashCode() ^ port ^ version.hashCode() ^ secure.hashCode(); + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpHeaderNames.java b/net-http/src/main/java/org/xbib/net/http/HttpHeaderNames.java new file mode 100644 index 0000000..ab206d3 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpHeaderNames.java @@ -0,0 +1,363 @@ +package org.xbib.net.http; + +/** + * Standard HTTP header names. + *

+ * These are all defined as lowercase to support HTTP/2 requirements while also not + * violating HTTP/1.x requirements. New header names should always be lowercase. + */ +public final class HttpHeaderNames { + /** + * {@code "accept"} + */ + public static final String ACCEPT = String.valueOf("accept"); + /** + * {@code "accept-charset"} + */ + public static final String ACCEPT_CHARSET = String.valueOf("accept-charset"); + /** + * {@code "accept-encoding"} + */ + public static final String ACCEPT_ENCODING = String.valueOf("accept-encoding"); + /** + * {@code "accept-language"} + */ + public static final String ACCEPT_LANGUAGE = String.valueOf("accept-language"); + /** + * {@code "accept-ranges"} + */ + public static final String ACCEPT_RANGES = String.valueOf("accept-ranges"); + /** + * {@code "accept-patch"} + */ + public static final String ACCEPT_PATCH = String.valueOf("accept-patch"); + /** + * {@code "access-control-allow-credentials"} + */ + public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = + String.valueOf("access-control-allow-credentials"); + /** + * {@code "access-control-allow-headers"} + */ + public static final String ACCESS_CONTROL_ALLOW_HEADERS = + String.valueOf("access-control-allow-headers"); + /** + * {@code "access-control-allow-methods"} + */ + public static final String ACCESS_CONTROL_ALLOW_METHODS = + String.valueOf("access-control-allow-methods"); + /** + * {@code "access-control-allow-origin"} + */ + public static final String ACCESS_CONTROL_ALLOW_ORIGIN = + String.valueOf("access-control-allow-origin"); + /** + * {@code "access-control-expose-headers"} + */ + public static final String ACCESS_CONTROL_EXPOSE_HEADERS = + String.valueOf("access-control-expose-headers"); + /** + * {@code "access-control-max-age"} + */ + public static final String ACCESS_CONTROL_MAX_AGE = String.valueOf("access-control-max-age"); + /** + * {@code "access-control-request-headers"} + */ + public static final String ACCESS_CONTROL_REQUEST_HEADERS = + String.valueOf("access-control-request-headers"); + /** + * {@code "access-control-request-method"} + */ + public static final String ACCESS_CONTROL_REQUEST_METHOD = + String.valueOf("access-control-request-method"); + /** + * {@code "age"} + */ + public static final String AGE = String.valueOf("age"); + /** + * {@code "allow"} + */ + public static final String ALLOW = String.valueOf("allow"); + /** + * {@code "authorization"} + */ + public static final String AUTHORIZATION = String.valueOf("authorization"); + /** + * {@code "cache-control"} + */ + public static final String CACHE_CONTROL = String.valueOf("cache-control"); + /** + * {@code "connection"} + */ + public static final String CONNECTION = String.valueOf("connection"); + /** + * {@code "content-base"} + */ + public static final String CONTENT_BASE = String.valueOf("content-base"); + /** + * {@code "content-encoding"} + */ + public static final String CONTENT_ENCODING = String.valueOf("content-encoding"); + /** + * {@code "content-language"} + */ + public static final String CONTENT_LANGUAGE = String.valueOf("content-language"); + /** + * {@code "content-length"} + */ + public static final String CONTENT_LENGTH = String.valueOf("content-length"); + /** + * {@code "content-location"} + */ + public static final String CONTENT_LOCATION = String.valueOf("content-location"); + /** + * {@code "content-transfer-encoding"} + */ + public static final String CONTENT_TRANSFER_ENCODING = String.valueOf("content-transfer-encoding"); + /** + * {@code "content-disposition"} + */ + public static final String CONTENT_DISPOSITION = String.valueOf("content-disposition"); + /** + * {@code "content-md5"} + */ + public static final String CONTENT_MD5 = String.valueOf("content-md5"); + /** + * {@code "content-range"} + */ + public static final String CONTENT_RANGE = String.valueOf("content-range"); + /** + * {@code "content-security-policy"} + */ + public static final String CONTENT_SECURITY_POLICY = String.valueOf("content-security-policy"); + /** + * {@code "content-type"} + */ + public static final String CONTENT_TYPE = String.valueOf("content-type"); + /** + * {@code "cookie"} + */ + public static final String COOKIE = String.valueOf("cookie"); + /** + * {@code "date"} + */ + public static final String DATE = String.valueOf("date"); + /** + * {@code "dnt"} + */ + public static final String DNT = String.valueOf("dnt"); + /** + * {@code "etag"} + */ + public static final String ETAG = String.valueOf("etag"); + /** + * {@code "expect"} + */ + public static final String EXPECT = String.valueOf("expect"); + /** + * {@code "expires"} + */ + public static final String EXPIRES = String.valueOf("expires"); + /** + * {@code "from"} + */ + public static final String FROM = String.valueOf("from"); + /** + * {@code "host"} + */ + public static final String HOST = String.valueOf("host"); + /** + * {@code "if-match"} + */ + public static final String IF_MATCH = String.valueOf("if-match"); + /** + * {@code "if-modified-since"} + */ + public static final String IF_MODIFIED_SINCE = String.valueOf("if-modified-since"); + /** + * {@code "if-none-match"} + */ + public static final String IF_NONE_MATCH = String.valueOf("if-none-match"); + /** + * {@code "if-range"} + */ + public static final String IF_RANGE = String.valueOf("if-range"); + /** + * {@code "if-unmodified-since"} + */ + public static final String IF_UNMODIFIED_SINCE = String.valueOf("if-unmodified-since"); + /** + * @deprecated use {@link #CONNECTION} + * + * {@code "keep-alive"} + */ + @Deprecated + public static final String KEEP_ALIVE = String.valueOf("keep-alive"); + /** + * {@code "last-modified"} + */ + public static final String LAST_MODIFIED = String.valueOf("last-modified"); + /** + * {@code "location"} + */ + public static final String LOCATION = String.valueOf("location"); + /** + * {@code "max-forwards"} + */ + public static final String MAX_FORWARDS = String.valueOf("max-forwards"); + /** + * {@code "origin"} + */ + public static final String ORIGIN = String.valueOf("origin"); + /** + * {@code "pragma"} + */ + public static final String PRAGMA = String.valueOf("pragma"); + /** + * {@code "proxy-authenticate"} + */ + public static final String PROXY_AUTHENTICATE = String.valueOf("proxy-authenticate"); + /** + * {@code "proxy-authorization"} + */ + public static final String PROXY_AUTHORIZATION = String.valueOf("proxy-authorization"); + /** + * @deprecated use {@link #CONNECTION} + * + * {@code "proxy-connection"} + */ + @Deprecated + public static final String PROXY_CONNECTION = String.valueOf("proxy-connection"); + /** + * {@code "range"} + */ + public static final String RANGE = String.valueOf("range"); + /** + * {@code "referer"} + */ + public static final String REFERER = String.valueOf("referer"); + /** + * {@code "retry-after"} + */ + public static final String RETRY_AFTER = String.valueOf("retry-after"); + /** + * {@code "sec-websocket-key1"} + */ + public static final String SEC_WEBSOCKET_KEY1 = String.valueOf("sec-websocket-key1"); + /** + * {@code "sec-websocket-key2"} + */ + public static final String SEC_WEBSOCKET_KEY2 = String.valueOf("sec-websocket-key2"); + /** + * {@code "sec-websocket-location"} + */ + public static final String SEC_WEBSOCKET_LOCATION = String.valueOf("sec-websocket-location"); + /** + * {@code "sec-websocket-origin"} + */ + public static final String SEC_WEBSOCKET_ORIGIN = String.valueOf("sec-websocket-origin"); + /** + * {@code "sec-websocket-protocol"} + */ + public static final String SEC_WEBSOCKET_PROTOCOL = String.valueOf("sec-websocket-protocol"); + /** + * {@code "sec-websocket-version"} + */ + public static final String SEC_WEBSOCKET_VERSION = String.valueOf("sec-websocket-version"); + /** + * {@code "sec-websocket-key"} + */ + public static final String SEC_WEBSOCKET_KEY = String.valueOf("sec-websocket-key"); + /** + * {@code "sec-websocket-accept"} + */ + public static final String SEC_WEBSOCKET_ACCEPT = String.valueOf("sec-websocket-accept"); + /** + * {@code "sec-websocket-protocol"} + */ + public static final String SEC_WEBSOCKET_EXTENSIONS = String.valueOf("sec-websocket-extensions"); + /** + * {@code "server"} + */ + public static final String SERVER = String.valueOf("server"); + /** + * {@code "set-cookie"} + */ + public static final String SET_COOKIE = String.valueOf("set-cookie"); + /** + * {@code "set-cookie2"} + */ + public static final String SET_COOKIE2 = String.valueOf("set-cookie2"); + /** + * {@code "te"} + */ + public static final String TE = String.valueOf("te"); + /** + * {@code "trailer"} + */ + public static final String TRAILER = String.valueOf("trailer"); + /** + * {@code "transfer-encoding"} + */ + public static final String TRANSFER_ENCODING = String.valueOf("transfer-encoding"); + /** + * {@code "upgrade"} + */ + public static final String UPGRADE = String.valueOf("upgrade"); + /** + * {@code "upgrade-insecure-requests"} + */ + public static final String UPGRADE_INSECURE_REQUESTS = String.valueOf("upgrade-insecure-requests"); + /** + * {@code "user-agent"} + */ + public static final String USER_AGENT = String.valueOf("user-agent"); + /** + * {@code "vary"} + */ + public static final String VARY = String.valueOf("vary"); + /** + * {@code "via"} + */ + public static final String VIA = String.valueOf("via"); + /** + * {@code "warning"} + */ + public static final String WARNING = String.valueOf("warning"); + /** + * {@code "websocket-location"} + */ + public static final String WEBSOCKET_LOCATION = String.valueOf("websocket-location"); + /** + * {@code "websocket-origin"} + */ + public static final String WEBSOCKET_ORIGIN = String.valueOf("websocket-origin"); + /** + * {@code "websocket-protocol"} + */ + public static final String WEBSOCKET_PROTOCOL = String.valueOf("websocket-protocol"); + /** + * {@code "www-authenticate"} + */ + public static final String WWW_AUTHENTICATE = String.valueOf("www-authenticate"); + /** + * {@code "x-frame-options"} + */ + public static final String X_FRAME_OPTIONS = String.valueOf("x-frame-options"); + /** + * {@code "x-requested-with"} + */ + public static final String X_REQUESTED_WITH = String.valueOf("x-requested-with"); + + /** + * {@code "x-forwarded-proto"} + */ + public static final String X_FORWARDED_PROTO = String.valueOf("x-forwarded-proto"); + + /** + * {@code "x-forwarded-host"} + */ + public static final String X_FORWARDED_HOST = String.valueOf("x-forwarded-host"); + + private HttpHeaderNames() { } +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpHeaderValues.java b/net-http/src/main/java/org/xbib/net/http/HttpHeaderValues.java new file mode 100644 index 0000000..d2596cc --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpHeaderValues.java @@ -0,0 +1,231 @@ +package org.xbib.net.http; + +/** + * Standard HTTP header values. + */ +public final class HttpHeaderValues { + /** + * {@code "application/json"} + */ + public static final String APPLICATION_JSON = "application/json"; + /** + * {@code "application/x-www-form-urlencoded"} + */ + public static final String APPLICATION_X_WWW_FORM_URLENCODED = + "application/x-www-form-urlencoded"; + /** + * {@code "application/octet-stream"} + */ + public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + /** + * {@code "application/xhtml+xml"} + */ + public static final String APPLICATION_XHTML = "application/xhtml+xml"; + /** + * {@code "application/xml"} + */ + public static final String APPLICATION_XML = String.valueOf("application/xml"); + /** + * {@code "application/zstd"} + */ + public static final String APPLICATION_ZSTD = String.valueOf("application/zstd"); + /** + * {@code "attachment"} + * See {@link HttpHeaderNames#CONTENT_DISPOSITION} + */ + public static final String ATTACHMENT = String.valueOf("attachment"); + /** + * {@code "base64"} + */ + public static final String BASE64 = String.valueOf("base64"); + /** + * {@code "binary"} + */ + public static final String BINARY = String.valueOf("binary"); + /** + * {@code "boundary"} + */ + public static final String BOUNDARY = String.valueOf("boundary"); + /** + * {@code "bytes"} + */ + public static final String BYTES = String.valueOf("bytes"); + /** + * {@code "charset"} + */ + public static final String CHARSET = String.valueOf("charset"); + /** + * {@code "chunked"} + */ + public static final String CHUNKED = String.valueOf("chunked"); + /** + * {@code "close"} + */ + public static final String CLOSE = String.valueOf("close"); + /** + * {@code "compress"} + */ + public static final String COMPRESS = String.valueOf("compress"); + /** + * {@code "100-continue"} + */ + public static final String CONTINUE = String.valueOf("100-continue"); + /** + * {@code "deflate"} + */ + public static final String DEFLATE = String.valueOf("deflate"); + /** + * {@code "x-deflate"} + */ + public static final String X_DEFLATE = String.valueOf("x-deflate"); + /** + * {@code "file"} + * See {@link HttpHeaderNames#CONTENT_DISPOSITION} + */ + public static final String FILE = String.valueOf("file"); + /** + * {@code "filename"} + * See {@link HttpHeaderNames#CONTENT_DISPOSITION} + */ + public static final String FILENAME = String.valueOf("filename"); + /** + * {@code "form-data"} + * See {@link HttpHeaderNames#CONTENT_DISPOSITION} + */ + public static final String FORM_DATA = String.valueOf("form-data"); + /** + * {@code "gzip"} + */ + public static final String GZIP = String.valueOf("gzip"); + /** + * {@code "br"} + */ + public static final String BR = String.valueOf("br"); + /** + * {@code "zstd"} + */ + public static final String ZSTD = String.valueOf("zstd"); + /** + * {@code "gzip,deflate"} + */ + public static final String GZIP_DEFLATE = String.valueOf("gzip,deflate"); + /** + * {@code "x-gzip"} + */ + public static final String X_GZIP = String.valueOf("x-gzip"); + /** + * {@code "identity"} + */ + public static final String IDENTITY = String.valueOf("identity"); + /** + * {@code "keep-alive"} + */ + public static final String KEEP_ALIVE = String.valueOf("keep-alive"); + /** + * {@code "max-age"} + */ + public static final String MAX_AGE = String.valueOf("max-age"); + /** + * {@code "max-stale"} + */ + public static final String MAX_STALE = String.valueOf("max-stale"); + /** + * {@code "min-fresh"} + */ + public static final String MIN_FRESH = String.valueOf("min-fresh"); + /** + * {@code "multipart/form-data"} + */ + public static final String MULTIPART_FORM_DATA = String.valueOf("multipart/form-data"); + /** + * {@code "multipart/mixed"} + */ + public static final String MULTIPART_MIXED = String.valueOf("multipart/mixed"); + /** + * {@code "must-revalidate"} + */ + public static final String MUST_REVALIDATE = String.valueOf("must-revalidate"); + /** + * {@code "name"} + * See {@link HttpHeaderNames#CONTENT_DISPOSITION} + */ + public static final String NAME = String.valueOf("name"); + /** + * {@code "no-cache"} + */ + public static final String NO_CACHE = String.valueOf("no-cache"); + /** + * {@code "no-store"} + */ + public static final String NO_STORE = String.valueOf("no-store"); + /** + * {@code "no-transform"} + */ + public static final String NO_TRANSFORM = String.valueOf("no-transform"); + /** + * {@code "none"} + */ + public static final String NONE = String.valueOf("none"); + /** + * {@code "0"} + */ + public static final String ZERO = String.valueOf("0"); + /** + * {@code "only-if-cached"} + */ + public static final String ONLY_IF_CACHED = String.valueOf("only-if-cached"); + /** + * {@code "private"} + */ + public static final String PRIVATE = String.valueOf("private"); + /** + * {@code "proxy-revalidate"} + */ + public static final String PROXY_REVALIDATE = String.valueOf("proxy-revalidate"); + /** + * {@code "public"} + */ + public static final String PUBLIC = String.valueOf("public"); + /** + * {@code "quoted-printable"} + */ + public static final String QUOTED_PRINTABLE = String.valueOf("quoted-printable"); + /** + * {@code "s-maxage"} + */ + public static final String S_MAXAGE = String.valueOf("s-maxage"); + /** + * {@code "text/css"} + */ + public static final String TEXT_CSS = String.valueOf("text/css"); + /** + * {@code "text/html"} + */ + public static final String TEXT_HTML = String.valueOf("text/html"); + /** + * {@code "text/event-stream"} + */ + public static final String TEXT_EVENT_STREAM = String.valueOf("text/event-stream"); + /** + * {@code "text/plain"} + */ + public static final String TEXT_PLAIN = String.valueOf("text/plain"); + /** + * {@code "trailers"} + */ + public static final String TRAILERS = String.valueOf("trailers"); + /** + * {@code "upgrade"} + */ + public static final String UPGRADE = String.valueOf("upgrade"); + /** + * {@code "websocket"} + */ + public static final String WEBSOCKET = String.valueOf("websocket"); + /** + * {@code "XmlHttpRequest"} + */ + public static final String XML_HTTP_REQUEST = String.valueOf("XMLHttpRequest"); + + private HttpHeaderValues() { } +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpHeaders.java b/net-http/src/main/java/org/xbib/net/http/HttpHeaders.java new file mode 100644 index 0000000..626c536 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpHeaders.java @@ -0,0 +1,93 @@ +package org.xbib.net.http; + +import org.xbib.datastructures.common.Pair; +import org.xbib.net.Headers; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + */ +public class HttpHeaders implements Headers { + + private final List> list; + + public HttpHeaders() { + this.list = new ArrayList<>(); + } + + public HttpHeaders(List> list) { + this.list = list; + } + + public static HttpHeaders of(List> list) { + return new HttpHeaders(list); + } + + public static HttpHeaders of(HttpHeaders headers) { + HttpHeaders httpHeaders = new HttpHeaders(); + headers.entries().forEach(e -> httpHeaders.add(e.getKey(), e.getValue())); + return httpHeaders; + } + + public HttpHeaders add(CharSequence name, String value) { + if (name != null && value != null) { + list.add(Pair.of(name.toString(), value)); + } + return this; + } + + public HttpHeaders add(CharSequence name, Iterable values) { + values.forEach(v -> { + if (v != null) { + list.add(Pair.of(name.toString(), v.toString())); + } + }); + return this; + } + + public HttpHeaders set(CharSequence name, String value) { + if (name != null && value != null) { + List> list = this.list.stream() + .filter(e -> !e.getKey().equalsIgnoreCase(name.toString())).collect(Collectors.toList()); + this.list.clear(); + this.list.addAll(list); + } + return add(name, value); + } + + public boolean containsHeader(CharSequence name) { + String k = name.toString(); + Pair me = list.stream().filter(e -> e.getKey().equalsIgnoreCase(k)).findFirst().orElse(null); + return me != null; + } + + public void remove(CharSequence name) { + this.list.removeIf(pair -> pair.getKey().equals(name.toString())); + } + + @Override + public String get(CharSequence header) { + String k = header.toString(); + return list.stream().filter(e -> e.getKey().equalsIgnoreCase(k)) + .map(Pair::getValue).findFirst().orElse(null); + } + + @Override + public List getAll(CharSequence header) { + String k = header.toString(); + return list.stream().filter(e -> e.getKey().equalsIgnoreCase(k)) + .map(Pair::getValue).collect(Collectors.toList()); + } + + @Override + public List> entries() { + return list; + } + + @Override + public String toString() { + return list.toString(); + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpMethod.java b/net-http/src/main/java/org/xbib/net/http/HttpMethod.java new file mode 100644 index 0000000..b8d104b --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpMethod.java @@ -0,0 +1,6 @@ +package org.xbib.net.http; + +public enum HttpMethod { + + GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH, PRI +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpResponseStatus.java b/net-http/src/main/java/org/xbib/net/http/HttpResponseStatus.java new file mode 100644 index 0000000..7e0a655 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpResponseStatus.java @@ -0,0 +1,555 @@ +package org.xbib.net.http; + +import java.util.Objects; + +/** + * The response code and its description of HTTP or its derived protocols, such as + * RTSP and + * ICAP. + */ +public class HttpResponseStatus implements Comparable { + + /** + * 100 Continue + */ + public static final HttpResponseStatus CONTINUE = newStatus(100, "Continue"); + + /** + * 101 Switching Protocols + */ + public static final HttpResponseStatus SWITCHING_PROTOCOLS = newStatus(101, "Switching Protocols"); + + /** + * 102 Processing (WebDAV, RFC2518) + */ + public static final HttpResponseStatus PROCESSING = newStatus(102, "Processing"); + + /** + * 200 OK + */ + public static final HttpResponseStatus OK = newStatus(200, "OK"); + + /** + * 201 Created + */ + public static final HttpResponseStatus CREATED = newStatus(201, "Created"); + + /** + * 202 Accepted + */ + public static final HttpResponseStatus ACCEPTED = newStatus(202, "Accepted"); + + /** + * 203 Non-Authoritative Information (since HTTP/1.1) + */ + public static final HttpResponseStatus NON_AUTHORITATIVE_INFORMATION = + newStatus(203, "Non-Authoritative Information"); + + /** + * 204 No Content + */ + public static final HttpResponseStatus NO_CONTENT = newStatus(204, "No Content"); + + /** + * 205 Reset Content + */ + public static final HttpResponseStatus RESET_CONTENT = newStatus(205, "Reset Content"); + + /** + * 206 Partial Content + */ + public static final HttpResponseStatus PARTIAL_CONTENT = newStatus(206, "Partial Content"); + + /** + * 207 Multi-Status (WebDAV, RFC2518) + */ + public static final HttpResponseStatus MULTI_STATUS = newStatus(207, "Multi-Status"); + + /** + * 300 Multiple Choices + */ + public static final HttpResponseStatus MULTIPLE_CHOICES = newStatus(300, "Multiple Choices"); + + /** + * 301 Moved Permanently + */ + public static final HttpResponseStatus MOVED_PERMANENTLY = newStatus(301, "Moved Permanently"); + + /** + * 302 Found + */ + public static final HttpResponseStatus FOUND = newStatus(302, "Found"); + + /** + * 303 See Other (since HTTP/1.1) + */ + public static final HttpResponseStatus SEE_OTHER = newStatus(303, "See Other"); + + /** + * 304 Not Modified + */ + public static final HttpResponseStatus NOT_MODIFIED = newStatus(304, "Not Modified"); + + /** + * 305 Use Proxy (since HTTP/1.1) + */ + public static final HttpResponseStatus USE_PROXY = newStatus(305, "Use Proxy"); + + /** + * 307 Temporary Redirect (since HTTP/1.1) + */ + public static final HttpResponseStatus TEMPORARY_REDIRECT = newStatus(307, "Temporary Redirect"); + + /** + * 308 Permanent Redirect (RFC7538) + */ + public static final HttpResponseStatus PERMANENT_REDIRECT = newStatus(308, "Permanent Redirect"); + + /** + * 400 Bad Request + */ + public static final HttpResponseStatus BAD_REQUEST = newStatus(400, "Bad Request"); + + /** + * 401 Unauthorized + */ + public static final HttpResponseStatus UNAUTHORIZED = newStatus(401, "Unauthorized"); + + /** + * 402 Payment Required + */ + public static final HttpResponseStatus PAYMENT_REQUIRED = newStatus(402, "Payment Required"); + + /** + * 403 Forbidden + */ + public static final HttpResponseStatus FORBIDDEN = newStatus(403, "Forbidden"); + + /** + * 404 Not Found + */ + public static final HttpResponseStatus NOT_FOUND = newStatus(404, "Not Found"); + + /** + * 405 Method Not Allowed + */ + public static final HttpResponseStatus METHOD_NOT_ALLOWED = newStatus(405, "Method Not Allowed"); + + /** + * 406 Not Acceptable + */ + public static final HttpResponseStatus NOT_ACCEPTABLE = newStatus(406, "Not Acceptable"); + + /** + * 407 Proxy Authentication Required + */ + public static final HttpResponseStatus PROXY_AUTHENTICATION_REQUIRED = + newStatus(407, "Proxy Authentication Required"); + + /** + * 408 Request Timeout + */ + public static final HttpResponseStatus REQUEST_TIMEOUT = newStatus(408, "Request Timeout"); + + /** + * 409 Conflict + */ + public static final HttpResponseStatus CONFLICT = newStatus(409, "Conflict"); + + /** + * 410 Gone + */ + public static final HttpResponseStatus GONE = newStatus(410, "Gone"); + + /** + * 411 Length Required + */ + public static final HttpResponseStatus LENGTH_REQUIRED = newStatus(411, "Length Required"); + + /** + * 412 Precondition Failed + */ + public static final HttpResponseStatus PRECONDITION_FAILED = newStatus(412, "Precondition Failed"); + + /** + * 413 Request Entity Too Large + */ + public static final HttpResponseStatus REQUEST_ENTITY_TOO_LARGE = + newStatus(413, "Request Entity Too Large"); + + /** + * 414 Request-URI Too Long + */ + public static final HttpResponseStatus REQUEST_URI_TOO_LONG = newStatus(414, "Request-URI Too Long"); + + /** + * 415 Unsupported Media Type + */ + public static final HttpResponseStatus UNSUPPORTED_MEDIA_TYPE = newStatus(415, "Unsupported Media Type"); + + /** + * 416 Requested Range Not Satisfiable + */ + public static final HttpResponseStatus REQUESTED_RANGE_NOT_SATISFIABLE = + newStatus(416, "Requested Range Not Satisfiable"); + + /** + * 417 Expectation Failed + */ + public static final HttpResponseStatus EXPECTATION_FAILED = newStatus(417, "Expectation Failed"); + + /** + * 421 Misdirected Request + * + * @see 421 (Misdirected Request) Status Code + */ + public static final HttpResponseStatus MISDIRECTED_REQUEST = newStatus(421, "Misdirected Request"); + + /** + * 422 Unprocessable Entity (WebDAV, RFC4918) + */ + public static final HttpResponseStatus UNPROCESSABLE_ENTITY = newStatus(422, "Unprocessable Entity"); + + /** + * 423 Locked (WebDAV, RFC4918) + */ + public static final HttpResponseStatus LOCKED = newStatus(423, "Locked"); + + /** + * 424 Failed Dependency (WebDAV, RFC4918) + */ + public static final HttpResponseStatus FAILED_DEPENDENCY = newStatus(424, "Failed Dependency"); + + /** + * 425 Unordered Collection (WebDAV, RFC3648) + */ + public static final HttpResponseStatus UNORDERED_COLLECTION = newStatus(425, "Unordered Collection"); + + /** + * 426 Upgrade Required (RFC2817) + */ + public static final HttpResponseStatus UPGRADE_REQUIRED = newStatus(426, "Upgrade Required"); + + /** + * 428 Precondition Required (RFC6585) + */ + public static final HttpResponseStatus PRECONDITION_REQUIRED = newStatus(428, "Precondition Required"); + + /** + * 429 Too Many Requests (RFC6585) + */ + public static final HttpResponseStatus TOO_MANY_REQUESTS = newStatus(429, "Too Many Requests"); + + /** + * 431 Request Header Fields Too Large (RFC6585) + */ + public static final HttpResponseStatus REQUEST_HEADER_FIELDS_TOO_LARGE = + newStatus(431, "Request Header Fields Too Large"); + + /** + * 500 Internal Server Error + */ + public static final HttpResponseStatus INTERNAL_SERVER_ERROR = newStatus(500, "Internal Server Error"); + + /** + * 501 Not Implemented + */ + public static final HttpResponseStatus NOT_IMPLEMENTED = newStatus(501, "Not Implemented"); + + /** + * 502 Bad Gateway + */ + public static final HttpResponseStatus BAD_GATEWAY = newStatus(502, "Bad Gateway"); + + /** + * 503 Service Unavailable + */ + public static final HttpResponseStatus SERVICE_UNAVAILABLE = newStatus(503, "Service Unavailable"); + + /** + * 504 Gateway Timeout + */ + public static final HttpResponseStatus GATEWAY_TIMEOUT = newStatus(504, "Gateway Timeout"); + + /** + * 505 HTTP Version Not Supported + */ + public static final HttpResponseStatus HTTP_VERSION_NOT_SUPPORTED = + newStatus(505, "HTTP Version Not Supported"); + + /** + * 506 Variant Also Negotiates (RFC2295) + */ + public static final HttpResponseStatus VARIANT_ALSO_NEGOTIATES = newStatus(506, "Variant Also Negotiates"); + + /** + * 507 Insufficient Storage (WebDAV, RFC4918) + */ + public static final HttpResponseStatus INSUFFICIENT_STORAGE = newStatus(507, "Insufficient Storage"); + + /** + * 510 Not Extended (RFC2774) + */ + public static final HttpResponseStatus NOT_EXTENDED = newStatus(510, "Not Extended"); + + /** + * 511 Network Authentication Required (RFC6585) + */ + public static final HttpResponseStatus NETWORK_AUTHENTICATION_REQUIRED = + newStatus(511, "Network Authentication Required"); + + private static HttpResponseStatus newStatus(int statusCode, String reasonPhrase) { + return new HttpResponseStatus(statusCode, reasonPhrase, true); + } + + /** + * Returns the {@link HttpResponseStatus} represented by the specified code. + * If the specified code is a standard HTTP status code, a cached instance + * will be returned. Otherwise, a new instance will be returned. + */ + public static HttpResponseStatus valueOf(int code) { + HttpResponseStatus status = valueOf0(code); + return status != null ? status : new HttpResponseStatus(code); + } + + private static HttpResponseStatus valueOf0(int code) { + switch (code) { + case 100: + return CONTINUE; + case 101: + return SWITCHING_PROTOCOLS; + case 102: + return PROCESSING; + case 200: + return OK; + case 201: + return CREATED; + case 202: + return ACCEPTED; + case 203: + return NON_AUTHORITATIVE_INFORMATION; + case 204: + return NO_CONTENT; + case 205: + return RESET_CONTENT; + case 206: + return PARTIAL_CONTENT; + case 207: + return MULTI_STATUS; + case 300: + return MULTIPLE_CHOICES; + case 301: + return MOVED_PERMANENTLY; + case 302: + return FOUND; + case 303: + return SEE_OTHER; + case 304: + return NOT_MODIFIED; + case 305: + return USE_PROXY; + case 307: + return TEMPORARY_REDIRECT; + case 308: + return PERMANENT_REDIRECT; + case 400: + return BAD_REQUEST; + case 401: + return UNAUTHORIZED; + case 402: + return PAYMENT_REQUIRED; + case 403: + return FORBIDDEN; + case 404: + return NOT_FOUND; + case 405: + return METHOD_NOT_ALLOWED; + case 406: + return NOT_ACCEPTABLE; + case 407: + return PROXY_AUTHENTICATION_REQUIRED; + case 408: + return REQUEST_TIMEOUT; + case 409: + return CONFLICT; + case 410: + return GONE; + case 411: + return LENGTH_REQUIRED; + case 412: + return PRECONDITION_FAILED; + case 413: + return REQUEST_ENTITY_TOO_LARGE; + case 414: + return REQUEST_URI_TOO_LONG; + case 415: + return UNSUPPORTED_MEDIA_TYPE; + case 416: + return REQUESTED_RANGE_NOT_SATISFIABLE; + case 417: + return EXPECTATION_FAILED; + case 421: + return MISDIRECTED_REQUEST; + case 422: + return UNPROCESSABLE_ENTITY; + case 423: + return LOCKED; + case 424: + return FAILED_DEPENDENCY; + case 425: + return UNORDERED_COLLECTION; + case 426: + return UPGRADE_REQUIRED; + case 428: + return PRECONDITION_REQUIRED; + case 429: + return TOO_MANY_REQUESTS; + case 431: + return REQUEST_HEADER_FIELDS_TOO_LARGE; + case 500: + return INTERNAL_SERVER_ERROR; + case 501: + return NOT_IMPLEMENTED; + case 502: + return BAD_GATEWAY; + case 503: + return SERVICE_UNAVAILABLE; + case 504: + return GATEWAY_TIMEOUT; + case 505: + return HTTP_VERSION_NOT_SUPPORTED; + case 506: + return VARIANT_ALSO_NEGOTIATES; + case 507: + return INSUFFICIENT_STORAGE; + case 510: + return NOT_EXTENDED; + case 511: + return NETWORK_AUTHENTICATION_REQUIRED; + } + return null; + } + + /** + * Returns the {@link HttpResponseStatus} represented by the specified {@code code} and {@code reasonPhrase}. + * If the specified code is a standard HTTP status {@code code} and {@code reasonPhrase}, a cached instance + * will be returned. Otherwise, a new instance will be returned. + * @param code The response code value. + * @param reasonPhrase The response code reason phrase. + * @return the {@link HttpResponseStatus} represented by the specified {@code code} and {@code reasonPhrase}. + */ + public static HttpResponseStatus valueOf(int code, String reasonPhrase) { + HttpResponseStatus responseStatus = valueOf0(code); + return responseStatus != null && responseStatus.reasonPhrase().contentEquals(reasonPhrase) ? responseStatus : + new HttpResponseStatus(code, reasonPhrase); + } + + private final int code; + private final String codeAsText; + private HttpStatusClass codeClass; + + private final String reasonPhrase; + + /** + * Creates a new instance with the specified {@code code} and the auto-generated default reason phrase. + */ + private HttpResponseStatus(int code) { + this(code, HttpStatusClass.valueOf(code).defaultReasonPhrase() + " (" + code + ')', false); + } + + /** + * Creates a new instance with the specified {@code code} and its {@code reasonPhrase}. + */ + public HttpResponseStatus(int code, String reasonPhrase) { + this(code, reasonPhrase, false); + } + + private HttpResponseStatus(int code, String reasonPhrase, boolean bytes) { + if (code < 0) { + throw new IllegalArgumentException("code"); + } + Objects.requireNonNull(reasonPhrase, "reasonPhrase"); + + for (int i = 0; i < reasonPhrase.length(); i ++) { + char c = reasonPhrase.charAt(i); + // Check prohibited characters. + switch (c) { + case '\n': case '\r': + throw new IllegalArgumentException( + "reasonPhrase contains one of the following prohibited characters: " + + "\\r\\n: " + reasonPhrase); + } + } + this.code = code; + this.codeAsText = Integer.toString(code); + this.reasonPhrase = reasonPhrase; + } + + /** + * Returns the code of this {@link HttpResponseStatus}. + */ + public int code() { + return code; + } + + /** + * Returns the status code as {@link String}. + */ + public String codeAsText() { + return codeAsText; + } + + /** + * Returns the reason phrase of this {@link HttpResponseStatus}. + */ + public String reasonPhrase() { + return reasonPhrase; + } + + /** + * Returns the class of this {@link HttpResponseStatus} + */ + public HttpStatusClass codeClass() { + HttpStatusClass type = this.codeClass; + if (type == null) { + this.codeClass = type = HttpStatusClass.valueOf(code); + } + return type; + } + + @Override + public int hashCode() { + return code(); + } + + /** + * Equality of {@link HttpResponseStatus} only depends on {@link #code()}. The + * reason phrase is not considered for equality. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof HttpResponseStatus)) { + return false; + } + + return code() == ((HttpResponseStatus) o).code(); + } + + /** + * Equality of {@link HttpResponseStatus} only depends on {@link #code()}. The + * reason phrase is not considered for equality. + */ + @Override + public int compareTo(HttpResponseStatus o) { + return code() - o.code(); + } + + @Override + public String toString() { + return new StringBuilder(reasonPhrase.length() + 4) + .append(codeAsText) + .append(' ') + .append(reasonPhrase) + .toString(); + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpStatusClass.java b/net-http/src/main/java/org/xbib/net/http/HttpStatusClass.java new file mode 100644 index 0000000..9c3d512 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpStatusClass.java @@ -0,0 +1,103 @@ +package org.xbib.net.http; + +/** + * The class of HTTP status. + */ +public enum HttpStatusClass { + /** + * The informational class (1xx) + */ + INFORMATIONAL(100, 200, "Informational"), + /** + * The success class (2xx) + */ + SUCCESS(200, 300, "Success"), + /** + * The redirection class (3xx) + */ + REDIRECTION(300, 400, "Redirection"), + /** + * The client error class (4xx) + */ + CLIENT_ERROR(400, 500, "Client Error"), + /** + * The server error class (5xx) + */ + SERVER_ERROR(500, 600, "Server Error"), + /** + * The unknown class + */ + UNKNOWN(0, 0, "Unknown Status") { + @Override + public boolean contains(int code) { + return code < 100 || code >= 600; + } + }; + + /** + * Returns the class of the specified HTTP status code. + */ + public static HttpStatusClass valueOf(int code) { + if (INFORMATIONAL.contains(code)) { + return INFORMATIONAL; + } + if (SUCCESS.contains(code)) { + return SUCCESS; + } + if (REDIRECTION.contains(code)) { + return REDIRECTION; + } + if (CLIENT_ERROR.contains(code)) { + return CLIENT_ERROR; + } + if (SERVER_ERROR.contains(code)) { + return SERVER_ERROR; + } + return UNKNOWN; + } + + /** + * Returns the class of the specified HTTP status code. + * @param code Just the numeric portion of the http status code. + */ + public static HttpStatusClass valueOf(CharSequence code) { + if (code != null && code.length() == 3) { + char c0 = code.charAt(0); + return isDigit(c0) && isDigit(code.charAt(1)) && isDigit(code.charAt(2)) ? valueOf(digit(c0) * 100) + : UNKNOWN; + } + return UNKNOWN; + } + + private static int digit(char c) { + return c - '0'; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private final int min; + private final int max; + private final String defaultReasonPhrase; + + HttpStatusClass(int min, int max, String defaultReasonPhrase) { + this.min = min; + this.max = max; + this.defaultReasonPhrase = defaultReasonPhrase; + } + + /** + * Returns {@code true} if and only if the specified HTTP status code falls into this class. + */ + public boolean contains(int code) { + return code >= min && code < max; + } + + /** + * Returns the default reason phrase of this HTTP status class. + */ + String defaultReasonPhrase() { + return defaultReasonPhrase; + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/HttpVersion.java b/net-http/src/main/java/org/xbib/net/http/HttpVersion.java new file mode 100644 index 0000000..108fe21 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/HttpVersion.java @@ -0,0 +1,254 @@ +package org.xbib.net.http; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The version of HTTP or its derived protocols, such as + * RTSP and + * ICAP. + */ +public class HttpVersion implements Comparable { + + private static final Pattern VERSION_PATTERN = + Pattern.compile("(\\S+)/(\\d+)\\.(\\d+)"); + + private static final String HTTP_1_0_STRING = "HTTP/1.0"; + + private static final String HTTP_1_1_STRING = "HTTP/1.1"; + + private static final String HTTP_2_0_STRING = "HTTP/2.0"; + + /** + * HTTP/1.0 + */ + public static final HttpVersion HTTP_1_0 = new HttpVersion("HTTP", 1, 0, false, true); + + /** + * HTTP/1.1 + */ + public static final HttpVersion HTTP_1_1 = new HttpVersion("HTTP", 1, 1, true, true); + + /** + * HTTP/2.0 + */ + public static final HttpVersion HTTP_2_0 = new HttpVersion("HTTP", 2, 0, true, true); + + /** + * Returns an existing or new {@link HttpVersion} instance which matches to + * the specified protocol version string. If the specified {@code text} is + * equal to {@code "HTTP/1.0"}, {@link #HTTP_1_0} will be returned. If the + * specified {@code text} is equal to {@code "HTTP/1.1"}, {@link #HTTP_1_1} + * will be returned. Otherwise, a new {@link HttpVersion} instance will be + * returned. + */ + public static HttpVersion valueOf(String text) { + Objects.requireNonNull(text, "text"); + text = text.trim(); + if (text.isEmpty()) { + throw new IllegalArgumentException("text is empty (possibly HTTP/0.9)"); + } + // Try to match without convert to uppercase first as this is what 99% of all clients + // will send anyway. Also there is a change to the RFC to make it clear that it is + // expected to be case-sensitive + // + // See: + // * https://trac.tools.ietf.org/wg/httpbis/trac/ticket/1 + // * https://trac.tools.ietf.org/wg/httpbis/trac/wiki + // + HttpVersion version = version0(text); + if (version == null) { + version = new HttpVersion(text, true); + } + return version; + } + + private static HttpVersion version0(String text) { + if (HTTP_1_1_STRING.equals(text)) { + return HTTP_1_1; + } + if (HTTP_1_0_STRING.equals(text)) { + return HTTP_1_0; + } + return null; + } + + private final String protocolName; + private final int majorVersion; + private final int minorVersion; + private final String text; + private final boolean keepAliveDefault; + private final byte[] bytes; + + /** + * Creates a new HTTP version with the specified version string. You will + * not need to create a new instance unless you are implementing a protocol + * derived from HTTP, such as + * RTSP and + * ICAP. + * + * @param keepAliveDefault + * {@code true} if and only if the connection is kept alive unless + * the {@code "Connection"} header is set to {@code "close"} explicitly. + */ + public HttpVersion(String text, boolean keepAliveDefault) { + text = checkNonEmptyAfterTrim(text, "text").toUpperCase(); + Matcher m = VERSION_PATTERN.matcher(text); + if (!m.matches()) { + throw new IllegalArgumentException("invalid version format: " + text); + } + protocolName = m.group(1); + majorVersion = Integer.parseInt(m.group(2)); + minorVersion = Integer.parseInt(m.group(3)); + this.text = protocolName + '/' + majorVersion + '.' + minorVersion; + this.keepAliveDefault = keepAliveDefault; + bytes = null; + } + + /** + * Creates a new HTTP version with the specified protocol name and version + * numbers. You will not need to create a new instance unless you are + * implementing a protocol derived from HTTP, such as + * RTSP and + * ICAP + * + * @param keepAliveDefault + * {@code true} if and only if the connection is kept alive unless + * the {@code "Connection"} header is set to {@code "close"} explicitly. + */ + public HttpVersion( + String protocolName, int majorVersion, int minorVersion, + boolean keepAliveDefault) { + this(protocolName, majorVersion, minorVersion, keepAliveDefault, false); + } + + private HttpVersion( + String protocolName, int majorVersion, int minorVersion, + boolean keepAliveDefault, boolean bytes) { + protocolName = checkNonEmptyAfterTrim(protocolName, "protocolName").toUpperCase(); + + for (int i = 0; i < protocolName.length(); i ++) { + if (Character.isISOControl(protocolName.charAt(i)) || + Character.isWhitespace(protocolName.charAt(i))) { + throw new IllegalArgumentException("invalid character in protocolName"); + } + } + checkPositiveOrZero(majorVersion, "majorVersion"); + checkPositiveOrZero(minorVersion, "minorVersion"); + this.protocolName = protocolName; + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + text = protocolName + '/' + majorVersion + '.' + minorVersion; + this.keepAliveDefault = keepAliveDefault; + if (bytes) { + this.bytes = text.getBytes(StandardCharsets.US_ASCII); + } else { + this.bytes = null; + } + } + + /** + * Returns the name of the protocol such as {@code "HTTP"} in {@code "HTTP/1.0"}. + */ + public String protocolName() { + return protocolName; + } + + /** + * Returns the name of the protocol such as {@code 1} in {@code "HTTP/1.0"}. + */ + public int majorVersion() { + return majorVersion; + } + + /** + * Returns the name of the protocol such as {@code 0} in {@code "HTTP/1.0"}. + */ + public int minorVersion() { + return minorVersion; + } + + /** + * Returns the full protocol version text such as {@code "HTTP/1.0"}. + */ + public String text() { + return text; + } + + /** + * Returns {@code true} if and only if the connection is kept alive unless + * the {@code "Connection"} header is set to {@code "close"} explicitly. + */ + public boolean isKeepAliveDefault() { + return keepAliveDefault; + } + + /** + * Returns the full protocol version text such as {@code "HTTP/1.0"}. + */ + @Override + public String toString() { + return text(); + } + + @Override + public int hashCode() { + return (protocolName().hashCode() * 31 + majorVersion()) * 31 + + minorVersion(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof HttpVersion)) { + return false; + } + + HttpVersion that = (HttpVersion) o; + return minorVersion() == that.minorVersion() && + majorVersion() == that.majorVersion() && + protocolName().equals(that.protocolName()); + } + + @Override + public int compareTo(HttpVersion o) { + int v = protocolName().compareTo(o.protocolName()); + if (v != 0) { + return v; + } + + v = majorVersion() - o.majorVersion(); + if (v != 0) { + return v; + } + + return minorVersion() - o.minorVersion(); + } + + private static int checkPositiveOrZero(int i, String name) { + if (i < 0) { + throw new IllegalArgumentException(name + " : " + i + " (expected: >= 0)"); + } + return i; + } + + private static String checkNonEmptyAfterTrim(final String value, final String name) { + String trimmed = checkNotNull(value, name).trim(); + return checkNonEmpty(trimmed, name); + } + + private static T checkNotNull(T arg, String text) { + if (arg == null) { + throw new NullPointerException(text); + } + return arg; + } + + private static String checkNonEmpty(final String value, final String name) { + if (checkNotNull(value, name).isEmpty()) { + throw new IllegalArgumentException("Param '" + name + "' must not be empty"); + } + return value; + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/Cookie.java b/net-http/src/main/java/org/xbib/net/http/cookie/Cookie.java new file mode 100644 index 0000000..e390e69 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/Cookie.java @@ -0,0 +1,150 @@ +package org.xbib.net.http.cookie; + +/** + * An interface defining an + * HTTP cookie. + */ +public interface Cookie extends Comparable { + + /** + * Returns the name of this {@link Cookie}. + * + * @return The name of this {@link Cookie} + */ + String name(); + + /** + * Returns the value of this {@link Cookie}. + * + * @return The value of this {@link Cookie} + */ + String value(); + + /** + * Sets the value of this {@link Cookie}. + * + * @param value The value to set + */ + Cookie setValue(String value); + + /** + * Returns true if the raw value of this {@link Cookie}, + * was wrapped with double quotes in original Set-Cookie header. + * + * @return If the value of this {@link Cookie} is to be wrapped + */ + boolean wrap(); + + /** + * Sets true if the value of this {@link Cookie} + * is to be wrapped with double quotes. + * + * @param wrap true if wrap + */ + Cookie setWrap(boolean wrap); + + /** + * Returns the domain of this {@link Cookie}. + * + * @return The domain of this {@link Cookie} + */ + String domain(); + + /** + * Sets the domain of this {@link Cookie}. + * + * @param domain The domain to use + */ + Cookie setDomain(String domain); + + /** + * Returns the path of this {@link Cookie}. + * + * @return The {@link Cookie}'s path + */ + String path(); + + /** + * Sets the path of this {@link Cookie}. + * + * @param path The path to use for this {@link Cookie} + */ + Cookie setPath(String path); + + /** + * Returns the maximum age of this {@link Cookie} in seconds or {@link Long#MIN_VALUE} if unspecified + * + * @return The maximum age of this {@link Cookie} + */ + long maxAge(); + + /** + * Sets the maximum age of this {@link Cookie} in seconds. + * If an age of {@code 0} is specified, this {@link Cookie} will be + * automatically removed by browser because it will expire immediately. + * If {@link Long#MIN_VALUE} is specified, this {@link Cookie} will be removed when the + * browser is closed. + * + * @param maxAge The maximum age of this {@link Cookie} in seconds + */ + Cookie setMaxAge(long maxAge); + + /** + * Checks to see if this {@link Cookie} is secure + * + * @return True if this {@link Cookie} is secure, otherwise false + */ + boolean isSecure(); + + /** + * Sets the security getStatus of this {@link Cookie} + * + * @param secure True if this {@link Cookie} is to be secure, otherwise false + */ + Cookie setSecure(boolean secure); + + /** + * Checks to see if this {@link Cookie} can only be accessed via HTTP. + * If this returns true, the {@link Cookie} cannot be accessed through + * client side script - But only if the browser supports it. + * For more information, please look here + * + * @return True if this {@link Cookie} is HTTP-only or false if it isn't + */ + boolean isHttpOnly(); + + /** + * Determines if this {@link Cookie} is HTTP only. + * If set to true, this {@link Cookie} cannot be accessed by a client + * side script. However, this works only if the browser supports it. + * For for information, please look + * here. + * + * @param httpOnly True if the {@link Cookie} is HTTP only, otherwise false. + */ + Cookie setHttpOnly(boolean httpOnly); + + /** + * Checks to see if this {@link Cookie} is valid on same site. + * + * {@code SameSite.STRICT} being the default mode. + * + * @return the same site value + */ + SameSite sameSite(); + + /** + * Determines if this {@link Cookie} is same site. + * If set to {@code SameSite.STRICT}, + * + * {@code SameSite.LAX} mode is adding one exception for the cookie to be sent if we’re not in a Same-Site context. + * The defined {@link Cookie} will also be sent for requests using a safe method (GET method for most) + * for top-level navigation, basically something resulting in the URL changing in the web browser address bar. + * + * {@code SameSite.STRICT} mode would prevent any session cookie to be sent for a website reached by following + * an external link (from an email, from search engines results, etc.), resulting for a user not being logged in. + * + * @param sameSite the same site value + */ + Cookie setSameSite(SameSite sameSite); +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/CookieBox.java b/net-http/src/main/java/org/xbib/net/http/cookie/CookieBox.java new file mode 100644 index 0000000..0fbaa6c --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/CookieBox.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.cookie; + +import java.util.LinkedHashSet; + +@SuppressWarnings("serial") +public class CookieBox extends LinkedHashSet { + + public CookieBox() { + super(); + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/CookieDecoder.java b/net-http/src/main/java/org/xbib/net/http/cookie/CookieDecoder.java new file mode 100644 index 0000000..542e6f6 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/CookieDecoder.java @@ -0,0 +1,41 @@ +package org.xbib.net.http.cookie; + +import java.nio.CharBuffer; + +/** + * Parent of client and server side cookie decoders. + */ +public abstract class CookieDecoder { + + protected final boolean strict; + + protected CookieDecoder(boolean strict) { + this.strict = strict; + } + + protected DefaultCookie initCookie(String header, int nameBegin, int nameEnd, int valueBegin, int valueEnd) { + if (nameBegin == -1 || nameBegin == nameEnd) { + return null; + } + if (valueBegin == -1) { + return null; + } + CharSequence wrappedValue = CharBuffer.wrap(header, valueBegin, valueEnd); + CharSequence unwrappedValue = CookieUtil.unwrapValue(wrappedValue); + if (unwrappedValue == null) { + return null; + } + String name = header.substring(nameBegin, nameEnd); + if (strict && CookieUtil.firstInvalidCookieNameOctet(name) >= 0) { + return null; + } + final boolean wrap = unwrappedValue.length() != valueEnd - valueBegin; + if (strict && CookieUtil.firstInvalidCookieValueOctet(unwrappedValue) >= 0) { + return null; + } + DefaultCookie cookie = new DefaultCookie(name); + cookie.setValue(unwrappedValue.toString()); + cookie.setWrap(wrap); + return cookie; + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/CookieEncoder.java b/net-http/src/main/java/org/xbib/net/http/cookie/CookieEncoder.java new file mode 100644 index 0000000..c99fddd --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/CookieEncoder.java @@ -0,0 +1,29 @@ +package org.xbib.net.http.cookie; + +/** + * Parent of Client and Server side cookie encoders + */ +public abstract class CookieEncoder { + + protected final boolean strict; + + protected CookieEncoder(boolean strict) { + this.strict = strict; + } + + protected void validateCookie(String name, String value) { + if (strict) { + int pos; + if ((pos = CookieUtil.firstInvalidCookieNameOctet(name)) >= 0) { + throw new IllegalArgumentException("Cookie name contains an invalid char: " + (name.charAt(pos))); + } + CharSequence unwrappedValue = CookieUtil.unwrapValue(value); + if (unwrappedValue == null) { + throw new IllegalArgumentException("Cookie value wrapping quotes are not balanced: " + value); + } + if ((pos = CookieUtil.firstInvalidCookieValueOctet(unwrappedValue)) >= 0) { + throw new IllegalArgumentException("Cookie value contains an invalid char: " + (value.charAt(pos))); + } + } + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/CookieHeaderNames.java b/net-http/src/main/java/org/xbib/net/http/cookie/CookieHeaderNames.java new file mode 100644 index 0000000..97e5b4a --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/CookieHeaderNames.java @@ -0,0 +1,18 @@ +package org.xbib.net.http.cookie; + +public interface CookieHeaderNames { + + String PATH = "Path"; + + String EXPIRES = "Expires"; + + String MAX_AGE = "Max-Age"; + + String DOMAIN = "Domain"; + + String SECURE = "Secure"; + + String HTTPONLY = "HTTPOnly"; + + String SAMESITE = "SameSite"; +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/CookieUtil.java b/net-http/src/main/java/org/xbib/net/http/cookie/CookieUtil.java new file mode 100644 index 0000000..fe78705 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/CookieUtil.java @@ -0,0 +1,181 @@ +package org.xbib.net.http.cookie; + +import java.util.BitSet; + +public final class CookieUtil { + + private static final BitSet VALID_COOKIE_NAME_OCTETS; + + private static final BitSet VALID_COOKIE_VALUE_OCTETS; + + private static final BitSet VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS; + + static { + int[] separators = new int[] { + '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t' + }; + VALID_COOKIE_NAME_OCTETS = validCookieNameOctets(separators); + VALID_COOKIE_VALUE_OCTETS = validCookieValueOctets(); + VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS = validCookieAttributeValueOctets(); + } + + /** + * Horizontal space + */ + public static final char SP = 32; + + /** + * Equals '=' + */ + public static final char EQUALS = 61; + + /** + * Semicolon ';' + */ + public static final char SEMICOLON = 59; + + /** + * Double quote '"' + */ + public static final char DOUBLE_QUOTE = 34; + + private CookieUtil() { + } + + private static BitSet validCookieNameOctets(int[] separators) { + BitSet bits = new BitSet(); + for (int i = 32; i < 127; i++) { + bits.set(i); + } + for (int separator : separators) { + bits.set(separator, false); + } + return bits; + } + + // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + // US-ASCII characters excluding CTLs, whitespace, DQUOTE, comma, semicolon, and backslash + private static BitSet validCookieValueOctets() { + BitSet bits = new BitSet(); + bits.set(0x21); + for (int i = 0x23; i <= 0x2B; i++) { + bits.set(i); + } + for (int i = 0x2D; i <= 0x3A; i++) { + bits.set(i); + } + for (int i = 0x3C; i <= 0x5B; i++) { + bits.set(i); + } + for (int i = 0x5D; i <= 0x7E; i++) { + bits.set(i); + } + return bits; + } + + // path-value = + private static BitSet validCookieAttributeValueOctets() { + BitSet bits = new BitSet(); + for (int i = 32; i < 127; i++) { + bits.set(i); + } + // ';' = 59 + bits.set(59, false); + return bits; + } + + /** + * @param buf a buffer where some cookies were maybe encoded + * @return the buffer String without the trailing separator, or null if no cookie was appended. + */ + public static String stripTrailingSeparatorOrNull(StringBuilder buf) { + return buf.length() == 0 ? null : stripTrailingSeparator(buf); + } + + public static String stripTrailingSeparator(StringBuilder buf) { + if (buf.length() > 0) { + buf.setLength(buf.length() - 2); + } + return buf.toString(); + } + + public static void add(StringBuilder sb, String name, long val) { + sb.append(name) + .append(EQUALS) + .append(val) + .append(SEMICOLON) + .append(SP); + } + + public static void add(StringBuilder sb, String name, String val) { + sb.append(name) + .append(EQUALS) + .append(val) + .append(SEMICOLON) + .append(SP); + } + + public static void add(StringBuilder sb, String name) { + sb.append(name) + .append(SEMICOLON) + .append(SP); + } + + public static void addQuoted(StringBuilder sb, String name, String val) { + if (val == null) { + val = ""; + } + sb.append(name) + .append(EQUALS) + .append(DOUBLE_QUOTE) + .append(val) + .append(DOUBLE_QUOTE) + .append(SEMICOLON) + .append(SP); + } + + public static int firstInvalidCookieNameOctet(CharSequence cs) { + return firstInvalidOctet(cs, VALID_COOKIE_NAME_OCTETS); + } + + public static int firstInvalidCookieValueOctet(CharSequence cs) { + return firstInvalidOctet(cs, VALID_COOKIE_VALUE_OCTETS); + } + + public static int firstInvalidOctet(CharSequence cs, BitSet bits) { + for (int i = 0; i < cs.length(); i++) { + char c = cs.charAt(i); + if (!bits.get(c)) { + return i; + } + } + return -1; + } + + public static CharSequence unwrapValue(CharSequence cs) { + final int len = cs.length(); + if (len > 0 && cs.charAt(0) == ('"')) { + if (len >= 2 && cs.charAt(len - 1) == ('"')) { + return len == 2 ? "" : cs.subSequence(1, len - 1); + } else { + return null; + } + } + return cs; + } + + public static String validateAttributeValue(String name, String value) { + if (value == null) { + return null; + } + value = value.trim(); + if (value.isEmpty()) { + return null; + } + int i = firstInvalidOctet(value, VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS); + if (i != -1) { + throw new IllegalArgumentException(name + " contains the prohibited characters: " + (value.charAt(i))); + } + return value; + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/DefaultCookie.java b/net-http/src/main/java/org/xbib/net/http/cookie/DefaultCookie.java new file mode 100644 index 0000000..31a92a0 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/DefaultCookie.java @@ -0,0 +1,232 @@ +package org.xbib.net.http.cookie; + +import java.util.Objects; + +/** + * The default {@link Cookie} implementation. + */ +public class DefaultCookie implements Cookie { + + private final String name; + + private String value; + + private boolean wrap; + + private String domain; + + private String path; + + private long maxAge = Long.MIN_VALUE; + + private boolean secure; + + private boolean httpOnly; + + private SameSite sameSite; + + public DefaultCookie(String name) { + this(name, null); + } + + public DefaultCookie(String name, String value) { + this.name = Objects.requireNonNull(name, "name").trim(); + if (this.name.isEmpty()) { + throw new IllegalArgumentException("empty name"); + } + if (value != null) { + setValue(value); + } + } + + @Override + public String name() { + return name; + } + + @Override + public String value() { + return value; + } + + @Override + public DefaultCookie setValue(String value) { + this.value = Objects.requireNonNull(value, "value"); + return this; + } + + @Override + public boolean wrap() { + return wrap; + } + + @Override + public DefaultCookie setWrap(boolean wrap) { + this.wrap = wrap; + return this; + } + + @Override + public String domain() { + return domain; + } + + @Override + public DefaultCookie setDomain(String domain) { + this.domain = CookieUtil.validateAttributeValue("domain", domain); + return this; + } + + @Override + public String path() { + return path; + } + + @Override + public DefaultCookie setPath(String path) { + this.path = CookieUtil.validateAttributeValue("path", path); + return this; + } + + @Override + public long maxAge() { + return maxAge; + } + + @Override + public DefaultCookie setMaxAge(long maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public DefaultCookie setSecure(boolean secure) { + this.secure = secure; + return this; + } + + @Override + public boolean isHttpOnly() { + return httpOnly; + } + + @Override + public DefaultCookie setHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + @Override + public DefaultCookie setSameSite(SameSite sameSite) { + this.sameSite = sameSite; + return this; + } + + @Override + public SameSite sameSite() { + return sameSite; + } + + @Override + public int hashCode() { + return name().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Cookie)) { + return false; + } + Cookie that = (Cookie) o; + if (!name().equals(that.name())) { + return false; + } + if (path() == null) { + if (that.path() != null) { + return false; + } + } else if (that.path() == null) { + return false; + } else if (!path().equals(that.path())) { + return false; + } + if (domain() == null) { + if (that.domain() != null) { + return false; + } + } else { + return domain().equalsIgnoreCase(that.domain()); + } + if (sameSite() == null) { + return that.sameSite() == null; + } else if (that.sameSite() == null) { + return false; + } else { + return sameSite().name().equalsIgnoreCase(that.sameSite().name()); + } + } + + @Override + public int compareTo(Cookie c) { + int v = name().compareTo(c.name()); + if (v != 0) { + return v; + } + if (path() == null) { + if (c.path() != null) { + return -1; + } + } else if (c.path() == null) { + return 1; + } else { + v = path().compareTo(c.path()); + if (v != 0) { + return v; + } + } + if (domain() == null) { + if (c.domain() != null) { + return -1; + } + } else if (c.domain() == null) { + return 1; + } else { + v = domain().compareToIgnoreCase(c.domain()); + return v; + } + return 0; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder() + .append(name()).append('=').append(value()); + if (domain() != null) { + buf.append(", domain=").append(domain()); + } + if (path() != null) { + buf.append(", path=").append(path()); + } + if (maxAge() >= 0) { + buf.append(", maxAge=").append(maxAge()).append('s'); + } + if (isSecure()) { + buf.append(", secure"); + } + if (isHttpOnly()) { + buf.append(", HTTPOnly"); + } + if (sameSite() != null) { + buf.append(", SameSite=").append(sameSite().name()); + } + return buf.toString(); + } +} diff --git a/net-http/src/main/java/org/xbib/net/http/cookie/SameSite.java b/net-http/src/main/java/org/xbib/net/http/cookie/SameSite.java new file mode 100644 index 0000000..10942e0 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/cookie/SameSite.java @@ -0,0 +1,5 @@ +package org.xbib.net.http.cookie; + +public enum SameSite { + STRICT, LAX, NONE +} diff --git a/net-http/src/main/java/org/xbib/net/http/package-info.java b/net-http/src/main/java/org/xbib/net/http/package-info.java new file mode 100644 index 0000000..29afdf0 --- /dev/null +++ b/net-http/src/main/java/org/xbib/net/http/package-info.java @@ -0,0 +1,4 @@ +/** + * Net classes for HTTP. + */ +package org.xbib.net.http; diff --git a/net-http/src/test/java/org/xbib/net/http/test/MimeParserTest.java b/net-http/src/test/java/org/xbib/net/http/test/MimeParserTest.java new file mode 100644 index 0000000..20aabdd --- /dev/null +++ b/net-http/src/test/java/org/xbib/net/http/test/MimeParserTest.java @@ -0,0 +1,11 @@ +package org.xbib.net.http.test; + +import org.junit.jupiter.api.Test; + +public class MimeParserTest { + + @Test + public void simpleMimeTest() throws Exception { + + } +} diff --git a/net-http/src/test/resources/hamster.a.templates b/net-http/src/test/resources/hamster.a.templates new file mode 100644 index 0000000..6dd20b7 --- /dev/null +++ b/net-http/src/test/resources/hamster.a.templates @@ -0,0 +1,58 @@ +/hamster/v2.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/airing/{id} +/hamster/v2.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v2.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v2.b/0/lookup/service/{id} +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v2.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/source/{id} +/hamster/v2.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v2.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=tvseries,episode +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=tvseries,episode +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=movie +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie +/gerbil/api/media/v1/logo/{style}/{id}.png +/gerbil/api/media/v1/image/{zoom}/{size}/{aspect}/{id}.jpg +/gerbil/api/media/v1/source/{id} diff --git a/net-http/src/test/resources/hamster.b.templates b/net-http/src/test/resources/hamster.b.templates new file mode 100644 index 0000000..ef5544c --- /dev/null +++ b/net-http/src/test/resources/hamster.b.templates @@ -0,0 +1,563 @@ +/hamster/v3.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v3.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v3.b/0/lookup/airing/{id} +/hamster/v3.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v3.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v3.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v3.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v3.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v3.b/0/lookup/service/{id} +/hamster/v3.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v3.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v3.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v3.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v3.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v3.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v3.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v3.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v3.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v3.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v3.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v3.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v3.b/0/lookup/source/{id} +/hamster/v3.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v3.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v3.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v3.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v3.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v4.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v4.b/0/lookup/airing/{id} +/hamster/v4.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v4.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v4.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v4.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v4.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v4.b/0/lookup/service/{id} +/hamster/v4.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v4.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v4.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v4.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v4.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v4.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v4.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v4.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v4.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v4.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v4.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v4.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v4.b/0/lookup/source/{id} +/hamster/v4.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v4.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v4.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v4.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v4.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v5.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v5.b/0/lookup/airing/{id} +/hamster/v5.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v5.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v5.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v5.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v5.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v5.b/0/lookup/service/{id} +/hamster/v5.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v5.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v5.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v5.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v5.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v5.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v5.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v5.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v5.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v5.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v5.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v5.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v5.b/0/lookup/source/{id} +/hamster/v5.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v5.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v5.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v5.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v5.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v6.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v6.b/0/lookup/airing/{id} +/hamster/v6.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v6.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v6.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v6.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v6.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v6.b/0/lookup/service/{id} +/hamster/v6.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v6.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v6.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v6.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v6.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v6.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v6.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v6.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v6.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v6.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v6.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v6.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v6.b/0/lookup/source/{id} +/hamster/v6.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v6.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v6.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v6.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v6.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v7.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v7.b/0/lookup/airing/{id} +/hamster/v7.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v7.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v7.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v7.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v7.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v7.b/0/lookup/service/{id} +/hamster/v7.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v7.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v7.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v7.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v7.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v7.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v7.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v7.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v7.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v7.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v7.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v7.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v7.b/0/lookup/source/{id} +/hamster/v7.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v7.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v7.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v7.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v7.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v8.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v8.b/0/lookup/airing/{id} +/hamster/v8.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v8.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v8.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v8.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v8.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v8.b/0/lookup/service/{id} +/hamster/v8.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v8.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v8.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v8.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v8.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v8.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v8.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v8.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v8.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v8.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v8.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v8.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v8.b/0/lookup/source/{id} +/hamster/v8.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v8.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v8.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v8.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v8.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v9.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v9.b/0/lookup/airing/{id} +/hamster/v9.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v9.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v9.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v9.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v9.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v9.b/0/lookup/service/{id} +/hamster/v9.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v9.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v9.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v9.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v9.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v9.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v9.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v9.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v9.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v9.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v9.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v9.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v9.b/0/lookup/source/{id} +/hamster/v9.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v9.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v9.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v9.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v9.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v10.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v10.b/0/lookup/airing/{id} +/hamster/v10.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v10.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v10.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v10.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v10.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v10.b/0/lookup/service/{id} +/hamster/v10.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v10.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v10.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v10.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v10.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v10.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v10.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v10.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v10.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v10.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v10.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v10.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v10.b/0/lookup/source/{id} +/hamster/v10.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v10.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v10.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v10.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v10.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3}:w +/hamster/v11.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v11.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v11.b/0/lookup/airing/{id} +/hamster/v11.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v11.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v11.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v11.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v11.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v11.b/0/lookup/service/{id} +/hamster/v11.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v11.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v11.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v11.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v11.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v11.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v11.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v11.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v11.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v11.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v11.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v11.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v11.b/0/lookup/source/{id} +/hamster/v11.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v11.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v11.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v11.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v11.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3}:w +/hamster/v12.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v12.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v12.b/0/lookup/airing/{id} +/hamster/v12.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v12.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v12.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v12.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v12.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v12.b/0/lookup/service/{id} +/hamster/v12.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v12.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v12.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v12.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v12.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v12.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v12.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v12.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v12.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v12.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v12.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v12.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v12.b/0/lookup/source/{id} +/hamster/v12.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v12.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v12.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v12.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v12.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v13.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v13.b/0/lookup/airing/{id} +/hamster/v13.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v13.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v13.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v13.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v13.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v13.b/0/lookup/service/{id} +/hamster/v13.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v13.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v13.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v13.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v13.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v13.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v13.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v13.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v13.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v13.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v13.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v13.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v13.b/0/lookup/source/{id} +/hamster/v13.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v13.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v13.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v13.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v13.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v14.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v14.b/0/lookup/airing/{id} +/hamster/v14.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v14.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v14.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v14.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v14.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v14.b/0/lookup/service/{id} +/hamster/v14.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v14.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v14.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v14.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v14.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v14.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v14.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v14.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v14.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v14.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v14.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v14.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v14.b/0/lookup/source/{id} +/hamster/v14.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v14.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v14.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v14.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v14.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/airing/{id} +/hamster/v2.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v2.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v2.b/0/lookup/service/{id} +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v2.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/source/{id} +/hamster/v2.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v2.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=tvseries,episode +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=tvseries,episode +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=movie +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/gerbil/api/media/v1/logo/{style}/{id}.png +/gerbil/api/media/v1/image/{zoom}/{size}/{aspect}/{id}.jpg +/gerbil/api/media/v1/source/{id} diff --git a/net-http/src/test/resources/hamster.c.templates b/net-http/src/test/resources/hamster.c.templates new file mode 100644 index 0000000..8ce42e3 --- /dev/null +++ b/net-http/src/test/resources/hamster.c.templates @@ -0,0 +1,60 @@ +/hamster/v2.b/0/lookup/season/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/season/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/series/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/airing/{id} +/hamster/v2.b/0/lookup/series/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/airing/{id}/synopses/first?by=length%3D{length},length%3D{length2},length%3D{length3},length%3D{length4} +/hamster/v2.b/0/lookup/other/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/lookup/movie/{id}/service/{serviceId}/appearances/{date}?size=20&page={page} +/hamster/v2.b/0/browse/service;country={country};postalCode={postalCode};msoId={msoId}?page={page}&size=20 +/hamster/v2.b/0/lookup/service/{id} +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=10 +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=1 +/hamster/v2.b/0/lookup/service/{id}/channels?page={page}&size=50 +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=10&duration=4&block={block}&inprogress=true +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=1&duration=24&inprogress=true +/hamster/v2.b/0/lookup/service/{id}/schedule/{date}?page={page}&size=50&duration=6&block={block}&inprogress=false +/hamster/v2.b/0/lookup/series/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/series/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/series/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/series/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/credits;isCast=true?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}/credits;isCast=false?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}/credits?in={in},{in2},{in3}&page={page}&size=20&by={by} +/hamster/v2.b/0/lookup/movie/{id}?in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/movie/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/source/{id} +/hamster/v2.b/0/batch/source/{id},{id2},{id3}/logos/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/source/{id}/logos?size=50&page={page}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/other/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/other/{id}/ratings;country={country} +/hamster/v2.b/0/batch/content/{id},{id2},{id3}/images/first?by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/content/{id}/images?size=50&page={page}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits;isCast=true?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits;isCast=false?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/credits?page={page}&size=20&by={by},{by2},{by3},{by4},{by5}&in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/ratings;country={country} +/hamster/v2.b/0/lookup/episode/{id}?in={in},{in2},{in3} +/hamster/v2.b/0/lookup/episode/{id}/synopses/first?by=length%3Dlong,length%3Dplain,length%3Dshort&in={in},{in2},{in3} +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie +/snake/v1/search?w={query}&rpr={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=tvseries,episode +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/search?gid={genreId}&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=tvseries,episode +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=movie +/snake/v1/search?gid={genreId}&XPID=pkg00%40{headendId}.prodigi&RPR={size}&os={next}&DS=255&RFTR=256&PN=12&XUA=ccc&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie +/snake/v1/recommendation?fid={id}&rpr={size}&os={next}&fty=group_id&DS=255&RFTR=256&PN=12&XUA=ccc&XPID=pkg00%40{headendId}.prodigi&TypeFilter=movie,episode,tvvideo,tvseries +/media/v1/logo/{style}/{id}.png +/media/v1/image/{zoom}/{size}/{aspect}/{id}.jpg +/media/v1/source/{id} +/mgmt/users/{user_id}/devices/{device_id}/channel diff --git a/net-http/src/test/resources/logging.properties b/net-http/src/test/resources/logging.properties new file mode 100644 index 0000000..3bb1ea4 --- /dev/null +++ b/net-http/src/test/resources/logging.properties @@ -0,0 +1,6 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +jdk.event.security.level=ALL diff --git a/net-http/src/test/resources/whale.a.templates b/net-http/src/test/resources/whale.a.templates new file mode 100644 index 0000000..3737756 --- /dev/null +++ b/net-http/src/test/resources/whale.a.templates @@ -0,0 +1,454 @@ +/whale/v2.b1/0/browse/vodfolder.(*,vodfolder);id={id} +/whale/v2.b1/0/browse/vodfolder.(*,vodassetvodfolders);id={id} +/whale/v2.b1/0/browse/vodfolder;id={id} +/whale/v2.b1/0/browse/vodfolder.(*,vodsystem);id={id} +/whale/v2.b1/0/browse/movie.(id,titles);id={id} +/whale/v2.b1/0/browse/tvseries.(id,latentfactors);id={id} +/whale/v2.b1/0/browse/tvseason.(id,vks);id={id} +/whale/v2.b1/0/browse/movie.(id,latentfactors);id={id} +/whale/v2.b1/0/browse/tvseries.(id,vks);id={id} +/whale/v2.b1/0/browse/tvseries.(id,credits);id={id} +/whale/v2.b1/0/browse/person.(id,zodiac);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,locale);id={id} +/whale/v2.b1/0/browse/keyword.(id,keywordtranslations);id={id} +/whale/v2.b1/0/browse/sportepisode.(id,sportseries);id={id} +/whale/v2.b1/0/browse/videotones;id={id} +/whale/v2.b1/0/browse/dcotz.(id,service);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,credits);id={id} +/whale/v2.b1/0/browse/sportepisode.(id,category);id={id} +/whale/v2.b1/0/browse/clu.(*,service);id={id} +/whale/v2.b1/0/browse/clu;id={id} +/whale/v2.b1/0/browse/clu.(*,tvsource);id={id} +/whale/v2.b1/0/browse/image.(*,imagefiles);id={id} +/whale/v2.b1/0/browse/image.(*,imgcat);id={id} +/whale/v2.b1/0/browse/image;id={id} +/whale/v2.b1/0/browse/tvsource.(id,airing);id={id} +/whale/v2.b1/0/browse/clu.(id,service);id={id} +/whale/v2.b1/0/browse/tvseries.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/sportseriess.(*,category);id={id} +/whale/v2.b1/0/browse/sportseriess.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/sportseriess.(*,sportepisode);id={id} +/whale/v2.b1/0/browse/sportseriess.(*,vgs);id={id} +/whale/v2.b1/0/browse/sportseries.(*,category);id={id} +/whale/v2.b1/0/browse/sportseries.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/sportseries.(*,sportepisode);id={id} +/whale/v2.b1/0/browse/sportseries;id={id} +/whale/v2.b1/0/browse/sportseries.(*,vgs);id={id} +/whale/v2.b1/0/browse/sportother.(*,category);id={id} +/whale/v2.b1/0/browse/sportother.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/sportother;id={id} +/whale/v2.b1/0/browse/sportother.(*,vgs);id={id} +/whale/v2.b1/0/browse/vodprograms.(*,vodasset);id={id} +/whale/v2.b1/0/browse/vodprograms.(*,other);id={id} +/whale/v2.b1/0/browse/vodprograms.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/vodprograms.(*,tvseries);id={id} +/whale/v2.b1/0/browse/vodprograms.(*,locale);id={id} +/whale/v2.b1/0/browse/vodprograms.(*,movie);id={id} +/whale/v2.b1/0/browse/statepprodigince;id={id} +/whale/v2.b1/0/browse/statepprodigince.(*,country);id={id} +/whale/v2.b1/0/browse/movie.(id,vmts);id={id} +/whale/v2.b1/0/browse/sportother.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/vodprogram.(id,tvseries);id={id} +/whale/v2.b1/0/browse/person.(id,gender);id={id} +/whale/v2.b1/0/browse/tvseason.(id,vis);id={id} +/whale/v2.b1/0/browse/statepprodigince.(id,country);id={id} +/whale/v2.b1/0/browse/vodsystem.(*,vodfolder);id={id} +/whale/v2.b1/0/browse/vodsystem.(*,vodasset);id={id} +/whale/v2.b1/0/browse/vodsystem.(*,vodservicepprodigider);id={id} +/whale/v2.b1/0/browse/vodsystem;id={id} +/whale/v2.b1/0/browse/videothemeflags;id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,category);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,tvseries);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,vks);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,credits);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,tvseason);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,fls);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvepisodes.(*,vis);id={id} +/whale/v2.b1/0/browse/tvseries.(id,vgs);id={id} +/whale/v2.b1/0/browse/genre.(id,genretranslations);id={id} +/whale/v2.b1/0/browse/tvseries.(id,vis);id={id} +/whale/v2.b1/0/browse/sportepisodes.(*,category);id={id} +/whale/v2.b1/0/browse/sportepisodes.(*,sportseries);id={id} +/whale/v2.b1/0/browse/sportepisodes.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/sportepisodes.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,fls);id={id} +/whale/v2.b1/0/browse/movie.(id,vvts);id={id} +/whale/v2.b1/0/browse/vodservicepprodigider.(id,vodsystem);id={id} +/whale/v2.b1/0/browse/award.(id,awardtype);id={id} +/whale/v2.b1/0/browse/vodprogram.(id,locale);id={id} +/whale/v2.b1/0/browse/sportseries.(id,vgs);id={id} +/whale/v2.b1/0/browse/videothemeflag;id={id} +/whale/v2.b1/0/browse/movie.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/tvseason.(id,videotranslations);id={id} +/whale/v2.b1/0/browse/award.(id,awardawardmediums);id={id} +/whale/v2.b1/0/browse/country.(id,statepprodigince);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,sportseries);id={id} +/whale/v2.b1/0/browse/credittype;id={id} +/whale/v2.b1/0/browse/credittype.(*,credittypetranslations);id={id} +/whale/v2.b1/0/browse/award.(*,awardnomination);id={id} +/whale/v2.b1/0/browse/award.(*,awardcategory);id={id} +/whale/v2.b1/0/browse/award;id={id} +/whale/v2.b1/0/browse/award.(*,awardawardmediums);id={id} +/whale/v2.b1/0/browse/award.(*,ais);id={id} +/whale/v2.b1/0/browse/award.(*,awardtype);id={id} +/whale/v2.b1/0/browse/movie.(id,vtfs);id={id} +/whale/v2.b1/0/browse/vodprogram.(id,other);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,videotranslations);id={id} +/whale/v2.b1/0/browse/movietheme.(id,moviethemetranslations);id={id} +/whale/v2.b1/0/browse/tvseason.(id,vgs);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,isynopsiss);id={id} +/whale/v2.b1/0/browse/other.(id,videotranslations);id={id} +/whale/v2.b1/0/browse/vodasset.(id,vodapplicationtype);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,vis);id={id} +/whale/v2.b1/0/browse/awardnominations.(*,anis);id={id} +/whale/v2.b1/0/browse/awardnominations.(*,award);id={id} +/whale/v2.b1/0/browse/awardnominations.(*,awardsource);id={id} +/whale/v2.b1/0/browse/gender;id={id} +/whale/v2.b1/0/browse/gender.(*,gendertranslations);id={id} +/whale/v2.b1/0/browse/movie.(id,credits);id={id} +/whale/v2.b1/0/browse/vodfolders.(*,vodfolder);id={id} +/whale/v2.b1/0/browse/vodfolders.(*,vodassetvodfolders);id={id} +/whale/v2.b1/0/browse/vodfolders.(*,vodsystem);id={id} +/whale/v2.b1/0/browse/person.(id,awardsupportingnames);id={id} +/whale/v2.b1/0/browse/tvseries.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/tvseries.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/tvseries.(*,credits);id={id} +/whale/v2.b1/0/browse/tvseries.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvseries.(*,vis);id={id} +/whale/v2.b1/0/browse/tvseries.(*,relevences);id={id} +/whale/v2.b1/0/browse/tvseries.(*,category);id={id} +/whale/v2.b1/0/browse/tvseries.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/tvseries.(*,latentfactors);id={id} +/whale/v2.b1/0/browse/tvseries.(*,vks);id={id} +/whale/v2.b1/0/browse/tvseries.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/tvseries.(*,tvseason);id={id} +/whale/v2.b1/0/browse/tvseries.(*,titles);id={id} +/whale/v2.b1/0/browse/tvseries;id={id} +/whale/v2.b1/0/browse/clu.(id,tvsource);id={id} +/whale/v2.b1/0/browse/tvseries.(id,tvepisode);id={id} +/whale/v2.b1/0/browse/timeperiod;id={id} +/whale/v2.b1/0/browse/image.(id,imgcat);id={id} +/whale/v2.b1/0/browse/movie.(id,vsgs);id={id} +/whale/v2.b1/0/browse/countrys.(*,countrytranslations);id={id} +/whale/v2.b1/0/browse/countrys.(*,statepprodigince);id={id} +/whale/v2.b1/0/browse/country.(*,countrytranslations);id={id} +/whale/v2.b1/0/browse/country;id={id} +/whale/v2.b1/0/browse/country.(*,statepprodigince);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,sportepisode);id={id} +/whale/v2.b1/0/browse/person.(id,credits);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,tvseason);id={id} +/whale/v2.b1/0/browse/awards.(*,awardnomination);id={id} +/whale/v2.b1/0/browse/awards.(*,awardcategory);id={id} +/whale/v2.b1/0/browse/awards.(*,awardawardmediums);id={id} +/whale/v2.b1/0/browse/awards.(*,ais);id={id} +/whale/v2.b1/0/browse/awards.(*,awardtype);id={id} +/whale/v2.b1/0/browse/award.(id,ais);id={id} +/whale/v2.b1/0/browse/locales;id={id} +/whale/v2.b1/0/browse/vodasset.(id,vodsystem);id={id} +/whale/v2.b1/0/browse/movie.(id,videotranslations);id={id} +/whale/v2.b1/0/browse/vodasset.(id,vodprogram);id={id} +/whale/v2.b1/0/browse/sportseries.(id,sportepisode);id={id} +/whale/v2.b1/0/browse/organization.(*,locale);id={id} +/whale/v2.b1/0/browse/organization;id={id} +/whale/v2.b1/0/browse/awardnomination.(id,awardsource);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,vodprogram);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,vgs);id={id} +/whale/v2.b1/0/browse/country.(id,countrytranslations);id={id} +/whale/v2.b1/0/browse/sportepisode.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,ialttitles);id={id} +/whale/v2.b1/0/browse/other.(id,category);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,tvepisode);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,credits);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,vis);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,relevences);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,category);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,latentfactors);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,vks);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,tvseason);id={id} +/whale/v2.b1/0/browse/tvseriess.(*,titles);id={id} +/whale/v2.b1/0/browse/vodfolder.(id,vodsystem);id={id} +/whale/v2.b1/0/browse/tvsource;id={id} +/whale/v2.b1/0/browse/tvsource.(*,airing);id={id} +/whale/v2.b1/0/browse/tvseason.(id,tvseries);id={id} +/whale/v2.b1/0/browse/persons.(*,awardsupportingnames);id={id} +/whale/v2.b1/0/browse/persons.(*,zodiac);id={id} +/whale/v2.b1/0/browse/persons.(*,credits);id={id} +/whale/v2.b1/0/browse/persons.(*,gender);id={id} +/whale/v2.b1/0/browse/persons.(*,pis);id={id} +/whale/v2.b1/0/browse/airing.(id,tvprogram);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,movie);id={id} +/whale/v2.b1/0/browse/tvseries.(id,vodprogram);id={id} +/whale/v2.b1/0/browse/image.(id,imagefiles);id={id} +/whale/v2.b1/0/browse/service.(*,clu);id={id} +/whale/v2.b1/0/browse/service.(*,dcotz);id={id} +/whale/v2.b1/0/browse/service;id={id} +/whale/v2.b1/0/browse/service.(*,serviceid);id={id} +/whale/v2.b1/0/browse/airing;id={id} +/whale/v2.b1/0/browse/airing.(*,tvsource);id={id} +/whale/v2.b1/0/browse/airing.(*,tvprogram);id={id} +/whale/v2.b1/0/browse/vodsystem.(id,vodasset);id={id} +/whale/v2.b1/0/browse/moodpicks;id={id} +/whale/v2.b1/0/browse/moodpicks.(*,moodpickstranslations);id={id} +/whale/v2.b1/0/browse/clus.(*,service);id={id} +/whale/v2.b1/0/browse/clus.(*,tvsource);id={id} +/whale/v2.b1/0/browse/person.(*,awardsupportingnames);id={id} +/whale/v2.b1/0/browse/person.(*,zodiac);id={id} +/whale/v2.b1/0/browse/person.(*,credits);id={id} +/whale/v2.b1/0/browse/person.(*,gender);id={id} +/whale/v2.b1/0/browse/person;id={id} +/whale/v2.b1/0/browse/person.(*,pis);id={id} +/whale/v2.b1/0/browse/vodfolder.(id,vodassetvodfolders);id={id} +/whale/v2.b1/0/browse/vodsystem.(id,vodservicepprodigider);id={id} +/whale/v2.b1/0/browse/vodprogram.(*,vodasset);id={id} +/whale/v2.b1/0/browse/vodprogram.(*,other);id={id} +/whale/v2.b1/0/browse/vodprogram.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/vodprogram.(*,tvseries);id={id} +/whale/v2.b1/0/browse/vodprogram.(*,locale);id={id} +/whale/v2.b1/0/browse/vodprogram.(*,movie);id={id} +/whale/v2.b1/0/browse/vodprogram;id={id} +/whale/v2.b1/0/browse/other.(id,vis);id={id} +/whale/v2.b1/0/browse/timeperiods;id={id} +/whale/v2.b1/0/browse/moodpicks.(id,moodpickstranslations);id={id} +/whale/v2.b1/0/browse/tvseason.(*,category);id={id} +/whale/v2.b1/0/browse/tvseason.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/tvseason.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/tvseason.(*,tvseries);id={id} +/whale/v2.b1/0/browse/tvseason.(*,vks);id={id} +/whale/v2.b1/0/browse/tvseason.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/tvseason.(*,credits);id={id} +/whale/v2.b1/0/browse/tvseason;id={id} +/whale/v2.b1/0/browse/tvseason.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvseason.(*,vis);id={id} +/whale/v2.b1/0/browse/tvseries.(id,titles);id={id} +/whale/v2.b1/0/browse/services.(*,clu);id={id} +/whale/v2.b1/0/browse/services.(*,dcotz);id={id} +/whale/v2.b1/0/browse/services.(*,serviceid);id={id} +/whale/v2.b1/0/browse/other.(id,vodprogram);id={id} +/whale/v2.b1/0/browse/others.(*,category);id={id} +/whale/v2.b1/0/browse/others.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/others.(*,vks);id={id} +/whale/v2.b1/0/browse/others.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/others.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/others.(*,credits);id={id} +/whale/v2.b1/0/browse/others.(*,vgs);id={id} +/whale/v2.b1/0/browse/others.(*,vis);id={id} +/whale/v2.b1/0/browse/vodfolder.(id,vodfolder);id={id} +/whale/v2.b1/0/browse/airings.(*,tvsource);id={id} +/whale/v2.b1/0/browse/airings.(*,tvprogram);id={id} +/whale/v2.b1/0/browse/vodassets.(*,vodassetvodfolders);id={id} +/whale/v2.b1/0/browse/vodassets.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/vodassets.(*,vodapplicationtype);id={id} +/whale/v2.b1/0/browse/vodassets.(*,vodshowtype);id={id} +/whale/v2.b1/0/browse/vodassets.(*,vodsystem);id={id} +/whale/v2.b1/0/browse/subgenre.(id,subgenretranslations);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,airing);id={id} +/whale/v2.b1/0/browse/sportother.(id,vgs);id={id} +/whale/v2.b1/0/browse/movie.(id,vgs);id={id} +/whale/v2.b1/0/browse/movie.(id,websites);id={id} +/whale/v2.b1/0/browse/category.(*,categorytranslations);id={id} +/whale/v2.b1/0/browse/category;id={id} +/whale/v2.b1/0/browse/gender.(id,gendertranslations);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,other);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,tvseason);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,other);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,tvseries);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,ialttitles);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,locale);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,movie);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,isynopsiss);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,icredits);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,sportseries);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,sportother);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,sportepisode);id={id} +/whale/v2.b1/0/browse/tvprogram.(*,tvseason);id={id} +/whale/v2.b1/0/browse/tvprogram;id={id} +/whale/v2.b1/0/browse/tvprogram.(*,airing);id={id} +/whale/v2.b1/0/browse/vodsystem.(id,vodfolder);id={id} +/whale/v2.b1/0/browse/airing.(id,tvsource);id={id} +/whale/v2.b1/0/browse/sportseries.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/sportother.(id,category);id={id} +/whale/v2.b1/0/browse/serviceid.(*,service);id={id} +/whale/v2.b1/0/browse/serviceid;id={id} +/whale/v2.b1/0/browse/other.(id,vgs);id={id} +/whale/v2.b1/0/browse/tvseries.(id,tvseason);id={id} +/whale/v2.b1/0/browse/sportothers.(*,category);id={id} +/whale/v2.b1/0/browse/sportothers.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/sportothers.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/person.(id,pis);id={id} +/whale/v2.b1/0/browse/awardcategory;id={id} +/whale/v2.b1/0/browse/awardcategory.(*,awardcategorytranslations);id={id} +/whale/v2.b1/0/browse/subgenre;id={id} +/whale/v2.b1/0/browse/subgenre.(*,subgenretranslations);id={id} +/whale/v2.b1/0/browse/movie.(id,fls);id={id} +/whale/v2.b1/0/browse/vodservicepprodigider;id={id} +/whale/v2.b1/0/browse/vodservicepprodigider.(*,vodsystem);id={id} +/whale/v2.b1/0/browse/other.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/movie.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/movie.(*,vmps);id={id} +/whale/v2.b1/0/browse/movie.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/movie.(*,vtfs);id={id} +/whale/v2.b1/0/browse/movie.(*,credits);id={id} +/whale/v2.b1/0/browse/movie.(*,fls);id={id} +/whale/v2.b1/0/browse/movie.(*,vsgs);id={id} +/whale/v2.b1/0/browse/movie.(*,websites);id={id} +/whale/v2.b1/0/browse/movie.(*,vgs);id={id} +/whale/v2.b1/0/browse/movie.(*,vis);id={id} +/whale/v2.b1/0/browse/movie.(*,relevences);id={id} +/whale/v2.b1/0/browse/movie.(*,vmts);id={id} +/whale/v2.b1/0/browse/movie.(*,vvts);id={id} +/whale/v2.b1/0/browse/movie.(*,category);id={id} +/whale/v2.b1/0/browse/movie.(*,latentfactors);id={id} +/whale/v2.b1/0/browse/movie.(*,vks);id={id} +/whale/v2.b1/0/browse/movie.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/movie.(*,titles);id={id} +/whale/v2.b1/0/browse/movie;id={id} +/whale/v2.b1/0/browse/movie.(*,vtps);id={id} +/whale/v2.b1/0/browse/movie.(*,release);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,tvseries);id={id} +/whale/v2.b1/0/browse/vodprogram.(id,movie);id={id} +/whale/v2.b1/0/browse/movie.(id,vis);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,vks);id={id} +/whale/v2.b1/0/browse/other.(id,credits);id={id} +/whale/v2.b1/0/browse/tvseries.(id,relevences);id={id} +/whale/v2.b1/0/browse/movie.(id,release);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,tvseries);id={id} +/whale/v2.b1/0/browse/genre.(*,genretranslations);id={id} +/whale/v2.b1/0/browse/genre;id={id} +/whale/v2.b1/0/browse/service.(id,serviceid);id={id} +/whale/v2.b1/0/browse/vodprogram.(id,tvepisode);id={id} +/whale/v2.b1/0/browse/awardnomination.(*,anis);id={id} +/whale/v2.b1/0/browse/awardnomination.(*,award);id={id} +/whale/v2.b1/0/browse/awardnomination;id={id} +/whale/v2.b1/0/browse/awardnomination.(*,awardsource);id={id} +/whale/v2.b1/0/browse/movie.(id,category);id={id} +/whale/v2.b1/0/browse/awardnomination.(id,anis);id={id} +/whale/v2.b1/0/browse/tvseries.(id,videotranslations);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,sportother);id={id} +/whale/v2.b1/0/browse/vodsystems.(*,vodfolder);id={id} +/whale/v2.b1/0/browse/vodsystems.(*,vodasset);id={id} +/whale/v2.b1/0/browse/vodsystems.(*,vodservicepprodigider);id={id} +/whale/v2.b1/0/browse/tvseason.(id,tvepisode);id={id} +/whale/v2.b1/0/browse/zodiacs;id={id} +/whale/v2.b1/0/browse/vodasset.(id,vodassetvodfolders);id={id} +/whale/v2.b1/0/browse/movietheme.(*,moviethemetranslations);id={id} +/whale/v2.b1/0/browse/movietheme;id={id} +/whale/v2.b1/0/browse/movie.(id,vodprogram);id={id} +/whale/v2.b1/0/browse/serviceid.(id,service);id={id} +/whale/v2.b1/0/browse/movie.(id,relevences);id={id} +/whale/v2.b1/0/browse/movie.(id,vks);id={id} +/whale/v2.b1/0/browse/tvseries.(id,category);id={id} +/whale/v2.b1/0/browse/credittype.(id,credittypetranslations);id={id} +/whale/v2.b1/0/browse/videotone;id={id} +/whale/v2.b1/0/browse/movie.(id,vmps);id={id} +/whale/v2.b1/0/browse/movie.(id,vtps);id={id} +/whale/v2.b1/0/browse/award.(id,awardcategory);id={id} +/whale/v2.b1/0/browse/tvseason.(id,category);id={id} +/whale/v2.b1/0/browse/dcotz.(*,service);id={id} +/whale/v2.b1/0/browse/dcotz;id={id} +/whale/v2.b1/0/browse/images.(*,imagefiles);id={id} +/whale/v2.b1/0/browse/images.(*,imgcat);id={id} +/whale/v2.b1/0/browse/awardnomination.(id,award);id={id} +/whale/v2.b1/0/browse/vodasset.(id,vodshowtype);id={id} +/whale/v2.b1/0/browse/vodasset.(*,vodassetvodfolders);id={id} +/whale/v2.b1/0/browse/vodasset.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/vodasset;id={id} +/whale/v2.b1/0/browse/vodasset.(*,vodapplicationtype);id={id} +/whale/v2.b1/0/browse/vodasset.(*,vodshowtype);id={id} +/whale/v2.b1/0/browse/vodasset.(*,vodsystem);id={id} +/whale/v2.b1/0/browse/other.(*,category);id={id} +/whale/v2.b1/0/browse/other.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/other.(*,vks);id={id} +/whale/v2.b1/0/browse/other.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/other.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/other.(*,credits);id={id} +/whale/v2.b1/0/browse/other;id={id} +/whale/v2.b1/0/browse/other.(*,vgs);id={id} +/whale/v2.b1/0/browse/other.(*,vis);id={id} +/whale/v2.b1/0/browse/other.(id,vks);id={id} +/whale/v2.b1/0/browse/tvseason.(id,tvprograms);id={id} +/whale/v2.b1/0/browse/sportepisode.(id,vgs);id={id} +/whale/v2.b1/0/browse/movies.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/movies.(*,vmps);id={id} +/whale/v2.b1/0/browse/movies.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/movies.(*,vtfs);id={id} +/whale/v2.b1/0/browse/movies.(*,credits);id={id} +/whale/v2.b1/0/browse/movies.(*,fls);id={id} +/whale/v2.b1/0/browse/movies.(*,vsgs);id={id} +/whale/v2.b1/0/browse/movies.(*,websites);id={id} +/whale/v2.b1/0/browse/movies.(*,vgs);id={id} +/whale/v2.b1/0/browse/movies.(*,vis);id={id} +/whale/v2.b1/0/browse/movies.(*,relevences);id={id} +/whale/v2.b1/0/browse/movies.(*,vmts);id={id} +/whale/v2.b1/0/browse/movies.(*,vvts);id={id} +/whale/v2.b1/0/browse/movies.(*,category);id={id} +/whale/v2.b1/0/browse/movies.(*,latentfactors);id={id} +/whale/v2.b1/0/browse/movies.(*,vks);id={id} +/whale/v2.b1/0/browse/movies.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/movies.(*,titles);id={id} +/whale/v2.b1/0/browse/movies.(*,vtps);id={id} +/whale/v2.b1/0/browse/movies.(*,release);id={id} +/whale/v2.b1/0/browse/locale;id={id} +/whale/v2.b1/0/browse/zodiac;id={id} +/whale/v2.b1/0/browse/vodprogram.(id,vodasset);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,other);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,tvseries);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,ialttitles);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,locale);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,movie);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,isynopsiss);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,icredits);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,sportseries);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,sportother);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,sportepisode);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,tvseason);id={id} +/whale/v2.b1/0/browse/tvprograms.(*,airing);id={id} +/whale/v2.b1/0/browse/category.(id,categorytranslations);id={id} +/whale/v2.b1/0/browse/award.(id,awardnomination);id={id} +/whale/v2.b1/0/browse/service.(id,dcotz);id={id} +/whale/v2.b1/0/browse/service.(id,clu);id={id} +/whale/v2.b1/0/browse/organization.(id,locale);id={id} +/whale/v2.b1/0/browse/awardcategory.(id,awardcategorytranslations);id={id} +/whale/v2.b1/0/browse/keyword;id={id} +/whale/v2.b1/0/browse/keyword.(*,keywordtranslations);id={id} +/whale/v2.b1/0/browse/tvepisode.(id,category);id={id} +/whale/v2.b1/0/browse/tvseason.(id,credits);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,category);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,tvseries);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,vks);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,vodprogram);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,credits);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,tvseason);id={id} +/whale/v2.b1/0/browse/tvepisode;id={id} +/whale/v2.b1/0/browse/tvepisode.(*,fls);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvepisode.(*,vis);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,category);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,tvepisode);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,videotranslations);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,tvseries);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,vks);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,credits);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvseasons.(*,vis);id={id} +/whale/v2.b1/0/browse/sportseries.(id,category);id={id} +/whale/v2.b1/0/browse/sportepisode.(*,category);id={id} +/whale/v2.b1/0/browse/sportepisode.(*,sportseries);id={id} +/whale/v2.b1/0/browse/sportepisode.(*,tvprograms);id={id} +/whale/v2.b1/0/browse/sportepisode;id={id} +/whale/v2.b1/0/browse/sportepisode.(*,vgs);id={id} +/whale/v2.b1/0/browse/tvprogram.(id,icredits);id={id} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b05dde7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,70 @@ +dependencyResolutionManagement { + versionCatalogs { + libs { + version('gradle', '7.5.1') + version('junit', '5.9.1') + version('net', '3.0.0') + version('netty', '4.1.84.Final') + version('netty-tcnative', '2.0.54.Final') + version('datastructures', '1.0.1') + version('groovy', '4.0.4') + version('config', '5.0.0') + library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') + library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit') + library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit') + library('junit4', 'junit', 'junit').version('4.13.2') + library('hamcrest', 'org.hamcrest', 'hamcrest-library').version('2.2') + library('net', 'org.xbib', 'net').versionRef('net') + library('net-mime', 'org.xbib', 'net-mime').versionRef('net') + library('net-security', 'org.xbib', 'net-security').versionRef('net') + library('net-bouncycastle', 'org.xbib', 'net-bouncycastle').versionRef('net') + library('netty-codec-http2', 'io.netty', 'netty-codec-http2').versionRef('netty') + library('netty-handler', 'io.netty', 'netty-handler').versionRef('netty') + library('netty-handler-proxy', 'io.netty', 'netty-handler-proxy').versionRef('netty') + library('netty-epoll', 'io.netty', 'netty-transport-native-epoll').versionRef('netty') + library('netty-kqueue', 'io.netty', 'netty-transport-native-kqueue').versionRef('netty') + library('netty-boringssl', 'io.netty', 'netty-tcnative-boringssl-static').versionRef('netty-tcnative') + library('bouncycastle', 'org.bouncycastle', 'bcpkix-jdk18on').version('1.71') + library('conscrypt', 'org.conscrypt', 'conscrypt-openjdk-uber').version('2.5.2') + library('jackson', 'com.fasterxml.jackson.core', 'jackson-databind').version('2.12.6') + library('guice', 'org.xbib', 'guice').version('4.4.2') + library('javassist', 'org.javassist', 'javassist').version('3.28.0-GA') + library('jna', 'net.java.dev.jna', 'jna').version('5.10.0') + library('config', 'org.xbib', 'config').versionRef('config') + library('settings-datastructures-json', 'org.xbib', 'settings-datastructures-json').versionRef('config') + library('settings-datastructures-yaml', 'org.xbib', 'settings-datastructures-yaml').versionRef('config') + library('datastructures-common', 'org.xbib', 'datastructures-common').versionRef('datastructures') + library('datastructures-json-tiny', 'org.xbib', 'datastructures-json-tiny').versionRef('datastructures') + library('datastructures-yaml-tiny', 'org.xbib', 'datastructures-yaml-tiny').versionRef('datastructures') + library('groovy-templates', 'org.apache.groovy', 'groovy-templates').versionRef('groovy') + library('jdbc-query', 'org.xbib', 'jdbc-query').version('0.0.5') + library('jdbc-connection-pool', 'org.xbib', 'jdbc-connection-pool').version('0.0.5') + library('event', 'org.xbib', 'event').version('0.0.1') + library('oracle', 'com.oracle.database.jdbc', 'ojdbc11').version('21.4.0.0') + library('webjars-bootstrap', 'org.webjars.bower', 'bootstrap').version('3.4.1') + library('webjars-jquery', 'org.webjars.bower', 'jquery').version('3.5.1') + library('webjars-fontawesome', 'org.webjars', 'font-awesome').version('5.14.0') + plugin('publish', 'com.gradle.plugin-publish').version('0.18.0') + } + } +} + +include 'net-http' +include 'net-http-client' +include 'net-http-client-simple' +include 'net-http-client-netty' +include 'net-http-client-netty-secure' +include 'net-http-netty-boringssl' +include 'net-http-netty-conscrypt' +include 'net-http-netty-epoll' +include 'net-http-netty-kqueue' +include 'net-http-server' +include 'net-http-server-netty' +include 'net-http-server-netty-secure' +include 'net-http-server-nio' +include 'net-http-server-simple' +include 'net-http-server-simple-secure' +include 'net-http-template-groovy' +include 'net-http-server-application-web' +include 'net-http-server-application-config' +include 'net-http-server-application-database'