Repository: Netflix/zuul Branch: master Commit: 658eb26797df Files: 414 Total size: 1.7 MB Directory structure: gitextract_tnrhk04n/ ├── .github/ │ ├── CODEOWNERS │ ├── dependabot.yml │ └── workflows/ │ ├── benchmark.yml │ ├── branch_snapshot.yml │ ├── gradle-wrapper-validation.yml │ ├── pr.yml │ ├── release.yml │ ├── snapshot.yml │ └── stale.yml ├── .gitignore ├── .netflixoss ├── CHANGELOG.md ├── LICENSE ├── OSSMETADATA ├── README.md ├── build.gradle ├── codequality/ │ └── checkstyle.xml ├── gradle/ │ └── wrapper/ │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── zuul-core/ │ ├── build.gradle │ └── src/ │ ├── jmh/ │ │ └── java/ │ │ └── com/ │ │ └── netflix/ │ │ └── zuul/ │ │ └── message/ │ │ └── HeadersBenchmark.java │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── netflix/ │ │ ├── config/ │ │ │ ├── DynamicIntegerSetProperty.java │ │ │ └── PatternListStringProperty.java │ │ ├── netty/ │ │ │ └── common/ │ │ │ ├── AbstrHttpConnectionExpiryHandler.java │ │ │ ├── ByteBufUtil.java │ │ │ ├── CategorizedThreadFactory.java │ │ │ ├── CloseOnIdleStateHandler.java │ │ │ ├── ConnectionCloseChannelAttributes.java │ │ │ ├── ConnectionCloseType.java │ │ │ ├── Http1ConnectionCloseHandler.java │ │ │ ├── Http1ConnectionExpiryHandler.java │ │ │ ├── Http2ConnectionCloseHandler.java │ │ │ ├── Http2ConnectionExpiryHandler.java │ │ │ ├── HttpChannelFlags.java │ │ │ ├── HttpClientLifecycleChannelHandler.java │ │ │ ├── HttpLifecycleChannelHandler.java │ │ │ ├── HttpRequestReadTimeoutEvent.java │ │ │ ├── HttpRequestReadTimeoutHandler.java │ │ │ ├── HttpServerLifecycleChannelHandler.java │ │ │ ├── RequestResponseCompleteEvent.java │ │ │ ├── SourceAddressChannelHandler.java │ │ │ ├── SslExceptionsHandler.java │ │ │ ├── SwallowSomeHttp2ExceptionsHandler.java │ │ │ ├── accesslog/ │ │ │ │ ├── AccessLogChannelHandler.java │ │ │ │ └── AccessLogPublisher.java │ │ │ ├── channel/ │ │ │ │ └── config/ │ │ │ │ ├── ChannelConfig.java │ │ │ │ ├── ChannelConfigKey.java │ │ │ │ ├── ChannelConfigValue.java │ │ │ │ └── CommonChannelConfigKeys.java │ │ │ ├── http2/ │ │ │ │ └── DynamicHttp2FrameLogger.java │ │ │ ├── metrics/ │ │ │ │ ├── EventLoopGroupMetrics.java │ │ │ │ ├── EventLoopMetrics.java │ │ │ │ ├── Http2MetricsChannelHandlers.java │ │ │ │ ├── HttpBodySizeRecordingChannelHandler.java │ │ │ │ ├── HttpMetricsChannelHandler.java │ │ │ │ ├── InstrumentedResourceLeakDetector.java │ │ │ │ └── PerEventLoopMetricsChannelHandler.java │ │ │ ├── proxyprotocol/ │ │ │ │ ├── ElbProxyProtocolChannelHandler.java │ │ │ │ ├── HAProxyMessageChannelHandler.java │ │ │ │ └── StripUntrustedProxyHeadersHandler.java │ │ │ ├── ssl/ │ │ │ │ ├── ServerSslConfig.java │ │ │ │ └── SslHandshakeInfo.java │ │ │ ├── status/ │ │ │ │ └── ServerStatusManager.java │ │ │ └── throttle/ │ │ │ ├── MaxInboundConnectionsHandler.java │ │ │ ├── RejectionType.java │ │ │ ├── RejectionUtils.java │ │ │ └── RequestRejectedEvent.java │ │ └── zuul/ │ │ ├── Attrs.java │ │ ├── BasicFilterUsageNotifier.java │ │ ├── BasicRequestCompleteHandler.java │ │ ├── DefaultFilterFactory.java │ │ ├── DynamicFilterLoader.java │ │ ├── ExecutionStatus.java │ │ ├── Filter.java │ │ ├── FilterCategory.java │ │ ├── FilterConstraint.java │ │ ├── FilterFactory.java │ │ ├── FilterFileManager.java │ │ ├── FilterLoader.java │ │ ├── FilterUsageNotifier.java │ │ ├── RequestCompleteHandler.java │ │ ├── StaticFilterLoader.java │ │ ├── ZuulApplicationInfo.java │ │ ├── constants/ │ │ │ ├── ZuulConstants.java │ │ │ └── ZuulHeaders.java │ │ ├── context/ │ │ │ ├── CommonContextKeys.java │ │ │ ├── Debug.java │ │ │ ├── SessionCleaner.java │ │ │ ├── SessionContext.java │ │ │ ├── SessionContextDecorator.java │ │ │ ├── SessionContextFactory.java │ │ │ └── ZuulSessionContextDecorator.java │ │ ├── exception/ │ │ │ ├── ErrorType.java │ │ │ ├── OutboundErrorType.java │ │ │ ├── OutboundException.java │ │ │ ├── RequestExpiredException.java │ │ │ ├── ZuulException.java │ │ │ └── ZuulFilterConcurrencyExceededException.java │ │ ├── filters/ │ │ │ ├── BaseFilter.java │ │ │ ├── BaseSyncFilter.java │ │ │ ├── Endpoint.java │ │ │ ├── FilterError.java │ │ │ ├── FilterRegistry.java │ │ │ ├── FilterSyncType.java │ │ │ ├── FilterType.java │ │ │ ├── MutableFilterRegistry.java │ │ │ ├── ShouldFilter.java │ │ │ ├── SyncZuulFilter.java │ │ │ ├── SyncZuulFilterAdapter.java │ │ │ ├── ZuulFilter.java │ │ │ ├── common/ │ │ │ │ ├── GZipResponseFilter.java │ │ │ │ └── SurgicalDebugFilter.java │ │ │ ├── endpoint/ │ │ │ │ ├── EndpointLifecycle.java │ │ │ │ ├── MissingEndpointHandlingFilter.java │ │ │ │ └── ProxyEndpoint.java │ │ │ ├── http/ │ │ │ │ ├── HttpInboundFilter.java │ │ │ │ ├── HttpInboundSyncFilter.java │ │ │ │ ├── HttpOutboundFilter.java │ │ │ │ ├── HttpOutboundSyncFilter.java │ │ │ │ └── HttpSyncEndpoint.java │ │ │ └── passport/ │ │ │ ├── InboundPassportStampingFilter.java │ │ │ ├── OutboundPassportStampingFilter.java │ │ │ └── PassportStampingFilter.java │ │ ├── logging/ │ │ │ └── Http2FrameLoggingPerClientIpHandler.java │ │ ├── message/ │ │ │ ├── Header.java │ │ │ ├── HeaderName.java │ │ │ ├── Headers.java │ │ │ ├── ZuulMessage.java │ │ │ ├── ZuulMessageImpl.java │ │ │ ├── http/ │ │ │ │ ├── Cookies.java │ │ │ │ ├── HttpHeaderNames.java │ │ │ │ ├── HttpHeaderNamesCache.java │ │ │ │ ├── HttpQueryParams.java │ │ │ │ ├── HttpRequestInfo.java │ │ │ │ ├── HttpRequestMessage.java │ │ │ │ ├── HttpRequestMessageImpl.java │ │ │ │ ├── HttpResponseInfo.java │ │ │ │ ├── HttpResponseMessage.java │ │ │ │ └── HttpResponseMessageImpl.java │ │ │ └── util/ │ │ │ └── HttpRequestBuilder.java │ │ ├── metrics/ │ │ │ ├── OriginStats.java │ │ │ └── OriginStatsFactory.java │ │ ├── monitoring/ │ │ │ ├── ConnCounter.java │ │ │ ├── ConnTimer.java │ │ │ ├── MonitoringHelper.java │ │ │ ├── Tracer.java │ │ │ └── TracerFactory.java │ │ ├── netty/ │ │ │ ├── ChannelUtils.java │ │ │ ├── NettyRequestAttemptFactory.java │ │ │ ├── RequestCancelledEvent.java │ │ │ ├── SpectatorUtils.java │ │ │ ├── connectionpool/ │ │ │ │ ├── BasicRequestStat.java │ │ │ │ ├── ClientChannelManager.java │ │ │ │ ├── ClientTimeoutHandler.java │ │ │ │ ├── ConnectionPoolConfig.java │ │ │ │ ├── ConnectionPoolConfigImpl.java │ │ │ │ ├── ConnectionPoolHandler.java │ │ │ │ ├── ConnectionPoolMetrics.java │ │ │ │ ├── DefaultClientChannelManager.java │ │ │ │ ├── DefaultOriginChannelInitializer.java │ │ │ │ ├── IConnectionPool.java │ │ │ │ ├── NettyClientConnectionFactory.java │ │ │ │ ├── OriginChannelInitializer.java │ │ │ │ ├── OriginConnectException.java │ │ │ │ ├── PerServerConnectionPool.java │ │ │ │ ├── PooledConnection.java │ │ │ │ ├── PooledConnectionFactory.java │ │ │ │ ├── RequestStat.java │ │ │ │ └── ZuulNettyExceptionMapper.java │ │ │ ├── filter/ │ │ │ │ ├── BaseZuulFilterRunner.java │ │ │ │ ├── EventExecutorScheduler.java │ │ │ │ ├── FilterConstraints.java │ │ │ │ ├── FilterRunner.java │ │ │ │ ├── ZuulEndPointRunner.java │ │ │ │ ├── ZuulFilterChainHandler.java │ │ │ │ └── ZuulFilterChainRunner.java │ │ │ ├── insights/ │ │ │ │ ├── PassportLoggingHandler.java │ │ │ │ ├── PassportStateHttpClientHandler.java │ │ │ │ ├── PassportStateHttpServerHandler.java │ │ │ │ ├── PassportStateListener.java │ │ │ │ ├── PassportStateOriginHandler.java │ │ │ │ └── ServerStateHandler.java │ │ │ ├── ratelimiting/ │ │ │ │ └── NullChannelHandlerProvider.java │ │ │ ├── server/ │ │ │ │ ├── BaseServerStartup.java │ │ │ │ ├── BaseZuulChannelInitializer.java │ │ │ │ ├── ClientConnectionsShutdown.java │ │ │ │ ├── ClientRequestReceiver.java │ │ │ │ ├── ClientResponseWriter.java │ │ │ │ ├── DefaultEventLoopConfig.java │ │ │ │ ├── DirectMemoryMonitor.java │ │ │ │ ├── EventLoopConfig.java │ │ │ │ ├── Http1MutualSslChannelInitializer.java │ │ │ │ ├── ListenerSpec.java │ │ │ │ ├── MethodBinding.java │ │ │ │ ├── NamedSocketAddress.java │ │ │ │ ├── OriginResponseReceiver.java │ │ │ │ ├── Server.java │ │ │ │ ├── ServerTimeout.java │ │ │ │ ├── SocketAddressProperty.java │ │ │ │ ├── ZuulDependencyKeys.java │ │ │ │ ├── ZuulServerChannelInitializer.java │ │ │ │ ├── http2/ │ │ │ │ │ ├── DummyChannelHandler.java │ │ │ │ │ ├── Http2Configuration.java │ │ │ │ │ ├── Http2ConnectionErrorHandler.java │ │ │ │ │ ├── Http2ContentLengthEnforcingHandler.java │ │ │ │ │ ├── Http2OrHttpHandler.java │ │ │ │ │ ├── Http2ResetFrameHandler.java │ │ │ │ │ ├── Http2SslChannelInitializer.java │ │ │ │ │ ├── Http2StreamErrorHandler.java │ │ │ │ │ ├── Http2StreamHeaderCleaner.java │ │ │ │ │ └── Http2StreamInitializer.java │ │ │ │ ├── psk/ │ │ │ │ │ ├── ClientPSKIdentityInfo.java │ │ │ │ │ ├── ExternalTlsPskProvider.java │ │ │ │ │ ├── PskCreationFailureException.java │ │ │ │ │ ├── TlsPskDecoder.java │ │ │ │ │ ├── TlsPskHandler.java │ │ │ │ │ ├── TlsPskServerProtocol.java │ │ │ │ │ ├── TlsPskUtils.java │ │ │ │ │ └── ZuulPskServer.java │ │ │ │ ├── push/ │ │ │ │ │ ├── PushAuthHandler.java │ │ │ │ │ ├── PushChannelInitializer.java │ │ │ │ │ ├── PushClientProtocolHandler.java │ │ │ │ │ ├── PushConnection.java │ │ │ │ │ ├── PushConnectionRegistry.java │ │ │ │ │ ├── PushMessageFactory.java │ │ │ │ │ ├── PushMessageSender.java │ │ │ │ │ ├── PushMessageSenderInitializer.java │ │ │ │ │ ├── PushProtocol.java │ │ │ │ │ ├── PushRegistrationHandler.java │ │ │ │ │ └── PushUserAuth.java │ │ │ │ └── ssl/ │ │ │ │ └── SslHandshakeInfoHandler.java │ │ │ ├── ssl/ │ │ │ │ ├── BaseSslContextFactory.java │ │ │ │ ├── ClientSslContextFactory.java │ │ │ │ └── SslContextFactory.java │ │ │ └── timeouts/ │ │ │ ├── HttpHeadersTimeoutHandler.java │ │ │ └── OriginTimeoutManager.java │ │ ├── niws/ │ │ │ ├── RequestAttempt.java │ │ │ └── RequestAttempts.java │ │ ├── origins/ │ │ │ ├── BasicNettyOrigin.java │ │ │ ├── BasicNettyOriginManager.java │ │ │ ├── InstrumentedOrigin.java │ │ │ ├── NettyOrigin.java │ │ │ ├── Origin.java │ │ │ ├── OriginConcurrencyExceededException.java │ │ │ ├── OriginManager.java │ │ │ ├── OriginName.java │ │ │ └── OriginThrottledException.java │ │ ├── passport/ │ │ │ ├── CurrentPassport.java │ │ │ ├── PassportItem.java │ │ │ ├── PassportState.java │ │ │ └── StartAndEnd.java │ │ ├── plugins/ │ │ │ └── Tracer.java │ │ ├── stats/ │ │ │ ├── AmazonInfoHolder.java │ │ │ ├── BasicRequestMetricsPublisher.java │ │ │ ├── ErrorStatsData.java │ │ │ ├── ErrorStatsManager.java │ │ │ ├── NamedCountingMonitor.java │ │ │ ├── RequestMetricsPublisher.java │ │ │ ├── RouteStatusCodeMonitor.java │ │ │ ├── StatsManager.java │ │ │ ├── monitoring/ │ │ │ │ ├── Monitor.java │ │ │ │ ├── MonitorRegistry.java │ │ │ │ └── NamedCount.java │ │ │ └── status/ │ │ │ ├── StatusCategory.java │ │ │ ├── StatusCategoryGroup.java │ │ │ ├── StatusCategoryUtils.java │ │ │ ├── ZuulStatusCategory.java │ │ │ └── ZuulStatusCategoryGroup.java │ │ └── util/ │ │ ├── Gzipper.java │ │ ├── HttpUtils.java │ │ ├── JsonUtility.java │ │ ├── ProxyUtils.java │ │ └── VipUtils.java │ └── test/ │ └── java/ │ └── com/ │ └── netflix/ │ ├── netty/ │ │ └── common/ │ │ ├── CloseOnIdleStateHandlerTest.java │ │ ├── HttpServerLifecycleChannelHandlerTest.java │ │ ├── SourceAddressChannelHandlerTest.java │ │ ├── metrics/ │ │ │ └── InstrumentedResourceLeakDetectorTest.java │ │ ├── proxyprotocol/ │ │ │ ├── ElbProxyProtocolChannelHandlerTest.java │ │ │ ├── HAProxyMessageChannelHandlerTest.java │ │ │ └── StripUntrustedProxyHeadersHandlerTest.java │ │ ├── ssl/ │ │ │ └── ServerSslConfigTest.java │ │ └── throttle/ │ │ └── MaxInboundConnectionsHandlerTest.java │ └── zuul/ │ ├── AttrsTest.java │ ├── DynamicFilterLoaderTest.java │ ├── StaticFilterLoaderTest.java │ ├── com/ │ │ └── netflix/ │ │ └── zuul/ │ │ └── netty/ │ │ └── server/ │ │ └── push/ │ │ └── PushConnectionTest.java │ ├── context/ │ │ ├── DebugTest.java │ │ └── SessionContextTest.java │ ├── filters/ │ │ ├── BaseFilterTest.java │ │ ├── common/ │ │ │ └── GZipResponseFilterTest.java │ │ └── endpoint/ │ │ └── ProxyEndpointTest.java │ ├── message/ │ │ ├── HeadersTest.java │ │ ├── ZuulMessageImplTest.java │ │ └── http/ │ │ ├── CookiesTest.java │ │ ├── HttpQueryParamsTest.java │ │ ├── HttpRequestMessageImplTest.java │ │ └── HttpResponseMessageImplTest.java │ ├── monitoring/ │ │ ├── ConnCounterTest.java │ │ └── ConnTimerTest.java │ ├── netty/ │ │ ├── NettyRequestAttemptFactoryTest.java │ │ ├── connectionpool/ │ │ │ ├── ClientTimeoutHandlerTest.java │ │ │ ├── ConnectionPoolConfigImplTest.java │ │ │ ├── ConnectionPoolMetricsTest.java │ │ │ ├── DefaultClientChannelManagerTest.java │ │ │ ├── PerServerConnectionPoolTest.java │ │ │ └── PooledConnectionTest.java │ │ ├── filter/ │ │ │ ├── BaseZuulFilterRunnerTest.java │ │ │ ├── EventExecutorSchedulerTest.java │ │ │ ├── FilterConstraintsTest.java │ │ │ ├── ZuulEndPointRunnerTest.java │ │ │ └── ZuulFilterChainRunnerTest.java │ │ ├── insights/ │ │ │ └── ServerStateHandlerTest.java │ │ ├── server/ │ │ │ ├── BaseZuulChannelInitializerTest.java │ │ │ ├── ClientConnectionsShutdownTest.java │ │ │ ├── ClientRequestReceiverTest.java │ │ │ ├── ClientResponseWriterTest.java │ │ │ ├── IoUringTest.java │ │ │ ├── OriginResponseReceiverTest.java │ │ │ ├── ServerTest.java │ │ │ ├── SocketAddressPropertyTest.java │ │ │ ├── http2/ │ │ │ │ ├── Http2ConnectionErrorHandlerTest.java │ │ │ │ ├── Http2ContentLengthEnforcingHandlerTest.java │ │ │ │ └── Http2OrHttpHandlerTest.java │ │ │ ├── push/ │ │ │ │ ├── PushAuthHandlerTest.java │ │ │ │ ├── PushConnectionRegistryTest.java │ │ │ │ ├── PushMessageSenderInitializerTest.java │ │ │ │ └── PushRegistrationHandlerTest.java │ │ │ └── ssl/ │ │ │ └── SslHandshakeInfoHandlerTest.java │ │ ├── ssl/ │ │ │ ├── BaseSslContextFactoryTest.java │ │ │ ├── ClientSslContextFactoryTest.java │ │ │ └── OpenSslTest.java │ │ └── timeouts/ │ │ ├── HttpHeadersTimeoutHandlerTest.java │ │ └── OriginTimeoutManagerTest.java │ ├── niws/ │ │ └── RequestAttemptTest.java │ ├── origins/ │ │ └── OriginNameTest.java │ ├── passport/ │ │ └── CurrentPassportTest.java │ ├── stats/ │ │ ├── ErrorStatsDataTest.java │ │ ├── ErrorStatsManagerTest.java │ │ ├── RouteStatusCodeMonitorTest.java │ │ ├── StatsManagerTest.java │ │ └── status/ │ │ └── ZuulStatusCategoryTest.java │ └── util/ │ ├── HttpUtilsTest.java │ ├── JsonUtilityTest.java │ └── VipUtilsTest.java ├── zuul-discovery/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── netflix/ │ │ └── zuul/ │ │ ├── discovery/ │ │ │ ├── DiscoveryResult.java │ │ │ ├── DynamicServerResolver.java │ │ │ ├── NonDiscoveryServer.java │ │ │ ├── ResolverResult.java │ │ │ └── SimpleMetaInfo.java │ │ └── resolver/ │ │ ├── Resolver.java │ │ └── ResolverListener.java │ └── test/ │ └── java/ │ └── com/ │ └── netflix/ │ └── zuul/ │ └── discovery/ │ ├── DiscoveryResultTest.java │ └── DynamicServerResolverTest.java ├── zuul-integration-test/ │ ├── build.gradle │ └── src/ │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── netflix/ │ │ ├── netty/ │ │ │ └── common/ │ │ │ └── metrics/ │ │ │ └── CustomLeakDetector.java │ │ └── zuul/ │ │ └── integration/ │ │ ├── BaseIntegrationTest.java │ │ ├── MultiEventLoopIntegrationTest.java │ │ ├── SingleEventLoopIntegrationTest.java │ │ ├── ZuulServerExtension.java │ │ └── server/ │ │ ├── Bootstrap.java │ │ ├── HeaderNames.java │ │ ├── OriginServerList.java │ │ ├── ServerStartup.java │ │ ├── TestUtil.java │ │ └── filters/ │ │ ├── BodyUtil.java │ │ ├── CrossThreadBoundaryFilter.java │ │ ├── InboundRoutesFilter.java │ │ ├── NeedsBodyBufferedInboundFilter.java │ │ ├── NeedsBodyBufferedOutboundFilter.java │ │ ├── RequestHeaderFilter.java │ │ └── ResponseHeaderFilter.java │ └── resources/ │ └── log4j2-test.xml ├── zuul-processor/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── netflix/ │ │ │ └── zuul/ │ │ │ └── filters/ │ │ │ └── processor/ │ │ │ └── FilterProcessor.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── gradle/ │ │ │ └── incremental.annotation.processors │ │ └── services/ │ │ └── javax.annotation.processing.Processor │ └── test/ │ └── java/ │ └── com/ │ └── netflix/ │ └── zuul/ │ └── filters/ │ └── processor/ │ ├── FilterProcessorTest.java │ ├── TestFilter.java │ ├── TopLevelFilter.java │ ├── override/ │ │ ├── SubpackageFilter.java │ │ └── package-info.java │ └── subpackage/ │ └── OverrideFilter.java └── zuul-sample/ ├── build.gradle └── src/ └── main/ ├── java/ │ └── com/ │ └── netflix/ │ └── zuul/ │ └── sample/ │ ├── Bootstrap.java │ ├── SampleServerStartup.java │ ├── SampleService.java │ ├── filters/ │ │ ├── Debug.java │ │ ├── endpoint/ │ │ │ └── Healthcheck.java │ │ ├── inbound/ │ │ │ ├── DebugRequest.java │ │ │ ├── Routes.java │ │ │ └── SampleServiceFilter.java │ │ └── outbound/ │ │ └── ZuulResponseFilter.java │ └── push/ │ ├── SamplePushAuthHandler.java │ ├── SamplePushMessageSender.java │ ├── SamplePushMessageSenderInitializer.java │ ├── SamplePushUserAuth.java │ ├── SampleSSEPushChannelInitializer.java │ ├── SampleSSEPushClientProtocolHandler.java │ ├── SampleWebSocketPushChannelInitializer.java │ └── SampleWebSocketPushClientProtocolHandler.java └── resources/ ├── application-benchmark.properties ├── application-test.properties ├── application.properties ├── log4j2.xml └── ssl/ ├── client.cert ├── client.key ├── server.cert ├── server.key ├── truststore.jks └── truststore.key ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # Default reviewers for Zuul OSS * @argha-c @jguerra @gavinbunney @lalernehl @lindseyreynolds @AlexanderEllis @fool1280 @tappenzeller @ilanachalom # Note: exclusions aren't well supported atm. # If needed, use workflows to exclude specific files from review. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gradle" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/benchmark.yml ================================================ name: benchmark on: workflow_dispatch: permissions: contents: read env: JDK: '21' DISTRIBUTION: 'zulu' GRADLE_COMMAND: './gradlew --no-daemon' jobs: benchmark: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up JDK ${{ env.JDK }} uses: actions/setup-java@v5 with: java-version: ${{ env.JDK }} distribution: ${{ env.DISTRIBUTION }} - name: JMH run: ${{ env.GRADLE_COMMAND }} clean :zuul-core:jmh ================================================ FILE: .github/workflows/branch_snapshot.yml ================================================ name: Branch Snapshot on: workflow_dispatch: inputs: branch: description: 'Branch to publish snapshot of' required: true default: 'master' repository: description: 'Repository name (override for forks)' required: false default: Netflix/zuul version: description: 'The version number to use' required: true jobs: build: runs-on: ubuntu-latest environment: Publish steps: - name: Setup Git run: | git config --global user.name 'Zuul Build' git config --global user.email 'zuul-build@netflix.com' - uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.event.inputs.branch }} repository: ${{ github.event.inputs.repository }} - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 cache: 'gradle' - name: Build snapshot run: ./gradlew build snapshot -Prelease.version="$BUILD_VERSION" env: BUILD_VERSION: ${{ github.event.inputs.version }} NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} ================================================ FILE: .github/workflows/gradle-wrapper-validation.yml ================================================ name: "Validate Gradle Wrapper" on: [push, pull_request] jobs: validation: name: "Gradle wrapper validation" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 ================================================ FILE: .github/workflows/pr.yml ================================================ name: PR Build on: [pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: java: [21] steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: ${{ matrix.java }} cache: 'gradle' - name: Build run: | sudo env "PATH=$PATH" bash -c "ulimit -l 65536 && ulimit -a && ./gradlew --no-daemon build" echo "Status of build: $?" validation: name: "Gradle Validation" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - v[0-9]+.[0-9]+.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+ jobs: build: runs-on: ubuntu-latest environment: Publish steps: - name: Setup Git run: | git config --global user.name 'Zuul Build' git config --global user.email 'zuul-build@netflix.com' - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 cache: 'gradle' - name: Build candidate if: contains(github.ref, '-rc.') run: ./gradlew --info --stacktrace -Prelease.useLastTag=true candidate env: NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} - name: Build release if: (!contains(github.ref, '-rc.')) run: ./gradlew --info -Prelease.useLastTag=true final env: NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} ================================================ FILE: .github/workflows/snapshot.yml ================================================ name: Snapshot on: push: branches: - master - zuul-v4 jobs: build: runs-on: ubuntu-latest environment: Publish steps: - name: Setup Git run: | git config --global user.name 'Zuul Build' git config --global user.email 'zuul-build@netflix.com' - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 cache: 'gradle' - name: Build snapshot run: ./gradlew build snapshot env: NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} ================================================ FILE: .github/workflows/stale.yml ================================================ name: 'Close stale issues and PRs' on: schedule: - cron: "*/10 5 * * *" jobs: stale: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v10 with: stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.' stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.' close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.' ================================================ FILE: .gitignore ================================================ # Compiled source # ################### *.com *.class *.dll *.exe *.o *.so # Packages # ############ # it's better to unpack these files and commit the raw source # git has its own built in compression methods *.7z *.dmg *.gz *.iso *.jar *.rar *.tar *.zip # Logs and databases # ###################### *.log # OS generated files # ###################### .DS_Store* ehthumbs.db Icon? Thumbs.db # Editor Files # ################ *~ *.swp # Gradle Files # ################ .gradle # Build output directies /target */target /build */build .m2 /classes # IntelliJ specific files/directories out .idea *.ipr *.iws *.iml atlassian-ide-plugin.xml # Visual Studio Code .vscode # Java heap profile *.hprof # Eclipse specific files/directories .classpath .project .settings .metadata zuul-sample/src/main/generated/ # NetBeans specific files/directories .nbattrs # publishing secrets secrets/signing-key ================================================ FILE: .netflixoss ================================================ jdk=8 ================================================ FILE: CHANGELOG.md ================================================ ================================================ FILE: LICENSE ================================================ 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 2012-2015 Netflix, Inc. 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. ================================================ FILE: OSSMETADATA ================================================ osslifecycle=active ================================================ FILE: README.md ================================================ [![Snapshot](https://github.com/Netflix/zuul/actions/workflows/snapshot.yml/badge.svg)](https://github.com/Netflix/zuul/actions/workflows/snapshot.yml) # Zuul Zuul is an L7 application gateway that provides capabilities for dynamic routing, monitoring, resiliency, security, and more. Please view the wiki for usage, information, HOWTO, etc https://github.com/Netflix/zuul/wiki Here are some links to help you learn more about the Zuul Project. Feel free to PR to add any other info, presentations, etc. --- Articles from Netflix: Zuul 1: http://techblog.netflix.com/2013/06/announcing-zuul-edge-service-in-cloud.html Zuul 2: https://netflixtechblog.com/open-sourcing-zuul-2-82ea476cb2b3 https://netflixtechblog.com/zuul-2-the-netflix-journey-to-asynchronous-non-blocking-systems-45947377fb5c https://netflixtechblog.com/the-show-must-go-on-securing-netflix-studios-at-scale-19b801c86479 --- Netflix presentations about Zuul: Strange Loop 2017 - Zuul 2: https://youtu.be/2oXqbLhMS_A AWS re:Invent 2018 - Scaling push messaging for millions of Netflix devices: https://youtu.be/IdR6N9B-S1E --- Slides from Netflix presentations about Zuul: http://www.slideshare.net/MikeyCohen1/zuul-netflix-springone-platform http://www.slideshare.net/MikeyCohen1/rethinking-cloud-proxies-54923218 https://github.com/strangeloop/StrangeLoop2017/blob/master/slides/ArthurGonigberg-ZuulsJourneyToNonBlocking.pdf https://www.slideshare.net/SusheelAroskar/scaling-push-messaging-for-millions-of-netflix-devices --- Projects Using Zuul: https://cloud.spring.io/ https://jhipster.github.io/ --- Info and examples from various projects: https://cloud.spring.io/spring-cloud-netflix/multi/multi__router_and_filter_zuul http://www.baeldung.com/spring-rest-with-zuul-proxy https://blog.heroku.com/using_netflix_zuul_to_proxy_your_microservices http://blog.ippon.tech/jhipster-3-0-introducing-microservices/ --- Other blog posts about Zuul: https://engineering.riotgames.com/news/riot-games-api-fulfilling-zuuls-destiny https://engineering.riotgames.com/news/riot-games-api-deep-dive http://instea.sk/2015/04/netflix-zuul-vs-nginx-performance/ --- # How to release Zuul This project uses a GitHub Action workflow for publishing a new release. The workflow is triggered by a Git tag. ``` git checkout master git tag vX.Y.Z git push --tags ``` ================================================ FILE: build.gradle ================================================ buildscript { dependencies { classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.83.0' } } plugins { id 'com.netflix.nebula.netflixoss' version '13.0.0' id "com.google.osdetector" version '1.7.3' id 'me.champeau.jmh' version '0.7.2' id 'org.openrewrite.rewrite' version '7.12.1' id 'net.ltgt.errorprone' version '4.1.0' id 'com.diffplug.spotless' version "8.1.0" id 'idea' } ext.githubProjectName = rootProject.name idea { project { languageLevel = '21' } } configurations.all { exclude group: 'asm', module: 'asm' exclude group: 'asm', module: 'asm-all' } allprojects { repositories { mavenCentral() } apply plugin: 'com.diffplug.spotless' spotless { enforceCheck false java { rootProject.hasProperty('spotlessJavaTarget') ? target(rootProject.getProperty('spotlessJavaTarget').split(",")) : target('src/*/java/**/*.java') removeUnusedImports('cleanthat-javaparser-unnecessaryimport') palantirJavaFormat() } } } subprojects { apply plugin: 'com.netflix.nebula.netflixoss' apply plugin: 'java' apply plugin: 'com.netflix.nebula.javadoc-jar' apply plugin: 'com.netflix.nebula.dependency-lock' apply plugin: 'me.champeau.jmh' apply plugin: 'org.openrewrite.rewrite' apply plugin: 'net.ltgt.errorprone' license { ignoreFailures = false excludes([ "**/META-INF/services/javax.annotation.processing.Processor", "**/META-INF/gradle/incremental.annotation.processors", "**/*.cert", "**/*.jks", "**/*.key", ]) } group = "com.netflix.${githubProjectName}" java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } tasks.withType(JavaCompile).configureEach { dependencies { errorprone "com.uber.nullaway:nullaway:0.12.4" errorprone "com.google.errorprone:error_prone_core:2.45.0" } options.compilerArgs << "-Werror" options.errorprone { check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.OFF) option("NullAway:AnnotatedPackages", "com.netflix.zuul") errorproneArgs.addAll( // Uncomment and remove -Werror javac flag to automatically apply fixes for a check. // N.B: disables all other checks while enabled. // "-XepPatchChecks:UnnecessaryParentheses", // "-XepPatchLocation:IN_PLACE", "-Xep:ClassCanBeStatic:OFF", "-Xep:EmptyBlockTag:OFF", "-Xep:FutureReturnValueIgnored:OFF", "-Xep:InlineMeSuggester:OFF", "-Xep:MissingSummary:OFF", ) } } eclipse { classpath { downloadSources = true downloadJavadoc = true } } tasks.withType(Javadoc).each { it.classpath = sourceSets.main.compileClasspath // Ignore Javadoc warnings for now, re-enable after Zuul 3. it.options.addStringOption('Xdoclint:none', '-quiet') } ext { libraries = [ guava: "com.google.guava:guava:33.3.0-jre", okhttp: 'com.squareup.okhttp3:okhttp:4.12.0', jupiterApi: 'org.junit.jupiter:junit-jupiter-api:5.13.+', jupiterParams: 'org.junit.jupiter:junit-jupiter-params:5.13.+', jupiterEngine: 'org.junit.jupiter:junit-jupiter-engine:5.13.+', junitPlatformLauncher: 'org.junit.platform:junit-platform-launcher:1.13.+', jupiterMockito: 'org.mockito:mockito-junit-jupiter:5.13.+', mockito: 'org.mockito:mockito-core:5.+', slf4j: "org.slf4j:slf4j-api:2.0.16", assertj: 'org.assertj:assertj-core:3.26.3', awaitility: 'org.awaitility:awaitility:4.2.2', lombok: 'org.projectlombok:lombok:1.18.42' ] } test { useJUnitPlatform() testLogging { showStandardStreams = true } maxParallelForks = Runtime.runtime.availableProcessors(); } } dependencies { rewrite(platform("org.openrewrite.recipe:rewrite-recipe-bom:3.12.1")) rewrite("org.openrewrite.recipe:rewrite-logging-frameworks") rewrite("org.openrewrite.recipe:rewrite-testing-frameworks") rewrite("org.openrewrite.recipe:rewrite-static-analysis") } rewrite { failOnDryRunResults = true activeRecipe("org.openrewrite.java.testing.junit5.JUnit5BestPractices") activeRecipe("org.openrewrite.java.logging.slf4j.Slf4jBestPractices") } ================================================ FILE: codequality/checkstyle.xml ================================================ ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ versions_ribbon=2.4.4 versions_netty=4.2.10.Final versions_brotli4j=1.16.0 release.scope=patch release.version=3.3.0-SNAPSHOT org.gradle.jvmargs=-Xms1g -Xmx2g com.netflix.testcontainers-cloud.enabled=false palantir.native.formatter=true ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # 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 # # https://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. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # 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 # 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 if ! command -v java >/dev/null 2>&1 then 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 fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 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" ) 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 # 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"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # 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" "$@" ================================================ FILE: gradlew.bat ================================================ @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 @rem SPDX-License-Identifier: Apache-2.0 @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=. @rem This is normally unused 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. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :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 ================================================ FILE: settings.gradle ================================================ rootProject.name='zuul' include 'zuul-core' include 'zuul-processor' include 'zuul-sample' include 'zuul-discovery' include 'zuul-integration-test' ================================================ FILE: zuul-core/build.gradle ================================================ apply plugin: "com.google.osdetector" apply plugin: "java-library" dependencies { compileOnly libraries.lombok testCompileOnly(libraries.lombok) annotationProcessor(libraries.lombok) implementation libraries.guava // TODO(carl-mastrangelo): this can be implementation; remove Logger from public api points. api libraries.slf4j implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' implementation 'org.bouncycastle:bctls-jdk18on:1.78.1' implementation 'com.fasterxml.jackson.core:jackson-core:2.19.2' api 'com.fasterxml.jackson.core:jackson-databind:2.19.2' api "com.netflix.archaius:archaius-core:0.7.12" api "com.netflix.spectator:spectator-api:latest.release" api "com.netflix.netflix-commons:netflix-commons-util:0.3.0" api project(":zuul-discovery") api "com.netflix.ribbon:ribbon-core:${versions_ribbon}" api "com.netflix.ribbon:ribbon-archaius:${versions_ribbon}" api "com.netflix.eureka:eureka-client:2.0.4" api "io.reactivex:rxjava:1.3.8" api platform("io.netty:netty-bom:${versions_netty}") // TODO(carl-mastrangelo): some of these could probably be implementation. Do a deeper check. api "io.netty:netty-common" api "io.netty:netty-buffer" api "io.netty:netty-codec-http" api "io.netty:netty-codec-http2" api "io.netty:netty-handler" api "io.netty:netty-transport" implementation "io.netty:netty-codec-haproxy" implementation (group: "io.netty", "name": "netty-transport-native-epoll", "classifier": "linux-x86_64") implementation (group: "io.netty", "name": "netty-transport-native-io_uring", "classifier": "linux-x86_64") implementation (group: "io.netty", "name": "netty-transport-native-kqueue", "classifier": "osx-x86_64") // We are using the long-form dependency syntax here because we want to // explicitly set the classifier. We do not have the version number so we can't use // Gradle's short-form dependency notation. runtimeOnly( group: "io.netty", name: "netty-tcnative-boringssl-static", classifier: "linux-x86_64" ) runtimeOnly( group: "io.netty", name: "netty-tcnative-boringssl-static", classifier: "linux-aarch_64" ) runtimeOnly( group: "io.netty", name: "netty-tcnative-boringssl-static", classifier: "osx-x86_64" ) runtimeOnly( group: "io.netty", name: "netty-tcnative-boringssl-static", classifier: "osx-aarch_64" ) implementation 'io.perfmark:perfmark-api:0.27.0' api 'jakarta.inject:jakarta.inject-api:2.0.1' api 'org.jspecify:jspecify:1.0.0' testImplementation libraries.jupiterApi, libraries.jupiterParams, libraries.jupiterEngine, libraries.junitPlatformLauncher, libraries.jupiterMockito, libraries.mockito, libraries.assertj, libraries.awaitility testImplementation 'commons-configuration:commons-configuration:1.10' testRuntimeOnly 'org.slf4j:slf4j-simple:2.0.17' jmh 'org.openjdk.jmh:jmh-core:1.+' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.+' jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.+' } // Silences log statements during tests. This still allows normal failures to be printed. test { testLogging { showStandardStreams = false } } // ./gradlew --no-daemon clean :zuul-core:jmh jmh { profilers = ["gc"] timeOnIteration = "1s" warmup = "1s" fork = 1 warmupIterations = 10 iterations = 5 // Not sure why duplicate classes are on the path. Something Nebula related I think. duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE } ================================================ FILE: zuul-core/src/jmh/java/com/netflix/zuul/message/HeadersBenchmark.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import java.util.List; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; @State(Scope.Thread) public class HeadersBenchmark { @State(Scope.Thread) public static class AddHeaders { @Param({"0", "1", "5", "10", "30"}) public int count; @Param({"10"}) public int nameLength; private String[] stringNames; private HeaderName[] names; private String[] values; @Setup public void setUp() { stringNames = new String[count]; names = new HeaderName[stringNames.length]; values = new String[stringNames.length]; for (int i = 0; i < stringNames.length; i++) { UUID uuid = new UUID( ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()); String name = uuid.toString(); assert name.length() >= nameLength; name = name.substring(0, nameLength); names[i] = new HeaderName(name); stringNames[i] = name; values[i] = name; } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public Headers addHeaders_string() { Headers headers = new Headers(); for (int i = 0; i < count; i++) { headers.add(stringNames[i], values[i]); } return headers; } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public Headers addHeaders_headerName() { Headers headers = new Headers(); for (int i = 0; i < count; i++) { headers.add(names[i], values[i]); } return headers; } } @State(Scope.Thread) public static class GetSetHeaders { @Param({"1", "5", "10", "30"}) public int count; @Param({"10"}) public int nameLength; private String[] stringNames; private HeaderName[] names; private String[] values; Headers headers; @Setup public void setUp() { headers = new Headers(); stringNames = new String[count]; names = new HeaderName[stringNames.length]; values = new String[stringNames.length]; for (int i = 0; i < stringNames.length; i++) { UUID uuid = new UUID( ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()); String name = uuid.toString(); assert name.length() >= nameLength; name = name.substring(0, nameLength); names[i] = new HeaderName(name); stringNames[i] = name; values[i] = name; headers.add(names[i], values[i]); } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public void setHeader_first() { headers.set(names[0], "blah"); } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public void setHeader_last() { headers.set(names[count - 1], "blah"); } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public List getHeader_first() { return headers.getAll(names[0]); } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public List getHeader_last() { return headers.getAll(names[count - 1]); } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public void entries(Blackhole blackhole) { for (Header header : headers.entries()) { blackhole.consume(header); } } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public Headers newHeaders() { return new Headers(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/config/DynamicIntegerSetProperty.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.config; import java.util.Set; public class DynamicIntegerSetProperty extends DynamicSetProperty { public DynamicIntegerSetProperty(String propName, String defaultValue) { super(propName, defaultValue); } public DynamicIntegerSetProperty(String propName, String defaultValue, String delimiterRegex) { super(propName, defaultValue, delimiterRegex); } public DynamicIntegerSetProperty(String propName, Set defaultValue) { super(propName, defaultValue); } public DynamicIntegerSetProperty(String propName, Set defaultValue, String delimiterRegex) { super(propName, defaultValue, delimiterRegex); } @Override protected Integer from(String value) { return Integer.valueOf(value); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/config/PatternListStringProperty.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.config; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 5/15/17 * Time: 4:38 PM */ public class PatternListStringProperty extends DerivedStringProperty> { private static final Logger LOG = LoggerFactory.getLogger(PatternListStringProperty.class); public PatternListStringProperty(String name, String defaultValue) { super(name, defaultValue); } @Override protected List derive(String value) { ArrayList ptns = new ArrayList<>(); if (value != null) { for (String ptnTxt : value.split(",", -1)) { try { ptns.add(Pattern.compile(ptnTxt.trim())); } catch (Exception e) { LOG.error( "Error parsing regex pattern list from property! name = {}, value = {}, pattern = {}", String.valueOf(this.getName()), String.valueOf(this.getValue()), String.valueOf(value)); } } } return ptns; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/AbstrHttpConnectionExpiryHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.config.CachedDynamicLongProperty; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.util.HttpUtils; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import java.util.concurrent.ThreadLocalRandom; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 7/17/17 * Time: 10:54 AM */ public abstract class AbstrHttpConnectionExpiryHandler extends ChannelOutboundHandlerAdapter { protected static final Logger LOG = LoggerFactory.getLogger(AbstrHttpConnectionExpiryHandler.class); protected static final CachedDynamicLongProperty MAX_EXPIRY_DELTA = new CachedDynamicLongProperty("server.connection.expiry.delta", 20 * 1000); protected final ConnectionCloseType connectionCloseType; protected final int maxRequests; protected final int maxExpiry; protected final long connectionStartTime; protected final long connectionExpiryTime; protected int requestCount = 0; protected int maxRequestsUnderBrownout = 0; public AbstrHttpConnectionExpiryHandler( ConnectionCloseType connectionCloseType, int maxRequestsUnderBrownout, int maxRequests, int maxExpiry) { this.connectionCloseType = connectionCloseType; this.maxRequestsUnderBrownout = maxRequestsUnderBrownout; this.maxRequests = maxRequests; this.maxExpiry = maxExpiry; this.connectionStartTime = System.currentTimeMillis(); long randomDelta = ThreadLocalRandom.current().nextLong(MAX_EXPIRY_DELTA.get()); this.connectionExpiryTime = connectionStartTime + maxExpiry + randomDelta; } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (isResponseHeaders(msg)) { // Update the request count attribute for this channel. requestCount++; if (isConnectionExpired(ctx.channel())) { // Flag this channel to be closed after response is written. Channel channel = HttpUtils.getMainChannel(ctx); ctx.channel() .attr(ConnectionCloseChannelAttributes.CLOSE_AFTER_RESPONSE) .set(ctx.newPromise()); ConnectionCloseType.setForChannel(channel, connectionCloseType); } } super.write(ctx, msg, promise); } protected boolean isConnectionExpired(Channel channel) { boolean expired = requestCount >= maxRequests(channel) || System.currentTimeMillis() > connectionExpiryTime; if (expired) { long lifetime = System.currentTimeMillis() - connectionStartTime; LOG.info( "Connection is expired. requestCount={}, lifetime={}, {}", requestCount, lifetime, ChannelUtils.channelInfoForLogging(channel)); } return expired; } protected abstract boolean isResponseHeaders(Object msg); protected int maxRequests(Channel ch) { if (HttpChannelFlags.IN_BROWNOUT.get(ch)) { return this.maxRequestsUnderBrownout; } else { return this.maxRequests; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/ByteBufUtil.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.zuul.message.ZuulMessage; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.ReferenceCounted; import io.netty.util.ResourceLeakDetector; /** * ByteBufUtil * * @author Arthur Gonigberg * @since October 20, 2022 */ public class ByteBufUtil { @SuppressWarnings("EnumOrdinal") private static final boolean isAdvancedLeakDetection = ResourceLeakDetector.getLevel().ordinal() >= ResourceLeakDetector.Level.ADVANCED.ordinal(); public static void touch(ReferenceCounted byteBuf, String hint, ZuulMessage msg) { if (isAdvancedLeakDetection) { byteBuf.touch(hint + msg); } } public static void touch(ReferenceCounted byteBuf, String hint) { if (isAdvancedLeakDetection) { byteBuf.touch(hint); } } public static void touch(ReferenceCounted byteBuf, String hint, String filterName) { if (isAdvancedLeakDetection) { byteBuf.touch(hint + filterName); } } public static void touch(HttpResponse originResponse, String hint, ZuulMessage msg) { if (isAdvancedLeakDetection && originResponse instanceof ReferenceCounted) { ((ReferenceCounted) originResponse).touch(hint + msg); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/CategorizedThreadFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.util.concurrent.FastThreadLocalThread; import java.util.concurrent.ThreadFactory; /** * User: Mike Smith * Date: 6/8/16 * Time: 11:49 AM */ public class CategorizedThreadFactory implements ThreadFactory { private final String category; private int num = 0; public CategorizedThreadFactory(String category) { super(); this.category = category; } @Override public Thread newThread(Runnable r) { FastThreadLocalThread t = new FastThreadLocalThread(r, category + "-" + num++); return t; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/CloseOnIdleStateHandler.java ================================================ /** * Copyright 2018 Netflix, Inc. *

* 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. */ package com.netflix.netty.common; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleStateEvent; /** * Just listens for the IdleStateEvent and closes the channel if received. */ public class CloseOnIdleStateHandler extends ChannelInboundHandlerAdapter { private final Counter counter; public CloseOnIdleStateHandler(Registry registry, String metricId) { this.counter = registry.counter("server.connections.idle.timeout", "id", metricId); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { super.userEventTriggered(ctx, evt); if (evt instanceof IdleStateEvent) { counter.increment(); ctx.close(); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/ConnectionCloseChannelAttributes.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import io.netty.channel.Channel; import io.netty.channel.ChannelPromise; import io.netty.util.AttributeKey; public class ConnectionCloseChannelAttributes { public static final AttributeKey CLOSE_AFTER_RESPONSE = AttributeKey.newInstance("CLOSE_AFTER_RESPONSE"); public static final AttributeKey CLOSE_TYPE = AttributeKey.newInstance("CLOSE_TYPE"); public static int gracefulCloseDelay(Channel channel) { ChannelConfig channelConfig = channel.attr(BaseZuulChannelInitializer.ATTR_CHANNEL_CONFIG).get(); Integer gracefulCloseDelay = channelConfig.get(CommonChannelConfigKeys.connCloseDelay); return gracefulCloseDelay == null ? 0 : gracefulCloseDelay; } public static boolean allowGracefulDelayed(Channel channel) { ChannelConfig channelConfig = channel.attr(BaseZuulChannelInitializer.ATTR_CHANNEL_CONFIG).get(); Boolean value = channelConfig.get(CommonChannelConfigKeys.http2AllowGracefulDelayed); return value == null ? false : value; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/ConnectionCloseType.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.channel.Channel; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 2:04 PM */ public enum ConnectionCloseType { IMMEDIATE, GRACEFUL, DELAYED_GRACEFUL; public static ConnectionCloseType fromChannel(Channel ch) { ConnectionCloseType type = ch.attr(ConnectionCloseChannelAttributes.CLOSE_TYPE).get(); if (type == null) { // Default to immediate. type = ConnectionCloseType.IMMEDIATE; } return type; } public static void setForChannel(Channel ch, ConnectionCloseType type) { ch.attr(ConnectionCloseChannelAttributes.CLOSE_TYPE).set(type); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/Http1ConnectionCloseHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 2:03 PM */ public class Http1ConnectionCloseHandler extends ChannelDuplexHandler { private static final Logger LOG = LoggerFactory.getLogger(Http1ConnectionCloseHandler.class); private final AtomicBoolean requestInflight = new AtomicBoolean(Boolean.FALSE); @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ChannelPromise closePromise = ctx.channel() .attr(ConnectionCloseChannelAttributes.CLOSE_AFTER_RESPONSE) .get(); if (msg instanceof HttpResponse response && closePromise != null) { // Add header to tell client that they should close this connection. response.headers().set(HttpHeaderNames.CONNECTION, "close"); } super.write(ctx, msg, promise); // Close the connection immediately after LastContent is written, rather than // waiting until the graceful-delay is up if this flag is set. if (msg instanceof LastHttpContent) { if (closePromise != null) { promise.addListener(future -> { ConnectionCloseType type = ConnectionCloseType.fromChannel(ctx.channel()); closeChannel(ctx, type, closePromise); }); } } } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // Track when there's an inflight request. if (evt instanceof HttpLifecycleChannelHandler.StartEvent) { requestInflight.set(Boolean.TRUE); } else if (evt instanceof HttpLifecycleChannelHandler.CompleteEvent) { requestInflight.set(Boolean.FALSE); } super.userEventTriggered(ctx, evt); } protected void closeChannel(ChannelHandlerContext ctx, ConnectionCloseType evt, ChannelPromise promise) { switch (evt) { case DELAYED_GRACEFUL: gracefully(ctx, promise); break; case GRACEFUL: gracefully(ctx, promise); break; case IMMEDIATE: immediately(ctx, promise); break; default: throw new IllegalArgumentException("Unknown ConnectionCloseEvent type! - " + String.valueOf(evt)); } } protected void gracefully(ChannelHandlerContext ctx, ChannelPromise promise) { Channel channel = ctx.channel(); if (channel.isActive()) { String channelId = channel.id().asShortText(); // In gracefulCloseDelay secs time, go ahead and close the connection if it hasn't already been. int gracefulCloseDelay = ConnectionCloseChannelAttributes.gracefulCloseDelay(channel); ctx.executor() .schedule( () -> { // Check that the client hasn't already closed the connection. if (channel.isActive()) { // If there is still an inflight request, then don't close the conn now. Instead // assume that it will be closed // either after the response finally gets written (due to us having set the // CLOSE_AFTER_RESPONSE flag), or when the IdleTimeout // for this conn fires. if (requestInflight.get()) { LOG.debug( "gracefully: firing graceful_shutdown event to close connection, but" + " request still inflight, so leaving. channel={}", channelId); } else { LOG.debug( "gracefully: firing graceful_shutdown event to close connection." + " channel={}", channelId); ctx.close(promise); } } else { LOG.debug("gracefully: connection already closed. channel={}", channelId); promise.setSuccess(); } }, gracefulCloseDelay, TimeUnit.SECONDS); } else { promise.setSuccess(); } } protected void immediately(ChannelHandlerContext ctx, ChannelPromise promise) { if (ctx.channel().isActive()) { ctx.close(promise); } else { promise.setSuccess(); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/Http1ConnectionExpiryHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.handler.codec.http.HttpResponse; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 9:58 AM */ public class Http1ConnectionExpiryHandler extends AbstrHttpConnectionExpiryHandler { public Http1ConnectionExpiryHandler(int maxRequests, int maxRequestsUnderBrownout, int maxExpiry) { super(ConnectionCloseType.GRACEFUL, maxRequestsUnderBrownout, maxRequests, maxExpiry); } @Override protected boolean isResponseHeaders(Object msg) { return msg instanceof HttpResponse; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/Http2ConnectionCloseHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.zuul.util.HttpUtils; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.DelegatingChannelPromiseNotifier; import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; import io.netty.handler.codec.http2.Http2DataFrame; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2HeadersFrame; import io.netty.util.concurrent.EventExecutor; import jakarta.inject.Inject; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 2:03 PM */ @ChannelHandler.Sharable public class Http2ConnectionCloseHandler extends ChannelDuplexHandler { private static final Logger LOG = LoggerFactory.getLogger(Http2ConnectionCloseHandler.class); private final Registry registry; private final Id counterBaseId; @Inject public Http2ConnectionCloseHandler(Registry registry) { super(); this.registry = registry; this.counterBaseId = registry.createId("server.connection.close.handled"); } private void incrementCounter(ConnectionCloseType closeType, int port) { registry.counter(counterBaseId .withTag("close_type", closeType.name()) .withTag("port", Integer.toString(port)) .withTag("protocol", "http2")) .increment(); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { // Close the connection immediately after LastContent is written, rather than // waiting until the graceful-delay is up if this flag is set. if (isEndOfRequestResponse(msg)) { Channel parent = HttpUtils.getMainChannel(ctx); ChannelPromise closeAfterPromise = shouldCloseAfter(ctx, parent); if (closeAfterPromise != null) { // Add listener to close the channel AFTER response has been sent. promise.addListener(future -> { // Close the parent (tcp connection) channel. closeChannel(ctx, closeAfterPromise); }); } } super.write(ctx, msg, promise); } /** * Look on both the stream channel, and the parent channel to see if the CLOSE_AFTER_RESPONSE flag has been set. * If so, return that promise. * * @param ctx * @param parent * @return */ private ChannelPromise shouldCloseAfter(ChannelHandlerContext ctx, Channel parent) { ChannelPromise closeAfterPromise = ctx.channel() .attr(ConnectionCloseChannelAttributes.CLOSE_AFTER_RESPONSE) .get(); if (closeAfterPromise == null) { closeAfterPromise = parent.attr(ConnectionCloseChannelAttributes.CLOSE_AFTER_RESPONSE) .get(); } return closeAfterPromise; } private boolean isEndOfRequestResponse(Object msg) { if (msg instanceof Http2HeadersFrame) { return ((Http2HeadersFrame) msg).isEndStream(); } if (msg instanceof Http2DataFrame) { return ((Http2DataFrame) msg).isEndStream(); } return false; } private void closeChannel(ChannelHandlerContext ctx, ChannelPromise promise) { Channel child = ctx.channel(); Channel parent = HttpUtils.getMainChannel(ctx); // 1. Check if already_closing flag on this stream channel. If there is, then success this promise and return. // If not, then add already_closing flag to this stream channel. // 2. Check if already_closing flag on the parent channel. // If so, then just return. // If not, then set already_closing on parent channel, and then allow through. if (isAlreadyClosing(child)) { promise.setSuccess(); return; } if (isAlreadyClosing(parent)) { return; } // Close according to the specified close type. ConnectionCloseType closeType = ConnectionCloseType.fromChannel(parent); Integer port = parent.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).get(); port = port == null ? -1 : port; incrementCounter(closeType, port); switch (closeType) { case DELAYED_GRACEFUL: gracefullyWithDelay(ctx.executor(), parent, promise); break; case GRACEFUL: case IMMEDIATE: immediate(parent, promise); break; default: throw new IllegalArgumentException("Unknown ConnectionCloseEvent type! - " + closeType); } } /** * WARNING: Found the OkHttp client gets confused by this behaviour (it ends up putting itself in a bad shutdown state * after receiving the first goaway frame, and then dropping any inflight responses but also timing out waiting for them). * * And worried that other http/2 stacks may be similar, so for now we should NOT use this. * * This is unfortunate, as FTL wanted this, and it is correct according to the spec. * * See this code in okhttp where it drops response header frame if state is already shutdown: * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/http2/Http2Connection.java#L609 */ private void gracefullyWithDelay(EventExecutor executor, Channel parent, ChannelPromise promise) { // See javadoc for explanation of why this may be disabled. boolean allowGracefulDelayed = ConnectionCloseChannelAttributes.allowGracefulDelayed(parent); if (!allowGracefulDelayed) { immediate(parent, promise); return; } if (!parent.isActive()) { promise.setSuccess(); return; } // First send a 'graceful shutdown' GOAWAY frame. /* "A server that is attempting to gracefully shut down a connection SHOULD send an initial GOAWAY frame with the last stream identifier set to 231-1 and a NO_ERROR code. This signals to the client that a shutdown is imminent and that initiating further requests is prohibited." -- https://http2.github.io/http2-spec/#GOAWAY */ DefaultHttp2GoAwayFrame goaway = new DefaultHttp2GoAwayFrame(Http2Error.NO_ERROR); goaway.setExtraStreamIds(Integer.MAX_VALUE); parent.writeAndFlush(goaway); LOG.debug( "gracefullyWithDelay: flushed initial go_away frame. channel={}", parent.id().asShortText()); // In N secs time, throw an error that causes the http2 codec to send another GOAWAY frame // (this time with accurate lastStreamId) and then close the connection. int gracefulCloseDelay = ConnectionCloseChannelAttributes.gracefulCloseDelay(parent); executor.schedule( () -> { // Check that the client hasn't already closed the connection (due to the earlier goaway we sent). if (parent.isActive()) { // NOTE - the netty Http2ConnectionHandler specifically does not send another goaway when we // call // channel.close() if one has already been sent .... so when we want more than one sent, we need // to do it // explicitly ourselves like this. LOG.debug( "gracefullyWithDelay: firing graceful_shutdown event to make netty send a final" + " go_away frame and then close connection. channel={}", parent.id().asShortText()); Http2Exception h2e = new Http2Exception(Http2Error.NO_ERROR, Http2Exception.ShutdownHint.GRACEFUL_SHUTDOWN); parent.pipeline().fireExceptionCaught(h2e); parent.close().addListener(future -> { promise.setSuccess(); }); } else { promise.setSuccess(); } }, gracefulCloseDelay, TimeUnit.SECONDS); } private void immediate(Channel parent, ChannelPromise promise) { if (parent.isActive()) { parent.close().addListener(new DelegatingChannelPromiseNotifier(promise)); } else { promise.setSuccess(); } } protected boolean isAlreadyClosing(Channel parentChannel) { // If already closing, then just return. // This will happen because close() is called a 2nd time after sending the goaway frame. if (HttpChannelFlags.CLOSING.get(parentChannel)) { return true; } else { HttpChannelFlags.CLOSING.set(parentChannel); return false; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/Http2ConnectionExpiryHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.channel.ChannelHandler; import io.netty.handler.codec.http2.Http2HeadersFrame; /** * This needs to be inserted in the pipeline after the Http2 Codex, but before any h2->h1 conversion. * * User: michaels@netflix.com * Date: 2/8/17 * Time: 9:58 AM */ @ChannelHandler.Sharable public class Http2ConnectionExpiryHandler extends AbstrHttpConnectionExpiryHandler { public Http2ConnectionExpiryHandler(int maxRequests, int maxRequestsUnderBrownout, int maxExpiry) { super(ConnectionCloseType.DELAYED_GRACEFUL, maxRequestsUnderBrownout, maxRequests, maxExpiry); } @Override protected boolean isResponseHeaders(Object msg) { return msg instanceof Http2HeadersFrame; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/HttpChannelFlags.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.util.Attribute; import io.netty.util.AttributeKey; /** * User: michaels@netflix.com * Date: 7/10/17 * Time: 4:29 PM */ public class HttpChannelFlags { public static final Flag IN_BROWNOUT = new Flag("_brownout"); public static final Flag CLOSING = new Flag("_connection_closing"); public static class Flag { private final AttributeKey attributeKey; public Flag(String name) { attributeKey = AttributeKey.newInstance(name); } public void set(Channel ch) { ch.attr(attributeKey).set(Boolean.TRUE); } public void set(ChannelHandlerContext ctx) { set(ctx.channel()); } public void remove(Channel ch) { ch.attr(attributeKey).set(null); } public boolean get(Channel ch) { Attribute attr = ch.attr(attributeKey); Boolean value = attr.get(); return (value == null) ? false : value; } public boolean get(ChannelHandlerContext ctx) { return get(ctx.channel()); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/HttpClientLifecycleChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; /** * @author michaels */ public class HttpClientLifecycleChannelHandler extends HttpLifecycleChannelHandler { public static final ChannelHandler INBOUND_CHANNEL_HANDLER = new HttpClientLifecycleInboundChannelHandler(); public static final ChannelHandler OUTBOUND_CHANNEL_HANDLER = new HttpClientLifecycleOutboundChannelHandler(); @ChannelHandler.Sharable private static class HttpClientLifecycleInboundChannelHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpResponse) { ctx.channel().attr(ATTR_HTTP_RESP).set((HttpResponse) msg); } try { super.channelRead(ctx, msg); } finally { if (msg instanceof LastHttpContent) { fireCompleteEventIfNotAlready(ctx, CompleteReason.SESSION_COMPLETE); } } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { try { super.channelInactive(ctx); } finally { fireCompleteEventIfNotAlready(ctx, CompleteReason.INACTIVE); } } } @ChannelHandler.Sharable private static class HttpClientLifecycleOutboundChannelHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof HttpRequest) { fireStartEvent(ctx, (HttpRequest) msg); } super.write(ctx, msg, promise); } @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { fireCompleteEventIfNotAlready(ctx, CompleteReason.DISCONNECT); super.disconnect(ctx, promise); } @Override public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { fireCompleteEventIfNotAlready(ctx, CompleteReason.DEREGISTER); super.deregister(ctx, promise); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); fireCompleteEventIfNotAlready(ctx, CompleteReason.EXCEPTION); } @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { fireCompleteEventIfNotAlready(ctx, CompleteReason.CLOSE); super.close(ctx, promise); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/HttpLifecycleChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.google.common.annotations.VisibleForTesting; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.Attribute; import io.netty.util.AttributeKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 5/24/16 * Time: 4:09 PM */ public abstract class HttpLifecycleChannelHandler { private static final Logger logger = LoggerFactory.getLogger(HttpLifecycleChannelHandler.class); public static final AttributeKey ATTR_HTTP_REQ = AttributeKey.newInstance("_http_request"); public static final AttributeKey ATTR_HTTP_RESP = AttributeKey.newInstance("_http_response"); public static final AttributeKey ATTR_HTTP_PIPELINE_REJECT = AttributeKey.newInstance("_http_pipeline_reject"); protected enum State { STARTED, COMPLETED } @VisibleForTesting protected static final AttributeKey ATTR_STATE = AttributeKey.newInstance("_httplifecycle_state"); protected static boolean fireStartEvent(ChannelHandlerContext ctx, HttpRequest request) { // Only allow this method to run once per request. Channel channel = ctx.channel(); Attribute attr = channel.attr(ATTR_STATE); State state = attr.get(); if (state == State.STARTED) { // This could potentially happen if a bad client sends a 2nd request on the same connection // without waiting for the response from the first. And we don't support HTTP Pipelining. logger.debug( "Received a http request on connection where we already have a request being processed. Closing" + " the connection now. channel = {}", channel.id().asLongText()); channel.attr(ATTR_HTTP_PIPELINE_REJECT).set(Boolean.TRUE); channel.close(); return false; } channel.attr(ATTR_STATE).set(State.STARTED); channel.attr(ATTR_HTTP_REQ).set(request); ctx.pipeline().fireUserEventTriggered(new StartEvent(request)); return true; } protected static boolean fireCompleteEventIfNotAlready(ChannelHandlerContext ctx, CompleteReason reason) { // Only allow this method to run once per request. Attribute attr = ctx.channel().attr(ATTR_STATE); State state = attr.get(); if (state == null || state != State.STARTED) { return false; } attr.set(State.COMPLETED); HttpRequest request = ctx.channel().attr(ATTR_HTTP_REQ).get(); HttpResponse response = ctx.channel().attr(ATTR_HTTP_RESP).get(); // Cleanup channel attributes. ctx.channel().attr(ATTR_HTTP_REQ).set(null); ctx.channel().attr(ATTR_HTTP_RESP).set(null); // Fire the event to whole pipeline. ctx.pipeline().fireUserEventTriggered(new CompleteEvent(reason, request, response)); return true; } protected static void addPassportState(ChannelHandlerContext ctx, PassportState state) { CurrentPassport passport = CurrentPassport.fromChannel(ctx.channel()); passport.add(state); } public enum CompleteReason { SESSION_COMPLETE, INACTIVE, // IDLE, DISCONNECT, DEREGISTER, PIPELINE_REJECT, EXCEPTION, CLOSE // FAILURE_CLIENT_CANCELLED, // FAILURE_CLIENT_TIMEOUT; // private final NfStatus nfStatus; // private final int responseStatus; // // CompleteReason(NfStatus nfStatus, int responseStatus) { // this.nfStatus = nfStatus; // this.responseStatus = responseStatus; // } // // CompleteReason() { // //For status that never gets returned back to client, like channel inactive // nfStatus = null; // responseStatus = 501; // } // // public NfStatus getNfStatus() { // return nfStatus; // } // // public int getResponseStatus() { // return responseStatus; // } } public static class StartEvent { private final HttpRequest request; public StartEvent(HttpRequest request) { this.request = request; } public HttpRequest getRequest() { return request; } } public static class CompleteEvent { private final CompleteReason reason; private final HttpRequest request; private final HttpResponse response; public CompleteEvent(CompleteReason reason, HttpRequest request, HttpResponse response) { this.reason = reason; this.request = request; this.response = response; } public CompleteReason getReason() { return reason; } public HttpRequest getRequest() { return request; } public HttpResponse getResponse() { return response; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/HttpRequestReadTimeoutEvent.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; /** * Indicates a timeout in reading the full http request. * * ie. time between receiving request headers and LastHttpContent of request body. */ public class HttpRequestReadTimeoutEvent { public static final HttpRequestReadTimeoutEvent INSTANCE = new HttpRequestReadTimeoutEvent(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/HttpRequestReadTimeoutHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.spectator.api.Counter; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.timeout.ReadTimeoutHandler; import java.util.concurrent.TimeUnit; /** * This handler times from the point a HttpRequest is read until the LastHttpContent is read, * and fires a HttpRequestTimeoutEvent if that time has exceed the configured timeout. * * Unlike ReadTimeoutHandler, this impl does NOT close the channel on a timeout. Only fires the * event. * * @author michaels */ public class HttpRequestReadTimeoutHandler extends ChannelInboundHandlerAdapter { private static final String HANDLER_NAME = "http_request_read_timeout_handler"; private static final String INTERNAL_HANDLER_NAME = "http_request_read_timeout_internal"; private final long timeout; private final TimeUnit unit; private final Counter httpRequestReadTimeoutCounter; protected HttpRequestReadTimeoutHandler(long timeout, TimeUnit unit, Counter httpRequestReadTimeoutCounter) { this.timeout = timeout; this.unit = unit; this.httpRequestReadTimeoutCounter = httpRequestReadTimeoutCounter; } /** * Factory which ensures that this handler is added to the pipeline using the * correct name. */ public static void addLast( ChannelPipeline pipeline, long timeout, TimeUnit unit, Counter httpRequestReadTimeoutCounter) { HttpRequestReadTimeoutHandler handler = new HttpRequestReadTimeoutHandler(timeout, unit, httpRequestReadTimeoutCounter); pipeline.addLast(HANDLER_NAME, handler); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof LastHttpContent) { removeInternalHandler(ctx); } else if (msg instanceof HttpRequest) { // Start timeout handler. InternalReadTimeoutHandler handler = new InternalReadTimeoutHandler(timeout, unit); ctx.pipeline().addBefore(HANDLER_NAME, INTERNAL_HANDLER_NAME, handler); } super.channelRead(ctx, msg); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof HttpRequestReadTimeoutEvent) { CurrentPassport.fromChannel(ctx.channel()).add(PassportState.IN_REQ_READ_TIMEOUT); removeInternalHandler(ctx); httpRequestReadTimeoutCounter.increment(); } super.userEventTriggered(ctx, evt); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { removeInternalHandler(ctx); super.handlerRemoved(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { removeInternalHandler(ctx); super.channelInactive(ctx); } protected void removeInternalHandler(ChannelHandlerContext ctx) { // Remove timeout handler if not already removed. ChannelHandlerContext handlerContext = ctx.pipeline().context(INTERNAL_HANDLER_NAME); if (handlerContext != null && !handlerContext.isRemoved()) { ctx.pipeline().remove(INTERNAL_HANDLER_NAME); } } static class InternalReadTimeoutHandler extends ReadTimeoutHandler { public InternalReadTimeoutHandler(long timeout, TimeUnit unit) { super(timeout, unit); } @Override protected void readTimedOut(ChannelHandlerContext ctx) throws Exception { ctx.fireUserEventTriggered(HttpRequestReadTimeoutEvent.INSTANCE); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/HttpServerLifecycleChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; import java.util.Objects; /** * @author michaels */ public final class HttpServerLifecycleChannelHandler extends HttpLifecycleChannelHandler { public static final class HttpServerLifecycleInboundChannelHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { // Fire start event, and if that succeeded, then allow processing to // continue to next handler in pipeline. if (fireStartEvent(ctx, (HttpRequest) msg)) { super.channelRead(ctx, msg); } else { ReferenceCountUtil.release(msg); } } else { super.channelRead(ctx, msg); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { fireCompleteEventIfNotAlready(ctx, CompleteReason.INACTIVE); super.channelInactive(ctx); } } public static final class HttpServerLifecycleOutboundChannelHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof HttpResponse) { ctx.channel().attr(ATTR_HTTP_RESP).set((HttpResponse) msg); } try { super.write(ctx, msg, promise); } finally { if (msg instanceof LastHttpContent) { boolean dontFireCompleteYet = false; if (msg instanceof HttpResponse) { // Handle case of 100 CONTINUE, where server sends an initial 100 status response to indicate to // client // that it can continue sending the initial request body. // ie. in this case we don't want to consider the state to be COMPLETE until after the 2nd // response. if (Objects.equals(((HttpResponse) msg).status(), HttpResponseStatus.CONTINUE)) { dontFireCompleteYet = true; } } if (!dontFireCompleteYet) { if (promise.isDone()) { fireCompleteEventIfNotAlready(ctx, CompleteReason.SESSION_COMPLETE); } else { promise.addListener(future -> { fireCompleteEventIfNotAlready(ctx, CompleteReason.SESSION_COMPLETE); }); } } } } } @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { fireCompleteEventIfNotAlready(ctx, CompleteReason.DISCONNECT); super.disconnect(ctx, promise); } @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { addPassportState(ctx, PassportState.SERVER_CH_CLOSE); // This will likely expand based on more specific reasons for completion if (ctx.channel() .attr(HttpLifecycleChannelHandler.ATTR_HTTP_PIPELINE_REJECT) .get() == null) { fireCompleteEventIfNotAlready(ctx, CompleteReason.CLOSE); } else { fireCompleteEventIfNotAlready(ctx, CompleteReason.PIPELINE_REJECT); } super.close(ctx, promise); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/RequestResponseCompleteEvent.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; /** * User: michaels@netflix.com * Date: 5/24/16 * Time: 1:04 PM */ public class RequestResponseCompleteEvent {} ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/SourceAddressChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.google.common.annotations.VisibleForTesting; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.AttributeKey; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.UnknownHostException; import javax.annotation.Nullable; /** * Stores the source IP address as an attribute of the channel. This has the advantage of allowing us to overwrite it if * we have more info (eg. ELB sends a HAProxyMessage with info of REAL source host + port). *

* User: michaels@netflix.com Date: 4/14/16 Time: 4:29 PM */ @ChannelHandler.Sharable public final class SourceAddressChannelHandler extends ChannelInboundHandlerAdapter { /** * Indicates the actual source (remote) address of the channel. This can be different than the one {@link Channel} * returns if the connection is being proxied. (e.g. over HAProxy) */ public static final AttributeKey ATTR_REMOTE_ADDR = AttributeKey.newInstance("_remote_addr"); /** * Indicates the destination address received from Proxy Protocol. Not set otherwise */ public static final AttributeKey ATTR_PROXY_PROTOCOL_DESTINATION_ADDRESS = AttributeKey.newInstance("_proxy_protocol_destination_address"); /** * Use {@link #ATTR_REMOTE_ADDR} instead. */ @Deprecated public static final AttributeKey ATTR_SOURCE_INET_ADDR = AttributeKey.newInstance("_source_inet_addr"); /** * The host address of the source. This is derived from {@link #ATTR_REMOTE_ADDR}. If the address is an IPv6 * address, the scope identifier is absent. */ public static final AttributeKey ATTR_SOURCE_ADDRESS = AttributeKey.newInstance("_source_address"); /** * Indicates the local address of the channel. This can be different than the one {@link Channel} returns if the * connection is being proxied. (e.g. over HAProxy) */ public static final AttributeKey ATTR_LOCAL_ADDR = AttributeKey.newInstance("_local_addr"); /** * Use {@link #ATTR_LOCAL_ADDR} instead. */ @Deprecated public static final AttributeKey ATTR_LOCAL_INET_ADDR = AttributeKey.newInstance("_local_inet_addr"); /** * The local address of this channel. This is derived from {@code channel.localAddress()}, or from the Proxy * Protocol preface if provided. If the address is an IPv6 address, the scope identifier is absent. Unlike {@link * #ATTR_SERVER_LOCAL_ADDRESS}, this value is overwritten with the Proxy Protocol local address (e.g. the LB's local * address), if enabled. */ public static final AttributeKey ATTR_LOCAL_ADDRESS = AttributeKey.newInstance("_local_address"); /** * The actual local address of the channel, in string form. If the address is an IPv6 address, the scope identifier * is absent. Unlike {@link #ATTR_LOCAL_ADDRESS}, this is not overwritten by the Proxy Protocol message if * present. * * @deprecated Use {@code channel.localAddress()} instead. */ @Deprecated public static final AttributeKey ATTR_SERVER_LOCAL_ADDRESS = AttributeKey.newInstance("_server_local_address"); /** * The port number of the local socket, or {@code -1} if not appropriate. This is not overwritten by the Proxy * Protocol message if present. */ public static final AttributeKey ATTR_SERVER_LOCAL_PORT = AttributeKey.newInstance("_server_local_port"); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.channel().attr(ATTR_REMOTE_ADDR).set(ctx.channel().remoteAddress()); InetSocketAddress sourceAddress = sourceAddress(ctx.channel()); ctx.channel().attr(ATTR_SOURCE_INET_ADDR).setIfAbsent(sourceAddress); ctx.channel().attr(ATTR_SOURCE_ADDRESS).setIfAbsent(getHostAddress(sourceAddress)); ctx.channel().attr(ATTR_LOCAL_ADDR).set(ctx.channel().localAddress()); InetSocketAddress localAddress = localAddress(ctx.channel()); ctx.channel().attr(ATTR_LOCAL_INET_ADDR).setIfAbsent(localAddress); ctx.channel().attr(ATTR_LOCAL_ADDRESS).setIfAbsent(getHostAddress(localAddress)); // ATTR_LOCAL_ADDRESS and ATTR_LOCAL_PORT get overwritten with what is received in // Proxy Protocol (via the LB), so set local server's address, port explicitly ctx.channel() .attr(ATTR_SERVER_LOCAL_ADDRESS) .setIfAbsent(localAddress.getAddress().getHostAddress()); ctx.channel().attr(ATTR_SERVER_LOCAL_PORT).setIfAbsent(localAddress.getPort()); super.channelActive(ctx); } /** * Returns the String form of a socket address, or {@code null} if there isn't one. */ @VisibleForTesting @Nullable static String getHostAddress(InetSocketAddress socketAddress) { InetAddress address = socketAddress.getAddress(); if (address instanceof Inet6Address) { // Strip the scope from the address since some other classes choke on it. // TODO(carl-mastrangelo): Consider adding this back in once issues like // https://github.com/google/guava/issues/2587 are fixed. try { return InetAddress.getByAddress(address.getAddress()).getHostAddress(); } catch (UnknownHostException e) { throw new RuntimeException(e); } } else if (address instanceof Inet4Address) { return address.getHostAddress(); } else { assert address == null; return null; } } private InetSocketAddress sourceAddress(Channel channel) { SocketAddress remoteSocketAddr = channel.remoteAddress(); if (remoteSocketAddr != null && InetSocketAddress.class.isAssignableFrom(remoteSocketAddr.getClass())) { InetSocketAddress inetSocketAddress = (InetSocketAddress) remoteSocketAddr; if (inetSocketAddress.getAddress() != null) { return inetSocketAddress; } } return null; } private InetSocketAddress localAddress(Channel channel) { SocketAddress localSocketAddress = channel.localAddress(); if (localSocketAddress != null && InetSocketAddress.class.isAssignableFrom(localSocketAddress.getClass())) { InetSocketAddress inetSocketAddress = (InetSocketAddress) localSocketAddress; if (inetSocketAddress.getAddress() != null) { return inetSocketAddress; } } return null; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/SslExceptionsHandler.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.spectator.api.Registry; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import javax.net.ssl.SSLHandshakeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Swallow specific SSL related exceptions to avoid propagating deep stack traces up the pipeline. * * @author Argha C * @since 4/17/23 */ @Sharable public class SslExceptionsHandler extends ChannelInboundHandlerAdapter { private static final Logger logger = LoggerFactory.getLogger(SslExceptionsHandler.class); private final Registry registry; public SslExceptionsHandler(Registry registry) { this.registry = registry; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // In certain cases, depending on the client, these stack traces can get very deep. // We intentionally avoid propagating this up the pipeline, to avoid verbose disk logging. if (cause.getCause() instanceof SSLHandshakeException) { logger.debug("SSL handshake failed on channel {}", ctx.channel(), cause); registry.counter("server.ssl.exception.swallowed", "cause", "SSLHandshakeException") .increment(); } else { super.exceptionCaught(ctx, cause); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/SwallowSomeHttp2ExceptionsHandler.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import com.netflix.spectator.api.Registry; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.unix.Errors; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ChannelHandler.Sharable public class SwallowSomeHttp2ExceptionsHandler extends ChannelOutboundHandlerAdapter { private static final Logger LOG = LoggerFactory.getLogger(SwallowSomeHttp2ExceptionsHandler.class); private final Registry registry; public SwallowSomeHttp2ExceptionsHandler(Registry registry) { this.registry = registry; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { incrementExceptionCounter(cause); if (cause instanceof Http2Exception h2e) { if (h2e.error() == Http2Error.NO_ERROR && h2e.shutdownHint().equals(Http2Exception.ShutdownHint.GRACEFUL_SHUTDOWN)) { // This is the exception we threw ourselves to make the http2 codec gracefully close the connection. So // just // swallow it so that it doesn't propagate and get logged. LOG.debug("Swallowed Http2Exception.ShutdownHint.GRACEFUL_SHUTDOWN ", cause); } else { super.exceptionCaught(ctx, cause); } } else if (cause instanceof Errors.NativeIoException) { LOG.debug("Swallowed NativeIoException", cause); } else { super.exceptionCaught(ctx, cause); } } private void incrementExceptionCounter(Throwable throwable) { registry.counter( "server.connection.pipeline.exception", "id", throwable.getClass().getSimpleName()) .increment(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/accesslog/AccessLogChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.accesslog; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.netty.common.SourceAddressChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.AttributeKey; import java.time.LocalDateTime; import java.time.ZoneId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 4/14/16 * Time: 3:51 PM */ public final class AccessLogChannelHandler { private static final AttributeKey ATTR_REQ_STATE = AttributeKey.newInstance("_accesslog_requeststate"); private static final Logger LOG = LoggerFactory.getLogger(AccessLogChannelHandler.class); public static class AccessLogInboundChannelHandler extends ChannelInboundHandlerAdapter { private final AccessLogPublisher publisher; public AccessLogInboundChannelHandler(AccessLogPublisher publisher) { this.publisher = publisher; } protected Integer getLocalPort(ChannelHandlerContext ctx) { return ctx.channel() .attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT) .get(); } protected String getRemoteIp(ChannelHandlerContext ctx) { return ctx.channel() .attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS) .get(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { RequestState state = new RequestState(); state.request = (HttpRequest) msg; state.startTimeNs = System.nanoTime(); state.requestBodySize = 0; ctx.channel().attr(ATTR_REQ_STATE).set(state); } if (msg instanceof HttpContent) { RequestState state = ctx.channel().attr(ATTR_REQ_STATE).get(); if (state != null) { state.requestBodySize += ((HttpContent) msg).content().readableBytes(); } } super.channelRead(ctx, msg); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof HttpLifecycleChannelHandler.CompleteEvent) { // Get the stored request, and remove the attr from channel to cleanup. RequestState state = ctx.channel().attr(ATTR_REQ_STATE).get(); ctx.channel().attr(ATTR_REQ_STATE).set(null); // Response complete, so now write to access log. long durationNs = System.nanoTime() - state.startTimeNs; Integer localPort = getLocalPort(ctx); String remoteIp = getRemoteIp(ctx); if (state.response == null) { LOG.debug( "Response null in AccessLog, Complete reason={}, duration={}, url={}, method={}", ((HttpLifecycleChannelHandler.CompleteEvent) evt).getReason(), durationNs / (1000 * 1000), state.request != null ? state.request.uri() : "-", state.request != null ? state.request.method() : "-"); } publisher.log( ctx.channel(), state.request, state.response, state.dateTime, localPort, remoteIp, durationNs, state.requestBodySize, state.responseBodySize); } super.userEventTriggered(ctx, evt); } } public static final class AccessLogOutboundChannelHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { RequestState state = ctx.channel().attr(ATTR_REQ_STATE).get(); if (msg instanceof HttpResponse) { state.response = (HttpResponse) msg; state.responseBodySize = 0; } if (msg instanceof HttpContent) { state.responseBodySize += ((HttpContent) msg).content().readableBytes(); } super.write(ctx, msg, promise); } } private static class RequestState { final LocalDateTime dateTime = LocalDateTime.now(ZoneId.systemDefault()); HttpRequest request; HttpResponse response; long startTimeNs; long requestBodySize = 0; long responseBodySize = 0; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/accesslog/AccessLogPublisher.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.accesslog; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicStringListProperty; import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Locale; import java.util.function.BiFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AccessLogPublisher { private static final char DELIM = '\t'; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; private static final List LOG_REQ_HEADERS = new DynamicStringListProperty( "zuul.access.log.requestheaders", "host,x-forwarded-for,x-forwarded-proto,x-forwarded-host,x-forwarded-port,user-agent") .get(); private static final List LOG_RESP_HEADERS = new DynamicStringListProperty("zuul.access.log.responseheaders", "server,via,content-type").get(); private static final DynamicIntProperty URI_LENGTH_LIMIT = new DynamicIntProperty("zuul.access.log.uri.length.limit", Integer.MAX_VALUE); private final Logger logger; private final BiFunction requestIdProvider; private static final Logger LOG = LoggerFactory.getLogger(AccessLogPublisher.class); public AccessLogPublisher(String loggerName, BiFunction requestIdProvider) { this.logger = LoggerFactory.getLogger(loggerName); this.requestIdProvider = requestIdProvider; } public void log( Channel channel, HttpRequest request, HttpResponse response, LocalDateTime dateTime, Integer localPort, String remoteIp, Long durationNs, Long requestBodySize, Long responseBodySize) { StringBuilder sb = new StringBuilder(512); String dateTimeStr = dateTime != null ? dateTime.format(DATE_TIME_FORMATTER) : "-----T-:-:-"; String remoteIpStr = (remoteIp != null && !remoteIp.isEmpty()) ? remoteIp : "-"; String port = localPort != null ? localPort.toString() : "-"; String method = request != null ? request.method().toString().toUpperCase(Locale.ROOT) : "-"; String uri = request != null ? request.uri() : "-"; if (uri.length() > URI_LENGTH_LIMIT.get()) { uri = uri.substring(0, URI_LENGTH_LIMIT.get()); } String status = response != null ? String.valueOf(response.status().code()) : "-"; String requestId = null; try { requestId = requestIdProvider.apply(channel, request); } catch (Exception ex) { LOG.error( "requestIdProvider failed in AccessLogPublisher method={}, uri={}, status={}", method, uri, status); } requestId = requestId != null ? requestId : "-"; // Convert duration to microseconds. String durationStr = (durationNs != null && durationNs > 0) ? String.valueOf(durationNs / 1000) : "-"; String requestBodySizeStr = (requestBodySize != null && requestBodySize > 0) ? requestBodySize.toString() : "-"; String responseBodySizeStr = (responseBodySize != null && responseBodySize > 0) ? responseBodySize.toString() : "-"; // Build the line. sb.append(dateTimeStr) .append(DELIM) .append(remoteIpStr) .append(DELIM) .append(port) .append(DELIM) .append(method) .append(DELIM) .append(uri) .append(DELIM) .append(status) .append(DELIM) .append(durationStr) .append(DELIM) .append(responseBodySizeStr) .append(DELIM) .append(requestId) .append(DELIM) .append(requestBodySizeStr); if (request != null && request.headers() != null) { includeMatchingHeaders(sb, LOG_REQ_HEADERS, request.headers()); } if (response != null && response.headers() != null) { includeMatchingHeaders(sb, LOG_RESP_HEADERS, response.headers()); } // Write to logger. String access = sb.toString(); logger.info(access); LOG.debug(access); } void includeMatchingHeaders(StringBuilder builder, List requiredHeaders, HttpHeaders headers) { for (String headerName : requiredHeaders) { String value = headerAsString(headers, headerName); builder.append(DELIM).append('\"').append(value).append('\"'); } } String headerAsString(HttpHeaders headers, String headerName) { List values = headers.getAll(headerName); return values.isEmpty() ? "-" : String.join(",", values); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/channel/config/ChannelConfig.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.channel.config; import java.util.HashMap; import java.util.Map; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 6:43 PM */ public class ChannelConfig implements Cloneable { private final HashMap parameters; public ChannelConfig() { parameters = new HashMap<>(); } public ChannelConfig(Map parameters) { this.parameters = new HashMap(parameters); } public void add(ChannelConfigValue param) { this.parameters.put(param.key(), param); } public void set(ChannelConfigKey key, T value) { this.parameters.put(key, new ChannelConfigValue<>(key, value)); } public T get(ChannelConfigKey key) { ChannelConfigValue ccv = parameters.get(key); T value = ccv == null ? null : (T) ccv.value(); if (value == null) { value = key.defaultValue(); } return value; } public ChannelConfigValue getConfig(ChannelConfigKey key) { return (ChannelConfigValue) parameters.get(key); } public boolean contains(ChannelConfigKey key) { return parameters.containsKey(key); } @Override public ChannelConfig clone() { return new ChannelConfig(parameters); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/channel/config/ChannelConfigKey.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.channel.config; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 6:17 PM */ public class ChannelConfigKey { private final String key; private final T defaultValue; public ChannelConfigKey(String key, T defaultValue) { this.key = key; this.defaultValue = defaultValue; } public ChannelConfigKey(String key) { this.key = key; this.defaultValue = null; } public String key() { return key; } public T defaultValue() { return defaultValue; } public boolean hasDefaultValue() { return defaultValue != null; } @Override public String toString() { return "ChannelConfigKey{" + "key='" + key + '\'' + ", defaultValue=" + defaultValue + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/channel/config/ChannelConfigValue.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.channel.config; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 6:41 PM */ public class ChannelConfigValue { private final ChannelConfigKey key; private final T value; public ChannelConfigValue(ChannelConfigKey key, T value) { this.key = key; this.value = value; } public ChannelConfigKey key() { return key; } public T value() { return value; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/channel/config/CommonChannelConfigKeys.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.channel.config; import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.zuul.netty.server.ServerTimeout; import com.netflix.zuul.netty.ssl.SslContextFactory; import io.netty.handler.ssl.SslContext; import io.netty.util.AsyncMapping; /** * User: michaels@netflix.com * Date: 2/8/17 * Time: 6:21 PM */ public class CommonChannelConfigKeys { public static final ChannelConfigKey withProxyProtocol = new ChannelConfigKey<>("withProxyProtocol", false); public static final ChannelConfigKey allowProxyHeadersWhen = new ChannelConfigKey<>("allowProxyHeadersWhen", StripUntrustedProxyHeadersHandler.AllowWhen.ALWAYS); public static final ChannelConfigKey preferProxyProtocolForClientIp = new ChannelConfigKey<>("preferProxyProtocolForClientIp", true); /** The Idle timeout of a connection, in milliseconds */ public static final ChannelConfigKey idleTimeout = new ChannelConfigKey<>("idleTimeout", 65000); public static final ChannelConfigKey serverTimeout = new ChannelConfigKey<>("serverTimeout"); /** The HTTP request read timeout, in milliseconds */ public static final ChannelConfigKey httpRequestReadTimeout = new ChannelConfigKey<>("httpRequestReadTimeout", 5000); /** The maximum number of inbound connections to proxy. */ public static final ChannelConfigKey maxConnections = new ChannelConfigKey<>("maxConnections", 20000); public static final ChannelConfigKey maxRequestsPerConnection = new ChannelConfigKey<>("maxRequestsPerConnection", 4000); public static final ChannelConfigKey maxRequestsPerConnectionInBrownout = new ChannelConfigKey<>("maxRequestsPerConnectionInBrownout", 100); public static final ChannelConfigKey connectionExpiry = new ChannelConfigKey<>("connectionExpiry", 20 * 60 * 1000); // SSL: public static final ChannelConfigKey isSSlFromIntermediary = new ChannelConfigKey<>("isSSlFromIntermediary", false); public static final ChannelConfigKey serverSslConfig = new ChannelConfigKey<>("serverSslConfig"); public static final ChannelConfigKey sslContextFactory = new ChannelConfigKey<>("sslContextFactory"); public static final ChannelConfigKey> sniMapping = new ChannelConfigKey<>("sniMapping"); // HTTP/2 specific: public static final ChannelConfigKey maxConcurrentStreams = new ChannelConfigKey<>("maxConcurrentStreams", 100); public static final ChannelConfigKey initialWindowSize = new ChannelConfigKey<>("initialWindowSize", 5242880); // 5MB /* The amount of time to wait before closing a connection that has the `Connection: Close` header, in seconds */ public static final ChannelConfigKey connCloseDelay = new ChannelConfigKey<>("connCloseDelay", 10); public static final ChannelConfigKey maxHttp2HeaderTableSize = new ChannelConfigKey<>("maxHttp2HeaderTableSize", 4096); public static final ChannelConfigKey maxHttp2HeaderListSize = new ChannelConfigKey<>("maxHttp2HeaderListSize"); public static final ChannelConfigKey http2AllowGracefulDelayed = new ChannelConfigKey<>("http2AllowGracefulDelayed", true); public static final ChannelConfigKey http2SwallowUnknownExceptionsOnConnClose = new ChannelConfigKey<>("http2SwallowUnknownExceptionsOnConnClose", false); public static final ChannelConfigKey http2CatchConnectionErrors = new ChannelConfigKey<>("http2CatchConnectionErrors", true); public static final ChannelConfigKey http2EncoderMaxResetFrames = new ChannelConfigKey<>("http2EncoderMaxResetFrames", 200); public static final ChannelConfigKey http2EncoderMaxResetFramesWindow = new ChannelConfigKey<>("http2EncoderMaxResetFramesWindow", 30); public static final ChannelConfigKey http2ConnectProtocolEnabled = new ChannelConfigKey<>("http2ConnectProtocolEnabled", false); } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/http2/DynamicHttp2FrameLogger.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.http2; import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.annotations.FormatString; import com.netflix.config.DynamicStringSetProperty; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http2.Http2Flags; import io.netty.handler.codec.http2.Http2FrameLogger; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.logging.LogLevel; import io.netty.util.AttributeKey; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogLevel; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; public class DynamicHttp2FrameLogger extends Http2FrameLogger { public static final AttributeKey ATTR_ENABLE = AttributeKey.valueOf("http2.frame.logger.enabled"); private static final int BUFFER_LENGTH_THRESHOLD = 64; private static final DynamicStringSetProperty FRAMES_TO_LOG = new DynamicStringSetProperty( "server.http2.logger.framestolog", "SETTINGS,WINDOW_UPDATE,HEADERS,GO_AWAY,RST_STREAM,PRIORITY,PING,PUSH_PROMISE"); private final InternalLogger logger; private final InternalLogLevel level; public DynamicHttp2FrameLogger(LogLevel level, Class clazz) { super(level, clazz); this.level = ObjectUtil.checkNotNull(level.toInternalLevel(), "level"); this.logger = ObjectUtil.checkNotNull(InternalLoggerFactory.getInstance(clazz), "logger"); } protected boolean enabled(ChannelHandlerContext ctx) { return ctx.channel().hasAttr(ATTR_ENABLE); } protected boolean enabled() { return logger.isEnabled(level); } @Override public void logData( Direction direction, ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endStream) { if (enabled()) { log( direction, "DATA", ctx, "streamId=%d, endStream=%b, length=%d", streamId, endStream, data.readableBytes()); } } @Override public void logHeaders( Direction direction, ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endStream) { if (enabled()) { log(direction, "HEADERS", ctx, "streamId=%d, headers=%s, endStream=%b", streamId, headers, endStream); } } @Override public void logHeaders( Direction direction, ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) { if (enabled()) { log( direction, "HEADERS", ctx, "streamId=%d, headers=%s, streamDependency=%d, weight=%d, " + "exclusive=%b, endStream=%b", streamId, headers, streamDependency, weight, exclusive, endStream); } } @Override public void logPriority( Direction direction, ChannelHandlerContext ctx, int streamId, int streamDependency, short weight, boolean exclusive) { if (enabled()) { log( direction, "PRIORITY", ctx, "streamId=%d, streamDependency=%d, weight=%d, exclusive=%b", streamId, streamDependency, weight, exclusive); } } @Override public void logRstStream(Direction direction, ChannelHandlerContext ctx, int streamId, long errorCode) { if (enabled()) { log(direction, "RST_STREAM", ctx, "streamId=%d, errorCode=%d", streamId, errorCode); } } @Override public void logSettingsAck(Direction direction, ChannelHandlerContext ctx) { if (enabled()) { log(direction, "SETTINGS", ctx, "ack=true"); } } @Override public void logSettings(Direction direction, ChannelHandlerContext ctx, Http2Settings settings) { if (enabled()) { log(direction, "SETTINGS", ctx, "ack=false, settings=%s", settings); } } @Override public void logPing(Direction direction, ChannelHandlerContext ctx, long data) { if (enabled()) { log(direction, "PING", ctx, "ack=false, length=%d", data); } } @Override public void logPingAck(Direction direction, ChannelHandlerContext ctx, long data) { if (enabled()) { log(direction, "PING", ctx, "ack=true, length=%d", data); } } @Override public void logPushPromise( Direction direction, ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) { if (enabled()) { log( direction, "PUSH_PROMISE", ctx, "streamId=%d, promisedStreamId=%d, headers=%s, padding=%d", streamId, promisedStreamId, headers, padding); } } @Override public void logGoAway( Direction direction, ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) { if (enabled()) { log( direction, "GO_AWAY", ctx, "lastStreamId=%d, errorCode=%d, length=%d, bytes=%s", lastStreamId, errorCode, debugData.readableBytes(), toString(debugData)); } } @Override public void logWindowsUpdate( Direction direction, ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) { if (enabled()) { log(direction, "WINDOW_UPDATE", ctx, "streamId=%d, windowSizeIncrement=%d", streamId, windowSizeIncrement); } } @Override public void logUnknownFrame( Direction direction, ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf data) { if (enabled()) { log( direction, "UNKNOWN", ctx, "frameType=%d, streamId=%d, flags=%d, length=%d, bytes=%s", frameType & 0xFF, streamId, flags.value(), data.readableBytes(), toString(data)); } } private String toString(ByteBuf buf) { if (level == InternalLogLevel.TRACE || buf.readableBytes() <= BUFFER_LENGTH_THRESHOLD) { // Log the entire buffer. return ByteBufUtil.hexDump(buf); } // Otherwise just log the first 64 bytes. int length = Math.min(buf.readableBytes(), BUFFER_LENGTH_THRESHOLD); return ByteBufUtil.hexDump(buf, buf.readerIndex(), length) + "..."; } @FormatMethod private void log( Direction direction, String frame, ChannelHandlerContext ctx, @FormatString String format, Object... args) { if (shouldLogFrame(frame)) { StringBuilder b = new StringBuilder(200) .append(direction.name()) .append(": ") .append(frame) .append(": ") .append(String.format(format, args)) .append(" -- ") .append(String.valueOf(ctx.channel())); logger.log(level, b.toString()); } } protected boolean shouldLogFrame(String frame) { return FRAMES_TO_LOG.get().contains(frame); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/EventLoopGroupMetrics.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.netflix.spectator.api.Registry; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.HashMap; import java.util.Map; /** * User: michaels@netflix.com * Date: 2/7/17 * Time: 3:17 PM */ @Singleton public class EventLoopGroupMetrics { private final ThreadLocal metricsForCurrentThread; private final Map byEventLoop = new HashMap<>(); @Inject public EventLoopGroupMetrics(Registry registry) { this.metricsForCurrentThread = ThreadLocal.withInitial(() -> { String name = nameForCurrentEventLoop(); EventLoopMetrics metrics = new EventLoopMetrics(registry, name); byEventLoop.put(Thread.currentThread(), metrics); return metrics; }); } public Map connectionsPerEventLoop() { Map map = new HashMap<>(byEventLoop.size()); for (Map.Entry entry : byEventLoop.entrySet()) { map.put(entry.getKey(), entry.getValue().currentConnectionsCount()); } return map; } public Map httpRequestsPerEventLoop() { Map map = new HashMap<>(byEventLoop.size()); for (Map.Entry entry : byEventLoop.entrySet()) { map.put(entry.getKey(), entry.getValue().currentHttpRequestsCount()); } return map; } public EventLoopMetrics getForCurrentEventLoop() { return metricsForCurrentThread.get(); } private static String nameForCurrentEventLoop() { // We're relying on the knowledge that we name the eventloop threads consistently. String threadName = Thread.currentThread().getName(); String parts[] = threadName.split("-ClientToZuulWorker-", -1); if (parts.length == 2) { return parts[1]; } return threadName; } interface EventLoopInfo { int currentConnectionsCount(); int currentHttpRequestsCount(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/EventLoopMetrics.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import java.util.concurrent.atomic.AtomicInteger; /** * User: michaels@netflix.com * Date: 2/7/17 * Time: 3:18 PM */ public class EventLoopMetrics implements EventLoopGroupMetrics.EventLoopInfo { private final String name; public final AtomicInteger currentRequests = new AtomicInteger(0); public final AtomicInteger currentConnections = new AtomicInteger(0); private final Registry registry; private final Id currentRequestsId; private final Id currentConnectionsId; public EventLoopMetrics(Registry registry, String eventLoopName) { this.name = eventLoopName; this.registry = registry; this.currentRequestsId = this.registry.createId("server.eventloop.http.requests.current"); this.currentConnectionsId = this.registry.createId("server.eventloop.connections.current"); } @Override public int currentConnectionsCount() { return currentConnections.get(); } @Override public int currentHttpRequestsCount() { return currentRequests.get(); } public void incrementCurrentRequests() { int value = this.currentRequests.incrementAndGet(); updateGauge(currentRequestsId, value); } public void decrementCurrentRequests() { int value = this.currentRequests.decrementAndGet(); updateGauge(currentRequestsId, value); } public void incrementCurrentConnections() { int value = this.currentConnections.incrementAndGet(); updateGauge(currentConnectionsId, value); } public void decrementCurrentConnections() { int value = this.currentConnections.decrementAndGet(); updateGauge(currentConnectionsId, value); } private void updateGauge(Id gaugeId, int value) { registry.gauge(gaugeId.withTag("eventloop", name)).set(value); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/Http2MetricsChannelHandlers.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.netflix.spectator.api.Registry; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2Frame; import io.netty.handler.codec.http2.Http2GoAwayFrame; import io.netty.handler.codec.http2.Http2ResetFrame; public class Http2MetricsChannelHandlers { private final Inbound inbound; private final Outbound outbound; public Http2MetricsChannelHandlers(Registry registry, String metricPrefix, String metricId) { super(); this.inbound = new Inbound(registry, metricId, metricPrefix); this.outbound = new Outbound(registry, metricId, metricPrefix); } public Inbound inbound() { return inbound; } public Outbound outbound() { return outbound; } protected void incrementErrorCounter(Registry registry, String counterName, String metricId, Http2Exception h2e) { String h2Error = h2e.error() != null ? h2e.error().name() : "NA"; String exceptionName = h2e.getClass().getSimpleName(); registry.counter(counterName, "id", metricId, "error", h2Error, "exception", exceptionName) .increment(); } protected void incrementCounter(Registry registry, String counterName, String metricId, Http2Frame frame) { long errorCode; if (frame instanceof Http2ResetFrame) { errorCode = ((Http2ResetFrame) frame).errorCode(); } else if (frame instanceof Http2GoAwayFrame) { errorCode = ((Http2GoAwayFrame) frame).errorCode(); } else { errorCode = -1; } registry.counter(counterName, "id", metricId, "frame", frame.name(), "error_code", Long.toString(errorCode)) .increment(); } @ChannelHandler.Sharable private class Inbound extends ChannelInboundHandlerAdapter { private final Registry registry; private final String metricId; private final String frameCounterName; private final String errorCounterName; public Inbound(Registry registry, String metricId, String metricPrefix) { this.registry = registry; this.metricId = metricId; this.frameCounterName = metricPrefix + ".http2.frame.inbound"; this.errorCounterName = metricPrefix + ".http2.error.inbound"; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { if (msg instanceof Http2Frame) { incrementCounter(registry, frameCounterName, metricId, (Http2Frame) msg); } } finally { super.channelRead(ctx, msg); } } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { try { if (evt instanceof Http2Frame) { incrementCounter(registry, frameCounterName, metricId, (Http2Frame) evt); } } finally { super.userEventTriggered(ctx, evt); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { try { if (cause instanceof Http2Exception) { incrementErrorCounter(registry, errorCounterName, metricId, (Http2Exception) cause); } } finally { super.exceptionCaught(ctx, cause); } } } @ChannelHandler.Sharable private class Outbound extends ChannelOutboundHandlerAdapter { private final Registry registry; private final String metricId; private final String frameCounterName; private final String errorCounterName; public Outbound(Registry registry, String metricId, String metricPrefix) { this.registry = registry; this.metricId = metricId; this.frameCounterName = metricPrefix + ".http2.frame.outbound"; this.errorCounterName = metricPrefix + ".http2.error.outbound"; } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { super.write(ctx, msg, promise); if (msg instanceof Http2Frame) { incrementCounter(registry, frameCounterName, metricId, (Http2Frame) msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { try { if (cause instanceof Http2Exception) { incrementErrorCounter(registry, errorCounterName, metricId, (Http2Exception) cause); } } finally { super.exceptionCaught(ctx, cause); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/HttpBodySizeRecordingChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.netflix.netty.common.HttpLifecycleChannelHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.AttributeKey; import jakarta.inject.Provider; /** * User: michaels@netflix.com * Date: 4/14/16 * Time: 3:51 PM */ public final class HttpBodySizeRecordingChannelHandler { private static final AttributeKey ATTR_STATE = AttributeKey.newInstance("_http_body_size_state"); public static Provider getCurrentInboundBodySize(Channel ch) { return new InboundBodySizeProvider(ch); } public static Provider getCurrentOutboundBodySize(Channel ch) { return new OutboundBodySizeProvider(ch); } private static State getOrCreateCurrentState(Channel ch) { State state = ch.attr(ATTR_STATE).get(); if (state == null) { state = createNewState(ch); } return state; } private static State createNewState(Channel ch) { State state = new State(); ch.attr(ATTR_STATE).set(state); return state; } public static final class InboundChannelHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { State state = null; // Reset the state as each new inbound request comes in. if (msg instanceof HttpRequest) { state = createNewState(ctx.channel()); } // Update the inbound body size with this chunk. if (msg instanceof HttpContent) { if (state == null) { state = getOrCreateCurrentState(ctx.channel()); } state.inboundBodySize += ((HttpContent) msg).content().readableBytes(); } super.channelRead(ctx, msg); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { try { super.userEventTriggered(ctx, evt); } finally { if (evt instanceof HttpLifecycleChannelHandler.CompleteEvent) { ctx.channel().attr(ATTR_STATE).set(null); } } } } public static final class OutboundChannelHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { State state = null; // Reset the state as each new outbound request goes out. if (msg instanceof HttpRequest) { state = createNewState(ctx.channel()); } // Update the outbound body size with this chunk. if (msg instanceof HttpContent) { if (state == null) { state = getOrCreateCurrentState(ctx.channel()); } state.outboundBodySize += ((HttpContent) msg).content().readableBytes(); } super.write(ctx, msg, promise); } } private static class State { long inboundBodySize = 0; long outboundBodySize = 0; } static class InboundBodySizeProvider implements Provider { private final Channel channel; public InboundBodySizeProvider(Channel channel) { this.channel = channel; } @Override public Long get() { State state = getOrCreateCurrentState(channel); return state == null ? 0 : state.inboundBodySize; } } static class OutboundBodySizeProvider implements Provider { private final Channel channel; public OutboundBodySizeProvider(Channel channel) { this.channel = channel; } @Override public Long get() { State state = getOrCreateCurrentState(channel); return state == null ? 0 : state.outboundBodySize; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/HttpMetricsChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Gauge; import com.netflix.spectator.api.Registry; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.AttributeKey; import java.util.concurrent.atomic.AtomicInteger; /** * User: michaels@netflix.com * Date: 4/14/16 * Time: 3:51 PM */ @ChannelHandler.Sharable public class HttpMetricsChannelHandler extends ChannelInboundHandlerAdapter { private static final AttributeKey ATTR_REQ_INFLIGHT = AttributeKey.newInstance("_httpmetrics_inflight"); private static final Object INFLIGHT = "is_inflight"; private static final AttributeKey ATTR_CURRENT_REQS = AttributeKey.newInstance("_server_http_current_count"); private final AtomicInteger currentRequests = new AtomicInteger(0); private final Registry registry; private final Gauge currentRequestsGauge; private final Counter unSupportedPipeliningCounter; public HttpMetricsChannelHandler(Registry registry, String name, String id) { super(); this.registry = registry; this.currentRequestsGauge = this.registry.gauge(this.registry.createId(name + ".http.requests.current", "id", id)); this.unSupportedPipeliningCounter = this.registry.counter(name + ".http.requests.pipelining.dropped", "id", id); } public static int getInflightRequestCountFromChannel(Channel ch) { AtomicInteger current = ch.attr(ATTR_CURRENT_REQS).get(); return current == null ? 0 : current.get(); } public int getInflightRequestsCount() { return currentRequests.get(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // Store a ref to the count of current inflight requests onto this channel. So that // other code can query it using getInflightRequestCountFromChannel(). ctx.channel().attr(ATTR_CURRENT_REQS).set(currentRequests); super.channelActive(ctx); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof HttpLifecycleChannelHandler.StartEvent) { incrementCurrentRequestsInFlight(ctx); } else if (evt instanceof CompleteEvent && ((CompleteEvent) evt).getReason() == CompleteReason.PIPELINE_REJECT) { unSupportedPipeliningCounter.increment(); } else if (evt instanceof CompleteEvent) { decrementCurrentRequestsIfOneInflight(ctx); } super.userEventTriggered(ctx, evt); } private void incrementCurrentRequestsInFlight(ChannelHandlerContext ctx) { currentRequestsGauge.set(currentRequests.incrementAndGet()); ctx.channel().attr(ATTR_REQ_INFLIGHT).set(INFLIGHT); } private void decrementCurrentRequestsIfOneInflight(ChannelHandlerContext ctx) { if (ctx.channel().attr(ATTR_REQ_INFLIGHT).getAndSet(null) != null) { currentRequestsGauge.set(currentRequests.decrementAndGet()); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/InstrumentedResourceLeakDetector.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.google.common.annotations.VisibleForTesting; import com.netflix.zuul.netty.SpectatorUtils; import io.netty.util.ResourceLeakDetector; import java.lang.reflect.Field; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * Pluggable ResourceLeakDetector to track metrics for leaks * * Author: Arthur Gonigberg * Date: September 20, 2016 */ public class InstrumentedResourceLeakDetector extends ResourceLeakDetector { @VisibleForTesting final AtomicInteger leakCounter; public InstrumentedResourceLeakDetector(Class resourceType, int samplingInterval) { super(resourceType, samplingInterval); this.leakCounter = SpectatorUtils.newGauge("NettyLeakDetector", resourceType.getSimpleName(), new AtomicInteger()); } public InstrumentedResourceLeakDetector(Class resourceType, int samplingInterval, long maxActive) { this(resourceType, samplingInterval); } @Override protected void reportTracedLeak(String resourceType, String records) { super.reportTracedLeak(resourceType, records); leakCounter.incrementAndGet(); resetReportedLeaks(); } @Override protected void reportUntracedLeak(String resourceType) { super.reportUntracedLeak(resourceType); leakCounter.incrementAndGet(); resetReportedLeaks(); } /** * This private field in the superclass needs to be reset so that we can continue reporting leaks even * if they're duplicates. This is ugly but ideally should not be called frequently (or at all). */ private void resetReportedLeaks() { try { Field reportedLeaks = ResourceLeakDetector.class.getDeclaredField("reportedLeaks"); reportedLeaks.setAccessible(true); Object f = reportedLeaks.get(this); if (f instanceof Map) { ((Map) f).clear(); } } catch (Throwable t) { // do nothing } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/metrics/PerEventLoopMetricsChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import com.netflix.netty.common.HttpLifecycleChannelHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.AttributeKey; /** * User: michaels@netflix.com * Date: 2/6/17 * Time: 2:21 PM */ public class PerEventLoopMetricsChannelHandler { private static final AttributeKey ATTR_REQ_INFLIGHT = AttributeKey.newInstance("_eventloop_metrics_inflight"); private static final Object INFLIGHT = "eventloop_is_inflight"; private final EventLoopGroupMetrics groupMetrics; public PerEventLoopMetricsChannelHandler(EventLoopGroupMetrics groupMetrics) { this.groupMetrics = groupMetrics; } @ChannelHandler.Sharable public class Connections extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { groupMetrics.getForCurrentEventLoop().incrementCurrentConnections(); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { try { super.channelInactive(ctx); } finally { groupMetrics.getForCurrentEventLoop().decrementCurrentConnections(); } } } @ChannelHandler.Sharable public class HttpRequests extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof HttpLifecycleChannelHandler.StartEvent) { incrementCurrentRequestsInFlight(ctx); } else if (evt instanceof HttpLifecycleChannelHandler.CompleteEvent) { decrementCurrentRequestsIfOneInflight(ctx); } super.userEventTriggered(ctx, evt); } private void incrementCurrentRequestsInFlight(ChannelHandlerContext ctx) { groupMetrics.getForCurrentEventLoop().incrementCurrentRequests(); ctx.channel().attr(ATTR_REQ_INFLIGHT).set(INFLIGHT); } private void decrementCurrentRequestsIfOneInflight(ChannelHandlerContext ctx) { if (ctx.channel().attr(ATTR_REQ_INFLIGHT).getAndSet(null) != null) { groupMetrics.getForCurrentEventLoop().decrementCurrentRequests(); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/proxyprotocol/ElbProxyProtocolChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.proxyprotocol; import com.google.common.base.Preconditions; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.spectator.api.Registry; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.ProtocolDetectionState; import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; /** * Decides if we need to decode a HAProxyMessage. If so, adds the decoder followed by the handler. * Else, removes itself from the pipeline. */ public final class ElbProxyProtocolChannelHandler extends ChannelInboundHandlerAdapter { public static final String NAME = ElbProxyProtocolChannelHandler.class.getSimpleName(); private final boolean withProxyProtocol; private final Registry registry; public ElbProxyProtocolChannelHandler(Registry registry, boolean withProxyProtocol) { this.withProxyProtocol = withProxyProtocol; this.registry = Preconditions.checkNotNull(registry); } public void addProxyProtocol(ChannelPipeline pipeline) { pipeline.addLast(NAME, this); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (!withProxyProtocol) { ctx.pipeline().remove(this); super.channelRead(ctx, msg); return; } ProtocolDetectionState haProxyState = getDetectionState(msg); if (haProxyState == ProtocolDetectionState.DETECTED) { ctx.pipeline() .addAfter(NAME, null, new HAProxyMessageChannelHandler()) .replace(this, null, new HAProxyMessageDecoder()); } else { int port = ctx.channel() .attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT) .get(); // This likely means initialization was requested with proxy protocol, but we encountered a non-ppv2 // message registry.counter( "zuul.hapm.decode", "success", "false", "port", String.valueOf(port), "needs_more_data", String.valueOf(haProxyState == ProtocolDetectionState.NEEDS_MORE_DATA)) .increment(); ctx.pipeline().remove(this); } super.channelRead(ctx, msg); } private ProtocolDetectionState getDetectionState(Object msg) { return HAProxyMessageDecoder.detectProtocol((ByteBuf) msg).state(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/proxyprotocol/HAProxyMessageChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.proxyprotocol; import com.google.common.annotations.VisibleForTesting; import com.google.common.net.InetAddresses; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.haproxy.HAProxyMessage; import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; import io.netty.handler.codec.haproxy.HAProxyTLV; import io.netty.util.AttributeKey; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.List; import java.util.stream.Collectors; /** * Copies any decoded HAProxyMessage into the channel attributes, and doesn't pass it any further along the pipeline. * Use in conjunction with HAProxyMessageDecoder if proxy protocol is enabled on the ELB. */ public final class HAProxyMessageChannelHandler extends ChannelInboundHandlerAdapter { public static final AttributeKey ATTR_HAPROXY_MESSAGE = AttributeKey.newInstance("_haproxy_message"); public static final AttributeKey ATTR_HAPROXY_VERSION = AttributeKey.newInstance("_haproxy_version"); public static final AttributeKey> ATTR_HAPROXY_CUSTOM_TLVS = AttributeKey.newInstance("_haproxy_tlvs"); @VisibleForTesting static final Attrs.Key HAPM_DEST_PORT = Attrs.newKey("hapm_port"); @VisibleForTesting static final Attrs.Key HAPM_DEST_IP_VERSION = Attrs.newKey("hapm_dst_ipproto"); @VisibleForTesting static final Attrs.Key HAPM_SRC_IP_VERSION = Attrs.newKey("hapm_src_ipproto"); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HAProxyMessage hapm) { Channel channel = ctx.channel(); channel.attr(ATTR_HAPROXY_MESSAGE).set(hapm); ctx.channel().closeFuture().addListener((ChannelFutureListener) future -> hapm.release()); channel.attr(ATTR_HAPROXY_VERSION).set(hapm.protocolVersion()); // Parse and persist any custom TLVs that might be part of the connection List tlvList = hapm.tlvs().stream() .filter(tlv -> tlv.type() == HAProxyTLV.Type.OTHER) .collect(Collectors.toList()); channel.attr(ATTR_HAPROXY_CUSTOM_TLVS).set(tlvList); // Get the real host and port that the client connected with. parseDstAddr(hapm, channel); parseSrcAddr(hapm, channel); // Remove ourselves (this handler) from the channel now, as this is conn. level info ctx.pipeline().remove(this); } } private void parseSrcAddr(HAProxyMessage hapm, Channel channel) { String sourceAddress = hapm.sourceAddress(); if (sourceAddress != null) { channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).set(sourceAddress); SocketAddress srcAddr; switch (hapm.proxiedProtocol()) { case UNKNOWN: throw new IllegalArgumentException("unknown proxy protocol" + sourceAddress); case TCP4: case TCP6: InetSocketAddress inetAddr; srcAddr = inetAddr = new InetSocketAddress(InetAddresses.forString(sourceAddress), hapm.sourcePort()); Attrs attrs = channel.attr(Server.CONN_DIMENSIONS).get(); if (inetAddr.getAddress() instanceof Inet4Address) { HAPM_SRC_IP_VERSION.put(attrs, "v4"); } else if (inetAddr.getAddress() instanceof Inet6Address) { HAPM_SRC_IP_VERSION.put(attrs, "v6"); } else { HAPM_SRC_IP_VERSION.put(attrs, "unknown"); } break; case UNIX_STREAM: // TODO: implement case UDP4: case UDP6: case UNIX_DGRAM: throw new IllegalArgumentException("unknown proxy protocol" + sourceAddress); default: throw new AssertionError(hapm.proxiedProtocol()); } channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).set(srcAddr); } } private void parseDstAddr(HAProxyMessage hapm, Channel channel) { String destinationAddress = hapm.destinationAddress(); if (destinationAddress != null) { channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS).set(destinationAddress); SocketAddress dstAddr; switch (hapm.proxiedProtocol()) { case UNKNOWN: throw new IllegalArgumentException("unknown proxy protocol" + destinationAddress); case TCP4: case TCP6: InetSocketAddress inetAddr = new InetSocketAddress(InetAddresses.forString(destinationAddress), hapm.destinationPort()); dstAddr = inetAddr; // set ppv2 attr explicitly because ATTR_LOCAL_ADDR could be non ppv2 channel.attr(SourceAddressChannelHandler.ATTR_PROXY_PROTOCOL_DESTINATION_ADDRESS) .set(inetAddr); Attrs attrs = channel.attr(Server.CONN_DIMENSIONS).get(); if (inetAddr.getAddress() instanceof Inet4Address) { HAPM_DEST_IP_VERSION.put(attrs, "v4"); } else if (inetAddr.getAddress() instanceof Inet6Address) { HAPM_DEST_IP_VERSION.put(attrs, "v6"); } else { HAPM_DEST_IP_VERSION.put(attrs, "unknown"); } HAPM_DEST_PORT.put(attrs, hapm.destinationPort()); break; case UNIX_STREAM: // TODO: implement case UDP4: case UDP6: case UNIX_DGRAM: throw new IllegalArgumentException("unknown proxy protocol" + destinationAddress); default: throw new AssertionError(hapm.proxiedProtocol()); } channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).set(dstAddr); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/proxyprotocol/StripUntrustedProxyHeadersHandler.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.proxyprotocol; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import com.netflix.config.DynamicStringListProperty; import com.netflix.netty.common.ssl.SslHandshakeInfo; import com.netflix.zuul.netty.server.ssl.SslHandshakeInfoHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.ssl.ClientAuth; import io.netty.util.AsciiString; import java.util.Collection; import java.util.List; /** * Strip out any X-Forwarded-* headers from inbound http requests if connection is not trusted. */ @ChannelHandler.Sharable public class StripUntrustedProxyHeadersHandler extends ChannelInboundHandlerAdapter { private static final DynamicStringListProperty XFF_BLACKLIST = new DynamicStringListProperty("zuul.proxy.headers.host.blacklist", ""); public enum AllowWhen { ALWAYS, MUTUAL_SSL_AUTH, NEVER } private static final Collection HEADERS_TO_STRIP = Sets.newHashSet( new AsciiString("x-forwarded-for"), new AsciiString("x-forwarded-port"), new AsciiString("x-forwarded-proto"), new AsciiString("x-forwarded-proto-version"), new AsciiString("x-real-ip")); private final AllowWhen allowWhen; public StripUntrustedProxyHeadersHandler(AllowWhen allowWhen) { this.allowWhen = allowWhen; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest req) { switch (allowWhen) { case NEVER: stripXFFHeaders(req); break; case MUTUAL_SSL_AUTH: if (!connectionIsUsingMutualSSLWithAuthEnforced(ctx.channel())) { stripXFFHeaders(req); } else { checkBlacklist(req, XFF_BLACKLIST.get()); } break; case ALWAYS: checkBlacklist(req, XFF_BLACKLIST.get()); break; default: // default to not allow. stripXFFHeaders(req); } } super.channelRead(ctx, msg); } @VisibleForTesting boolean connectionIsUsingMutualSSLWithAuthEnforced(Channel ch) { boolean is = false; SslHandshakeInfo sslHandshakeInfo = ch.attr(SslHandshakeInfoHandler.ATTR_SSL_INFO).get(); if (sslHandshakeInfo != null) { if (sslHandshakeInfo.getClientAuthRequirement() == ClientAuth.REQUIRE) { is = true; } } return is; } @VisibleForTesting void stripXFFHeaders(HttpRequest req) { HttpHeaders headers = req.headers(); for (AsciiString headerName : HEADERS_TO_STRIP) { headers.remove(headerName); } } @VisibleForTesting void checkBlacklist(HttpRequest req, List blacklist) { // blacklist headers from if (blacklist.stream().anyMatch(h -> h.equalsIgnoreCase(req.headers().get(HttpHeaderNames.HOST)))) { stripXFFHeaders(req); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/ssl/ServerSslConfig.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.ssl; import com.netflix.config.DynamicLongProperty; import io.netty.handler.ssl.ClientAuth; import java.io.File; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; /** * Server-side SSL/TLS configuration including protocols, ciphers, certificate * material, client authentication, and session settings. */ @Getter @Builder @AllArgsConstructor(access = AccessLevel.PACKAGE) public class ServerSslConfig { private static final DynamicLongProperty DEFAULT_SESSION_TIMEOUT = new DynamicLongProperty("server.ssl.session.timeout", (18 * 60)); // 18 hours private static final List DEFAULT_CIPHERS; static { try { SSLContext context = SSLContext.getDefault(); SSLSocketFactory sf = context.getSocketFactory(); DEFAULT_CIPHERS = List.of(sf.getSupportedCipherSuites()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } private final String[] protocols; private final List ciphers; private final File certChainFile; private final File keyFile; @Builder.Default private final ClientAuth clientAuth = ClientAuth.NONE; private final File clientAuthTrustStoreFile; private final String clientAuthTrustStorePassword; private final File clientAuthTrustStorePasswordFile; @Builder.Default private final long sessionTimeout = DEFAULT_SESSION_TIMEOUT.get(); @Builder.Default private final boolean sessionTicketsEnabled = false; /** * @deprecated Use {@link ServerSslConfig#builder()} instead. */ @Deprecated public ServerSslConfig(String[] protocols, String[] ciphers, File certChainFile, File keyFile) { this(protocols, ciphers, certChainFile, keyFile, ClientAuth.NONE, null, (File) null, false); } /** * @deprecated Use {@link ServerSslConfig#builder()} instead. */ @Deprecated public ServerSslConfig( String[] protocols, String[] ciphers, File certChainFile, File keyFile, ClientAuth clientAuth) { this(protocols, ciphers, certChainFile, keyFile, clientAuth, null, (File) null, true); } /** * @deprecated Use {@link ServerSslConfig#builder()} instead. */ @Deprecated public ServerSslConfig( String[] protocols, String[] ciphers, File certChainFile, File keyFile, ClientAuth clientAuth, File clientAuthTrustStoreFile, File clientAuthTrustStorePasswordFile, boolean sessionTicketsEnabled) { this.protocols = protocols; this.ciphers = ciphers != null ? Arrays.asList(ciphers) : null; this.certChainFile = certChainFile; this.keyFile = keyFile; this.clientAuth = clientAuth; this.clientAuthTrustStoreFile = clientAuthTrustStoreFile; this.clientAuthTrustStorePassword = null; this.clientAuthTrustStorePasswordFile = clientAuthTrustStorePasswordFile; this.sessionTimeout = DEFAULT_SESSION_TIMEOUT.get(); this.sessionTicketsEnabled = sessionTicketsEnabled; } /** * @deprecated Use {@link ServerSslConfig#builder()} instead. */ @Deprecated public ServerSslConfig( String[] protocols, String[] ciphers, File certChainFile, File keyFile, ClientAuth clientAuth, File clientAuthTrustStoreFile, String clientAuthTrustStorePassword, boolean sessionTicketsEnabled) { this.protocols = protocols; this.ciphers = Arrays.asList(ciphers); this.certChainFile = certChainFile; this.keyFile = keyFile; this.clientAuth = clientAuth; this.clientAuthTrustStoreFile = clientAuthTrustStoreFile; this.clientAuthTrustStorePassword = clientAuthTrustStorePassword; this.clientAuthTrustStorePasswordFile = null; this.sessionTimeout = DEFAULT_SESSION_TIMEOUT.get(); this.sessionTicketsEnabled = sessionTicketsEnabled; } public static List getDefaultCiphers() { return DEFAULT_CIPHERS; } /** * @deprecated Use {@link ServerSslConfig#builder()} instead. */ @Deprecated public static ServerSslConfig withDefaultCiphers(File certChainFile, File keyFile, String... protocols) { return ServerSslConfig.builder() .protocols(protocols) .ciphers(getDefaultCiphers()) .certChainFile(certChainFile) .keyFile(keyFile) .build(); } @Override public String toString() { return "ServerSslConfig{" + "protocols=" + Arrays.toString(protocols) + ", ciphers=" + ciphers + ", certChainFile=" + certChainFile + ", keyFile=" + keyFile + ", clientAuth=" + clientAuth + ", clientAuthTrustStoreFile=" + clientAuthTrustStoreFile + ", sessionTimeout=" + sessionTimeout + ", sessionTicketsEnabled=" + sessionTicketsEnabled + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/ssl/SslHandshakeInfo.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.ssl; import com.netflix.zuul.netty.server.psk.ClientPSKIdentityInfo; import io.netty.handler.ssl.ClientAuth; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; /** * Captures TLS handshake details for a connection, including the negotiated protocol, * cipher suite, named group, and client authentication state. */ @Builder @AllArgsConstructor(access = AccessLevel.PACKAGE) public class SslHandshakeInfo { private final String requestedSni; private final String protocol; private final String cipherSuite; private final String namedGroup; private final ClientAuth clientAuthRequirement; private final Certificate serverCertificate; private final X509Certificate clientCertificate; private final boolean isOfIntermediary; private final boolean usingExternalPSK; private final ClientPSKIdentityInfo clientPSKIdentityInfo; /** * Use {@link SslHandshakeInfo#builder()} instead. */ @Deprecated public SslHandshakeInfo( boolean isOfIntermediary, String protocol, String cipherSuite, ClientAuth clientAuthRequirement, Certificate serverCertificate, X509Certificate clientCertificate) { this("", isOfIntermediary, protocol, cipherSuite, clientAuthRequirement, serverCertificate, clientCertificate); } /** * Use {@link SslHandshakeInfo#builder()} instead. */ @Deprecated public SslHandshakeInfo( String requestedSni, boolean isOfIntermediary, String protocol, String cipherSuite, ClientAuth clientAuthRequirement, Certificate serverCertificate, X509Certificate clientCertificate) { this( requestedSni, isOfIntermediary, protocol, cipherSuite, clientAuthRequirement, serverCertificate, clientCertificate, false, null); } /** * Use {@link SslHandshakeInfo#builder()} instead. */ @Deprecated public SslHandshakeInfo( boolean isOfIntermediary, String protocol, String cipherSuite, ClientAuth clientAuthRequirement, Certificate serverCertificate, X509Certificate clientCertificate, boolean usingExternalPSK, ClientPSKIdentityInfo clientPSKIdentityInfo) { this( "", isOfIntermediary, protocol, cipherSuite, clientAuthRequirement, serverCertificate, clientCertificate, usingExternalPSK, clientPSKIdentityInfo); } /** * Use {@link SslHandshakeInfo#builder()} instead. */ @Deprecated public SslHandshakeInfo( String requestedSni, boolean isOfIntermediary, String protocol, String cipherSuite, ClientAuth clientAuthRequirement, Certificate serverCertificate, X509Certificate clientCertificate, boolean usingExternalPSK, ClientPSKIdentityInfo clientPSKIdentityInfo) { this.requestedSni = requestedSni; this.protocol = protocol; this.cipherSuite = cipherSuite; this.namedGroup = null; this.clientAuthRequirement = clientAuthRequirement; this.serverCertificate = serverCertificate; this.clientCertificate = clientCertificate; this.isOfIntermediary = isOfIntermediary; this.usingExternalPSK = usingExternalPSK; this.clientPSKIdentityInfo = clientPSKIdentityInfo; } public String getRequestedSni() { return requestedSni; } public boolean isOfIntermediary() { return isOfIntermediary; } public String getProtocol() { return protocol; } public String getCipherSuite() { return cipherSuite; } public String getNamedGroup() { return namedGroup; } public ClientAuth getClientAuthRequirement() { return clientAuthRequirement; } public Certificate getServerCertificate() { return serverCertificate; } public X509Certificate getClientCertificate() { return clientCertificate; } public boolean usingExternalPSK() { return usingExternalPSK; } public ClientPSKIdentityInfo geClientPSKIdentityInfo() { return clientPSKIdentityInfo; } @Override public String toString() { return "SslHandshakeInfo{" + "protocol='" + protocol + '\'' + ", cipherSuite='" + cipherSuite + '\'' + ", namedGroup='" + namedGroup + '\'' + ", clientAuthRequirement=" + clientAuthRequirement + ", serverCertificate=" + serverCertificate + ", clientCertificate=" + clientCertificate + ", isOfIntermediary=" + isOfIntermediary + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/status/ServerStatusManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.status; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; import jakarta.inject.Inject; import jakarta.inject.Singleton; /** * User: michaels@netflix.com * Date: 7/6/17 * Time: 3:37 PM */ @Singleton public class ServerStatusManager { private final ApplicationInfoManager applicationInfoManager; @Inject public ServerStatusManager(ApplicationInfoManager applicationInfoManager) { this.applicationInfoManager = applicationInfoManager; } public void localStatus(InstanceInfo.InstanceStatus status) { applicationInfoManager.setInstanceStatus(status); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/throttle/MaxInboundConnectionsHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.throttle; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Closes any incoming new connections if current count is above a configured threshold. * * When a connection is throttled, the channel is closed, and then a CONNECTION_THROTTLED_EVENT event is fired * not notify any other interested handlers. * */ @ChannelHandler.Sharable public class MaxInboundConnectionsHandler extends ChannelInboundHandlerAdapter { public static final AttributeKey ATTR_CH_THROTTLED = AttributeKey.newInstance("_channel_throttled"); private static final Logger LOG = LoggerFactory.getLogger(MaxInboundConnectionsHandler.class); private static final AtomicInteger connections = new AtomicInteger(0); private final Counter connectionThrottled; private final int maxConnections; public MaxInboundConnectionsHandler(Registry registry, String metricId, int maxConnections) { this.maxConnections = maxConnections; this.connectionThrottled = registry.counter("server.connections.throttled", "id", metricId); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { if (maxConnections > 0) { int currentCount = connections.getAndIncrement(); if (currentCount + 1 > maxConnections) { LOG.warn( "Throttling incoming connection as above configured max connections threshold of {}", maxConnections); Channel channel = ctx.channel(); channel.attr(ATTR_CH_THROTTLED).set(Boolean.TRUE); CurrentPassport.fromChannel(channel).add(PassportState.SERVER_CH_THROTTLING); channel.close(); connectionThrottled.increment(); } } super.channelActive(ctx); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (ctx.channel().attr(ATTR_CH_THROTTLED).get() != null) { // Discard this msg as channel is in process of being closed. ReferenceCountUtil.safeRelease(msg); } else { super.channelRead(ctx, msg); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (maxConnections > 0) { connections.decrementAndGet(); } super.channelInactive(ctx); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/throttle/RejectionType.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.throttle; /** * Indicates a rejection type for DoS protection. While similar, rejection is distinct from throttling in that * throttling is intended for non-malicious traffic. */ public enum RejectionType { // "It's not you, it's me." /** * Indicates that the request should not be allowed. An HTTP response will be generated as a result. */ REJECT, /** * Indicates that the connection should be closed, not allowing the request to proceed. No HTTP response will be * returned. */ CLOSE, /** * Allows the request to proceed, followed by closing the connection. This is typically used in conjunction with * throttling handling, where the response may need to be handled by the filter chain. It is not expected that the * request will be proxied. */ ALLOW_THEN_CLOSE; } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/throttle/RejectionUtils.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.throttle; import com.netflix.netty.common.ConnectionCloseChannelAttributes; import com.netflix.netty.common.proxyprotocol.HAProxyMessageChannelHandler; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import com.netflix.zuul.stats.status.StatusCategory; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.EmptyHttpHeaders; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; 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.util.ReferenceCountUtil; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * A collection of rejection related utilities useful for failing requests. These are tightly coupled with the channel * pipeline, but can be called from different handlers. */ public final class RejectionUtils { // TODO(carl-mastrangelo): add tests for this. public static final HttpResponseStatus REJECT_CLOSING_STATUS = new HttpResponseStatus(999, "Closing(Rejection)"); /** * Closes the connection without sending a response, and fires a {@link RequestRejectedEvent} back up the pipeline. * * @param nfStatus the status to use for metric reporting * @param reason the reason for rejecting the request. This is not sent back to the client. * @param request the request that is being rejected. * @param injectedLatencyMillis optional parameter to delay sending a response. The reject notification is still * sent up the pipeline. */ public static void rejectByClosingConnection( ChannelHandlerContext ctx, StatusCategory nfStatus, String reason, HttpRequest request, @Nullable Integer injectedLatencyMillis) { // Notify other handlers before closing the conn. notifyHandlers(ctx, nfStatus, REJECT_CLOSING_STATUS, reason, request); if (injectedLatencyMillis != null && injectedLatencyMillis > 0) { // Delay closing the connection for configured time. ctx.executor() .schedule( () -> { CurrentPassport.fromChannel(ctx.channel()).add(PassportState.SERVER_CH_REJECTING); ctx.close(); }, injectedLatencyMillis, TimeUnit.MILLISECONDS); } else { // Close the connection immediately. CurrentPassport.fromChannel(ctx.channel()).add(PassportState.SERVER_CH_REJECTING); ctx.close(); } } /** * Sends a rejection response back to the client, and fires a {@link RequestRejectedEvent} back up the pipeline. * * @param ctx the channel handler processing the request * @param nfStatus the status to use for metric reporting * @param reason the reason for rejecting the request. This is not sent back to the client. * @param request the request that is being rejected. * @param injectedLatencyMillis optional parameter to delay sending a response. The reject notification is still * sent up the pipeline. * @param rejectedCode the HTTP code to send back to the client. * @param rejectedBody the HTTP body to be sent back. It is assumed to be of type text/plain. * @param rejectionHeaders additional HTTP headers to add to the rejection response */ public static void sendRejectionResponse( ChannelHandlerContext ctx, StatusCategory nfStatus, String reason, HttpRequest request, @Nullable Integer injectedLatencyMillis, HttpResponseStatus rejectedCode, String rejectedBody, Map rejectionHeaders) { boolean shouldClose = closeConnectionAfterReject(ctx.channel()); // Write out a rejection response message. FullHttpResponse response = createRejectionResponse(rejectedCode, rejectedBody, shouldClose, rejectionHeaders); if (injectedLatencyMillis != null && injectedLatencyMillis > 0) { // Delay writing the response for configured time. ctx.executor() .schedule( () -> { CurrentPassport.fromChannel(ctx.channel()).add(PassportState.IN_REQ_REJECTED); ctx.writeAndFlush(response); }, injectedLatencyMillis, TimeUnit.MILLISECONDS); } else { // Write the response immediately. CurrentPassport.fromChannel(ctx.channel()).add(PassportState.IN_REQ_REJECTED); ctx.writeAndFlush(response); } // Notify other handlers that we've rejected this request. notifyHandlers(ctx, nfStatus, rejectedCode, reason, request); } /** * Marks the given channel for being closed after the next response. * * @param ctx the channel handler processing the request */ public static void allowThenClose(ChannelHandlerContext ctx) { // Just flag this channel to be closed after response complete. ctx.channel() .attr(ConnectionCloseChannelAttributes.CLOSE_AFTER_RESPONSE) .set(ctx.newPromise()); // And allow this request through without rejecting. } /** * Throttle either by sending rejection response message, or by closing the connection now, or just drop the * message. Only call this if ThrottleResult.shouldThrottle() returned {@code true}. * * @param ctx the channel handler processing the request * @param msg the request that is being rejected. * @param rejectionType the type of rejection * @param nfStatus the status to use for metric reporting * @param reason the reason for rejecting the request. This is not sent back to the client. * @param injectedLatencyMillis optional parameter to delay sending a response. The reject notification is still * sent up the pipeline. * @param rejectedCode the HTTP code to send back to the client. * @param rejectedBody the HTTP body to be sent back. It is assumed to be of type text/plain. * @param rejectionHeaders additional HTTP headers to add to the rejection response */ public static void handleRejection( ChannelHandlerContext ctx, Object msg, RejectionType rejectionType, StatusCategory nfStatus, String reason, @Nullable Integer injectedLatencyMillis, HttpResponseStatus rejectedCode, String rejectedBody, Map rejectionHeaders) { boolean shouldDropMessage = false; if (rejectionType == RejectionType.REJECT || rejectionType == RejectionType.CLOSE) { shouldDropMessage = true; } boolean shouldRejectNow = false; if (rejectionType == RejectionType.REJECT && msg instanceof LastHttpContent) { shouldRejectNow = true; } else if (rejectionType == RejectionType.CLOSE && msg instanceof HttpRequest) { shouldRejectNow = true; } else if (rejectionType == RejectionType.ALLOW_THEN_CLOSE && msg instanceof HttpRequest) { shouldRejectNow = true; } if (shouldRejectNow) { // Send a rejection response. HttpRequest request = msg instanceof HttpRequest ? (HttpRequest) msg : null; reject( ctx, rejectionType, nfStatus, reason, request, injectedLatencyMillis, rejectedCode, rejectedBody, rejectionHeaders); } if (shouldDropMessage) { ReferenceCountUtil.safeRelease(msg); } else { ctx.fireChannelRead(msg); } } /** * Switches on the rejection type to decide how to reject the request and or close the conn. * * @param ctx the channel handler processing the request * @param rejectionType the type of rejection * @param nfStatus the status to use for metric reporting * @param reason the reason for rejecting the request. This is not sent back to the client. * @param request the request that is being rejected. * @param injectedLatencyMillis optional parameter to delay sending a response. The reject notification is still * sent up the pipeline. * @param rejectedCode the HTTP code to send back to the client. * @param rejectedBody the HTTP body to be sent back. It is assumed to be of type text/plain. */ public static void reject( ChannelHandlerContext ctx, RejectionType rejectionType, StatusCategory nfStatus, String reason, HttpRequest request, @Nullable Integer injectedLatencyMillis, HttpResponseStatus rejectedCode, String rejectedBody) { reject( ctx, rejectionType, nfStatus, reason, request, injectedLatencyMillis, rejectedCode, rejectedBody, Collections.emptyMap()); } /** * Switches on the rejection type to decide how to reject the request and or close the conn. * * @param ctx the channel handler processing the request * @param rejectionType the type of rejection * @param nfStatus the status to use for metric reporting * @param reason the reason for rejecting the request. This is not sent back to the client. * @param request the request that is being rejected. * @param injectedLatencyMillis optional parameter to delay sending a response. The reject notification is still * sent up the pipeline. * @param rejectedCode the HTTP code to send back to the client. * @param rejectedBody the HTTP body to be sent back. It is assumed to be of type text/plain. * @param rejectionHeaders additional HTTP headers to add to the rejection response */ public static void reject( ChannelHandlerContext ctx, RejectionType rejectionType, StatusCategory nfStatus, String reason, HttpRequest request, @Nullable Integer injectedLatencyMillis, HttpResponseStatus rejectedCode, String rejectedBody, Map rejectionHeaders) { switch (rejectionType) { case REJECT: sendRejectionResponse( ctx, nfStatus, reason, request, injectedLatencyMillis, rejectedCode, rejectedBody, rejectionHeaders); return; case CLOSE: rejectByClosingConnection(ctx, nfStatus, reason, request, injectedLatencyMillis); return; case ALLOW_THEN_CLOSE: allowThenClose(ctx); return; } throw new AssertionError("Bad rejection type: " + rejectionType); } private static void notifyHandlers( ChannelHandlerContext ctx, StatusCategory nfStatus, HttpResponseStatus status, String reason, HttpRequest request) { RequestRejectedEvent event = new RequestRejectedEvent(request, nfStatus, status, reason); // Send this from the beginning of the pipeline, as it may be sent from the ClientRequestReceiver. ctx.pipeline().fireUserEventTriggered(event); } private static boolean closeConnectionAfterReject(Channel channel) { if (channel.hasAttr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION)) { return channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION) .get() == HAProxyProtocolVersion.V2; } else { return false; } } private static FullHttpResponse createRejectionResponse( HttpResponseStatus status, String plaintextMessage, boolean closeConnection, Map rejectionHeaders) { ByteBuf body = Unpooled.wrappedBuffer(plaintextMessage.getBytes(StandardCharsets.UTF_8)); int length = body.readableBytes(); DefaultHttpHeaders headers = new DefaultHttpHeaders(); headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"); headers.set(HttpHeaderNames.CONTENT_LENGTH, length); if (closeConnection) { headers.set(HttpHeaderNames.CONNECTION, "close"); } rejectionHeaders.forEach(headers::add); return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, body, headers, EmptyHttpHeaders.INSTANCE); } private RejectionUtils() {} } ================================================ FILE: zuul-core/src/main/java/com/netflix/netty/common/throttle/RequestRejectedEvent.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.throttle; import com.netflix.zuul.stats.status.StatusCategory; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import jakarta.annotation.Nullable; public record RequestRejectedEvent( HttpRequest request, StatusCategory nfStatus, HttpResponseStatus httpStatus, String reason, @Nullable String reasonMessage) { public RequestRejectedEvent( HttpRequest request, StatusCategory nfStatus, HttpResponseStatus httpStatus, String reason) { this(request, nfStatus, httpStatus, reason, null); } // leaving behind old getters for backwards compatibility public StatusCategory getNfStatus() { return nfStatus; } public HttpResponseStatus getHttpStatus() { return httpStatus; } public String getReason() { return reason; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/Attrs.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.google.common.annotations.VisibleForTesting; import java.util.Collections; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; import javax.annotation.Nullable; /** * A heterogeneous map of attributes. * *

Implementation Note: this class is not a proper Map or Collection, in order to encourage callers * to refer to the keys by their literal name. In the past, finding where a Key was used was difficult, * so this class is somewhat of an experiment to try to make tracking down usage easier. If it becomes * too onerous to use this, consider making this class extend AbstractMap. */ public final class Attrs { final IdentityHashMap, Object> storage = new IdentityHashMap<>(); public static Key newKey(String keyName) { return new Key<>(keyName); } public static final class Key { private final String name; /** * Returns the value in the attributes, or {@code null} if absent. */ @Nullable @SuppressWarnings("unchecked") public T get(Attrs attrs) { Objects.requireNonNull(attrs, "attrs"); return (T) attrs.storage.get(this); } /** * Returns the value in the attributes or {@code defaultValue} if absent. * @throws NullPointerException if defaultValue is null. */ @SuppressWarnings("unchecked") public T getOrDefault(Attrs attrs, T defaultValue) { Objects.requireNonNull(attrs, "attrs"); Objects.requireNonNull(defaultValue, "defaultValue"); T result = (T) attrs.storage.get(this); if (result != null) { return result; } return defaultValue; } public void put(Attrs attrs, T value) { Objects.requireNonNull(attrs, "attrs"); Objects.requireNonNull(value); attrs.storage.put(this, value); } public String name() { return name; } private Key(String name) { this.name = Objects.requireNonNull(name); } @Override public String toString() { return "Key{" + name + '}'; } } private Attrs() {} public static Attrs newInstance() { return new Attrs(); } public Set> keySet() { return Collections.unmodifiableSet(new LinkedHashSet<>(storage.keySet())); } public void forEach(BiConsumer, Object> consumer) { storage.forEach(consumer); } public int size() { return storage.size(); } @Override public String toString() { return "Attrs{" + storage + '}'; } @Override @VisibleForTesting public boolean equals(Object other) { if (!(other instanceof Attrs)) { return false; } Attrs that = (Attrs) other; return Objects.equals(this.storage, that.storage); } @Override @VisibleForTesting public int hashCode() { return Objects.hash(storage); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/BasicFilterUsageNotifier.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.spectator.api.Registry; import com.netflix.zuul.filters.ZuulFilter; import jakarta.inject.Inject; /** * Publishes a counter metric for each filter on each use. */ public class BasicFilterUsageNotifier implements FilterUsageNotifier { private final Registry registry; @Inject public BasicFilterUsageNotifier(Registry registry) { this.registry = registry; } @Override public void notify(ZuulFilter filter, ExecutionStatus status) { registry.counter( "zuul.filter-" + filter.getClass().getSimpleName(), "status", status.name(), "filtertype", filter.filterType().toString()) .increment(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/BasicRequestCompleteHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.stats.RequestMetricsPublisher; import jakarta.inject.Inject; import javax.annotation.Nullable; /** * User: michaels@netflix.com * Date: 6/4/15 * Time: 4:26 PM */ public class BasicRequestCompleteHandler implements RequestCompleteHandler { @Inject @Nullable private RequestMetricsPublisher requestMetricsPublisher; @Override public void handle(HttpRequestInfo inboundRequest, HttpResponseMessage response) { SessionContext context = inboundRequest.getContext(); // Publish request-level metrics. if (requestMetricsPublisher != null) { requestMetricsPublisher.collectAndPublish(context); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/DefaultFilterFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.filters.ZuulFilter; import java.lang.reflect.InvocationTargetException; /** * Default factory for creating instances of ZuulFilter. */ public class DefaultFilterFactory implements FilterFactory { /** * Returns a new implementation of ZuulFilter as specified by the provided * Class. The Class is instantiated using its nullary constructor. * * @param clazz the Class to instantiate * @return A new instance of ZuulFilter */ @Override public ZuulFilter newInstance(Class clazz) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException { return (ZuulFilter) clazz.getDeclaredConstructor().newInstance(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/DynamicFilterLoader.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.filters.FilterRegistry; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton public final class DynamicFilterLoader implements FilterLoader { private static final Logger LOG = LoggerFactory.getLogger(DynamicFilterLoader.class); private final ConcurrentMap filterClassLastModified = new ConcurrentHashMap<>(); private final ConcurrentMap>> hashFiltersByType = new ConcurrentHashMap<>(); private final ConcurrentMap> filtersByNameAndType = new ConcurrentHashMap<>(); private final FilterRegistry filterRegistry; private final FilterFactory filterFactory; @Inject public DynamicFilterLoader(FilterRegistry filterRegistry, FilterFactory filterFactory) { this.filterRegistry = filterRegistry; this.filterFactory = filterFactory; } /** * @return the total number of Zuul filters */ public int filterInstanceMapSize() { return filterRegistry.size(); } private void putFilter(String filterName, ZuulFilter filter, long lastModified) { if (!filterRegistry.isMutable()) { LOG.warn("Filter registry is not mutable, discarding {}", filterName); return; } SortedSet> set = hashFiltersByType.get(filter.filterType()); if (set != null) { hashFiltersByType.remove(filter.filterType()); // rebuild this list } String nameAndType = filter.filterType() + ":" + filter.filterName(); filtersByNameAndType.put(nameAndType, filter); filterRegistry.put(filterName, filter); filterClassLastModified.put(filterName, lastModified); } /** * Load and cache filters by className * * @param classNames The class names to load * @return List of the loaded filters * @throws Exception If any specified filter fails to load, this will abort. This is a safety mechanism so we can * prevent running in a partially loaded state. */ @Override public List> putFiltersForClasses(String[] classNames) throws Exception { List> newFilters = new ArrayList<>(); for (String className : classNames) { newFilters.add(putFilterForClassName(className)); } return Collections.unmodifiableList(newFilters); } @Override public ZuulFilter putFilterForClassName(String className) throws Exception { Class clazz = Class.forName(className); if (!ZuulFilter.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("Specified filter class does not implement ZuulFilter interface!"); } else { ZuulFilter filter = filterFactory.newInstance(clazz); putFilter(className, filter, System.currentTimeMillis()); return filter; } } /** * Returns a list of filters by the filterType specified */ @Override public SortedSet> getFiltersByType(FilterType filterType) { SortedSet> set = hashFiltersByType.get(filterType); if (set != null) { return set; } set = new TreeSet<>(FILTER_COMPARATOR); for (ZuulFilter filter : filterRegistry.getAllFilters()) { if (filter.filterType().equals(filterType)) { set.add(filter); } } hashFiltersByType.putIfAbsent(filterType, set); return Collections.unmodifiableSortedSet(set); } @Override public ZuulFilter getFilterByNameAndType(String name, FilterType type) { if (name == null || type == null) { return null; } String nameAndType = type + ":" + name; return filtersByNameAndType.get(nameAndType); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/ExecutionStatus.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; public enum ExecutionStatus { SUCCESS, SKIPPED, DISABLED, FAILED, BODY_AWAIT, ASYNC_AWAIT, } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/Filter.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.filters.FilterSyncType; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Identifies a {@link ZuulFilter}. */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Filter { /** * The order in which to run. See {@link ZuulFilter#filterOrder()}. */ int order(); /** * Indicates the type of this filter. */ FilterType type() default FilterType.INBOUND; /** * Category of the filter. */ FilterCategory category() default FilterCategory.HTTP; /** * Indicates if this is a synchronous filter. */ FilterSyncType sync() default FilterSyncType.SYNC; /** * Indicates if this filter has any constraints that should prevent it from executing */ Class[] constraints() default {}; @Target({ElementType.PACKAGE}) @Retention(RetentionPolicy.CLASS) @Documented @interface FilterPackageName { String value(); } /** * Indicates that the annotated filter should run after another filter in the chain, if the other filter is present. * In the case of inbound filters, this implies that the annotated filter should have an order greater than the * filters listed. For outbound filters, the order of this filter should be less than the ones listed. Usage of * this annotation should be used on homogeneous filter types. Additionally, this should not be applied to endpoint * filters. */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface ApplyAfter { Class>[] value(); } /** * Indicates that the annotated filter should run before another filter in the chain, if the other filter is present. * In the case of inbound filters, this implies that the annotated filter should have an order less than the * filters listed. For outbound filters, the order of this filter should be greater than the ones listed. Usage of * this annotation should be used on homogeneous filter types. Additionally, this should not be applied to endpoint * filters. * *

Prefer to use this {@link ApplyAfter} instead. This annotation is meant in case where it may be infeasible * to use {@linkplain ApplyAfter}. (such as due to dependency cycles) * * @see ApplyAfter */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface ApplyBefore { Class>[] value(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/FilterCategory.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul; /** * Categorization of filters. */ public enum FilterCategory { ABUSE( "abuse", "Abuse detection and protection filters, such as rate-limiting, malicious request detection, geo-blocking"), ACCESS("access", "Authentication and authorization filters"), ADMIN("admin", "Admin only filters providing operational support"), CHAOS("chaos", "Failure injection testing and resilience support"), CONTEXT_DECORATOR("context-decorator", "Decorate context based on request and detected client"), HEALTHCHECK("healthcheck", "Support for healthcheck endpoints"), HTTP("http", "Filter operating on HTTP request/response protocol features"), ORIGIN("origin", "Origin connectivity filters"), OBSERVABILITY("observability", "Filters providing observability features"), OVERLOAD("overload", "Filters to respond on the server being in an overloaded state such as brownout"), ROUTING("routing", "Filters which make routing decisions"), UNSPECIFIED("unspecified", "Default category when no category is specified"), ; private final String code; private final String description; FilterCategory(String code, String description) { this.code = code; this.description = description; } public String getCode() { return code; } public String getDescription() { return description; } @Override public String toString() { return code; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/FilterConstraint.java ================================================ /* * Copyright 2026 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.message.ZuulMessage; import lombok.NonNull; /** * A filter constraint can be registered on {@link Filter#constraints()} to indicate that a given filter should * not be run against a ZuulMessage. FilterConstraint's act as a centralized way to implement logic that would otherwise * need to be duplicated across multiple {@link com.netflix.zuul.filters.ShouldFilter#shouldFilter(ZuulMessage)} * * @author Justin Guerra * @since 1/9/26 */ public interface FilterConstraint { boolean isConstrained(@NonNull ZuulMessage msg); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/FilterFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.filters.ZuulFilter; /** * Interface to provide instances of ZuulFilter from a given class. */ public interface FilterFactory { /** * Returns an instance of the specified class. * * @param clazz the Class to instantiate * @return an instance of ZuulFilter * @throws Exception if an error occurs */ ZuulFilter newInstance(Class clazz) throws Exception; } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/FilterFileManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class manages the loading of filters from a set of known packages. * * @author Mikey Cohen * Date: 12/7/11 * Time: 12:09 PM */ @Singleton public class FilterFileManager { private static final Logger LOG = LoggerFactory.getLogger(FilterFileManager.class); private final FilterFileManagerConfig config; private final FilterLoader filterLoader; @Inject public FilterFileManager(FilterFileManagerConfig config, FilterLoader filterLoader) { this.config = config; this.filterLoader = filterLoader; } @Inject public void init() throws Exception { if (!config.enabled) { return; } long startTime = System.currentTimeMillis(); filterLoader.putFiltersForClasses(config.getClassNames()); LOG.warn("Finished loading all zuul filters. Duration = {} ms.", (System.currentTimeMillis() - startTime)); } public static class FilterFileManagerConfig { private final String[] classNames; boolean enabled; public FilterFileManagerConfig(String[] classNames) { this(classNames, true); } public FilterFileManagerConfig(String[] classNames, boolean enabled) { this.classNames = classNames; this.enabled = enabled; } public String[] getClassNames() { return classNames; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/FilterLoader.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.SortedSet; /** * This class is one of the core classes in Zuul. It compiles, loads from a File, and checks if source code changed. * It also holds ZuulFilters by filterType. */ public interface FilterLoader { /** * Load and cache filters by className. * * @param classNames The class names to load * @return List of the loaded filters * @throws Exception If any specified filter fails to load, this will abort. This is a safety mechanism so we can * prevent running in a partially loaded state. */ List> putFiltersForClasses(String[] classNames) throws Exception; ZuulFilter putFilterForClassName(String className) throws Exception; /** * Returns a sorted set of filters by the filterType specified. */ SortedSet> getFiltersByType(FilterType filterType); ZuulFilter getFilterByNameAndType(String name, FilterType type); Comparator> FILTER_COMPARATOR = Comparator.>comparingInt(ZuulFilter::filterOrder).thenComparing(ZuulFilter::filterName); Comparator>> FILTER_CLASS_COMPARATOR = Comparator.>>comparingInt( c -> Objects.requireNonNull(c.getAnnotation(Filter.class), () -> "missing annotation: " + c) .order()) .thenComparing(Class::getName); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/FilterUsageNotifier.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.filters.ZuulFilter; /** * Interface to implement for registering a callback for each time a filter * is used. * * User: michaels * Date: 5/13/14 * Time: 9:55 PM */ public interface FilterUsageNotifier { void notify(ZuulFilter filter, ExecutionStatus status); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/RequestCompleteHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpResponseMessage; public interface RequestCompleteHandler { void handle(HttpRequestInfo inboundRequest, HttpResponseMessage response); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/StaticFilterLoader.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul; import com.google.errorprone.annotations.DoNotCall; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import jakarta.inject.Inject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An immutable static collection of filters. */ public final class StaticFilterLoader implements FilterLoader { private static final Logger logger = LoggerFactory.getLogger(StaticFilterLoader.class); public static final String RESOURCE_NAME = "META-INF/zuul/allfilters"; private final Map>> filtersByType; private final Map>> filtersByTypeAndName; @Inject public StaticFilterLoader( FilterFactory filterFactory, Set>> filterTypes) { Map>> filtersByType = new EnumMap<>(FilterType.class); Map>> filtersByName = new EnumMap<>(FilterType.class); for (Class> clz : filterTypes) { try { ZuulFilter f = filterFactory.newInstance(clz); filtersByType .computeIfAbsent(f.filterType(), k -> new TreeSet<>(FILTER_COMPARATOR)) .add(f); filtersByName .computeIfAbsent(f.filterType(), k -> new HashMap<>()) .put(f.filterName(), f); } catch (RuntimeException | Error e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } for (Entry>> entry : filtersByType.entrySet()) { entry.setValue(Collections.unmodifiableSortedSet(entry.getValue())); } Map>> immutableFiltersByName = new EnumMap<>(FilterType.class); for (Entry>> entry : filtersByName.entrySet()) { immutableFiltersByName.put(entry.getKey(), Collections.unmodifiableMap(entry.getValue())); } this.filtersByTypeAndName = Collections.unmodifiableMap(immutableFiltersByName); this.filtersByType = Collections.unmodifiableMap(filtersByType); } public static Set>> loadFilterTypesFromResources(ClassLoader loader) throws IOException { Set>> filterTypes = new LinkedHashSet<>(); for (URL url : Collections.list(loader.getResources(RESOURCE_NAME))) { try (InputStream is = url.openStream(); InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { String line; while ((line = br.readLine()) != null) { String trimmed = line.trim(); if (!trimmed.isEmpty()) { Class clz; try { clz = Class.forName(trimmed, false, loader); } catch (ClassNotFoundException e) { // This can happen if a filter is deleted, but the annotation processor doesn't // remove it from the list. This is mainly a problem with IntelliJ, which // forces append only annotation processors. Incremental recompilation drops // most of the classpath, making the processor unable to reconstruct the filter // list. To work around this problem, use the stale, cached filter list from // the initial full compilation and add to it. This makes incremental // compilation work later, at the cost of polluting the filter list. It's a // better experience to log a warning (and do a clean build), than to // mysteriously classes. logger.warn("Missing Filter", e); continue; } @SuppressWarnings("unchecked") Class> filterClz = (Class>) clz.asSubclass(ZuulFilter.class); filterTypes.add(filterClz); } } } } return Collections.unmodifiableSet(filterTypes); } @Override @DoNotCall public List> putFiltersForClasses(String[] classNames) { throw new UnsupportedOperationException(); } @Override @DoNotCall public ZuulFilter putFilterForClassName(String className) { throw new UnsupportedOperationException(); } @Override public SortedSet> getFiltersByType(FilterType filterType) { return filtersByType.get(filterType); } @Override @Nullable public ZuulFilter getFilterByNameAndType(String name, FilterType type) { Map> filtersByName = filtersByTypeAndName.get(type); if (filtersByName == null) { return null; } return filtersByName.get(name); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/ZuulApplicationInfo.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul; /** * Metadata about the Zuul instance/ application name and "stack" * @author Mikey Cohen * Date: 2/15/13 * Time: 1:56 PM */ public class ZuulApplicationInfo { public static String applicationName; public static String stack; public static String getApplicationName() { return applicationName; } public static void setApplicationName(String applicationName) { ZuulApplicationInfo.applicationName = applicationName; } public static String getStack() { return stack; } public static void setStack(String stack) { ZuulApplicationInfo.stack = stack; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/constants/ZuulConstants.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.constants; /** * property constants * Date: 5/15/13 * Time: 2:22 PM */ public class ZuulConstants { public static final String ZUUL_NIWS_CLIENTLIST = "zuul.niws.clientlist"; public static final String DEFAULT_NFASTYANAX_READCONSISTENCY = "default.nfastyanax.readConsistency"; public static final String DEFAULT_NFASTYANAX_WRITECONSISTENCY = "default.nfastyanax.writeConsistency"; public static final String DEFAULT_NFASTYANAX_SOCKETTIMEOUT = "default.nfastyanax.socketTimeout"; public static final String DEFAULT_NFASTYANAX_MAXCONNSPERHOST = "default.nfastyanax.maxConnsPerHost"; public static final String DEFAULT_NFASTYANAX_MAXTIMEOUTWHENEXHAUSTED = "default.nfastyanax.maxTimeoutWhenExhausted"; public static final String DEFAULT_NFASTYANAX_MAXFAILOVERCOUNT = "default.nfastyanax.maxFailoverCount"; public static final String DEFAULT_NFASTYANAX_FAILOVERWAITTIME = "default.nfastyanax.failoverWaitTime"; public static final String ZUUL_CASSANDRA_KEYSPACE = "zuul.cassandra.keyspace"; public static final String ZUUL_CASSANDRA_MAXCONNECTIONSPERHOST = "zuul.cassandra.maxConnectionsPerHost"; public static final String ZUUL_CASSANDRA_HOST = "zuul.cassandra.host"; public static final String ZUUL_CASSANDRA_PORT = "zuul.cassandra.port"; public static final String ZUUL_EUREKA = "zuul.eureka."; public static final String ZUUL_AUTODETECT_BACKEND_VIPS = "zuul.autodetect-backend-vips"; public static final String ZUUL_RIBBON_NAMESPACE = "zuul.ribbon.namespace"; public static final String ZUUL_RIBBON_VIPADDRESS_TEMPLATE = "zuul.ribbon.vipAddress.template"; public static final String ZUUL_CASSANDRA_CACHE_MAX_SIZE = "zuul.cassandra.cache.max-size"; public static final String ZUUL_HTTPCLIENT = "zuul.httpClient."; public static final String ZUUL_USE_ACTIVE_FILTERS = "zuul.use.active.filters"; public static final String ZUUL_USE_CANARY_FILTERS = "zuul.use.canary.filters"; public static final String ZUUL_FILTER_PRE_PATH = "zuul.filter.pre.path"; public static final String ZUUL_FILTER_POST_PATH = "zuul.filter.post.path"; public static final String ZUUL_FILTER_ROUTING_PATH = "zuul.filter.routing.path"; public static final String ZUUL_FILTER_CUSTOM_PATH = "zuul.filter.custom.path"; public static final String ZUUL_FILTER_ADMIN_ENABLED = "zuul.filter.admin.enabled"; public static final String ZUUL_FILTER_ADMIN_REDIRECT = "zuul.filter.admin.redirect.path"; public static final String ZUUL_DEBUG_REQUEST = "zuul.debug.request"; public static final String ZUUL_DEBUG_PARAMETER = "zuul.debug.parameter"; public static final String ZUUL_ROUTER_ALT_ROUTE_VIP = "zuul.router.alt.route.vip"; public static final String ZUUL_ROUTER_ALT_ROUTE_HOST = "zuul.router.alt.route.host"; public static final String ZUUL_ROUTER_ALT_ROUTE_PERMYRIAD = "zuul.router.alt.route.permyriad"; public static final String ZUUL_ROUTER_ALT_ROUTE_MAXLIMIT = "zuul.router.alt.route.maxlimit"; public static final String ZUUL_NIWS_DEFAULTCLIENT = "zuul.niws.defaultClient"; public static final String ZUUL_DEFAULT_HOST = "zuul.default.host"; public static final String ZUUL_HOST_SOCKET_TIMEOUT_MILLIS = "zuul.host.socket-timeout-millis"; public static final String ZUUL_HOST_CONNECT_TIMEOUT_MILLIS = "zuul.host.connect-timeout-millis"; public static final String ZUUL_INCLUDE_DEBUG_HEADER = "zuul.include-debug-header"; public static final String ZUUL_INITIAL_STREAM_BUFFER_SIZE = "zuul.initial-stream-buffer-size"; public static final String ZUUL_SET_CONTENT_LENGTH = "zuul.set-content-length"; public static final String ZUUL_DEBUGFILTERS_DISABLED = "zuul.debugFilters.disabled"; public static final String ZUUL_DEBUG_VIP = "zuul.debug.vip"; public static final String ZUUL_DEBUG_HOST = "zuul.debug.host"; public static final String ZUUL_REQUEST_BODY_MAX_SIZE = "zuul.body.request.max.size"; public static final String ZUUL_RESPONSE_BODY_MAX_SIZE = "zuul.body.response.max.size"; // Prevent instantiation private ZuulConstants() { throw new AssertionError("Must not instantiate constant utility class"); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/constants/ZuulHeaders.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.constants; /** * HTTP Headers that are accessed or added by Zuul * User: mcohen * Date: 5/15/13 * Time: 4:38 PM */ public class ZuulHeaders { /* Standard headers */ public static final String TRANSFER_ENCODING = "transfer-encoding"; public static final String CHUNKED = "chunked"; public static final String ORIGIN = "Origin"; public static final String CONTENT_ENCODING = "Content-Encoding"; public static final String ACCEPT_ENCODING = "accept-encoding"; public static final String CONNECTION = "Connection"; public static final String KEEP_ALIVE = "keep-alive"; public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; public static final String X_FORWARDED_FOR = "X-Forwarded-For"; public static final String HOST = "Host"; public static final String X_ORIGINATING_URL = "X-Originating-URL"; /* X-Zuul headers */ public static final String X_ZUUL = "X-Zuul"; public static final String X_ZUUL_STATUS = X_ZUUL + "-Status"; public static final String X_ZUUL_PROXY_ATTEMPTS = X_ZUUL + "-Proxy-Attempts"; public static final String X_ZUUL_INSTANCE = X_ZUUL + "-Instance"; public static final String X_ZUUL_ERROR_CAUSE = X_ZUUL + "-Error-Cause"; public static final String X_ZUUL_SURGICAL_FILTER = X_ZUUL + "-Surgical-Filter"; public static final String X_ZUUL_FILTER_EXECUTION_STATUS = X_ZUUL + "-Filter-Executions"; // Prevent instantiation private ZuulHeaders() { throw new AssertionError("Must not instantiate constant utility class"); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/CommonContextKeys.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import com.google.common.collect.ImmutableList; import com.netflix.client.config.IClientConfig; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.stats.status.StatusCategory; import io.netty.channel.Channel; import jakarta.inject.Provider; import java.net.InetAddress; import java.util.Map; /** * Common Context Keys * * Author: Arthur Gonigberg * Date: November 21, 2017 */ public class CommonContextKeys { public static final SessionContext.Key STATUS_CATEGORY = SessionContext.newKey("status_category"); public static final SessionContext.Key STATUS_CATEGORY_REASON = SessionContext.newKey("status_category_reason"); public static final SessionContext.Key ORIGIN_STATUS_CATEGORY = SessionContext.newKey("origin_status_category"); public static final SessionContext.Key ORIGIN_STATUS_CATEGORY_REASON = SessionContext.newKey("origin_status_category_reason"); public static final SessionContext.Key ORIGIN_STATUS = SessionContext.newKey("origin_status"); public static final SessionContext.Key REQUEST_ATTEMPTS = SessionContext.newKey("request_attempts"); public static final SessionContext.Key BROWNOUT_REASON = SessionContext.newKey("brownout_reason"); public static final SessionContext.Key REST_CLIENT_CONFIG = SessionContext.newKey("rest_client_config"); public static final SessionContext.Key> ZUUL_ENDPOINT = SessionContext.newKey("_zuul_endpoint"); public static final SessionContext.Key> ZUUL_ORIGIN_CHOSEN_HOST_ADDR_MAP_KEY = SessionContext.newKey("_zuul_origin_chosen_host_addr_map"); public static final SessionContext.Key ORIGIN_CHANNEL = SessionContext.newKey("_origin_channel"); public static final String ORIGIN_MANAGER = "origin_manager"; public static final SessionContext.Key> ROUTING_LOG = SessionContext.newKey("routing_log"); public static final String USE_FULL_VIP_NAME = "use_full_vip_name"; public static final String ACTUAL_VIP = "origin_vip_actual"; public static final String ORIGIN_VIP_SECURE = "origin_vip_secure"; /** * The original client destination address Zuul by a proxy running Proxy Protocol. * Will only be set if both Zuul and the connected proxy are both using set to use Proxy Protocol. */ public static final String PROXY_PROTOCOL_DESTINATION_ADDRESS = "proxy_protocol_destination_address"; public static final String SSL_HANDSHAKE_INFO = "ssl_handshake_info"; public static final String GZIPPER = "gzipper"; public static final String OVERRIDE_GZIP_REQUESTED = "overrideGzipRequested"; /* Netty-specific keys */ public static final String NETTY_HTTP_REQUEST = "_netty_http_request"; public static final String NETTY_SERVER_CHANNEL_HANDLER_CONTEXT = "_netty_server_channel_handler_context"; public static final String REQ_BODY_DCS = "_request_body_dcs"; public static final String RESP_BODY_DCS = "_response_body_dcs"; public static final SessionContext.Key> REQ_BODY_SIZE_PROVIDER = SessionContext.newKey("request_body_size"); public static final SessionContext.Key> RESP_BODY_SIZE_PROVIDER = SessionContext.newKey("response_body_size"); public static final SessionContext.Key PASSPORT = SessionContext.newKey("_passport"); public static final SessionContext.Key ZUUL_USE_DECODED_URI = SessionContext.newKey("zuul_use_decoded_uri"); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/Debug.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import com.netflix.zuul.message.Header; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpResponseInfo; import io.netty.util.ReferenceCounted; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import rx.Observable; /** * Simple wrapper class around the RequestContext for setting and managing Request level Debug data. * @author Mikey Cohen * Date: 1/25/12 * Time: 2:26 PM */ public class Debug { public static void setDebugRequest(SessionContext ctx, boolean bDebug) { ctx.setDebugRequest(bDebug); } public static void setDebugRequestHeadersOnly(SessionContext ctx, boolean bHeadersOnly) { ctx.setDebugRequestHeadersOnly(bHeadersOnly); } public static boolean debugRequestHeadersOnly(SessionContext ctx) { return ctx.debugRequestHeadersOnly(); } public static void setDebugRouting(SessionContext ctx, boolean bDebug) { ctx.setDebugRouting(bDebug); } public static boolean debugRequest(SessionContext ctx) { return ctx.debugRequest(); } public static boolean debugRouting(SessionContext ctx) { return ctx.debugRouting(); } public static void addRoutingDebug(SessionContext ctx, String line) { List rd = getRoutingDebug(ctx); rd.add(line); } public static void addRequestDebugForMessage(SessionContext ctx, ZuulMessage message, String prefix) { for (Header header : message.getHeaders().entries()) { Debug.addRequestDebug(ctx, prefix + " " + header.getKey() + " " + header.getValue()); } if (message.hasBody()) { String bodyStr = message.getBodyAsText(); Debug.addRequestDebug(ctx, prefix + " " + bodyStr); } } /** * * @return Returns the list of routiong debug messages */ public static List getRoutingDebug(SessionContext ctx) { List rd = (List) ctx.get("routingDebug"); if (rd == null) { rd = new ArrayList(); ctx.set("routingDebug", rd); } return rd; } /** * Adds a line to the Request debug messages * @param line */ public static void addRequestDebug(SessionContext ctx, String line) { List rd = getRequestDebug(ctx); rd.add(line); } /** * * @return returns the list of request debug messages */ public static List getRequestDebug(SessionContext ctx) { List rd = (List) ctx.get("requestDebug"); if (rd == null) { rd = new ArrayList(); ctx.set("requestDebug", rd); } return rd; } /** * Adds debug details about changes that a given filter made to the request context. * @param filterName * @param copy */ public static void compareContextState(String filterName, SessionContext context, SessionContext copy) { // TODO - only comparing Attributes. Need to compare the messages too. // Ensure that the routingDebug property already exists, otherwise we'll have a ConcurrentModificationException // below getRoutingDebug(context); Iterator it = context.keySet().iterator(); String key = it.next(); while (key != null) { if ((!key.equals("routingDebug") && !key.equals("requestDebug"))) { Object newValue = context.get(key); Object oldValue = copy.get(key); if (!(newValue instanceof ReferenceCounted) && !(oldValue instanceof ReferenceCounted)) { if (oldValue == null && newValue != null) { addRoutingDebug(context, "{" + filterName + "} added " + key + "=" + newValue.toString()); } else if (oldValue != null && newValue != null) { if (!oldValue.equals(newValue)) { addRoutingDebug(context, "{" + filterName + "} changed " + key + "=" + newValue.toString()); } } } } if (it.hasNext()) { key = it.next(); } else { key = null; } } } public static Observable writeDebugRequest( SessionContext context, HttpRequestInfo request, boolean isInbound) { Observable obs = null; if (Debug.debugRequest(context)) { String prefix = isInbound ? "REQUEST_INBOUND" : "REQUEST_OUTBOUND"; String arrow = ">"; Debug.addRequestDebug( context, String.format( "%s:: %s LINE: %s %s %s", prefix, arrow, request.getMethod().toUpperCase(Locale.ROOT), request.getPathAndQuery(), request.getProtocol())); obs = Debug.writeDebugMessage(context, request, prefix, arrow); } if (obs == null) { obs = Observable.just(Boolean.FALSE); } return obs; } public static Observable writeDebugResponse( SessionContext context, HttpResponseInfo response, boolean isInbound) { Observable obs = null; if (Debug.debugRequest(context)) { String prefix = isInbound ? "RESPONSE_INBOUND" : "RESPONSE_OUTBOUND"; String arrow = "<"; Debug.addRequestDebug(context, String.format("%s:: %s STATUS: %s", prefix, arrow, response.getStatus())); obs = Debug.writeDebugMessage(context, response, prefix, arrow); } if (obs == null) { obs = Observable.just(Boolean.FALSE); } return obs; } public static Observable writeDebugMessage( SessionContext context, ZuulMessage msg, String prefix, String arrow) { Observable obs = null; for (Header header : msg.getHeaders().entries()) { Debug.addRequestDebug( context, String.format("%s:: %s HDR: %s:%s", prefix, arrow, header.getKey(), header.getValue())); } // Capture the response body into a Byte array for later usage. if (msg.hasBody()) { if (!Debug.debugRequestHeadersOnly(context)) { // Convert body to a String and add to debug log. String body = msg.getBodyAsText(); Debug.addRequestDebug(context, String.format("%s:: %s BODY: %s", prefix, arrow, body)); } } if (obs == null) { obs = Observable.just(Boolean.FALSE); } return obs; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/SessionCleaner.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import rx.Observable; /** * User: michaels@netflix.com * Date: 8/3/15 * Time: 12:30 PM */ public interface SessionCleaner { Observable cleanup(SessionContext context); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/SessionContext.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.netflix.config.DynamicPropertyFactory; import com.netflix.zuul.filters.FilterError; import com.netflix.zuul.message.http.HttpResponseMessage; import jakarta.annotation.Nullable; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import lombok.NonNull; /** * Represents the context between client and origin server for the duration of the dedicated connection/session * between them. But we're currently still only modelling single request/response pair per session. * * NOTE: Not threadsafe, and not intended to be used concurrently. * * User: Mike Smith * Date: 4/28/15 * Time: 6:45 PM */ public final class SessionContext extends HashMap implements Cloneable { private static final int INITIAL_SIZE = DynamicPropertyFactory.getInstance() .getIntProperty("com.netflix.zuul.context.SessionContext.initialSize", 60) .get(); private static final int EVENT_PROPERTIES_INITIAL_SIZE = DynamicPropertyFactory.getInstance() .getIntProperty("com.netflix.zuul.context.SessionContext.eventProperties.initialSize", 128) .get(); private boolean brownoutMode = false; private boolean shouldStopFilterProcessing = false; private boolean shouldSendErrorResponse = false; private boolean errorResponseSent = false; private boolean debugRouting = false; private boolean debugRequest = false; private boolean debugRequestHeadersOnly = false; private boolean cancelled = false; private static final String KEY_UUID = "_uuid"; private static final String KEY_VIP = "routeVIP"; private static final String KEY_ENDPOINT = "_endpoint"; private static final String KEY_STATIC_RESPONSE = "_static_response"; private static final String KEY_EVENT_PROPS = "eventProperties"; private static final String KEY_FILTER_ERRORS = "_filter_errors"; private static final String KEY_FILTER_EXECS = "_filter_executions"; private final IdentityHashMap, ?> typedMap = new IdentityHashMap<>(); /** * A Key is type-safe, identity-based key into the Session Context. * @param */ public static final class Key { private final String name; private final Supplier defaultValueSupplier; private Key(String name, Supplier defaultValueSupplier) { this.name = Objects.requireNonNull(name, "name"); this.defaultValueSupplier = defaultValueSupplier; } @Override public String toString() { return "Key{" + name + '}'; } public String name() { return name; } /** * This method exists solely to indicate that Keys are based on identity and not name. */ @Override public boolean equals(Object o) { return super.equals(o); } /** * This method exists solely to indicate that Keys are based on identity and not name. */ @Override public int hashCode() { return super.hashCode(); } public T defaultValue() { return defaultValueSupplier != null ? defaultValueSupplier.get() : null; } } @SuppressWarnings("UnnecessaryStringBuilder") public SessionContext() { // Use a higher than default initial capacity for the hashmap as we generally have more than the default // 16 entries. super(INITIAL_SIZE); put(KEY_FILTER_EXECS, new StringBuilder()); put(KEY_EVENT_PROPS, new HashMap(EVENT_PROPERTIES_INITIAL_SIZE)); put(KEY_FILTER_ERRORS, new ArrayList()); } public static Key newKey(String name) { return newKey(name, null); } public static Key newKey(String name, Supplier defaultValueSupplier) { return new Key<>(name, defaultValueSupplier); } /** * {@inheritDoc} * *

This method exists for static analysis. */ @Override public Object get(Object key) { return super.get(key); } /** * Returns the value in the context, or {@code null} if absent. */ @SuppressWarnings("unchecked") @Nullable public T get(@NonNull Key key) { T value = (T) typedMap.get(key); if (value == null) { value = key.defaultValue(); } return value; } /** * Returns the value in the context, or default value from the * typed key default value supplier if absent. */ @NonNull public T getOrDefault(@NonNull Key key) { return Objects.requireNonNull(this.get(key), "expected non-null value or defaultValue supplier"); } /** * Returns the value in the context, or {@code defaultValue} if absent. */ @SuppressWarnings("unchecked") public T getOrDefault(Key key, T defaultValue) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(defaultValue, "defaultValue"); T value = (T) typedMap.get(Objects.requireNonNull(key)); if (value != null) { return value; } return defaultValue; } /** * {@inheritDoc} * *

This method exists for static analysis. */ @Override public boolean containsKey(Object key) { return super.containsKey(key); } /** * Checks for the existence of the key in the context. */ public boolean containsKey(Key key) { return typedMap.containsKey(Objects.requireNonNull(key, "key")); } /** * {@inheritDoc} * *

This method exists for static analysis. */ @Override public Object put(String key, Object value) { return super.put(key, value); } /** * Returns the previous value associated with key, or {@code null} if there was no mapping for key. Unlike * {@link #put(String, Object)}, this will never return a null value if the key is present in the map. */ @Nullable @CanIgnoreReturnValue public T put(Key key, T value) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(value, "value"); @SuppressWarnings("unchecked") // Sorry. T res = ((Map, T>) (Map) typedMap).put(key, value); return res; } /** * {@inheritDoc} * *

This method exists for static analysis. */ @Override public boolean remove(Object key, Object value) { return super.remove(key, value); } public boolean remove(Key key, T value) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(value, "value"); @SuppressWarnings("unchecked") // sorry boolean res = ((Map, T>) (Map) typedMap).remove(key, value); return res; } /** * {@inheritDoc} * *

This method exists for static analysis. */ @Override public Object remove(Object key) { return super.remove(key); } public T remove(Key key) { Objects.requireNonNull(key, "key"); @SuppressWarnings("unchecked") // sorry T res = ((Map, T>) (Map) typedMap).remove(key); return res; } public Set> keys() { return Collections.unmodifiableSet(new HashSet<>(typedMap.keySet())); } /** * Makes a copy of the RequestContext. This is used for debugging. */ @Override public SessionContext clone() { // TODO(carl-mastrangelo): copy over the type safe keys return (SessionContext) super.clone(); } public String getString(String key) { return (String) get(key); } /** * Convenience method to return a boolean value for a given key * * @return true or false depending what was set. default is false */ public boolean getBoolean(String key) { return getBoolean(key, false); } /** * Convenience method to return a boolean value for a given key * * @return true or false depending what was set. default defaultResponse */ public boolean getBoolean(String key, boolean defaultResponse) { Boolean b = (Boolean) get(key); if (b != null) { return b; } return defaultResponse; } /** * sets a key value to Boolean.TRUE */ public void set(String key) { put(key, Boolean.TRUE); } /** * puts the key, value into the map. a null value will remove the key from the map * */ public void set(String key, Object value) { if (value != null) { put(key, value); } else { remove(key); } } public String getUUID() { return getString(KEY_UUID); } public void setUUID(String uuid) { set(KEY_UUID, uuid); } public void setStaticResponse(HttpResponseMessage response) { set(KEY_STATIC_RESPONSE, response); } public HttpResponseMessage getStaticResponse() { return (HttpResponseMessage) get(KEY_STATIC_RESPONSE); } /** * Gets the throwable that will be use in the Error endpoint. * */ public Throwable getError() { return (Throwable) get("_error"); } /** * Sets throwable to use for generating a response in the Error endpoint. */ public void setError(Throwable th) { put("_error", th); } public String getErrorEndpoint() { return (String) get("_error-endpoint"); } public void setErrorEndpoint(String name) { put("_error-endpoint", name); } /** * sets debugRouting */ public void setDebugRouting(boolean bDebug) { this.debugRouting = bDebug; } /** * @return "debugRouting" */ public boolean debugRouting() { return debugRouting; } /** * sets "debugRequestHeadersOnly" to bHeadersOnly * */ public void setDebugRequestHeadersOnly(boolean bHeadersOnly) { this.debugRequestHeadersOnly = bHeadersOnly; } /** * @return "debugRequestHeadersOnly" */ public boolean debugRequestHeadersOnly() { return this.debugRequestHeadersOnly; } /** * sets "debugRequest" */ public void setDebugRequest(boolean bDebug) { this.debugRequest = bDebug; } /** * gets debugRequest * * @return debugRequest */ public boolean debugRequest() { return this.debugRequest; } /** * removes "routeHost" key */ public void removeRouteHost() { remove("routeHost"); } /** * sets routeHost * * @param routeHost a URL */ public void setRouteHost(URL routeHost) { set("routeHost", routeHost); } /** * @return "routeHost" URL */ public URL getRouteHost() { return (URL) get("routeHost"); } /** * appends filter name and status to the filter execution history for the * current request */ public void addFilterExecutionSummary(String name, String status, long time) { StringBuilder sb = getFilterExecutionSummary(); if (sb.length() > 0) { sb.append(", "); } sb.append(name) .append('[') .append(status) .append(']') .append('[') .append(time) .append("ms]"); } /** * @return String that represents the filter execution history for the current request */ public StringBuilder getFilterExecutionSummary() { return (StringBuilder) get(KEY_FILTER_EXECS); } public boolean shouldSendErrorResponse() { return this.shouldSendErrorResponse; } /** * Set this to true to indicate that the Error endpoint should be applied after * the end of the current filter processing phase. * */ public void setShouldSendErrorResponse(boolean should) { this.shouldSendErrorResponse = should; } public boolean errorResponseSent() { return this.errorResponseSent; } public void setErrorResponseSent(boolean should) { this.errorResponseSent = should; } /** * This can be used by filters for flagging if the server is getting overloaded, and then choose * to disable/sample/rate-limit some optional features. * */ public boolean isInBrownoutMode() { return brownoutMode; } /** * Flag the server is getting overloaded. * @deprecated use setInBrownoutMode(String reason) */ @Deprecated public void setInBrownoutMode() { this.brownoutMode = true; } public void setInBrownoutMode(@NonNull String reason) { this.brownoutMode = true; put(CommonContextKeys.BROWNOUT_REASON, reason); } public @Nullable String getBrownoutReason() { return get(CommonContextKeys.BROWNOUT_REASON); } /** * This is typically set by a filter when wanting to reject a request, and also reduce load on the server * by not processing any subsequent filters for this request. */ public void stopFilterProcessing() { shouldStopFilterProcessing = true; } public boolean shouldStopFilterProcessing() { return shouldStopFilterProcessing; } /** * returns the routeVIP; that is the Eureka "vip" of registered instances * */ public String getRouteVIP() { return (String) get(KEY_VIP); } /** * sets routeVIP; that is the Eureka "vip" of registered instances */ public void setRouteVIP(String sVip) { set(KEY_VIP, sVip); } public void setEndpoint(String endpoint) { put(KEY_ENDPOINT, endpoint); } public String getEndpoint() { return (String) get(KEY_ENDPOINT); } public void setEventProperty(String key, Object value) { getEventProperties().put(key, value); } public Map getEventProperties() { return (Map) this.get(KEY_EVENT_PROPS); } public List getFilterErrors() { return (List) get(KEY_FILTER_ERRORS); } public void setOriginReportedDuration(int duration) { put("_originReportedDuration", duration); } public int getOriginReportedDuration() { Object value = get("_originReportedDuration"); if (value != null) { return (Integer) value; } return -1; } public boolean isCancelled() { return cancelled; } public void cancel() { this.cancelled = true; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/SessionContextDecorator.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; /** * User: michaels@netflix.com * Date: 2/25/15 * Time: 4:09 PM */ public interface SessionContextDecorator { public SessionContext decorate(SessionContext ctx); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/SessionContextFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import com.netflix.zuul.message.ZuulMessage; import rx.Observable; public interface SessionContextFactory { public ZuulMessage create(SessionContext context, T nativeRequest, V nativeResponse); public Observable write(ZuulMessage msg, V nativeResponse); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/context/ZuulSessionContextDecorator.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import com.netflix.netty.common.metrics.HttpBodySizeRecordingChannelHandler; import com.netflix.util.UUIDFactory; import com.netflix.util.concurrent.ConcurrentUUIDFactory; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.origins.OriginManager; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import jakarta.inject.Inject; import jakarta.inject.Singleton; /** * Base Session Context Decorator * * Author: Arthur Gonigberg * Date: November 21, 2017 */ @Singleton public class ZuulSessionContextDecorator implements SessionContextDecorator { private static final UUIDFactory UUID_FACTORY = new ConcurrentUUIDFactory(); private final OriginManager originManager; @Inject public ZuulSessionContextDecorator(OriginManager originManager) { this.originManager = originManager; } @Override public SessionContext decorate(SessionContext ctx) { // TODO split out commons parts from BaseSessionContextDecorator ChannelHandlerContext nettyCtx = (ChannelHandlerContext) ctx.get(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT); if (nettyCtx == null) { return null; } Channel channel = nettyCtx.channel(); // set injected origin manager ctx.put(CommonContextKeys.ORIGIN_MANAGER, originManager); // TODO /* // The throttle result info. ThrottleResult throttleResult = channel.attr(HttpRequestThrottleChannelHandler.ATTR_THROTTLE_RESULT).get(); ctx.set(CommonContextKeys.THROTTLE_RESULT, throttleResult);*/ // Add a container for request attempts info. ctx.put(CommonContextKeys.REQUEST_ATTEMPTS, new RequestAttempts()); // Providers for getting the size of read/written request and response body sizes from channel. ctx.put( CommonContextKeys.REQ_BODY_SIZE_PROVIDER, HttpBodySizeRecordingChannelHandler.getCurrentInboundBodySize(channel)); ctx.put( CommonContextKeys.RESP_BODY_SIZE_PROVIDER, HttpBodySizeRecordingChannelHandler.getCurrentOutboundBodySize(channel)); CurrentPassport passport = CurrentPassport.fromChannel(channel); ctx.put(CommonContextKeys.PASSPORT, passport); ctx.setUUID(UUID_FACTORY.generateRandomUuid().toString()); return ctx; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/exception/ErrorType.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.exception; import com.netflix.client.ClientException; import com.netflix.config.DynamicIntProperty; import com.netflix.zuul.stats.status.StatusCategory; /** * Error Type * * Author: Arthur Gonigberg * Date: November 28, 2017 */ public interface ErrorType { String PROP_PREFIX = "zuul.error.outbound"; DynamicIntProperty ERROR_TYPE_READ_TIMEOUT_STATUS = new DynamicIntProperty(PROP_PREFIX + ".readtimeout.status", 504); DynamicIntProperty ERROR_TYPE_CONNECT_ERROR_STATUS = new DynamicIntProperty(PROP_PREFIX + ".connecterror.status", 502); DynamicIntProperty ERROR_TYPE_SERVICE_UNAVAILABLE_STATUS = new DynamicIntProperty(PROP_PREFIX + ".serviceunavailable.status", 503); DynamicIntProperty ERROR_TYPE_ORIGIN_CONCURRENCY_EXCEEDED_STATUS = new DynamicIntProperty(PROP_PREFIX + ".originconcurrencyexceeded.status", 503); DynamicIntProperty ERROR_TYPE_ERROR_STATUS_RESPONSE_STATUS = new DynamicIntProperty(PROP_PREFIX + ".errorstatusresponse.status", 500); DynamicIntProperty ERROR_TYPE_NOSERVERS_STATUS = new DynamicIntProperty(PROP_PREFIX + ".noservers.status", 502); DynamicIntProperty ERROR_TYPE_ORIGIN_SERVER_MAX_CONNS_STATUS = new DynamicIntProperty(PROP_PREFIX + ".servermaxconns.status", 503); DynamicIntProperty ERROR_TYPE_ORIGIN_RESET_CONN_STATUS = new DynamicIntProperty(PROP_PREFIX + ".originresetconnection.status", 504); DynamicIntProperty ERROR_TYPE_OTHER_STATUS = new DynamicIntProperty(PROP_PREFIX + ".other.status", 500); int getStatusCodeToReturn(); StatusCategory getStatusCategory(); ClientException.ErrorType getClientErrorType(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/exception/OutboundErrorType.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.exception; import com.netflix.client.ClientException; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.ZuulStatusCategory; /** * Outbound Error Type * * Author: Arthur Gonigberg * Date: November 28, 2017 */ public enum OutboundErrorType implements ErrorType { READ_TIMEOUT( ERROR_TYPE_READ_TIMEOUT_STATUS.get(), ZuulStatusCategory.FAILURE_ORIGIN_READ_TIMEOUT, ClientException.ErrorType.READ_TIMEOUT_EXCEPTION), CONNECT_ERROR( ERROR_TYPE_CONNECT_ERROR_STATUS.get(), ZuulStatusCategory.FAILURE_ORIGIN_CONNECTIVITY, ClientException.ErrorType.CONNECT_EXCEPTION), SERVICE_UNAVAILABLE( ERROR_TYPE_SERVICE_UNAVAILABLE_STATUS.get(), ZuulStatusCategory.FAILURE_ORIGIN_THROTTLED, ClientException.ErrorType.SERVER_THROTTLED), ERROR_STATUS_RESPONSE( ERROR_TYPE_ERROR_STATUS_RESPONSE_STATUS.get(), ZuulStatusCategory.FAILURE_ORIGIN, ClientException.ErrorType.GENERAL), NO_AVAILABLE_SERVERS( ERROR_TYPE_NOSERVERS_STATUS.get(), ZuulStatusCategory.FAILURE_ORIGIN_NO_SERVERS, ClientException.ErrorType.CONNECT_EXCEPTION), ORIGIN_SERVER_MAX_CONNS( ERROR_TYPE_ORIGIN_SERVER_MAX_CONNS_STATUS.get(), ZuulStatusCategory.FAILURE_LOCAL_THROTTLED_ORIGIN_SERVER_MAXCONN, ClientException.ErrorType.CLIENT_THROTTLED), RESET_CONNECTION( ERROR_TYPE_ORIGIN_RESET_CONN_STATUS.get(), ZuulStatusCategory.FAILURE_ORIGIN_RESET_CONNECTION, ClientException.ErrorType.CONNECT_EXCEPTION), CLOSE_NOTIFY_CONNECTION( 502, ZuulStatusCategory.FAILURE_ORIGIN_CLOSE_NOTIFY_CONNECTION, ClientException.ErrorType.CONNECT_EXCEPTION), CANCELLED(400, ZuulStatusCategory.FAILURE_CLIENT_CANCELLED, ClientException.ErrorType.SOCKET_TIMEOUT_EXCEPTION), ORIGIN_CONCURRENCY_EXCEEDED( ERROR_TYPE_ORIGIN_CONCURRENCY_EXCEEDED_STATUS.get(), ZuulStatusCategory.FAILURE_LOCAL_THROTTLED_ORIGIN_CONCURRENCY, ClientException.ErrorType.SERVER_THROTTLED), HEADER_FIELDS_TOO_LARGE( 431, ZuulStatusCategory.FAILURE_LOCAL_HEADER_FIELDS_TOO_LARGE, ClientException.ErrorType.GENERAL), OTHER(ERROR_TYPE_OTHER_STATUS.get(), ZuulStatusCategory.FAILURE_LOCAL, ClientException.ErrorType.GENERAL); private static final String NAME_PREFIX = "ORIGIN_"; private final int statusCodeToReturn; private final StatusCategory statusCategory; private final ClientException.ErrorType clientErrorType; OutboundErrorType( int statusCodeToReturn, StatusCategory statusCategory, ClientException.ErrorType clientErrorType) { this.statusCodeToReturn = statusCodeToReturn; this.statusCategory = statusCategory; this.clientErrorType = clientErrorType; } @Override public int getStatusCodeToReturn() { return statusCodeToReturn; } @Override public StatusCategory getStatusCategory() { return statusCategory; } @Override public ClientException.ErrorType getClientErrorType() { return clientErrorType; } @Override public String toString() { return NAME_PREFIX + name(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/exception/OutboundException.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.exception; import com.netflix.zuul.niws.RequestAttempt; import com.netflix.zuul.niws.RequestAttempts; /** * Outbound Exception Decorator * * User: Mike Smith * Date: 10/21/15 * Time: 11:46 AM */ public class OutboundException extends ZuulException { private final ErrorType outboundErrorType; private final RequestAttempts requestAttempts; public OutboundException(ErrorType outboundErrorType, RequestAttempts requestAttempts) { super(outboundErrorType.toString(), outboundErrorType.toString(), true); this.outboundErrorType = outboundErrorType; this.requestAttempts = requestAttempts; this.setStatusCode(outboundErrorType.getStatusCodeToReturn()); this.dontLogAsError(); } public OutboundException(ErrorType outboundErrorType, RequestAttempts requestAttempts, Throwable cause) { super(outboundErrorType.toString(), cause.getMessage(), true); this.outboundErrorType = outboundErrorType; this.requestAttempts = requestAttempts; this.setStatusCode(outboundErrorType.getStatusCodeToReturn()); this.dontLogAsError(); } public RequestAttempt getFinalRequestAttempt() { return requestAttempts == null ? null : requestAttempts.getFinalAttempt(); } public ErrorType getOutboundErrorType() { return outboundErrorType; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/exception/RequestExpiredException.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.exception; /** * @author Argha C * @since 4/26/23 */ public class RequestExpiredException extends ZuulException { public RequestExpiredException(String message) { super(message, true); setStatusCode(504); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/exception/ZuulException.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.exception; /** * All handled exceptions in Zuul are ZuulExceptions * @author Mikey Cohen * Date: 10/20/11 * Time: 4:33 PM */ public class ZuulException extends RuntimeException { private final String errorCause; private int statusCode = 500; private boolean shouldLogAsError = true; /** * Source Throwable, message, status code and info about the cause * @param sMessage * @param throwable * @param errorCause */ public ZuulException(String sMessage, Throwable throwable, String errorCause) { super(sMessage, throwable); this.errorCause = errorCause; } /** * error message, status code and info about the cause * @param sMessage * @param errorCause */ public ZuulException(String sMessage, String errorCause) { this(sMessage, errorCause, false); } public ZuulException(String sMessage, String errorCause, boolean noStackTrace) { super(sMessage, null, noStackTrace, !noStackTrace); this.errorCause = errorCause; } public ZuulException(Throwable throwable, String sMessage, boolean noStackTrace) { super(sMessage, throwable, noStackTrace, !noStackTrace); this.errorCause = "GENERAL"; } public ZuulException(Throwable throwable) { super(throwable); this.errorCause = "GENERAL"; } public ZuulException(String sMessage) { this(sMessage, false); } public ZuulException(String sMessage, boolean noStackTrace) { super(sMessage, null, noStackTrace, !noStackTrace); this.errorCause = "GENERAL"; } public int getStatusCode() { return statusCode; } public void setStatusCode(int statusCode) { this.statusCode = statusCode; } public void dontLogAsError() { shouldLogAsError = false; } public boolean shouldLogAsError() { return shouldLogAsError; } public String getErrorCause() { return errorCause; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/exception/ZuulFilterConcurrencyExceededException.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.exception; import com.netflix.zuul.filters.ZuulFilter; public class ZuulFilterConcurrencyExceededException extends ZuulException { public ZuulFilterConcurrencyExceededException(ZuulFilter filter, int concurrencyLimit) { super(filter.filterName() + " exceeded concurrency limit of " + concurrencyLimit, true); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/BaseFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.spectator.api.Counter; import com.netflix.zuul.exception.ZuulFilterConcurrencyExceededException; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.netty.SpectatorUtils; import io.netty.handler.codec.http.HttpContent; import java.util.concurrent.atomic.AtomicInteger; /** * Base abstract class for ZuulFilters. The base class defines abstract methods to define: filterType() - to classify a * filter by type. Standard types in Zuul are "pre" for pre-routing filtering, "route" for routing to an origin, "post" * for post-routing filters, "error" for error handling. We also support a "static" type for static responses see * StaticResponseFilter. *

* filterOrder() must also be defined for a filter. Filters may have the same filterOrder if precedence is not * important for a filter. filterOrders do not need to be sequential. *

* ZuulFilters may be disabled using Archaius Properties. *

* By default ZuulFilters are static; they don't carry state. This may be overridden by overriding the isStaticFilter() * property to false * * @author Mikey Cohen Date: 10/26/11 Time: 4:29 PM */ public abstract class BaseFilter implements ZuulFilter { private final String baseName; private final AtomicInteger concurrentCount; private final Counter concurrencyRejections; private final CachedDynamicBooleanProperty filterDisabled; protected final CachedDynamicIntProperty filterConcurrencyCustom; protected final CachedDynamicIntProperty filterConcurrencyDefault; private final CachedDynamicBooleanProperty concurrencyProtectionEnabled; private static final int DEFAULT_FILTER_CONCURRENCY_LIMIT = 4000; protected BaseFilter() { baseName = getClass().getSimpleName() + "." + filterType(); concurrentCount = SpectatorUtils.newGauge("zuul.filter.concurrency.current", baseName, new AtomicInteger(0)); concurrencyRejections = SpectatorUtils.newCounter("zuul.filter.concurrency.rejected", baseName); filterDisabled = new CachedDynamicBooleanProperty(disablePropertyName(), false); concurrencyProtectionEnabled = new CachedDynamicBooleanProperty("zuul.filter.concurrency.protect.enabled", true); filterConcurrencyDefault = new CachedDynamicIntProperty("zuul.filter.concurrency.limit.default", DEFAULT_FILTER_CONCURRENCY_LIMIT); filterConcurrencyCustom = new CachedDynamicIntProperty(maxConcurrencyPropertyName(), DEFAULT_FILTER_CONCURRENCY_LIMIT); } @Override public String filterName() { return getClass().getName(); } @Override public boolean overrideStopFilterProcessing() { return false; } /** * The name of the Archaius property to disable this filter. by default it is zuul.[classname].[filtertype].disable */ public String disablePropertyName() { return "zuul." + baseName + ".disable"; } /** * The name of the Archaius property for this filter's max concurrency. by default it is * zuul.[classname].[filtertype].concurrency.limit */ public String maxConcurrencyPropertyName() { return "zuul." + baseName + ".concurrency.limit"; } /** * If true, the filter has been disabled by archaius and will not be run. */ @Override public boolean isDisabled() { return filterDisabled.get(); } @Override public O getDefaultOutput(I input) { return (O) input; } @Override public FilterSyncType getSyncType() { return FilterSyncType.ASYNC; } @Override public String toString() { return String.valueOf(filterType()) + ":" + String.valueOf(filterName()); } @Override public boolean needsBodyBuffered(I input) { return false; } @Override public HttpContent processContentChunk(ZuulMessage zuulMessage, HttpContent chunk) { return chunk; } @Override public void incrementConcurrency() throws ZuulFilterConcurrencyExceededException { int limit = calculateConcurency(); if (concurrencyProtectionEnabled.get() && (concurrentCount.get() >= limit)) { concurrencyRejections.increment(); throw new ZuulFilterConcurrencyExceededException(this, limit); } concurrentCount.incrementAndGet(); } protected int calculateConcurency() { int customLimit = filterConcurrencyCustom.get(); return customLimit != DEFAULT_FILTER_CONCURRENCY_LIMIT ? customLimit : filterConcurrencyDefault.get(); } @Override public void decrementConcurrency() { concurrentCount.decrementAndGet(); } public int getConcurrency() { return concurrentCount.get(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/BaseSyncFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.zuul.message.ZuulMessage; import rx.Observable; /** * User: michaels@netflix.com * Date: 5/8/15 * Time: 2:46 PM */ public abstract class BaseSyncFilter extends BaseFilter implements SyncZuulFilter { /** * A wrapper implementation of applyAsync() that is intended just to aggregate a non-blocking apply() method * in an Observable. * * A subclass filter should override this method if doing any IO. */ @Override public Observable applyAsync(I input) { return Observable.just(this.apply(input)); } @Override public FilterSyncType getSyncType() { return FilterSyncType.SYNC; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/Endpoint.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.zuul.message.ZuulMessage; /** * User: Mike Smith * Date: 5/16/15 * Time: 1:57 PM */ public abstract class Endpoint extends BaseFilter { @Override public int filterOrder() { // Set all Endpoint filters to order of 0, because they are not processed sequentially like other filter types. return 0; } @Override public FilterType filterType() { return FilterType.ENDPOINT; } @Override public boolean shouldFilter(I msg) { // Always true, because Endpoint filters are chosen by name instead. return true; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/FilterError.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; /** * User: michaels@netflix.com * Date: 5/7/15 * Time: 10:19 AM */ public class FilterError implements Cloneable { private final String filterName; private final String filterType; private Throwable exception = null; public FilterError(String filterName, String filterType, Throwable exception) { this.filterName = filterName; this.filterType = filterType; this.exception = exception; } public String getFilterName() { return filterName; } public String getFilterType() { return filterType; } public Throwable getException() { return exception; } @Override public Object clone() { return new FilterError(filterName, filterType, exception); } @Override public String toString() { return "FilterError{" + "filterName='" + filterName + '\'' + ", filterType='" + filterType + '\'' + ", exception=" + exception + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/FilterRegistry.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import java.util.Collection; import javax.annotation.Nullable; public interface FilterRegistry { @Nullable ZuulFilter get(String key); int size(); Collection> getAllFilters(); /** * Indicates if this registry can be modified. Implementations should not change the return; * they return the same value each time. */ boolean isMutable(); /** * Removes the filter from the registry, and returns it. Returns {@code null} no such filter * was found. Callers should check {@link #isMutable()} before calling this method. * * @throws IllegalStateException if this registry is not mutable. */ @Nullable ZuulFilter remove(String key); /** * Stores the filter into the registry. If an existing filter was present with the same key, * it is removed. Callers should check {@link #isMutable()} before calling this method. * * @throws IllegalStateException if this registry is not mutable. */ void put(String key, ZuulFilter filter); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/FilterSyncType.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; /** * User: Mike Smith * Date: 11/13/15 * Time: 9:13 PM */ public enum FilterSyncType { SYNC, ASYNC } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/FilterType.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import java.util.Locale; /** * User: Mike Smith * Date: 11/13/15 * Time: 7:50 PM */ public enum FilterType { INBOUND("in"), ENDPOINT("end"), OUTBOUND("out"); private final String shortName; private FilterType(String shortName) { this.shortName = shortName; } @Override public String toString() { return shortName; } public static FilterType parse(String str) { str = str.toLowerCase(Locale.ROOT); switch (str) { case "in": return INBOUND; case "out": return OUTBOUND; case "end": return ENDPOINT; default: throw new IllegalArgumentException("Unknown filter type! type=" + String.valueOf(str)); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/MutableFilterRegistry.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import jakarta.inject.Singleton; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; @Singleton public final class MutableFilterRegistry implements FilterRegistry { private final ConcurrentHashMap> filters = new ConcurrentHashMap<>(); @Nullable @Override public ZuulFilter remove(String key) { return filters.remove(Objects.requireNonNull(key, "key")); } @Override @Nullable public ZuulFilter get(String key) { return filters.get(Objects.requireNonNull(key, "key")); } @Override public void put(String key, ZuulFilter filter) { filters.putIfAbsent(Objects.requireNonNull(key, "key"), Objects.requireNonNull(filter, "filter")); } @Override public int size() { return filters.size(); } @Override public Collection> getAllFilters() { return Collections.unmodifiableList(new ArrayList<>(filters.values())); } @Override public boolean isMutable() { return true; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/ShouldFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.zuul.message.ZuulMessage; /** * User: michaels@netflix.com * Date: 5/7/15 * Time: 3:31 PM */ public interface ShouldFilter { /** * a "true" return from this method means that the apply() method should be invoked * * @return true if the apply() method should be invoked. false will not invoke the apply() method */ boolean shouldFilter(T msg); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/SyncZuulFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.zuul.message.ZuulMessage; /** * User: michaels@netflix.com * Date: 11/16/15 * Time: 2:07 PM */ public interface SyncZuulFilter extends ZuulFilter { O apply(I input); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/SyncZuulFilterAdapter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.zuul.message.ZuulMessage; import io.netty.handler.codec.http.HttpContent; import rx.Observable; /** * Base class to help implement SyncZuulFilter. Note that the class BaseSyncFilter does exist but it derives from * BaseFilter which in turn creates a new instance of CachedDynamicBooleanProperty for "filterDisabled" every time you * create a new instance of the ZuulFilter. Normally it is not too much of a concern as the instances of ZuulFilters * are "effectively" singleton and are cached by ZuulFilterLoader. However, if you ever have a need for instantiating a * new ZuulFilter instance per request - aka EdgeProxyEndpoint or Inbound/Outbound PassportStampingFilter creating new * instances of CachedDynamicBooleanProperty per instance of ZuulFilter will quickly kill your server's performance in * two ways - * a) Instances of CachedDynamicBooleanProperty are *very* heavy CPU wise to create due to extensive hookups machinery * in their constructor * b) They leak memory as they add themselves to some ConcurrentHashMap and are never garbage collected. * * TL;DR use this as a base class for your ZuulFilter if you intend to create new instances of ZuulFilter * Created by saroskar on 6/8/17. */ public abstract class SyncZuulFilterAdapter implements SyncZuulFilter { @Override public boolean isDisabled() { return false; } @Override public boolean shouldFilter(I msg) { return true; } @Override public int filterOrder() { // Set all Endpoint filters to order of 0, because they are not processed sequentially like other filter types. return 0; } @Override public FilterType filterType() { return FilterType.ENDPOINT; } @Override public boolean overrideStopFilterProcessing() { return false; } @Override public Observable applyAsync(I input) { return Observable.just(apply(input)); } @Override public FilterSyncType getSyncType() { return FilterSyncType.SYNC; } @Override public boolean needsBodyBuffered(I input) { return false; } @Override public HttpContent processContentChunk(ZuulMessage zuulMessage, HttpContent chunk) { return chunk; } @Override public void incrementConcurrency() { // NOOP for sync filters } @Override public void decrementConcurrency() { // NOOP for sync filters } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/ZuulFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.FilterCategory; import com.netflix.zuul.FilterConstraint; import com.netflix.zuul.exception.ZuulFilterConcurrencyExceededException; import com.netflix.zuul.message.ZuulMessage; import io.netty.handler.codec.http.HttpContent; import rx.Observable; /** * Base interface for ZuulFilters * * @author Mikey Cohen * Date: 10/27/11 * Time: 3:03 PM */ public interface ZuulFilter extends ShouldFilter { boolean isDisabled(); String filterName(); /** * filterOrder() must also be defined for a filter. Filters may have the same filterOrder if precedence is not * important for a filter. filterOrders do not need to be sequential. * * @return the int order of a filter */ default int filterOrder() { Filter f = getClass().getAnnotation(Filter.class); if (f != null) { return f.order(); } throw new UnsupportedOperationException("not implemented"); } /** * to classify a filter by type. Standard types in Zuul are "in" for pre-routing filtering, * "end" for routing to an origin, "out" for post-routing filters. * * @return FilterType */ default FilterType filterType() { Filter f = getClass().getAnnotation(Filter.class); if (f != null) { return f.type(); } throw new UnsupportedOperationException("not implemented"); } /** * Classify a filter by category. * * @return FilterCategory the classification of this filter */ default FilterCategory category() { Filter f = getClass().getAnnotation(Filter.class); if (f != null) { return f.category(); } else { return FilterCategory.UNSPECIFIED; } } default Class[] constraints() { Filter annotation = getClass().getAnnotation(Filter.class); if (annotation != null) { return annotation.constraints(); } else { return null; } } /** * Whether this filter's shouldFilter() method should be checked, and apply() called, even * if SessionContext.stopFilterProcessing has been set. * * @return boolean */ boolean overrideStopFilterProcessing(); /** * Called by zuul filter runner before sending request through this filter. The filter can throw * ZuulFilterConcurrencyExceededException if it has reached its concurrent requests limit and does * not wish to process the request. Generally only useful for async filters. */ void incrementConcurrency() throws ZuulFilterConcurrencyExceededException; /** * if shouldFilter() is true, this method will be invoked. this method is the core method of a ZuulFilter */ Observable applyAsync(I input); /** * Called by zuul filter after request is processed by this filter. * */ void decrementConcurrency(); default FilterSyncType getSyncType() { Filter f = getClass().getAnnotation(Filter.class); if (f != null) { return f.sync(); } throw new UnsupportedOperationException("not implemented"); } /** * Choose a default message to use if the applyAsync() method throws an exception. * * @return ZuulMessage */ O getDefaultOutput(I input); /** * Filter indicates it needs to read and buffer whole body before it can operate on the messages by returning true. * The decision can be made at runtime, looking at the request type. For example if the incoming message is a MSL * message MSL decryption filter can return true here to buffer whole MSL message before it tries to decrypt it. * @return true if this filter needs to read whole body before it can run, false otherwise */ boolean needsBodyBuffered(I input); /** * Optionally transform HTTP content chunk received. */ HttpContent processContentChunk(ZuulMessage zuulMessage, HttpContent chunk); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/common/GZipResponseFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.common; import com.google.common.annotations.VisibleForTesting; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.config.DynamicStringSetProperty; import com.netflix.zuul.Filter; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpOutboundSyncFilter; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpHeaderNames; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.util.Gzipper; import com.netflix.zuul.util.HttpUtils; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.LastHttpContent; import java.util.Locale; /** * General-purpose filter for gzipping/ungzipping response bodies if requested/needed. This should be run as late as * possible to ensure final encoded body length is considered * *

You can just subclass this in your project, and use as-is. * * @author Mike Smith */ @Filter(order = 110, type = FilterType.OUTBOUND) public class GZipResponseFilter extends HttpOutboundSyncFilter { private static final DynamicStringSetProperty GZIPPABLE_CONTENT_TYPES = new DynamicStringSetProperty( "zuul.gzip.contenttypes", "text/html,application/x-javascript,text/css,application/javascript,text/javascript,text/plain,text/xml," + "application/json,application/vnd.ms-fontobject,application/x-font-opentype,application/x-font-truetype," + "application/x-font-ttf,application/xml,font/eot,font/opentype,font/otf,image/svg+xml,image/vnd.microsoft.icon," + "text/event-stream", ","); // https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits private static final CachedDynamicIntProperty MIN_BODY_SIZE_FOR_GZIP = new CachedDynamicIntProperty("zuul.min.gzip.body.size", 860); private static final CachedDynamicBooleanProperty ENABLED = new CachedDynamicBooleanProperty("zuul.response.gzip.filter.enabled", true); @Override public boolean shouldFilter(HttpResponseMessage response) { if (!ENABLED.get() || !response.hasBody() || response.getContext().isInBrownoutMode()) { return false; } if (response.getContext().get(CommonContextKeys.GZIPPER) != null) { return true; } // A flag on SessionContext can be set to override normal mechanism of checking if client accepts gzip.; HttpRequestInfo request = response.getInboundRequest(); Boolean overrideIsGzipRequested = (Boolean) response.getContext().get(CommonContextKeys.OVERRIDE_GZIP_REQUESTED); boolean isGzipRequested = (overrideIsGzipRequested == null) ? HttpUtils.acceptsGzip(request.getHeaders()) : overrideIsGzipRequested; // Check the headers to see if response is already gzipped. Headers respHeaders = response.getHeaders(); boolean isResponseCompressed = HttpUtils.isCompressed(respHeaders); // Decide what to do.; boolean shouldGzip = isGzippableContentType(response) && isGzipRequested && !isResponseCompressed && isRightSizeForGzip(response); if (shouldGzip) { response.getContext().set(CommonContextKeys.GZIPPER, getGzipper()); } return shouldGzip; } protected Gzipper getGzipper() { return new Gzipper(); } @VisibleForTesting boolean isRightSizeForGzip(HttpResponseMessage response) { Integer bodySize = HttpUtils.getBodySizeIfKnown(response); // bodySize == null is chunked encoding which is eligible for gzip compression return (bodySize == null) || (bodySize >= MIN_BODY_SIZE_FOR_GZIP.get()); } @Override public HttpResponseMessage apply(HttpResponseMessage response) { // set Gzip headers Headers respHeaders = response.getHeaders(); respHeaders.set(HttpHeaderNames.CONTENT_ENCODING, "gzip"); respHeaders.remove(HttpHeaderNames.CONTENT_LENGTH); return response; } private boolean isGzippableContentType(HttpResponseMessage response) { String ct = response.getHeaders().getFirst(HttpHeaderNames.CONTENT_TYPE); if (ct != null) { int charsetIndex = ct.indexOf(';'); if (charsetIndex > 0) { ct = ct.substring(0, charsetIndex); } return GZIPPABLE_CONTENT_TYPES.get().contains(ct.toLowerCase(Locale.ROOT)); } return false; } @Override public HttpContent processContentChunk(ZuulMessage resp, HttpContent chunk) { Gzipper gzipper = (Gzipper) resp.getContext().get(CommonContextKeys.GZIPPER); gzipper.write(chunk); if (chunk instanceof LastHttpContent) { gzipper.finish(); return new DefaultLastHttpContent(gzipper.getByteBuf()); } else { return new DefaultHttpContent(gzipper.getByteBuf()); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/common/SurgicalDebugFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.common; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicStringProperty; import com.netflix.zuul.Filter; import com.netflix.zuul.constants.ZuulConstants; import com.netflix.zuul.constants.ZuulHeaders; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpInboundSyncFilter; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import java.util.Objects; /** * This is an abstract filter that will route requests that match the patternMatches() method to a debug Eureka "VIP" or * host specified by zuul.debug.vip or zuul.debug.host. * * @author Mikey Cohen * Date: 6/27/12 * Time: 12:54 PM */ @Filter(order = 99, type = FilterType.INBOUND) public class SurgicalDebugFilter extends HttpInboundSyncFilter { /** * Returning true by the pattern or logic implemented in this method will route the request to the specified origin * * Override this method when using this filter to add your own pattern matching logic. * * @return true if this request should be routed to the debug origin */ protected boolean patternMatches(HttpRequestMessage request) { return false; } @Override public int filterOrder() { return 99; } @Override public boolean shouldFilter(HttpRequestMessage request) { DynamicBooleanProperty debugFilterShutoff = new DynamicBooleanProperty(ZuulConstants.ZUUL_DEBUGFILTERS_DISABLED, false); if (debugFilterShutoff.get()) { return false; } if (isDisabled()) { return false; } String isSurgicalFilterRequest = request.getHeaders().getFirst(ZuulHeaders.X_ZUUL_SURGICAL_FILTER); // don't apply filter if it was already applied boolean notAlreadyFiltered = !Objects.equals(isSurgicalFilterRequest, "true"); return notAlreadyFiltered && patternMatches(request); } @Override public HttpRequestMessage apply(HttpRequestMessage request) { DynamicStringProperty routeVip = new DynamicStringProperty(ZuulConstants.ZUUL_DEBUG_VIP, null); DynamicStringProperty routeHost = new DynamicStringProperty(ZuulConstants.ZUUL_DEBUG_HOST, null); SessionContext ctx = request.getContext(); if (routeVip.get() != null || routeHost.get() != null) { ctx.set("routeHost", routeHost.get()); ctx.set("routeVIP", routeVip.get()); request.getHeaders().set(ZuulHeaders.X_ZUUL_SURGICAL_FILTER, "true"); HttpQueryParams queryParams = request.getQueryParams(); queryParams.set("debugRequest", "true"); ctx.setDebugRequest(true); ctx.set("zuulToZuul", true); } return request; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/endpoint/EndpointLifecycle.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.endpoint; /** * Lifecycle contract for endpoints that manage their own async response flow. * *

Endpoints that acquire origin connections, manage streams, or otherwise hold * resources should implement this to ensure proper cleanup when a request * completes or errors. * *

The {@link #finish(boolean)} method is called by * {@link com.netflix.zuul.netty.filter.ZuulFilterChainHandler#fireEndpointFinish} * when the request lifecycle ends. */ public interface EndpointLifecycle { /** * Called when the request completes or errors, allowing the endpoint to release * resources (connections, streams, etc.). * * @param error {@code true} if the request ended due to an error */ void finish(boolean error); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/endpoint/MissingEndpointHandlingFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.endpoint; import com.netflix.zuul.Filter; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.SyncZuulFilterAdapter; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by saroskar on 2/13/17. */ @Filter(order = 0, type = FilterType.ENDPOINT) public final class MissingEndpointHandlingFilter extends SyncZuulFilterAdapter { private final String name; private static final Logger LOG = LoggerFactory.getLogger(MissingEndpointHandlingFilter.class); public MissingEndpointHandlingFilter(String name) { this.name = name; } @Override public HttpResponseMessage apply(HttpRequestMessage request) { SessionContext zuulCtx = request.getContext(); zuulCtx.setErrorResponseSent(true); String errMesg = "Missing Endpoint filter, name = " + name; zuulCtx.setError(new ZuulException(errMesg, true)); LOG.error(errMesg); return new HttpResponseMessageImpl(zuulCtx, request, 500); } @Override public String filterName() { return name; } @Override public HttpResponseMessage getDefaultOutput(HttpRequestMessage input) { return HttpResponseMessageImpl.defaultErrorResponse(input); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/endpoint/ProxyEndpoint.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.endpoint; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.errorprone.annotations.ForOverride; import com.netflix.client.ClientException; import com.netflix.client.config.IClientConfigKey; import com.netflix.config.DynamicIntegerSetProperty; import com.netflix.netty.common.ByteBufUtil; import com.netflix.spectator.api.Counter; import com.netflix.zuul.Filter; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.Debug; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.exception.ErrorType; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.exception.OutboundException; import com.netflix.zuul.exception.RequestExpiredException; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.SyncZuulFilterAdapter; import com.netflix.zuul.message.HeaderName; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpHeaderNames; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.netty.NettyRequestAttemptFactory; import com.netflix.zuul.netty.SpectatorUtils; import com.netflix.zuul.netty.connectionpool.BasicRequestStat; import com.netflix.zuul.netty.connectionpool.ClientTimeoutHandler; import com.netflix.zuul.netty.connectionpool.DefaultOriginChannelInitializer; import com.netflix.zuul.netty.connectionpool.PooledConnection; import com.netflix.zuul.netty.connectionpool.RequestStat; import com.netflix.zuul.netty.filter.FilterRunner; import com.netflix.zuul.netty.server.ClientRequestReceiver; import com.netflix.zuul.netty.server.MethodBinding; import com.netflix.zuul.netty.server.OriginResponseReceiver; import com.netflix.zuul.netty.timeouts.OriginTimeoutManager; import com.netflix.zuul.niws.RequestAttempt; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.origins.NettyOrigin; import com.netflix.zuul.origins.Origin; import com.netflix.zuul.origins.OriginManager; import com.netflix.zuul.origins.OriginName; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import com.netflix.zuul.util.HttpUtils; import com.netflix.zuul.util.ProxyUtils; import com.netflix.zuul.util.VipUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.Promise; import io.perfmark.PerfMark; import io.perfmark.TaskCloseable; import java.net.InetAddress; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Not thread safe! New instance of this class is created per HTTP/1.1 request proxied to the origin but NOT for each * attempt/retry. All the retry attempts for a given HTTP/1.1 request proxied share the same EdgeProxyEndpoint instance * Created by saroskar on 5/31/17. */ @Filter(order = 0, type = FilterType.ENDPOINT) public class ProxyEndpoint extends SyncZuulFilterAdapter implements EndpointLifecycle, GenericFutureListener> { private static final String ZUUL_ORIGIN_ATTEMPT_IPADDR_MAP_KEY = "_zuul_origin_attempt_ipaddr_map"; private static final String ZUUL_ORIGIN_REQUEST_URI = "_zuul_origin_request_uri"; private final ChannelHandlerContext channelCtx; private final FilterRunner responseFilters; protected final AtomicReference chosenServer; protected final AtomicReference chosenHostAddr; /* Individual request related state */ protected final HttpRequestMessage zuulRequest; protected final SessionContext context; @Nullable protected final NettyOrigin origin; protected final RequestAttempts requestAttempts; protected final CurrentPassport passport; protected final NettyRequestAttemptFactory requestAttemptFactory; protected final OriginTimeoutManager originTimeoutManager; protected MethodBinding methodBinding; protected HttpResponseMessage zuulResponse; protected boolean startedSendingResponseToClient; protected Duration timeLeftForAttempt; /* Individual retry related state */ private volatile PooledConnection originConn; private volatile OriginResponseReceiver originResponseReceiver; private AtomicInteger concurrentReqCount; private volatile boolean receivedChunkAfterProxyStarted; protected int attemptNum; protected RequestAttempt currentRequestAttempt; protected List requestStats = new ArrayList<>(); protected RequestStat currentRequestStat; public static final Set IDEMPOTENT_HTTP_METHODS = Sets.newHashSet("GET", "HEAD", "OPTIONS"); private static final DynamicIntegerSetProperty RETRIABLE_STATUSES_FOR_IDEMPOTENT_METHODS = new DynamicIntegerSetProperty("zuul.retry.allowed.statuses.idempotent", "500"); /** * Indicates how long Zuul should remember throttle events for an origin. As of this writing, throttling is used * to decide to cache request bodies. */ private static final Set REQUEST_HEADERS_TO_REMOVE = Sets.newHashSet(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE); private static final Set RESPONSE_HEADERS_TO_REMOVE = Sets.newHashSet(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE); public static final String POOLED_ORIGIN_CONNECTION_KEY = "_origin_pooled_conn"; private static final Logger logger = LoggerFactory.getLogger(ProxyEndpoint.class); private static final Counter NO_RETRY_INCOMPLETE_BODY = SpectatorUtils.newCounter("zuul.no.retry", "incomplete_body"); private static final Counter NO_RETRY_RESP_STARTED = SpectatorUtils.newCounter("zuul.no.retry", "resp_started"); public ProxyEndpoint( HttpRequestMessage inMesg, ChannelHandlerContext ctx, FilterRunner filters, MethodBinding methodBinding) { this(inMesg, ctx, filters, methodBinding, new NettyRequestAttemptFactory()); } public ProxyEndpoint( HttpRequestMessage inMesg, ChannelHandlerContext ctx, FilterRunner filters, MethodBinding methodBinding, NettyRequestAttemptFactory requestAttemptFactory) { channelCtx = ctx; responseFilters = filters; zuulRequest = transformRequest(inMesg); context = zuulRequest.getContext(); origin = getOrigin(zuulRequest); originTimeoutManager = getTimeoutManager(origin); requestAttempts = RequestAttempts.getFromSessionContext(context); passport = CurrentPassport.fromSessionContext(context); chosenServer = new AtomicReference<>(DiscoveryResult.EMPTY); chosenHostAddr = new AtomicReference<>(); concurrentReqCount = new AtomicInteger(); this.methodBinding = methodBinding; this.requestAttemptFactory = requestAttemptFactory; } public int getAttemptNum() { return attemptNum; } public RequestAttempts getRequestAttempts() { return requestAttempts; } protected RequestAttempt getCurrentRequestAttempt() { return currentRequestAttempt; } public CurrentPassport getPassport() { return passport; } public NettyOrigin getOrigin() { return origin; } /** * Get the implementing origin. *

* Note: this method gets called in the constructor so if overloading it or any methods called within, you cannot * rely on your own constructor parameters. */ @Nullable protected NettyOrigin getOrigin(HttpRequestMessage request) { SessionContext context = request.getContext(); OriginManager originManager = (OriginManager) context.get(CommonContextKeys.ORIGIN_MANAGER); if (Debug.debugRequest(context)) { ImmutableList.Builder routingLogEntries = context.get(CommonContextKeys.ROUTING_LOG); if (routingLogEntries != null) { for (String entry : routingLogEntries.build()) { Debug.addRequestDebug(context, "RoutingLog: " + entry); } } } String primaryRoute = context.getRouteVIP(); if (Strings.isNullOrEmpty(primaryRoute)) { // If no vip selected, leave origin null, then later the handleNoOriginSelected() method will be invoked. return null; } NettyOrigin origin = null; // allow implementers to override the origin with custom injection logic OriginName overrideOriginName = injectCustomOriginName(request); if (overrideOriginName != null) { // Use the custom vip instead if one has been provided. origin = getOrCreateOrigin(originManager, overrideOriginName, request.reconstructURI(), context); } else { // This is the normal flow - that a RoutingFilter has assigned a route OriginName originName = getOriginName(context); origin = getOrCreateOrigin(originManager, originName, request.reconstructURI(), context); } verifyOrigin(context, request, origin); // Update the routeVip on context to show the actual raw VIP from the clientConfig of the chosen Origin. if (origin != null) { context.set( CommonContextKeys.ACTUAL_VIP, origin.getClientConfig().get(IClientConfigKey.Keys.DeploymentContextBasedVipAddresses)); context.set( CommonContextKeys.ORIGIN_VIP_SECURE, origin.getClientConfig().get(IClientConfigKey.Keys.IsSecure)); } return origin; } public HttpRequestMessage getZuulRequest() { return zuulRequest; } // Unlink OriginResponseReceiver from origin channel pipeline so that we no longer receive events private Channel unlinkFromOrigin() { if (originResponseReceiver != null) { originResponseReceiver.unlinkFromClientRequest(); originResponseReceiver = null; } if (concurrentReqCount.get() > 0) { origin.recordProxyRequestEnd(); concurrentReqCount.decrementAndGet(); } Channel origCh = null; if (originConn != null) { origCh = originConn.getChannel(); originConn = null; } return origCh; } private void releasePartialResponse(HttpResponse partialResponse) { if (partialResponse != null && ReferenceCountUtil.refCnt(partialResponse) > 0) { ReferenceCountUtil.safeRelease(partialResponse); } } @Override public void finish(boolean error) { Channel origCh = unlinkFromOrigin(); while (concurrentReqCount.get() > 0) { origin.recordProxyRequestEnd(); concurrentReqCount.decrementAndGet(); } if (currentRequestStat != null) { if (error) { currentRequestStat.generalError(); } } // Publish each of the request stats (ie. one for each attempt). if (!requestStats.isEmpty()) { int indexFinal = requestStats.size() - 1; for (int i = 0; i < requestStats.size(); i++) { RequestStat stat = requestStats.get(i); // Tag the final and non-final attempts. stat.finalAttempt(i == indexFinal); stat.finishIfNotAlready(); } } if (error && (origCh != null)) { origCh.close(); } } /* Zuul filter methods */ @Override public String filterName() { return "ProxyEndpoint"; } @Override public HttpResponseMessage apply(HttpRequestMessage input) { // If no Origin has been selected, then just return a 404 static response. // handle any exception here try { if (origin == null) { handleNoOriginSelected(); return null; } origin.onRequestExecutionStart(zuulRequest); proxyRequestToOrigin(); // Doesn't return origin response to caller, calls invokeNext() internally in response filter chain return null; } catch (Exception ex) { handleError(ex); return null; } } @Override public HttpContent processContentChunk(ZuulMessage zuulReq, HttpContent chunk) { if (originConn != null) { if (chunk instanceof LastHttpContent && !receivedChunkAfterProxyStarted) { // if everything except the LastHttpContent was buffered, then buffer the last chunk so this request // is considered replayable zuulReq.bufferBodyContents(chunk.retain()); } // Connected to origin, stream request body without buffering receivedChunkAfterProxyStarted = true; ByteBufUtil.touch(chunk, "ProxyEndpoint writing chunk to origin, request: ", zuulReq); originConn.getChannel().writeAndFlush(chunk); return null; } // Not connected to origin yet, let caller buffer the request body ByteBufUtil.touch(chunk, "ProxyEndpoint buffering chunk to origin, request: ", zuulReq); return chunk; } @Override public HttpResponseMessage getDefaultOutput(HttpRequestMessage input) { return null; } public void invokeNext(HttpResponseMessage zuulResponse) { try { methodBinding.bind(() -> filterResponse(zuulResponse)); } catch (Exception ex) { unlinkFromOrigin(); logger.error("Error in invokeNext resp", ex); channelCtx.fireExceptionCaught(ex); } } public void invokeNext(HttpContent chunk) { try { ByteBufUtil.touch(chunk, "ProxyEndpoint received chunk from origin, request: ", zuulRequest); methodBinding.bind(() -> filterResponseChunk(chunk)); } catch (Exception ex) { ByteBufUtil.touch(chunk, "ProxyEndpoint exception processing chunk from origin, request: ", zuulRequest); unlinkFromOrigin(); logger.error("Error in invokeNext content", ex); channelCtx.fireExceptionCaught(ex); } } private void filterResponse(HttpResponseMessage zuulResponse) { if (responseFilters != null) { responseFilters.filter(zuulResponse); } else { channelCtx.fireChannelRead(zuulResponse); } } private void filterResponseChunk(HttpContent chunk) { if (context.isCancelled() || !channelCtx.channel().isActive()) { SpectatorUtils.newCounter( "zuul.origin.strayChunk", origin == null ? "none" : origin.getName().getMetricId()) .increment(); unlinkFromOrigin(); ReferenceCountUtil.safeRelease(chunk); return; } if (chunk instanceof LastHttpContent) { unlinkFromOrigin(); } if (responseFilters != null) { responseFilters.filter(zuulResponse, chunk); } else { channelCtx.fireChannelRead(chunk); } } private void storeAndLogOriginRequestInfo() { Map eventProps = context.getEventProperties(); // These two maps appear to be almost the same but are slightly different. Also, the types in the map don't // match exactly what needs to happen, so this is more of a To-Do. ZUUL_ORIGIN_ATTEMPT_IPADDR_MAP_KEY is // supposed to be the mapping of IP addresses of the server. This is (AFAICT) only used for logging. It is // an IP address semantically, but a String here. The two should be swapped. // ZUUL_ORIGIN_CHOSEN_HOST_ADDR_MAP_KEY is almost always an IP address, but may some times be a hostname in // case the discovery info is not an IP. Map attemptToIpAddressMap = (Map) eventProps.get(ZUUL_ORIGIN_ATTEMPT_IPADDR_MAP_KEY); Map attemptToChosenHostMap = (Map) eventProps.get(CommonContextKeys.ZUUL_ORIGIN_CHOSEN_HOST_ADDR_MAP_KEY.name()); if (attemptToIpAddressMap == null) { attemptToIpAddressMap = new HashMap<>(); } if (attemptToChosenHostMap == null) { attemptToChosenHostMap = new HashMap<>(); } // the chosen server can be null in the case of a timeout exception that skips acquiring a new origin connection String ipAddr = origin.getIpAddrFromServer(chosenServer.get()); if (ipAddr != null) { attemptToIpAddressMap.put(attemptNum, ipAddr); eventProps.put(ZUUL_ORIGIN_ATTEMPT_IPADDR_MAP_KEY, attemptToIpAddressMap); } if (chosenHostAddr.get() != null) { attemptToChosenHostMap.put(attemptNum, chosenHostAddr.get()); eventProps.put(CommonContextKeys.ZUUL_ORIGIN_CHOSEN_HOST_ADDR_MAP_KEY.name(), attemptToChosenHostMap); context.put(CommonContextKeys.ZUUL_ORIGIN_CHOSEN_HOST_ADDR_MAP_KEY, attemptToChosenHostMap); } eventProps.put(ZUUL_ORIGIN_REQUEST_URI, zuulRequest.getPathAndQuery()); } protected void updateOriginRpsTrackers(NettyOrigin origin, int attempt) { // override } private void proxyRequestToOrigin() { Promise promise = null; try { attemptNum += 1; /* * Before connecting to the origin, we need to compute how much time we have left for this attempt. This * method is also intended to validate deadline and timeouts boundaries for the request as a whole and could * throw an exception, skipping the logic below. */ timeLeftForAttempt = originTimeoutManager.computeReadTimeout(zuulRequest, attemptNum); currentRequestStat = createRequestStat(); origin.preRequestChecks(zuulRequest); concurrentReqCount.incrementAndGet(); // update RPS trackers updateOriginRpsTrackers(origin, attemptNum); // We pass this AtomicReference here and the origin impl will assign the chosen server to it. promise = origin.connectToOrigin( zuulRequest, channelCtx.channel().eventLoop(), attemptNum, passport, chosenServer, chosenHostAddr); storeAndLogOriginRequestInfo(); currentRequestAttempt = origin.newRequestAttempt(chosenServer.get(), chosenHostAddr.get(), context, attemptNum); requestAttempts.add(currentRequestAttempt); passport.add(PassportState.ORIGIN_CONN_ACQUIRE_START); if (promise.isDone()) { operationComplete(promise); } else { promise.addListener(this); } } catch (Exception ex) { if (ex instanceof RequestExpiredException) { logger.debug("Request deadline expired while connecting to origin, UUID {}", context.getUUID(), ex); } else { logger.error("Error while connecting to origin, UUID {}", context.getUUID(), ex); } storeAndLogOriginRequestInfo(); if (promise != null && !promise.isDone()) { promise.setFailure(ex); } else { errorFromOrigin(ex); } } } /** * Override to track your own request stats. */ protected RequestStat createRequestStat() { BasicRequestStat basicRequestStat = new BasicRequestStat(); requestStats.add(basicRequestStat); RequestStat.putInSessionContext(basicRequestStat, context); return basicRequestStat; } @Override public void operationComplete(Future connectResult) { // MUST run this within bindingcontext to support ThreadVariables. try { methodBinding.bind(() -> { DiscoveryResult server = chosenServer.get(); /* TODO(argha-c): This reliance on mutable update of the `chosenServer` must be improved. * @see DiscoveryResult.EMPTY indicates that the loadbalancer found no available servers. */ if (!Objects.equals(server, DiscoveryResult.EMPTY)) { if (currentRequestStat != null) { currentRequestStat.server(server); } origin.onRequestStartWithServer(zuulRequest, server, attemptNum); } // Handle the connection establishment result. if (connectResult.isSuccess()) { onOriginConnectSucceeded(connectResult.getNow(), timeLeftForAttempt); } else { onOriginConnectFailed(connectResult.cause()); } }); } catch (Throwable ex) { logger.error( "Uncaught error in operationComplete(). Closing the server channel now. {}", ChannelUtils.channelInfoForLogging(channelCtx.channel()), ex); unlinkFromOrigin(); // Fire exception here to ensure that server channel gets closed, so clients don't hang. channelCtx.fireExceptionCaught(ex); } } private void onOriginConnectSucceeded(PooledConnection conn, Duration readTimeout) { passport.add(PassportState.ORIGIN_CONN_ACQUIRE_END); if (context.isCancelled()) { logger.info("Client cancelled after successful origin connect: {}", conn.getChannel()); // conn isn't actually busy so we can put it in the pool conn.setConnectionState(PooledConnection.ConnectionState.WRITE_READY); conn.release(); } else { // Update the RequestAttempt to reflect the readTimeout chosen. currentRequestAttempt.setReadTimeout(readTimeout.toMillis()); // Start sending the request to origin now. writeClientRequestToOrigin(conn, readTimeout); } } private void onOriginConnectFailed(Throwable cause) { passport.add(PassportState.ORIGIN_CONN_ACQUIRE_FAILED); if (!context.isCancelled()) { errorFromOrigin(cause); } } private void writeClientRequestToOrigin(PooledConnection conn, Duration readTimeout) { Channel ch = conn.getChannel(); passport.setOnChannel(ch); // set read timeout on origin channel ch.attr(ClientTimeoutHandler.ORIGIN_RESPONSE_READ_TIMEOUT).set(readTimeout); context.put(CommonContextKeys.ORIGIN_CHANNEL, ch); context.set(POOLED_ORIGIN_CONNECTION_KEY, conn); preWriteToOrigin(chosenServer.get(), zuulRequest); ChannelPipeline pipeline = ch.pipeline(); originResponseReceiver = getOriginResponseReceiver(); pipeline.addBefore( DefaultOriginChannelInitializer.CONNECTION_POOL_HANDLER, OriginResponseReceiver.CHANNEL_HANDLER_NAME, originResponseReceiver); ch.write(zuulRequest); writeBufferedBodyContent(zuulRequest, ch); ch.flush(); // Get ready to read origin's response syncClientAndOriginChannels(channelCtx.channel(), ch); ch.read(); originConn = conn; channelCtx.read(); } protected void syncClientAndOriginChannels(Channel clientChannel, Channel originChannel) { // Add override for custom syncing between client and origin channels. } protected OriginResponseReceiver getOriginResponseReceiver() { return new OriginResponseReceiver(this); } protected void preWriteToOrigin(DiscoveryResult chosenServer, HttpRequestMessage zuulRequest) { // override for custom metrics or processing } private static void writeBufferedBodyContent(HttpRequestMessage zuulRequest, Channel channel) { zuulRequest.getBodyContents().forEach((chunk) -> { channel.write(chunk.retain()); }); } protected boolean isRemoteZuulRetriesBelowRetryLimit(int maxAllowedRetries) { // override for custom header checking.. return true; } protected boolean isBelowRetryLimit() { int maxAllowedRetries = origin.getMaxRetriesForRequest(context); return (attemptNum <= maxAllowedRetries) && isRemoteZuulRetriesBelowRetryLimit(maxAllowedRetries); } public void errorFromOrigin(Throwable ex) { try { // Flag that there was an origin server related error for the loadbalancer to choose // whether to circuit-trip this server. if (originConn != null) { // NOTE: if originConn is null, then these stats will have been incremented within // PerServerConnectionPool // so don't need to be here. originConn.getServer().incrementSuccessiveConnectionFailureCount(); originConn.getServer().addToFailureCount(); originConn.flagShouldClose(); } // detach from current origin Channel originCh = unlinkFromOrigin(); methodBinding.bind(() -> processErrorFromOrigin(ex, originCh)); } catch (Exception e) { channelCtx.fireExceptionCaught(ex); } } private void processErrorFromOrigin(Throwable ex, Channel origCh) { try { SessionContext zuulCtx = context; ErrorType err = requestAttemptFactory.mapNettyToOutboundErrorType(ex); // Be cautious about how much we log about errors from origins, as it can have perf implications at high // rps. if (zuulCtx.isInBrownoutMode()) { // Don't include the stacktrace or the channel info. logger.warn( "{}, origin = {}: {}", err.getStatusCategory().name(), origin.getName(), String.valueOf(ex)); } else { String origChInfo = (origCh != null) ? ChannelUtils.channelInfoForLogging(origCh) : ""; if (logger.isInfoEnabled()) { // Include the stacktrace. logger.warn( "{}, origin = {}, origin channel info = {}", err.getStatusCategory().name(), origin.getName(), origChInfo, ex); } else { logger.warn( "{}, origin = {}, {}, origin channel info = {}", err.getStatusCategory().name(), origin.getName(), String.valueOf(ex), origChInfo); } } // Update the NIWS stat. if (currentRequestStat != null) { currentRequestStat.failAndSetErrorCode(err); } // Update RequestAttempt info. if (currentRequestAttempt != null) { currentRequestAttempt.complete(-1, currentRequestStat.duration(), ex); } postErrorProcessing(ex, zuulCtx, err, chosenServer.get(), attemptNum); ClientException niwsEx = new ClientException( ClientException.ErrorType.valueOf(err.getClientErrorType().name())); if (!Objects.equals(chosenServer.get(), DiscoveryResult.EMPTY)) { origin.onRequestExceptionWithServer(zuulRequest, chosenServer.get(), attemptNum, niwsEx); } boolean retryable = isRetryable(err); if (retryable) { origin.adjustRetryPolicyIfNeeded(zuulRequest); } if (retryable && isBelowRetryLimit()) { // retry request with different origin passport.add(PassportState.ORIGIN_RETRY_START); proxyRequestToOrigin(); } else { // Record the exception in context. An error filter should later run which can translate this into an // app-specific error response if needed. zuulCtx.setError(ex); zuulCtx.setShouldSendErrorResponse(true); StatusCategoryUtils.storeStatusCategoryIfNotAlreadyFailure(zuulCtx, err.getStatusCategory()); origin.recordFinalError(zuulRequest, ex); origin.onRequestExecutionFailed(zuulRequest, chosenServer.get(), attemptNum - 1, niwsEx); // Send error response to client handleError(ex); } } catch (Exception e) { // Use original origin returned exception handleError(ex); } } protected void postErrorProcessing( Throwable ex, SessionContext zuulCtx, ErrorType err, DiscoveryResult chosenServer, int attemptNum) { // override for custom processing } private void handleError(Throwable cause) { ZuulException ze = (cause instanceof ZuulException) ? (ZuulException) cause : requestAttemptFactory.mapNettyToOutboundException(cause, context); logger.debug("Proxy endpoint failed.", cause); if (!startedSendingResponseToClient) { startedSendingResponseToClient = true; zuulResponse = new HttpResponseMessageImpl(context, zuulRequest, ze.getStatusCode()); zuulResponse .getHeaders() .add( "Connection", "close"); // TODO - why close the connection? maybe don't always want this to happen ... zuulResponse.finishBufferedBodyIfIncomplete(); invokeNext(zuulResponse); } else { channelCtx.fireExceptionCaught(ze); } } private void handleNoOriginSelected() { StatusCategoryUtils.setStatusCategory(context, ZuulStatusCategory.SUCCESS_LOCAL_NO_ROUTE); startedSendingResponseToClient = true; zuulResponse = new HttpResponseMessageImpl(context, zuulRequest, 404); zuulResponse.finishBufferedBodyIfIncomplete(); invokeNext(zuulResponse); } protected boolean isRetryable(ErrorType err) { if ((err == OutboundErrorType.RESET_CONNECTION) || (err == OutboundErrorType.CONNECT_ERROR) || (err == OutboundErrorType.READ_TIMEOUT && IDEMPOTENT_HTTP_METHODS.contains( zuulRequest.getMethod().toUpperCase(Locale.ROOT)))) { return isRequestReplayable(); } return false; } /** * Request is replayable on a different origin IFF * A) we have not started to send response back to the client AND * B) we have not lost any of its body chunks */ protected boolean isRequestReplayable() { if (startedSendingResponseToClient) { NO_RETRY_RESP_STARTED.increment(); return false; } if (!zuulRequest.hasCompleteBody()) { NO_RETRY_INCOMPLETE_BODY.increment(); return false; } return true; } public void responseFromOrigin(HttpResponse originResponse) { try (TaskCloseable ignore = PerfMark.traceTask("ProxyEndpoint.responseFromOrigin")) { PerfMark.attachTag("uuid", zuulRequest, r -> r.getContext().getUUID()); PerfMark.attachTag("path", zuulRequest, HttpRequestInfo::getPath); ByteBufUtil.touch(originResponse, "ProxyEndpoint handling response from origin, request: ", zuulRequest); methodBinding.bind(() -> processResponseFromOrigin(originResponse)); } catch (Exception ex) { unlinkFromOrigin(); releasePartialResponse(originResponse); logger.error("Error in responseFromOrigin", ex); channelCtx.fireExceptionCaught(ex); } } private void processResponseFromOrigin(HttpResponse originResponse) { if (originResponse.status().code() >= 500) { handleOriginNonSuccessResponse(originResponse, chosenServer.get()); } else { handleOriginSuccessResponse(originResponse, chosenServer.get()); } } protected void handleOriginSuccessResponse(HttpResponse originResponse, DiscoveryResult chosenServer) { origin.recordSuccessResponse(); if (originConn != null) { originConn.getServer().clearSuccessiveConnectionFailureCount(); } int respStatus = originResponse.status().code(); long duration = 0; if (currentRequestStat != null) { currentRequestStat.updateWithHttpStatusCode(respStatus); duration = currentRequestStat.duration(); } if (currentRequestAttempt != null) { currentRequestAttempt.complete(respStatus, duration, null); } // separate nfstatus for 404 so that we can notify origins ByteBufUtil.touch(originResponse, "ProxyEndpoint handling successful response, request: ", zuulRequest); StatusCategory statusCategory = respStatus == 404 ? ZuulStatusCategory.SUCCESS_NOT_FOUND : ZuulStatusCategory.SUCCESS; zuulResponse = buildZuulHttpResponse(originResponse, statusCategory, context.getError()); invokeNext(zuulResponse); } private HttpResponseMessage buildZuulHttpResponse( HttpResponse httpResponse, StatusCategory statusCategory, Throwable ex) { startedSendingResponseToClient = true; // Translate the netty HttpResponse into a zuul HttpResponseMessage. SessionContext zuulCtx = context; int respStatus = httpResponse.status().code(); HttpResponseMessage zuulResponse = new HttpResponseMessageImpl(zuulCtx, zuulRequest, respStatus); Headers respHeaders = zuulResponse.getHeaders(); for (Map.Entry entry : httpResponse.headers()) { respHeaders.add(entry.getKey(), entry.getValue()); } // Try to decide if this response has a body or not based on the headers (as we won't yet have // received any of the content). // NOTE that we also later may override this if it is Chunked encoding, but we receive // a LastHttpContent without any prior HttpContent's. if (HttpUtils.hasChunkedTransferEncodingHeader(zuulResponse) || HttpUtils.hasNonZeroContentLengthHeader(zuulResponse)) { zuulResponse.setHasBody(true); } // Store this original response info for future reference (ie. for metrics and access logging purposes). zuulResponse.storeInboundResponse(); channelCtx.channel().attr(ClientRequestReceiver.ATTR_ZUUL_RESP).set(zuulResponse); if (httpResponse instanceof DefaultFullHttpResponse) { ByteBufUtil.touch( httpResponse, "ProxyEndpoint converting Netty response to Zuul response, request: ", zuulRequest); ByteBuf chunk = ((DefaultFullHttpResponse) httpResponse).content(); zuulResponse.bufferBodyContents(new DefaultLastHttpContent(chunk)); } // Invoke any Ribbon execution listeners. // Request was a success even if server may have responded with an error code 5XX. if (originConn != null) { origin.onRequestExecutionSuccess(zuulRequest, zuulResponse, originConn.getServer(), attemptNum); } // Collect some info about the received response. origin.recordFinalResponse(zuulResponse); origin.recordFinalError(zuulRequest, ex); StatusCategoryUtils.setStatusCategory(zuulCtx, statusCategory); zuulCtx.setError(ex); zuulCtx.put("origin_http_status", Integer.toString(respStatus)); return transformResponse(zuulResponse); } private HttpResponseMessage transformResponse(HttpResponseMessage resp) { RESPONSE_HEADERS_TO_REMOVE.stream().forEach(s -> resp.getHeaders().remove(s)); return resp; } protected void handleOriginNonSuccessResponse(HttpResponse originResponse, DiscoveryResult chosenServer) { int respStatus = originResponse.status().code(); OutboundException obe; StatusCategory statusCategory; ClientException.ErrorType niwsErrorType; if (respStatus == 503) { statusCategory = ZuulStatusCategory.FAILURE_ORIGIN_THROTTLED; niwsErrorType = ClientException.ErrorType.SERVER_THROTTLED; obe = new OutboundException(OutboundErrorType.SERVICE_UNAVAILABLE, requestAttempts); if (currentRequestStat != null) { currentRequestStat.updateWithHttpStatusCode(respStatus); currentRequestStat.serviceUnavailable(); } } else { statusCategory = ZuulStatusCategory.FAILURE_ORIGIN; niwsErrorType = ClientException.ErrorType.GENERAL; obe = new OutboundException(OutboundErrorType.ERROR_STATUS_RESPONSE, requestAttempts); if (currentRequestStat != null) { currentRequestStat.updateWithHttpStatusCode(respStatus); currentRequestStat.generalError(); } } obe.setStatusCode(respStatus); long duration = 0; if (currentRequestStat != null) { duration = currentRequestStat.duration(); } if (currentRequestAttempt != null) { currentRequestAttempt.complete(respStatus, duration, obe); } // Flag this error with the ExecutionListener. origin.onRequestExceptionWithServer(zuulRequest, chosenServer, attemptNum, new ClientException(niwsErrorType)); boolean retryable5xxResponse = isRetryable5xxResponse(zuulRequest, originResponse); if (retryable5xxResponse) { origin.originRetryPolicyAdjustmentIfNeeded(zuulRequest, originResponse); origin.adjustRetryPolicyIfNeeded(zuulRequest); } if (retryable5xxResponse && isBelowRetryLimit()) { logger.debug( "Retrying: status={}, attemptNum={}, maxRetries={}, startedSendingResponseToClient={}," + " hasCompleteBody={}, method={}", respStatus, attemptNum, origin.getMaxRetriesForRequest(context), startedSendingResponseToClient, zuulRequest.hasCompleteBody(), zuulRequest.getMethod()); // detach from current origin. ByteBufUtil.touch(originResponse, "ProxyEndpoint handling non-success retry, request: ", zuulRequest); unlinkFromOrigin(); releasePartialResponse(originResponse); // ensure body reader indexes are reset so retry is able to access the body buffer // otherwise when the body is read by netty (in writeBufferedBodyContent) the body will appear empty zuulRequest.resetBodyReader(); // retry request with different origin passport.add(PassportState.ORIGIN_RETRY_START); proxyRequestToOrigin(); } else { SessionContext zuulCtx = context; logger.info( "Sending error to client: status={}, attemptNum={}, maxRetries={}," + " startedSendingResponseToClient={}, hasCompleteBody={}, method={}", respStatus, attemptNum, origin.getMaxRetriesForRequest(zuulCtx), startedSendingResponseToClient, zuulRequest.hasCompleteBody(), zuulRequest.getMethod()); // This is a final response after all retries that will go to the client ByteBufUtil.touch(originResponse, "ProxyEndpoint handling non-success response, request: ", zuulRequest); zuulResponse = buildZuulHttpResponse(originResponse, statusCategory, obe); invokeNext(zuulResponse); } } public boolean isRetryable5xxResponse( HttpRequestMessage zuulRequest, HttpResponse originResponse) { // int retryNum, int maxRetries) { if (isRequestReplayable()) { int status = originResponse.status().code(); if (status == 503 || originIndicatesRetryableInternalServerError(originResponse)) { return true; } // Retry if this is an idempotent http method AND status code was retriable for idempotent methods. else if (RETRIABLE_STATUSES_FOR_IDEMPOTENT_METHODS.get().contains(status) && IDEMPOTENT_HTTP_METHODS.contains(zuulRequest.getMethod().toUpperCase(Locale.ROOT))) { return true; } } return false; } protected boolean originIndicatesRetryableInternalServerError(HttpResponse response) { // override for custom origin headers for retry return false; } /* static utility methods */ protected HttpRequestMessage transformRequest(HttpRequestMessage requestMsg) { HttpRequestMessage massagedRequest = massageRequestURI(requestMsg); Headers headers = massagedRequest.getHeaders(); REQUEST_HEADERS_TO_REMOVE.forEach(headerName -> headers.remove(headerName.getName())); addCustomRequestHeaders(headers); // Add X-Forwarded headers if not already there. ProxyUtils.addXForwardedHeaders(massagedRequest); return massagedRequest; } protected void addCustomRequestHeaders(Headers headers) { // override to add custom headers } @VisibleForTesting static HttpRequestMessage massageRequestURI(HttpRequestMessage request) { SessionContext context = request.getContext(); String modifiedPath; HttpQueryParams modifiedQueryParams = null; String uri = null; if (context.get("requestURI") != null) { uri = (String) context.get("requestURI"); } // If another filter has specified an overrideURI, then use that instead of requested URI. Object override = context.get("overrideURI"); if (override != null) { uri = override.toString(); } if (uri != null) { int index = uri.indexOf('?'); if (index != -1) { // Strip the query string off of the URI. String paramString = uri.substring(index + 1); modifiedPath = uri.substring(0, index); modifiedQueryParams = HttpQueryParams.parse(paramString); } else { modifiedPath = uri; } request.setPath(modifiedPath); if (modifiedQueryParams != null) { request.setQueryParams(modifiedQueryParams); } } return request; } @Nonnull protected OriginName getOriginName(SessionContext context) { String clientName = getClientName(context); return OriginName.fromVip(context.getRouteVIP(), clientName); } @Nonnull protected String getClientName(SessionContext context) { // make sure the restClientName will never be a raw VIP in cases where it's the fallback for another route // assignment String restClientVIP = context.getRouteVIP(); boolean useFullName = context.getBoolean(CommonContextKeys.USE_FULL_VIP_NAME); return useFullName ? restClientVIP : VipUtils.getVIPPrefix(restClientVIP); } /** * Inject your own custom VIP based on your own processing *

* Note: this method gets called in the constructor so if overloading it or any methods called within, you cannot * rely on your own constructor parameters. * * @return {@code null} if unused. */ @Nullable protected OriginName injectCustomOriginName(HttpRequestMessage request) { // override for custom vip injection return null; } private NettyOrigin getOrCreateOrigin( OriginManager originManager, OriginName originName, String uri, SessionContext ctx) { NettyOrigin origin = originManager.getOrigin(originName, uri, ctx); if (origin == null) { // If no pre-registered and configured RestClient found for this VIP, then register one using default NIWS // properties. logger.debug( "Attempting to register RestClient for client that has not been configured. originName={}, uri={}", originName, uri); origin = originManager.createOrigin(originName, uri, ctx); } return origin; } private void verifyOrigin(SessionContext context, HttpRequestMessage request, Origin primaryOrigin) { if (primaryOrigin == null) { String vip = context.getRouteVIP(); // If no origin found then add specific error-cause metric tag, and throw an exception with 404 status. StatusCategoryUtils.setStatusCategory( context, ZuulStatusCategory.SUCCESS_LOCAL_NO_ROUTE, "Unable to find an origin client matching `" + vip + "` to handle request"); String causeName = "RESTCLIENT_NOTFOUND"; originNotFound(context, causeName); ZuulException ze = new ZuulException( "No origin found for request. name=" + vip + ", uri=" + request.reconstructURI(), causeName); ze.setStatusCode(404); throw ze; } } @ForOverride protected void originNotFound(SessionContext context, String causeName) { // override for metrics or custom processing } @ForOverride protected OriginTimeoutManager getTimeoutManager(NettyOrigin origin) { return new OriginTimeoutManager(origin); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/http/HttpInboundFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.http; import com.netflix.zuul.filters.BaseFilter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.http.HttpRequestMessage; /** * User: michaels@netflix.com * Date: 5/29/15 * Time: 3:22 PM */ public abstract class HttpInboundFilter extends BaseFilter { @Override public FilterType filterType() { return FilterType.INBOUND; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/http/HttpInboundSyncFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.http; import com.netflix.zuul.filters.BaseSyncFilter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.http.HttpRequestMessage; /** * User: michaels@netflix.com * Date: 5/29/15 * Time: 3:22 PM */ public abstract class HttpInboundSyncFilter extends BaseSyncFilter { @Override public FilterType filterType() { return FilterType.INBOUND; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/http/HttpOutboundFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.http; import com.netflix.zuul.filters.BaseFilter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.http.HttpResponseMessage; /** * User: michaels@netflix.com * Date: 5/29/15 * Time: 3:23 PM */ public abstract class HttpOutboundFilter extends BaseFilter { @Override public FilterType filterType() { return FilterType.OUTBOUND; } @Override public HttpResponseMessage getDefaultOutput(HttpResponseMessage input) { return input; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/http/HttpOutboundSyncFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.http; import com.netflix.zuul.filters.BaseSyncFilter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.http.HttpResponseMessage; /** * User: michaels@netflix.com * Date: 5/29/15 * Time: 3:23 PM */ public abstract class HttpOutboundSyncFilter extends BaseSyncFilter { @Override public FilterType filterType() { return FilterType.OUTBOUND; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/http/HttpSyncEndpoint.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.http; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.zuul.filters.Endpoint; import com.netflix.zuul.filters.SyncZuulFilter; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.LastHttpContent; import rx.Observable; import rx.Subscriber; /** * User: Mike Smith * Date: 6/16/15 * Time: 12:23 AM */ public abstract class HttpSyncEndpoint extends Endpoint implements SyncZuulFilter { // Feature flag for enabling this while we get some real data for the impact. private static final CachedDynamicBooleanProperty WAIT_FOR_LASTCONTENT = new CachedDynamicBooleanProperty("zuul.endpoint.sync.wait_for_lastcontent", true); private static final String KEY_FOR_SUBSCRIBER = "_HttpSyncEndpoint_subscriber"; @Override public HttpResponseMessage getDefaultOutput(HttpRequestMessage request) { return HttpResponseMessageImpl.defaultErrorResponse(request); } @Override public Observable applyAsync(HttpRequestMessage input) { if (WAIT_FOR_LASTCONTENT.get() && !input.hasCompleteBody()) { // Return an observable that won't complete until after we have received the LastContent from client (ie. // that we've // received the whole request body), so that we can't potentially corrupt the clients' http state on this // connection. return Observable.create(subscriber -> { ZuulMessage response = this.apply(input); ResponseState state = new ResponseState(response, subscriber); input.getContext().set(KEY_FOR_SUBSCRIBER, state); }); } else { return Observable.just(this.apply(input)); } } @Override public HttpContent processContentChunk(ZuulMessage zuulMessage, HttpContent chunk) { // Only call onNext() after we've received the LastContent of request from client. if (chunk instanceof LastHttpContent) { ResponseState state = (ResponseState) zuulMessage.getContext().get(KEY_FOR_SUBSCRIBER); if (state != null) { state.subscriber.onNext(state.response); state.subscriber.onCompleted(); zuulMessage.getContext().remove(KEY_FOR_SUBSCRIBER); } } return super.processContentChunk(zuulMessage, chunk); } @Override public void incrementConcurrency() { // NOOP, since this is supposed to be a SYNC filter in spirit } @Override public void decrementConcurrency() { // NOOP, since this is supposed to be a SYNC filter in spirit } private static class ResponseState { final ZuulMessage response; final Subscriber subscriber; public ResponseState(ZuulMessage response, Subscriber subscriber) { this.response = response; this.subscriber = subscriber; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/passport/InboundPassportStampingFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.passport; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.passport.PassportState; /** * Created by saroskar on 3/14/17. */ @Filter(order = 0, type = FilterType.INBOUND) public final class InboundPassportStampingFilter extends PassportStampingFilter { public InboundPassportStampingFilter(PassportState stamp) { super(stamp); } @Override public FilterType filterType() { return FilterType.INBOUND; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/passport/OutboundPassportStampingFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.passport; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.passport.PassportState; /** * Created by saroskar on 3/14/17. */ @Filter(order = 0, type = FilterType.OUTBOUND) public final class OutboundPassportStampingFilter extends PassportStampingFilter { public OutboundPassportStampingFilter(PassportState stamp) { super(stamp); } @Override public FilterType filterType() { return FilterType.OUTBOUND; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/filters/passport/PassportStampingFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.passport; import com.netflix.zuul.filters.SyncZuulFilterAdapter; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; /** * Created by saroskar on 2/18/17. */ public abstract class PassportStampingFilter extends SyncZuulFilterAdapter { private final PassportState stamp; private final String name; public PassportStampingFilter(PassportState stamp) { this.stamp = stamp; this.name = filterType().name() + "-" + stamp.name() + "-Filter"; } @Override public String filterName() { return name; } @Override public T getDefaultOutput(T input) { return input; } @Override public T apply(T input) { CurrentPassport.fromSessionContext(input.getContext()).add(stamp); return input; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/logging/Http2FrameLoggingPerClientIpHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.logging; import com.netflix.config.DynamicStringSetProperty; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.http2.DynamicHttp2FrameLogger; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * Be aware that this will only work correctly for devices connected _directly_ to Zuul - ie. connected * through an ELB TCP Listener. And not through FTL either. */ public class Http2FrameLoggingPerClientIpHandler extends ChannelInboundHandlerAdapter { private static final DynamicStringSetProperty IPS = new DynamicStringSetProperty("server.http2.frame.logging.ips", ""); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { String clientIP = ctx.channel() .attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS) .get(); if (IPS.get().contains(clientIP)) { ctx.channel().attr(DynamicHttp2FrameLogger.ATTR_ENABLE).set(Boolean.TRUE); ctx.pipeline().remove(this); } } finally { super.channelRead(ctx, msg); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/Header.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; /** * Represents a single header from a {@link Headers} object. */ public final class Header { private final HeaderName name; private final String value; public Header(HeaderName name, String value) { if (name == null) { throw new NullPointerException("Header name cannot be null!"); } this.name = name; this.value = value; } public String getKey() { return name.getName(); } public HeaderName getName() { return name; } public String getValue() { return value; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Header header = (Header) o; if (!name.equals(header.name)) { return false; } return !(value != null ? !value.equals(header.value) : header.value != null); } @Override public int hashCode() { int result = name.hashCode(); result = 31 * result + (value != null ? value.hashCode() : 0); return result; } @Override public String toString() { return String.format("%s: %s", name, value); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/HeaderName.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import java.util.Locale; /** * Immutable, case-insensitive wrapper around Header name. * * User: Mike Smith * Date: 7/29/15 * Time: 1:07 PM */ public final class HeaderName { private final String name; private final String normalised; private final int hashCode; public HeaderName(String name) { if (name == null) { throw new NullPointerException("HeaderName cannot be null!"); } this.name = name; this.normalised = normalize(name); this.hashCode = this.normalised.hashCode(); } HeaderName(String name, String normalised) { this.name = name; this.normalised = normalised; this.hashCode = normalised.hashCode(); } /** * Gets the original, non-normalized name for this header. */ public String getName() { return name; } public String getNormalised() { return normalised; } static String normalize(String s) { return s.toLowerCase(Locale.ROOT); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof HeaderName)) { return false; } HeaderName that = (HeaderName) o; return this.normalised.equals(that.normalised); } @Override public int hashCode() { return hashCode; } @Override public String toString() { return name; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/Headers.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import com.google.common.annotations.VisibleForTesting; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Spectator; import com.netflix.zuul.exception.ZuulException; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Predicate; import javax.annotation.Nullable; /** * An abstraction over a collection of http headers. Allows multiple headers with same name, and header names are * compared case insensitively. * * There are methods for getting and setting headers by String AND by HeaderName. When possible, use the HeaderName * variants and cache the HeaderName instances somewhere, to avoid case-insensitive String comparisons. */ public final class Headers { private static final int ABSENT = -1; private final List originalNames; private final List names; private final List values; private static final Counter invalidHeaderCounter = Spectator.globalRegistry().counter("zuul.header.invalid.char"); public static Headers copyOf(Headers original) { return new Headers(Objects.requireNonNull(original, "original")); } public Headers() { originalNames = new ArrayList<>(); names = new ArrayList<>(); values = new ArrayList<>(); } public Headers(int initialSize) { originalNames = new ArrayList<>(initialSize); names = new ArrayList<>(initialSize); values = new ArrayList<>(initialSize); } private Headers(Headers original) { originalNames = new ArrayList<>(original.originalNames); names = new ArrayList<>(original.names); values = new ArrayList<>(original.values); } /** * Get the first value found for this key even if there are multiple. If none, then * return {@code null}. */ @Nullable public String getFirst(String headerName) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); return getFirstNormal(normalName); } /** * Get the first value found for this key even if there are multiple. If none, then * return {@code null}. */ @Nullable public String getFirst(HeaderName headerName) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); return getFirstNormal(normalName); } /** * Get the first value found for this key even if there are multiple. If none, then * return the specified defaultValue. */ public String getFirst(String headerName, String defaultValue) { Objects.requireNonNull(defaultValue, "defaultValue"); String value = getFirst(headerName); if (value != null) { return value; } return defaultValue; } /** * Get the first value found for this key even if there are multiple. If none, then * return the specified defaultValue. */ public String getFirst(HeaderName headerName, String defaultValue) { Objects.requireNonNull(defaultValue, "defaultValue"); String value = getFirst(headerName); if (value != null) { return value; } return defaultValue; } @Nullable private String getFirstNormal(String name) { for (int i = 0; i < size(); i++) { if (name(i).equals(name)) { return value(i); } } return null; } /** * Returns all header values associated with the name. */ public List getAll(String headerName) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); return getAllNormal(normalName); } /** * Returns all header values associated with the name. */ public List getAll(HeaderName headerName) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); return getAllNormal(normalName); } private List getAllNormal(String normalName) { List results = null; for (int i = 0; i < size(); i++) { if (name(i).equals(normalName)) { if (results == null) { results = new ArrayList<>(1); } results.add(value(i)); } } if (results == null) { return Collections.emptyList(); } else { return Collections.unmodifiableList(results); } } /** * Iterates over the header entries with the given consumer. The first argument will be the normalised header * name as returned by {@link HeaderName#getNormalised()}. The second argument will be the value. Do not modify * the headers during iteration. */ public void forEachNormalised(BiConsumer entryConsumer) { for (int i = 0; i < size(); i++) { entryConsumer.accept(name(i), value(i)); } } /** * Replace any/all entries with this key, with this single entry. * * If value is {@code null}, then not added, but any existing header of same name is removed. */ public void set(String headerName, @Nullable String value) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); setNormal(headerName, normalName, value); } /** * Replace any/all entries with this key, with this single entry. * * If value is {@code null}, then not added, but any existing header of same name is removed. */ public void set(HeaderName headerName, String value) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); setNormal(headerName.getName(), normalName, value); } /** * Replace any/all entries with this key, with this single entry and validate. * * If value is {@code null}, then not added, but any existing header of same name is removed. * * @throws ZuulException on invalid name or value */ public void setAndValidate(String headerName, @Nullable String value) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); setNormal(validateField(headerName), validateField(normalName), validateField(value)); } /** * Replace any/all entries with this key, with this single entry and validate. * * If value is {@code null}, then not added, but any existing header of same name is removed. * * @throws ZuulException on invalid name or value */ public void setAndValidate(HeaderName headerName, String value) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); setNormal(validateField(headerName.getName()), validateField(normalName), validateField(value)); } /** * Replace any/all entries with this key, with this single entry if the key and entry are valid. * * If value is {@code null}, then not added, but any existing header of same name is removed. */ public void setIfValid(HeaderName headerName, String value) { Objects.requireNonNull(headerName, "headerName"); if (isValid(headerName.getName()) && isValid(value)) { String normalName = headerName.getNormalised(); setNormal(headerName.getName(), normalName, value); } } /** * Replace any/all entries with this key, with this single entry if the key and entry are valid. * * If value is {@code null}, then not added, but any existing header of same name is removed. */ public void setIfValid(String headerName, @Nullable String value) { Objects.requireNonNull(headerName, "headerName"); if (isValid(headerName) && isValid(value)) { String normalName = HeaderName.normalize(headerName); setNormal(headerName, normalName, value); } } private void setNormal(String originalName, String normalName, @Nullable String value) { int i = findNormal(normalName); if (i == ABSENT) { if (value != null) { addNormal(originalName, normalName, value); } return; } if (value != null) { value(i, value); originalName(i, originalName); i++; } clearMatchingStartingAt(i, normalName, /* removed= */ null); } /** * Returns the first index entry that has a matching name. Returns {@link #ABSENT} if absent. */ private int findNormal(String normalName) { for (int i = 0; i < size(); i++) { if (name(i).equals(normalName)) { return i; } } return -1; } /** * Removes entries that match the name, starting at the given index. */ private void clearMatchingStartingAt(int i, String normalName, @Nullable Collection removed) { // This works by having separate read and write indexes, that iterate along the list. // Values that don't match are moved to the front, leaving garbage values in place. // At the end, all values at and values are garbage and are removed. int w = i; for (int r = i; r < size(); r++) { if (!name(r).equals(normalName)) { originalName(w, originalName(r)); name(w, name(r)); value(w, value(r)); w++; } else if (removed != null) { removed.add(value(r)); } } truncate(w); } /** * Adds the name and value to the headers, except if the name is already present. Unlike * {@link #set(String, String)}, this method does not accept a {@code null} value. * * @return if the value was successfully added. */ public boolean setIfAbsent(String headerName, String value) { Objects.requireNonNull(value, "value"); String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); return setIfAbsentNormal(headerName, normalName, value); } /** * Adds the name and value to the headers, except if the name is already present. Unlike * {@link #set(HeaderName, String)}, this method does not accept a {@code null} value. * * @return if the value was successfully added. */ public boolean setIfAbsent(HeaderName headerName, String value) { Objects.requireNonNull(value, "value"); String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); return setIfAbsentNormal(headerName.getName(), normalName, value); } private boolean setIfAbsentNormal(String originalName, String normalName, String value) { int i = findNormal(normalName); if (i != ABSENT) { return false; } addNormal(originalName, normalName, value); return true; } /** * Validates and adds the name and value to the headers, except if the name is already present. Unlike * {@link #set(String, String)}, this method does not accept a {@code null} value. * * @return if the value was successfully added. */ public boolean setIfAbsentAndValid(String headerName, String value) { Objects.requireNonNull(value, "value"); Objects.requireNonNull(headerName, "headerName"); if (isValid(headerName) && isValid(value)) { String normalName = HeaderName.normalize(headerName); return setIfAbsentNormal(headerName, normalName, value); } return false; } /** * Validates and adds the name and value to the headers, except if the name is already present. Unlike * {@link #set(HeaderName, String)}, this method does not accept a {@code null} value. * * @return if the value was successfully added. */ public boolean setIfAbsentAndValid(HeaderName headerName, String value) { Objects.requireNonNull(value, "value"); Objects.requireNonNull(headerName, "headerName"); if (isValid(headerName.getName()) && isValid(value)) { String normalName = headerName.getNormalised(); return setIfAbsentNormal(headerName.getName(), normalName, value); } return false; } /** * Adds the name and value to the headers. */ public void add(String headerName, String value) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); Objects.requireNonNull(value, "value"); addNormal(headerName, normalName, value); } /** * Adds the name and value to the headers. */ public void add(HeaderName headerName, String value) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); Objects.requireNonNull(value, "value"); addNormal(headerName.getName(), normalName, value); } /** * Adds the name and value to the headers and validate. * * @throws ZuulException on invalid name or value */ public void addAndValidate(String headerName, String value) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); Objects.requireNonNull(value, "value"); addNormal(validateField(headerName), validateField(normalName), validateField(value)); } /** * Adds the name and value to the headers and validate * * @throws ZuulException on invalid name or value */ public void addAndValidate(HeaderName headerName, String value) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); Objects.requireNonNull(value, "value"); addNormal(validateField(headerName.getName()), validateField(normalName), validateField(value)); } /** * Adds the name and value to the headers if valid */ public void addIfValid(String headerName, String value) { Objects.requireNonNull(headerName, "headerName"); Objects.requireNonNull(value, "value"); if (isValid(headerName) && isValid(value)) { String normalName = HeaderName.normalize(headerName); addNormal(headerName, normalName, value); } } /** * Adds the name and value to the headers if valid */ public void addIfValid(HeaderName headerName, String value) { Objects.requireNonNull(headerName, "headerName"); Objects.requireNonNull(value, "value"); if (isValid(headerName.getName()) && isValid(value)) { String normalName = headerName.getNormalised(); addNormal(headerName.getName(), normalName, value); } } /** * Adds all the headers into this headers object. */ public void putAll(Headers headers) { for (int i = 0; i < headers.size(); i++) { addNormal(headers.originalName(i), headers.name(i), headers.value(i)); } } /** * Removes the header entries that match the given header name, and returns them as a list. */ public List remove(String headerName) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); return removeNormal(normalName); } /** * Removes the header entries that match the given header name, and returns them as a list. */ public List remove(HeaderName headerName) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); return removeNormal(normalName); } private List removeNormal(String normalName) { List removed = new ArrayList<>(); clearMatchingStartingAt(0, normalName, removed); return Collections.unmodifiableList(removed); } /** * Removes all header entries that match the given predicate. Do not access the header list from inside the * {@link Predicate#test} body. * * @return if any elements were removed. */ public boolean removeIf(Predicate> filter) { Objects.requireNonNull(filter, "filter"); boolean removed = false; int w = 0; for (int r = 0; r < size(); r++) { if (filter.test(new SimpleImmutableEntry<>(new HeaderName(originalName(r), name(r)), value(r)))) { removed = true; } else { originalName(w, originalName(r)); name(w, name(r)); value(w, value(r)); w++; } } truncate(w); return removed; } /** * Returns the collection of headers. */ public Collection

entries() { List
entries = new ArrayList<>(size()); for (int i = 0; i < size(); i++) { entries.add(new Header(new HeaderName(originalName(i), name(i)), value(i))); } return Collections.unmodifiableList(entries); } /** * Returns a set of header names found in this headers object. If there are duplicate header names, the first * one present takes precedence. */ public Set keySet() { Set headerNames = new LinkedHashSet<>(size()); for (int i = 0; i < size(); i++) { HeaderName headerName = new HeaderName(originalName(i), name(i)); // We actually do need to check contains before adding to the set because the original name may change. // In this case, the first name wins. if (!headerNames.contains(headerName)) { headerNames.add(headerName); } } return Collections.unmodifiableSet(headerNames); } /** * Returns if there is a header entry that matches the given name. */ public boolean contains(String headerName) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); return findNormal(normalName) != ABSENT; } /** * Returns if there is a header entry that matches the given name. */ public boolean contains(HeaderName headerName) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); return findNormal(normalName) != ABSENT; } /** * Returns if there is a header entry that matches the given name and value. */ public boolean contains(String headerName, String value) { String normalName = HeaderName.normalize(Objects.requireNonNull(headerName, "headerName")); Objects.requireNonNull(value, "value"); return containsNormal(normalName, value); } /** * Returns if there is a header entry that matches the given name and value. */ public boolean contains(HeaderName headerName, String value) { String normalName = Objects.requireNonNull(headerName, "headerName").getNormalised(); Objects.requireNonNull(value, "value"); return containsNormal(normalName, value); } private boolean containsNormal(String normalName, String value) { for (int i = 0; i < size(); i++) { if (name(i).equals(normalName) && value(i).equals(value)) { return true; } } return false; } /** * Returns the number of header entries. */ public int size() { return names.size(); } /** * This method should only be used for testing, as it is expensive to call. */ @Override @VisibleForTesting public int hashCode() { return asMap().hashCode(); } /** * Equality on headers is not clearly defined, but this method makes an attempt to do so. This method should * only be used for testing, as it is expensive to call. Two headers object are considered equal if they have * the same, normalized header names, and have the corresponding header values in the same order. */ @Override @VisibleForTesting public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof Headers)) { return false; } Headers other = (Headers) obj; return asMap().equals(other.asMap()); } private Map> asMap() { Map> map = new LinkedHashMap<>(size()); for (int i = 0; i < size(); i++) { map.computeIfAbsent(name(i), k -> new ArrayList<>(1)).add(value(i)); } // Return an unwrapped collection since it should not ever be returned on the API. return map; } /** * This is used for debugging. It is fairly expensive to construct, so don't call it on a hot path. */ @Override public String toString() { return asMap().toString(); } private String originalName(int i) { return originalNames.get(i); } private void originalName(int i, String originalName) { originalNames.set(i, originalName); } private String name(int i) { return names.get(i); } private void name(int i, String name) { names.set(i, name); } private String value(int i) { return values.get(i); } private void value(int i, String val) { values.set(i, val); } private void addNormal(String originalName, String normalName, String value) { originalNames.add(originalName); names.add(normalName); values.add(value); } /** * Removes all elements at and after the given index. */ private void truncate(int i) { for (int k = size() - 1; k >= i; k--) { originalNames.remove(k); names.remove(k); values.remove(k); } } /** * Checks if the given value is compliant with our RFC 7230 based check */ private static boolean isValid(@Nullable String value) { if (value == null || findInvalid(value) == ABSENT) { return true; } invalidHeaderCounter.increment(); return false; } /** * Checks if the input value is compliant with our RFC 7230 based check * Returns input value if valid, raises ZuulException otherwise */ private static String validateField(@Nullable String value) { if (value != null) { int pos = findInvalid(value); if (pos != ABSENT) { invalidHeaderCounter.increment(); throw new ZuulException("Invalid header field: char " + (int) value.charAt(pos) + " in string " + value + " does not comply with RFC 7230"); } } return value; } /** * Validated the input value based on RFC 7230 but more lenient. * Currently, only ASCII control characters are considered invalid. * * Returns the index of first invalid character. Returns {@link #ABSENT} if absent. */ private static int findInvalid(String value) { for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); // ASCII non-control characters, per RFC 7230 but slightly more lenient if (c < 31 || c == 127) { return i; } } return ABSENT; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/ZuulMessage.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.ZuulFilter; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.LastHttpContent; import javax.annotation.Nullable; /** * Represents a message that propagates through the Zuul filter chain. */ public interface ZuulMessage extends Cloneable { /** * Returns the session context of this message. */ SessionContext getContext(); /** * Returns the headers for this message. They may be request or response headers, depending on the underlying type * of this object. For some messages, there may be no headers, such as with chunked requests or responses. In this * case, a non-{@code null} default headers value will be returned. */ Headers getHeaders(); /** * Sets the headers for this message. * * @throws NullPointerException if newHeaders is {@code null}. */ void setHeaders(Headers newHeaders); /** * Returns if this message has an attached body. For requests, this is typically an HTTP POST body. For * responses, this is typically the HTTP response. */ boolean hasBody(); /** * Declares that this message has a body. This method is automatically called when {@link #bufferBodyContents} * is invoked. */ void setHasBody(boolean hasBody); /** * Returns the message body. * This is the entire buffered body, regardless of whether the underlying body chunks have been read or not. * If there is no message body, this returns {@code null}. */ @Nullable byte[] getBody(); /** * Returns the length of the entire buffered message body, or {@code 0} if there isn't a message present. */ int getBodyLength(); /** * Sets the message body. Note: if the {@code body} is {@code null}, this may not reset the body presence as * returned by {@link #hasBody}. The body is considered complete after calling this method. */ void setBody(@Nullable byte[] body); /** * Sets the message body as UTF-8 encoded text. Note that this does NOT set any headers related to the * Content-Type; callers must set or reset the content type to UTF-8. The body is considered complete after * calling this method. */ void setBodyAsText(@Nullable String bodyText); /** * Appends an HTTP content chunk to this message. Callers should be careful not to add multiple chunks that * implement {@link LastHttpContent}. * * @throws NullPointerException if {@code chunk} is {@code null}. */ void bufferBodyContents(HttpContent chunk); /** * Returns the HTTP content chunks that are part of this message. Callers should avoid retaining the return value, * as the contents may change with subsequent body mutations. */ Iterable getBodyContents(); /** * Sets the message body to be complete if it was not already so. * * @return {@code true} if the body was not yet complete, or else false. */ boolean finishBufferedBodyIfIncomplete(); /** * Indicates that the message contains a content chunk the implements {@link LastHttpContent}. */ boolean hasCompleteBody(); /** * Passes the body content chunks through the given filter, and sets them back into this message. */ void runBufferedBodyContentThroughFilter(ZuulFilter filter); /** * Clears the content chunks of this body, calling {@code release()} in the process. Users SHOULD call this method * when the body content is no longer needed. */ void disposeBufferedBody(); /** * Gets the body of this message as UTF-8 text, or {@code null} if there is no body. */ @Nullable String getBodyAsText(); /** * Reset the chunked body reader indexes. Users SHOULD call this method before retrying requests * as the chunked body buffer will have had the reader indexes changed during channel writes. */ void resetBodyReader(); /** * Returns the maximum body size that this message is willing to hold. This value value should be more than the * sum of lengths of the body chunks. The max body size may not be strictly enforced, and is informational. */ int getMaxBodySize(); /** * Returns a copy of this message. */ ZuulMessage clone(); /** * Returns a string that represents this message which is suitable for debugging. */ String getInfoForLogging(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/ZuulMessageImpl.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicPropertyFactory; import com.netflix.netty.common.ByteBufUtil; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.http.HttpHeaderNames; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.LastHttpContent; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * User: michaels@netflix.com * Date: 2/20/15 * Time: 3:10 PM */ public class ZuulMessageImpl implements ZuulMessage { protected static final DynamicIntProperty MAX_BODY_SIZE_PROP = DynamicPropertyFactory.getInstance().getIntProperty("zuul.message.body.max.size", 25 * 1000 * 1024); protected final SessionContext context; protected Headers headers; private boolean hasBody; private boolean bodyBufferedCompletely; private final List bodyChunks; public ZuulMessageImpl(SessionContext context) { this(context, new Headers()); } public ZuulMessageImpl(SessionContext context, Headers headers) { this.context = context == null ? new SessionContext() : context; this.headers = headers == null ? new Headers() : headers; this.bodyChunks = new ArrayList<>(16); } @Override public SessionContext getContext() { return context; } @Override public Headers getHeaders() { return headers; } @Override public void setHeaders(Headers newHeaders) { this.headers = newHeaders; } @Override public int getMaxBodySize() { return MAX_BODY_SIZE_PROP.get(); } @Override public void setHasBody(boolean hasBody) { this.hasBody = hasBody; } @Override public boolean hasBody() { return hasBody; } @Override public boolean hasCompleteBody() { return bodyBufferedCompletely; } @Override public void bufferBodyContents(HttpContent chunk) { setHasBody(true); ByteBufUtil.touch(chunk, "ZuulMessage buffering body content."); bodyChunks.add(chunk); if (chunk instanceof LastHttpContent) { ByteBufUtil.touch(chunk, "ZuulMessage buffering body content complete."); bodyBufferedCompletely = true; } } private void setContentLength(int length) { headers.remove(HttpHeaderNames.TRANSFER_ENCODING); headers.set(HttpHeaderNames.CONTENT_LENGTH, Integer.toString(length)); } @Override public void setBodyAsText(String bodyText) { disposeBufferedBody(); if (!Strings.isNullOrEmpty(bodyText)) { byte[] bytes = bodyText.getBytes(Charsets.UTF_8); bufferBodyContents(new DefaultLastHttpContent(Unpooled.wrappedBuffer(bytes))); setContentLength(bytes.length); } else { bufferBodyContents(new DefaultLastHttpContent()); setContentLength(0); } } @Override public void setBody(byte[] body) { disposeBufferedBody(); if (body != null && body.length > 0) { ByteBuf content = Unpooled.copiedBuffer(body); bufferBodyContents(new DefaultLastHttpContent(content)); setContentLength(body.length); } else { bufferBodyContents(new DefaultLastHttpContent()); setContentLength(0); } } @Override public String getBodyAsText() { byte[] body = getBody(); return (body != null && body.length > 0) ? new String(getBody(), Charsets.UTF_8) : null; } @Override public byte[] getBody() { if (bodyChunks.size() == 0) { return null; } int size = this.getBodyLength(); byte[] body = new byte[size]; int offset = 0; for (HttpContent chunk : bodyChunks) { ByteBuf content = chunk.content(); int len = content.writerIndex(); // writer idx tracks the total readable bytes in the buffer content.getBytes(0, body, offset, len); offset += len; } return body; } @Override public int getBodyLength() { int size = 0; for (HttpContent chunk : bodyChunks) { // writer index tracks the total number of bytes written to the buffer regardless of buffer reads size += chunk.content().writerIndex(); } return size; } @Override public Iterable getBodyContents() { return Collections.unmodifiableList(bodyChunks); } @Override public void resetBodyReader() { for (HttpContent chunk : bodyChunks) { chunk.content().resetReaderIndex(); } } @Override public boolean finishBufferedBodyIfIncomplete() { if (!bodyBufferedCompletely) { bufferBodyContents(new DefaultLastHttpContent()); return true; } return false; } @Override public void disposeBufferedBody() { bodyChunks.forEach(chunk -> { if ((chunk != null) && (chunk.refCnt() > 0)) { ByteBufUtil.touch(chunk, "ZuulMessage disposing buffered body"); chunk.release(); } }); bodyChunks.clear(); } @Override public void runBufferedBodyContentThroughFilter(ZuulFilter filter) { // Loop optimized for the common case: Most filters' processContentChunk() return // original chunk passed in as is without any processing String filterName = filter.filterName(); for (int i = 0; i < bodyChunks.size(); i++) { HttpContent origChunk = bodyChunks.get(i); ByteBufUtil.touch(origChunk, "ZuulMessage processing chunk, filter: ", filterName); HttpContent filteredChunk = filter.processContentChunk(this, origChunk); ByteBufUtil.touch(filteredChunk, "ZuulMessage processing filteredChunk, filter: ", filterName); if ((filteredChunk != null) && (filteredChunk != origChunk)) { // filter actually did some processing, set the new chunk in and release the old chunk. bodyChunks.set(i, filteredChunk); int refCnt = origChunk.refCnt(); if (refCnt > 0) { origChunk.release(refCnt); } } } } @Override public ZuulMessage clone() { ZuulMessageImpl copy = new ZuulMessageImpl(context.clone(), Headers.copyOf(headers)); this.bodyChunks.forEach(chunk -> { chunk.retain(); copy.bufferBodyContents(chunk); }); return copy; } /** * Override this in more specific subclasses to add request/response info for logging purposes. */ @Override public String getInfoForLogging() { return "ZuulMessage"; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/Cookies.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import io.netty.handler.codec.http.cookie.Cookie; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * User: Mike Smith * Date: 6/18/15 * Time: 12:04 AM */ public class Cookies { private final Map> map = new HashMap<>(); private final List all = new ArrayList<>(); public void add(Cookie cookie) { map.computeIfAbsent(cookie.name(), k -> new ArrayList<>(1)).add(cookie); all.add(cookie); } public List getAll() { return all; } public Set getNames() { return Collections.unmodifiableSet(map.keySet()); } public List get(String name) { return map.get(name); } public Cookie getFirst(String name) { List found = map.get(name); if (found == null || found.isEmpty()) { return null; } return found.get(0); } public String getFirstValue(String name) { Cookie c = getFirst(name); String value; if (c != null) { value = c.value(); } else { value = null; } return value; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpHeaderNames.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicPropertyFactory; import com.netflix.zuul.message.HeaderName; /** * A cache of both constants for common HTTP header names, and custom added header names. * * Primarily to be used as a performance optimization for avoiding repeatedly doing lower-casing and * case-insensitive comparisons of StringS. * * User: Mike Smith * Date: 8/5/15 * Time: 12:33 PM */ public final class HttpHeaderNames { private static final DynamicIntProperty MAX_CACHE_SIZE = DynamicPropertyFactory.getInstance() .getIntProperty("com.netflix.zuul.message.http.HttpHeaderNames.maxCacheSize", 30); private static final HttpHeaderNamesCache HEADER_NAME_CACHE = new HttpHeaderNamesCache(100, MAX_CACHE_SIZE.get()); public static final HeaderName COOKIE = HEADER_NAME_CACHE.get("Cookie"); public static final HeaderName SET_COOKIE = HEADER_NAME_CACHE.get("Set-Cookie"); public static final HeaderName DATE = HEADER_NAME_CACHE.get("Date"); public static final HeaderName CONNECTION = HEADER_NAME_CACHE.get("Connection"); public static final HeaderName KEEP_ALIVE = HEADER_NAME_CACHE.get("Keep-Alive"); public static final HeaderName HOST = HEADER_NAME_CACHE.get("Host"); public static final HeaderName SERVER = HEADER_NAME_CACHE.get("Server"); public static final HeaderName VIA = HEADER_NAME_CACHE.get("Via"); public static final HeaderName USER_AGENT = HEADER_NAME_CACHE.get("User-Agent"); public static final HeaderName REFERER = HEADER_NAME_CACHE.get("Referer"); public static final HeaderName ORIGIN = HEADER_NAME_CACHE.get("Origin"); public static final HeaderName LOCATION = HEADER_NAME_CACHE.get("Location"); public static final HeaderName UPGRADE = HEADER_NAME_CACHE.get("Upgrade"); public static final HeaderName CONTENT_TYPE = HEADER_NAME_CACHE.get("Content-Type"); public static final HeaderName CONTENT_LENGTH = HEADER_NAME_CACHE.get("Content-Length"); public static final HeaderName CONTENT_ENCODING = HEADER_NAME_CACHE.get("Content-Encoding"); public static final HeaderName ACCEPT = HEADER_NAME_CACHE.get("Accept"); public static final HeaderName ACCEPT_ENCODING = HEADER_NAME_CACHE.get("Accept-Encoding"); public static final HeaderName ACCEPT_LANGUAGE = HEADER_NAME_CACHE.get("Accept-Language"); public static final HeaderName TRANSFER_ENCODING = HEADER_NAME_CACHE.get("Transfer-Encoding"); public static final HeaderName TE = HEADER_NAME_CACHE.get("TE"); public static final HeaderName RANGE = HEADER_NAME_CACHE.get("Range"); public static final HeaderName ACCEPT_RANGES = HEADER_NAME_CACHE.get("Accept-Ranges"); public static final HeaderName ALLOW = HEADER_NAME_CACHE.get("Allow"); public static final HeaderName VARY = HEADER_NAME_CACHE.get("Vary"); public static final HeaderName LAST_MODIFIED = HEADER_NAME_CACHE.get("Last-Modified"); public static final HeaderName ETAG = HEADER_NAME_CACHE.get("ETag"); public static final HeaderName EXPIRES = HEADER_NAME_CACHE.get("Expires"); public static final HeaderName CACHE_CONTROL = HEADER_NAME_CACHE.get("Cache-Control"); public static final HeaderName EDGE_CONTROL = HEADER_NAME_CACHE.get("Edge-Control"); public static final HeaderName PRAGMA = HEADER_NAME_CACHE.get("Pragma"); public static final HeaderName X_FORWARDED_HOST = HEADER_NAME_CACHE.get("X-Forwarded-Host"); public static final HeaderName X_FORWARDED_FOR = HEADER_NAME_CACHE.get("X-Forwarded-For"); public static final HeaderName X_FORWARDED_PORT = HEADER_NAME_CACHE.get("X-Forwarded-Port"); public static final HeaderName X_FORWARDED_PROTO = HEADER_NAME_CACHE.get("X-Forwarded-Proto"); public static final HeaderName X_FORWARDED_PROTO_VERSION = HEADER_NAME_CACHE.get("X-Forwarded-Proto-Version"); public static final HeaderName ACCESS_CONTROL_ALLOW_ORIGIN = HEADER_NAME_CACHE.get("Access-Control-Allow-Origin"); public static final HeaderName ACCESS_CONTROL_ALLOW_CREDENTIALS = HEADER_NAME_CACHE.get("Access-Control-Allow-Credentials"); public static final HeaderName ACCESS_CONTROL_ALLOW_HEADERS = HEADER_NAME_CACHE.get("Access-Control-Allow-Headers"); public static final HeaderName ACCESS_CONTROL_ALLOW_METHODS = HEADER_NAME_CACHE.get("Access-Control-Allow-Methods"); public static final HeaderName ACCESS_CONTROL_REQUEST_HEADERS = HEADER_NAME_CACHE.get("Access-Control-Request-Headers"); public static final HeaderName ACCESS_CONTROL_EXPOSE_HEADERS = HEADER_NAME_CACHE.get("Access-Control-Expose-Headers"); public static final HeaderName ACCESS_CONTROL_MAX_AGE_HEADERS = HEADER_NAME_CACHE.get("Access-Control-Max-Age"); public static final HeaderName STRICT_TRANSPORT_SECURITY = HEADER_NAME_CACHE.get("Strict-Transport-Security"); public static final HeaderName LINK = HEADER_NAME_CACHE.get("Link"); /** * Looks up the name in the cache, and if does not exist, then creates and adds a new one * (up to the max cache size). * * @param name * @return HeaderName - never null. */ public static HeaderName get(String name) { return HEADER_NAME_CACHE.get(name); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpHeaderNamesCache.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.netflix.zuul.message.HeaderName; import java.util.concurrent.ConcurrentHashMap; /** * User: Mike Smith * Date: 8/5/15 * Time: 1:08 PM */ public class HttpHeaderNamesCache { private final ConcurrentHashMap cache; private final int maxSize; public HttpHeaderNamesCache(int initialSize, int maxSize) { this.cache = new ConcurrentHashMap<>(initialSize); this.maxSize = maxSize; } public boolean isFull() { return cache.size() >= maxSize; } public HeaderName get(String name) { // Check in the static cache for this headername if available. // NOTE: we do this lookup case-sensitively, as doing case-INSENSITIVELY removes the purpose of // caching the object in the first place (ie. the expensive operation we want to avoid by caching // is the case-insensitive string comparisons). HeaderName hn = cache.get(name); if (hn == null) { // Here we're accepting that the isFull check is not happening atomically with the put, as we don't mind // too much if the cache overfills a bit. if (isFull()) { hn = new HeaderName(name); } else { hn = cache.computeIfAbsent(name, (newName) -> new HeaderName(newName)); } } return hn; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpQueryParams.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.google.common.base.Strings; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; /** * User: michaels * Date: 2/24/15 * Time: 10:58 AM */ public class HttpQueryParams implements Cloneable { private final ListMultimap delegate; private final boolean immutable; private final Map trailingEquals; public HttpQueryParams() { delegate = LinkedListMultimap.create(); immutable = false; trailingEquals = new HashMap<>(); } private HttpQueryParams(ListMultimap delegate) { this.delegate = delegate; immutable = ImmutableListMultimap.class.isAssignableFrom(delegate.getClass()); trailingEquals = new HashMap<>(); } public static HttpQueryParams parse(String queryString) { HttpQueryParams queryParams = new HttpQueryParams(); if (queryString == null) { return queryParams; } StringTokenizer st = new StringTokenizer(queryString, "&"); int i; while (st.hasMoreTokens()) { String s = st.nextToken(); i = s.indexOf("="); // key-value query param if (i > 0) { String name = s.substring(0, i); String value = s.substring(i + 1); try { name = URLDecoder.decode(name, StandardCharsets.UTF_8); value = URLDecoder.decode(value, StandardCharsets.UTF_8); } catch (Exception e) { // do nothing } queryParams.add(name, value); // respect trailing equals for key-only params if (s.endsWith("=") && value.isEmpty()) { queryParams.setTrailingEquals(name, true); } } // key only else if (!s.isEmpty()) { String name = s; try { name = URLDecoder.decode(name, StandardCharsets.UTF_8); } catch (Exception e) { // do nothing } queryParams.add(name, ""); } } return queryParams; } /** * Get the first value found for this key even if there are multiple. If none, then * return null. */ public String getFirst(String name) { List values = delegate.get(name); if (!values.isEmpty()) { return values.get(0); } return null; } public List get(String name) { return delegate.get(name.toLowerCase(Locale.ROOT)); } public boolean contains(String name) { return delegate.containsKey(name); } public boolean contains(String name, String value) { return delegate.containsEntry(name, value); } /** * Per https://tools.ietf.org/html/rfc7230#page-19, query params are to be treated as case sensitive. * However, as a utility, this exists to allow us to do a case insensitive match on demand. */ public boolean containsIgnoreCase(String name) { return delegate.containsKey(name) || delegate.containsKey(name.toLowerCase(Locale.ROOT)); } /** * Replace any/all entries with this key, with this single entry. */ public void set(String name, String value) { delegate.removeAll(name); delegate.put(name, value); } public void add(String name, String value) { delegate.put(name, value); } public void removeAll(String name) { delegate.removeAll(name); } public void clear() { delegate.clear(); } public Collection> entries() { return delegate.entries(); } public Set keySet() { return delegate.keySet(); } public String toEncodedString() { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : entries()) { sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); if (!Strings.isNullOrEmpty(entry.getValue())) { sb.append('='); sb.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); } else if (isTrailingEquals(entry.getKey())) { sb.append('='); } sb.append('&'); } // Remove trailing '&'. if (!sb.isEmpty() && sb.charAt(sb.length() - 1) == '&') { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : entries()) { sb.append(entry.getKey()); if (!Strings.isNullOrEmpty(entry.getValue())) { sb.append('='); sb.append(entry.getValue()); } sb.append('&'); } // Remove trailing '&'. if (!sb.isEmpty() && sb.charAt(sb.length() - 1) == '&') { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } @Override protected HttpQueryParams clone() { HttpQueryParams copy = new HttpQueryParams(); copy.delegate.putAll(this.delegate); return copy; } public HttpQueryParams immutableCopy() { return new HttpQueryParams(ImmutableListMultimap.copyOf(delegate)); } public boolean isImmutable() { return immutable; } public boolean isTrailingEquals(String key) { return trailingEquals.getOrDefault(key, false); } public void setTrailingEquals(String key, boolean trailingEquals) { this.trailingEquals.put(key, trailingEquals); } @Override public int hashCode() { return delegate.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!(obj instanceof HttpQueryParams)) { return false; } HttpQueryParams hqp2 = (HttpQueryParams) obj; return Iterables.elementsEqual(delegate.entries(), hqp2.delegate.entries()); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpRequestInfo.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import java.util.Optional; /** * User: Mike Smith * Date: 7/15/15 * Time: 1:18 PM */ public interface HttpRequestInfo extends ZuulMessage { String getProtocol(); String getMethod(); String getPath(); HttpQueryParams getQueryParams(); String getPathAndQuery(); @Override Headers getHeaders(); String getClientIp(); String getScheme(); int getPort(); String getServerName(); @Override int getMaxBodySize(); @Override String getInfoForLogging(); String getOriginalHost(); String getOriginalScheme(); String getOriginalProtocol(); int getOriginalPort(); /** * Reflects the actual destination port that the client intended to communicate with, * in preference to the port Zuul was listening on. In the case where proxy protocol is * enabled, this should reflect the destination IP encoded in the TCP payload by the load balancer. */ default Optional getClientDestinationPort() { throw new UnsupportedOperationException(); } String reconstructURI(); /** Parse and lazily cache the request cookies. */ Cookies parseCookies(); /** * Force parsing/re-parsing of the cookies. May want to do this if headers * have been mutated since cookies were first parsed. */ Cookies reParseCookies(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpRequestMessage.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.netflix.zuul.message.ZuulMessage; /** * User: Mike Smith * Date: 7/15/15 * Time: 5:36 PM */ public interface HttpRequestMessage extends HttpRequestInfo { void setProtocol(String protocol); void setMethod(String method); void setPath(String path); void setScheme(String scheme); void setServerName(String serverName); @Override ZuulMessage clone(); void storeInboundRequest(); HttpRequestInfo getInboundRequest(); void setQueryParams(HttpQueryParams queryParams); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpRequestMessageImpl.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.google.common.annotations.VisibleForTesting; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.config.DynamicStringProperty; import com.netflix.util.Pair; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.ZuulMessageImpl; import com.netflix.zuul.util.HttpUtils; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels * Date: 2/24/15 * Time: 10:54 AM */ public class HttpRequestMessageImpl implements HttpRequestMessage { private static final Logger LOG = LoggerFactory.getLogger(HttpRequestMessageImpl.class); private static final CachedDynamicBooleanProperty STRICT_HOST_HEADER_VALIDATION = new CachedDynamicBooleanProperty("zuul.HttpRequestMessage.host.header.strict.validation", true); private static final CachedDynamicIntProperty MAX_BODY_SIZE_PROP = new CachedDynamicIntProperty("zuul.HttpRequestMessage.body.max.size", 15 * 1000 * 1024); private static final CachedDynamicBooleanProperty CLEAN_COOKIES = new CachedDynamicBooleanProperty("zuul.HttpRequestMessage.cookies.clean", false); /** ":::"-delimited list of regexes to strip out of the cookie headers. */ private static final DynamicStringProperty REGEX_PTNS_TO_STRIP_PROP = new DynamicStringProperty("zuul.request.cookie.cleaner.strip", " Secure,"); private static final List RE_STRIP; static { RE_STRIP = new ArrayList<>(); for (String ptn : REGEX_PTNS_TO_STRIP_PROP.get().split(":::", -1)) { RE_STRIP.add(Pattern.compile(ptn)); } } private static final String URI_SCHEME_SEP = "://"; private static final String URI_SCHEME_HTTP = "http"; private static final String URI_SCHEME_HTTPS = "https"; private final boolean immutable; private final ZuulMessage message; private String protocol; private String method; private String path; private String decodedPath; private HttpQueryParams queryParams; private String clientIp; private String scheme; private int port; private String serverName; private final SocketAddress clientRemoteAddress; private HttpRequestInfo inboundRequest = null; private Cookies parsedCookies = null; // These attributes are populated only if immutable=true. private String reconstructedUri = null; private String pathAndQuery = null; private String infoForLogging = null; private String originalHost = null; private static final SocketAddress UNDEFINED_CLIENT_DEST_ADDRESS = new SocketAddress() { @Override public String toString() { return "Undefined destination address."; } }; public HttpRequestMessageImpl( SessionContext context, String protocol, String method, String path, HttpQueryParams queryParams, Headers headers, String clientIp, String scheme, int port, String serverName) { this( context, protocol, method, path, queryParams, headers, clientIp, scheme, port, serverName, UNDEFINED_CLIENT_DEST_ADDRESS, false); } public HttpRequestMessageImpl( SessionContext context, String protocol, String method, String path, HttpQueryParams queryParams, Headers headers, String clientIp, String scheme, int port, String serverName, SocketAddress clientRemoteAddress, boolean immutable) { this.immutable = immutable; this.message = new ZuulMessageImpl(context, headers); this.protocol = protocol; this.method = method; this.path = path; try { this.decodedPath = URLDecoder.decode(path, "UTF-8"); } catch (Exception e) { // fail to decode URI // just set decodedPath to original path this.decodedPath = path; } // Don't allow this to be null. this.queryParams = queryParams == null ? new HttpQueryParams() : queryParams; this.clientIp = clientIp; this.scheme = scheme; this.port = port; this.serverName = serverName; this.clientRemoteAddress = clientRemoteAddress; } private void immutableCheck() { if (immutable) { throw new IllegalStateException( "This HttpRequestMessageImpl is immutable. No mutating operations allowed!"); } } @Override public SessionContext getContext() { return message.getContext(); } @Override public Headers getHeaders() { return message.getHeaders(); } @Override public void setHeaders(Headers newHeaders) { immutableCheck(); message.setHeaders(newHeaders); } @Override public void setHasBody(boolean hasBody) { message.setHasBody(hasBody); } @Override public boolean hasBody() { return message.hasBody(); } @Override public void bufferBodyContents(HttpContent chunk) { message.bufferBodyContents(chunk); } @Override public void setBodyAsText(String bodyText) { message.setBodyAsText(bodyText); } @Override public void setBody(byte[] body) { message.setBody(body); } @Override public boolean finishBufferedBodyIfIncomplete() { return message.finishBufferedBodyIfIncomplete(); } @Override public Iterable getBodyContents() { return message.getBodyContents(); } @Override public void runBufferedBodyContentThroughFilter(ZuulFilter filter) { message.runBufferedBodyContentThroughFilter(filter); } @Override public String getBodyAsText() { return message.getBodyAsText(); } @Override public byte[] getBody() { return message.getBody(); } @Override public int getBodyLength() { return message.getBodyLength(); } @Override public void resetBodyReader() { message.resetBodyReader(); } @Override public boolean hasCompleteBody() { return message.hasCompleteBody(); } @Override public void disposeBufferedBody() { message.disposeBufferedBody(); } @Override public String getProtocol() { return protocol; } @Override public void setProtocol(String protocol) { immutableCheck(); this.protocol = protocol; } @Override public String getMethod() { return method; } @Override public void setMethod(String method) { immutableCheck(); this.method = method; } @Override public String getPath() { if (Objects.equals(message.getContext().get(CommonContextKeys.ZUUL_USE_DECODED_URI), Boolean.TRUE)) { return decodedPath; } return path; } public String getDecodedPath() { return decodedPath; } @Override public void setPath(String path) { immutableCheck(); this.path = path; this.decodedPath = path; } @Override public HttpQueryParams getQueryParams() { return queryParams; } @Override public String getPathAndQuery() { // If this instance is immutable, then lazy-cache. if (immutable) { if (pathAndQuery == null) { pathAndQuery = generatePathAndQuery(); } return pathAndQuery; } else { return generatePathAndQuery(); } } protected String generatePathAndQuery() { if (queryParams != null && queryParams.entries().size() > 0) { return getPath() + "?" + queryParams.toEncodedString(); } else { return getPath(); } } @Override public String getClientIp() { return clientIp; } @Deprecated @VisibleForTesting void setClientIp(String clientIp) { immutableCheck(); this.clientIp = clientIp; } /** * Note: this is meant to be used typically in cases where TLS termination happens on instance. * For CLB/ALB fronted traffic, consider using {@link #getOriginalScheme()} instead. */ @Override public String getScheme() { return scheme; } @Override public void setScheme(String scheme) { immutableCheck(); this.scheme = scheme; } @Override public int getPort() { return port; } @Deprecated @VisibleForTesting void setPort(int port) { immutableCheck(); this.port = port; } @Override public String getServerName() { return serverName; } @Override public void setServerName(String serverName) { immutableCheck(); this.serverName = serverName; } @Override public Cookies parseCookies() { if (parsedCookies == null) { parsedCookies = reParseCookies(); } return parsedCookies; } @Override public Cookies reParseCookies() { Cookies cookies = new Cookies(); for (String aCookieHeader : getHeaders().getAll(HttpHeaderNames.COOKIE)) { try { if (CLEAN_COOKIES.get()) { aCookieHeader = cleanCookieHeader(aCookieHeader); } List decoded = ServerCookieDecoder.LAX.decodeAll(aCookieHeader); for (Cookie cookie : decoded) { cookies.add(cookie); } } catch (Exception e) { LOG.warn( "Error parsing request Cookie header. cookie={}, request-info={}", aCookieHeader, getInfoForLogging(), e); } } parsedCookies = cookies; return cookies; } @VisibleForTesting static String cleanCookieHeader(String cookie) { for (Pattern stripPtn : RE_STRIP) { Matcher matcher = stripPtn.matcher(cookie); if (matcher.find()) { cookie = matcher.replaceAll(""); } } return cookie; } @Override public int getMaxBodySize() { return MAX_BODY_SIZE_PROP.get(); } @Override public ZuulMessage clone() { HttpRequestMessageImpl clone = new HttpRequestMessageImpl( message.getContext().clone(), protocol, method, path, queryParams.clone(), Headers.copyOf(message.getHeaders()), clientIp, scheme, port, serverName, clientRemoteAddress, immutable); if (getInboundRequest() != null) { clone.inboundRequest = (HttpRequestInfo) getInboundRequest().clone(); } return clone; } protected HttpRequestInfo copyRequestInfo() { HttpRequestMessageImpl req = new HttpRequestMessageImpl( message.getContext(), protocol, method, path, queryParams.immutableCopy(), Headers.copyOf(message.getHeaders()), clientIp, scheme, port, serverName, clientRemoteAddress, true); req.setHasBody(hasBody()); return req; } @Override public void storeInboundRequest() { inboundRequest = copyRequestInfo(); } @Override public HttpRequestInfo getInboundRequest() { return inboundRequest; } @Override public void setQueryParams(HttpQueryParams queryParams) { immutableCheck(); this.queryParams = queryParams; } @Override public String getInfoForLogging() { // If this instance is immutable, then lazy-cache generating this info. if (immutable) { if (infoForLogging == null) { infoForLogging = generateInfoForLogging(); } return infoForLogging; } else { return generateInfoForLogging(); } } protected String generateInfoForLogging() { HttpRequestInfo req = getInboundRequest() == null ? this : getInboundRequest(); StringBuilder sb = new StringBuilder() .append("uri=") .append(req.reconstructURI()) .append(", method=") .append(req.getMethod()) .append(", clientip=") .append(HttpUtils.getClientIP(req)); return sb.toString(); } /** * The originally request host. This will NOT include port. * * The Host header may contain port, but in this method we strip it out for consistency - use the * getOriginalPort method for that. */ @Override public String getOriginalHost() { try { if (originalHost == null) { originalHost = getOriginalHost(getHeaders(), getServerName()); } return originalHost; } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } @VisibleForTesting static String getOriginalHost(Headers headers, String serverName) throws URISyntaxException { String xForwardedHost = headers.getFirst(HttpHeaderNames.X_FORWARDED_HOST); if (xForwardedHost != null) { return xForwardedHost; } Pair host = parseHostHeader(headers); if (host.first() != null) { return host.first(); } return serverName; } @Override public String getOriginalScheme() { String scheme = getHeaders().getFirst(HttpHeaderNames.X_FORWARDED_PROTO); if (scheme == null) { scheme = getScheme(); } return scheme; } @Override public String getOriginalProtocol() { String proto = getHeaders().getFirst(HttpHeaderNames.X_FORWARDED_PROTO_VERSION); if (proto == null) { proto = getProtocol(); } return proto; } @Override public int getOriginalPort() { return getOriginalPort(getContext(), getHeaders(), getPort()); } @VisibleForTesting static int getOriginalPort(SessionContext context, Headers headers, int serverPort) { if (context.containsKey(CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS)) { return ((InetSocketAddress) context.get(CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS)).getPort(); } String portStr = headers.getFirst(HttpHeaderNames.X_FORWARDED_PORT); if (portStr != null && !portStr.isEmpty()) { return Integer.parseInt(portStr); } try { // Check if port was specified on a Host header. Pair host = parseHostHeader(headers); if (host.second() != -1) { return host.second(); } } catch (URISyntaxException e) { LOG.debug("Invalid host header, falling back to serverPort", e); } return serverPort; } @Override public Optional getClientDestinationPort() { if (clientRemoteAddress instanceof InetSocketAddress inetSocketAddress) { return Optional.of(inetSocketAddress.getPort()); } else { return Optional.empty(); } } /** * Attempt to parse the Host header from the collection of headers * and return the hostname and port components. * * @return Hostname and Port pair. * Hostname may be null. Port may be -1 when no valid port is found in the host header. */ private static Pair parseHostHeader(Headers headers) throws URISyntaxException { String host = headers.getFirst(HttpHeaderNames.HOST); if (host == null) { return new Pair<>(null, -1); } try { // attempt to use default URI parsing - this can fail when not strictly following RFC2396, // for example, having underscores in host names will fail parsing URI uri = new URI(/* scheme= */ null, host, /* path= */ null, /* query= */ null, /* fragment= */ null); if (uri.getHost() != null) { return new Pair<>(uri.getHost(), uri.getPort()); } } catch (URISyntaxException e) { LOG.debug("URI parsing failed", e); } if (STRICT_HOST_HEADER_VALIDATION.get()) { throw new URISyntaxException(host, "Invalid host"); } // fallback to using a colon split // valid IPv6 addresses would have been handled already so any colon is safely assumed a port separator String[] components = host.split(":", -1); if (components.length > 2) { // handle case with unbracketed IPv6 addresses return new Pair<>(null, -1); } String parsedHost = components[0]; int parsedPort = -1; if (components.length > 1) { try { parsedPort = Integer.parseInt(components[1]); } catch (NumberFormatException e) { // ignore failing to parse port numbers and fallback to default port LOG.debug("Parsing of host port component failed", e); } } return new Pair<>(parsedHost, parsedPort); } /** * Attempt to reconstruct the full URI that the client used. * * @return String */ @Override public String reconstructURI() { // If this instance is immutable, then lazy-cache reconstructing the uri. if (immutable) { if (reconstructedUri == null) { reconstructedUri = _reconstructURI(); } return reconstructedUri; } else { return _reconstructURI(); } } protected String _reconstructURI() { try { StringBuilder uri = new StringBuilder(100); String scheme = getOriginalScheme().toLowerCase(Locale.ROOT); uri.append(scheme); uri.append(URI_SCHEME_SEP).append(getOriginalHost()); int port = getOriginalPort(); if ((scheme.equals(URI_SCHEME_HTTP) && port == 80) || (scheme.equals(URI_SCHEME_HTTPS) && port == 443)) { // Don't need to include port. } else { uri.append(':').append(port); } uri.append(getPathAndQuery()); return uri.toString(); } catch (IllegalArgumentException e) { // This is not really so bad, just debug log it and move on. LOG.debug("Error reconstructing request URI!", e); return ""; } catch (Exception e) { LOG.error("Error reconstructing request URI!", e); return ""; } } @Override public String toString() { return "HttpRequestMessageImpl{" + "immutable=" + immutable + ", message=" + message + ", protocol='" + protocol + '\'' + ", method='" + method + '\'' + ", path='" + path + '\'' + ", queryParams=" + queryParams + ", clientIp='" + clientIp + '\'' + ", scheme='" + scheme + '\'' + ", port=" + port + ", serverName='" + serverName + '\'' + ", inboundRequest=" + inboundRequest + ", parsedCookies=" + parsedCookies + ", reconstructedUri='" + reconstructedUri + '\'' + ", pathAndQuery='" + pathAndQuery + '\'' + ", originalHost='" + originalHost + '\'' + ", infoForLogging='" + infoForLogging + '\'' + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpResponseInfo.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.netflix.zuul.message.ZuulMessage; /** * User: michaels@netflix.com * Date: 7/6/15 * Time: 5:27 PM */ public interface HttpResponseInfo extends ZuulMessage { int getStatus(); /** The immutable request that was originally received from client. */ HttpRequestInfo getInboundRequest(); @Override ZuulMessage clone(); @Override String getInfoForLogging(); Cookies parseSetCookieHeader(String setCookieValue); boolean hasSetCookieWithName(String cookieName); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpResponseMessage.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import io.netty.handler.codec.http.cookie.Cookie; /** * User: Mike Smith * Date: 7/16/15 * Time: 12:45 AM */ public interface HttpResponseMessage extends HttpResponseInfo { void setStatus(int status); @Override int getMaxBodySize(); void addSetCookie(Cookie cookie); void setSetCookie(Cookie cookie); boolean removeExistingSetCookie(String cookieName); /** The mutable request that will be sent to Origin. */ HttpRequestMessage getOutboundRequest(); /** The immutable response that was received from Origin. */ HttpResponseInfo getInboundResponse(); /** This should be called after response received from Origin, to store * a copy of the response as-is. */ void storeInboundResponse(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/http/HttpResponseMessageImpl.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicPropertyFactory; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.Header; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.ZuulMessageImpl; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.cookie.ClientCookieDecoder; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels * Date: 2/24/15 * Time: 10:54 AM */ public class HttpResponseMessageImpl implements HttpResponseMessage { private static final DynamicIntProperty MAX_BODY_SIZE_PROP = DynamicPropertyFactory.getInstance() .getIntProperty("zuul.HttpResponseMessage.body.max.size", 25 * 1000 * 1024); private static final Logger LOG = LoggerFactory.getLogger(HttpResponseMessageImpl.class); private final ZuulMessage message; private final HttpRequestMessage outboundRequest; private int status; private HttpResponseInfo inboundResponse = null; public HttpResponseMessageImpl(SessionContext context, HttpRequestMessage request, int status) { this(context, new Headers(), request, status); } public HttpResponseMessageImpl(SessionContext context, Headers headers, HttpRequestMessage request, int status) { this.message = new ZuulMessageImpl(context, headers); this.outboundRequest = request; if (this.outboundRequest.getInboundRequest() == null) { LOG.warn( "HttpResponseMessage created with a request that does not have a stored inboundRequest! Probably a" + " bug in the filter that is creating this response.", new RuntimeException("Invalid HttpRequestMessage")); } this.status = status; } public static HttpResponseMessage defaultErrorResponse(HttpRequestMessage request) { HttpResponseMessage resp = new HttpResponseMessageImpl(request.getContext(), request, 500); resp.finishBufferedBodyIfIncomplete(); return resp; } @Override public Headers getHeaders() { return message.getHeaders(); } @Override public SessionContext getContext() { return message.getContext(); } @Override public void setHeaders(Headers newHeaders) { message.setHeaders(newHeaders); } @Override public void setHasBody(boolean hasBody) { message.setHasBody(hasBody); } @Override public boolean hasBody() { return message.hasBody(); } @Override public void bufferBodyContents(HttpContent chunk) { message.bufferBodyContents(chunk); } @Override public void setBodyAsText(String bodyText) { message.setBodyAsText(bodyText); } @Override public void setBody(byte[] body) { message.setBody(body); } @Override public String getBodyAsText() { return message.getBodyAsText(); } @Override public byte[] getBody() { return message.getBody(); } @Override public int getBodyLength() { return message.getBodyLength(); } @Override public boolean hasCompleteBody() { return message.hasCompleteBody(); } @Override public boolean finishBufferedBodyIfIncomplete() { return message.finishBufferedBodyIfIncomplete(); } @Override public Iterable getBodyContents() { return message.getBodyContents(); } @Override public void resetBodyReader() { message.resetBodyReader(); } @Override public void runBufferedBodyContentThroughFilter(ZuulFilter filter) { message.runBufferedBodyContentThroughFilter(filter); } @Override public void disposeBufferedBody() { message.disposeBufferedBody(); } @Override public HttpRequestInfo getInboundRequest() { return outboundRequest.getInboundRequest(); } @Override public HttpRequestMessage getOutboundRequest() { return outboundRequest; } @Override public int getStatus() { return status; } @Override public void setStatus(int status) { this.status = status; } @Override public int getMaxBodySize() { return MAX_BODY_SIZE_PROP.get(); } @Override public Cookies parseSetCookieHeader(String setCookieValue) { Cookies cookies = new Cookies(); cookies.add(ClientCookieDecoder.STRICT.decode(setCookieValue)); return cookies; } @Override public boolean hasSetCookieWithName(String cookieName) { for (String setCookieValue : getHeaders().getAll(HttpHeaderNames.SET_COOKIE)) { Cookie cookie = ClientCookieDecoder.STRICT.decode(setCookieValue); if (cookie.name().equalsIgnoreCase(cookieName)) { return true; } } return false; } @Override public boolean removeExistingSetCookie(String cookieName) { String cookieNamePrefix = cookieName + "="; boolean dirty = false; Headers filtered = new Headers(); for (Header hdr : getHeaders().entries()) { if (hdr.getName().equals(HttpHeaderNames.SET_COOKIE)) { String value = hdr.getValue(); // Strip out this set-cookie as requested. if (value.startsWith(cookieNamePrefix)) { // Don't copy it. dirty = true; } else { // Copy all other headers. filtered.add(hdr.getName(), hdr.getValue()); } } else { // Copy all other headers. filtered.add(hdr.getName(), hdr.getValue()); } } if (dirty) { setHeaders(filtered); } return dirty; } @Override public void addSetCookie(Cookie cookie) { getHeaders().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.LAX.encode(cookie)); } @Override public void setSetCookie(Cookie cookie) { getHeaders().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.LAX.encode(cookie)); } @Override public ZuulMessage clone() { // TODO - not sure if should be cloning the outbound request object here or not.... HttpResponseMessageImpl clone = new HttpResponseMessageImpl( getContext().clone(), Headers.copyOf(getHeaders()), getOutboundRequest(), getStatus()); if (getInboundResponse() != null) { clone.inboundResponse = (HttpResponseInfo) getInboundResponse().clone(); } return clone; } protected HttpResponseInfo copyResponseInfo() { HttpResponseMessageImpl response = new HttpResponseMessageImpl( getContext(), Headers.copyOf(getHeaders()), getOutboundRequest(), getStatus()); response.setHasBody(hasBody()); return response; } @Override public String toString() { return "HttpResponseMessageImpl{" + "message=" + message + ", outboundRequest=" + outboundRequest + ", status=" + status + ", inboundResponse=" + inboundResponse + '}'; } @Override public void storeInboundResponse() { inboundResponse = copyResponseInfo(); } @Override public HttpResponseInfo getInboundResponse() { return inboundResponse; } @Override public String getInfoForLogging() { HttpRequestInfo req = getInboundRequest() == null ? getOutboundRequest() : getInboundRequest(); StringBuilder sb = new StringBuilder() .append(req.getInfoForLogging()) .append(",proxy-status=") .append(getStatus()); return sb.toString(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/message/util/HttpRequestBuilder.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.util; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; import java.util.Objects; /** * Builder for a zuul http request. *exclusively* for use in unit tests. * * For default values initialized in the constructor: *
 * {@code new HttpRequestBuilder(context).withDefaults();}
 *
* * For overrides : *
 * {@code new HttpRequestBuilder(context).withHeaders(httpHeaders).withQueryParams(requestParams).build();}
 * 
* @author Argha C * @since 5/11/21 */ public final class HttpRequestBuilder { private final SessionContext sessionContext; private final String protocol; private String method; private String path; private HttpQueryParams queryParams; private Headers headers; private final String clientIp; private final String scheme; private int port; private String serverName; private boolean isBuilt; public HttpRequestBuilder(SessionContext context) { sessionContext = Objects.requireNonNull(context); protocol = HttpVersion.HTTP_1_1.text(); method = "get"; path = "/"; queryParams = new HttpQueryParams(); headers = new Headers(); clientIp = "::1"; scheme = "https"; port = 443; isBuilt = false; } /** * Builds a request with basic defaults * * @return `HttpRequestMessage` */ public HttpRequestMessage withDefaults() { return build(); } public HttpRequestBuilder withHost(String hostName) { serverName = Objects.requireNonNull(hostName); return this; } public HttpRequestBuilder withHeaders(Headers requestHeaders) { headers = Objects.requireNonNull(requestHeaders); return this; } public HttpRequestBuilder withQueryParams(HttpQueryParams requestParams) { this.queryParams = Objects.requireNonNull(requestParams); return this; } public HttpRequestBuilder withMethod(HttpMethod httpMethod) { method = Objects.requireNonNull(httpMethod).name(); return this; } public HttpRequestBuilder withUri(String uri) { path = Objects.requireNonNull(uri); return this; } public HttpRequestBuilder withPort(int port) { this.port = port; return this; } /** * Used to build a request with overridden values * * @return `HttpRequestMessage` */ public HttpRequestMessage build() { if (isBuilt) { throw new IllegalStateException("Builder must only be invoked once!"); } isBuilt = true; return new HttpRequestMessageImpl( sessionContext, protocol, method, path, queryParams, headers, clientIp, scheme, port, serverName); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/metrics/OriginStats.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.metrics; /** * User: michaels@netflix.com * Date: 3/20/15 * Time: 5:55 PM */ public interface OriginStats { public void started(); public void completed(boolean success, long totalTimeMS); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/metrics/OriginStatsFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.metrics; /** * User: michaels@netflix.com * Date: 3/20/15 * Time: 6:14 PM */ public interface OriginStatsFactory { public OriginStats create(String name); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/monitoring/ConnCounter.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; import com.netflix.spectator.api.Gauge; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.util.HashMap; import java.util.Map; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A counter for connection stats. Not thread-safe. */ @SuppressWarnings("ErroneousBitwiseExpression") public final class ConnCounter { private static final Logger logger = LoggerFactory.getLogger(ConnCounter.class); private static final AttributeKey CONN_COUNTER = AttributeKey.newInstance("zuul.conncounter"); private static final int LOCK_COUNT = 256; private static final int LOCK_MASK = LOCK_COUNT - 1; private static final Attrs EMPTY = Attrs.newInstance(); /** * An array of locks to guard the gauges. This is the same as Guava's Striped, but avoids the dep. *

* This can be removed after https://github.com/Netflix/spectator/issues/862 is fixed. */ private static final Object[] locks = new Object[LOCK_COUNT]; static { assert (LOCK_COUNT & LOCK_MASK) == 0; for (int i = 0; i < locks.length; i++) { locks[i] = new Object(); } } private final Registry registry; private final Channel chan; private final Id metricBase; private String lastCountKey; private final Map counts = new HashMap<>(); private ConnCounter(Registry registry, Channel chan, Id metricBase) { this.registry = Objects.requireNonNull(registry); this.chan = Objects.requireNonNull(chan); this.metricBase = Objects.requireNonNull(metricBase); } public static ConnCounter install(Channel chan, Registry registry, Id metricBase) { ConnCounter counter = new ConnCounter(registry, chan, metricBase); if (!chan.attr(CONN_COUNTER).compareAndSet(null, counter)) { throw new IllegalStateException("pre-existing counter already present"); } return counter; } public static ConnCounter from(Channel chan) { Objects.requireNonNull(chan); ConnCounter counter = chan.attr(CONN_COUNTER).get(); if (counter != null) { return counter; } if (chan.parent() != null && (counter = chan.parent().attr(CONN_COUNTER).get()) != null) { return counter; } throw new IllegalStateException("no counter on channel"); } public void increment(String event) { increment(event, EMPTY); } public void increment(String event, Attrs extraDimensions) { Objects.requireNonNull(event); Objects.requireNonNull(extraDimensions); if (counts.containsKey(event)) { // TODO(carl-mastrangelo): make this throw IllegalStateException after verifying this doesn't happen. logger.warn("Duplicate conn counter increment {}", event); return; } Attrs connDims = chan.attr(Server.CONN_DIMENSIONS).get(); Map dimTags = new HashMap<>(connDims.size() + extraDimensions.size()); connDims.forEach((k, v) -> dimTags.put(k.name(), String.valueOf(v))); extraDimensions.forEach((k, v) -> dimTags.put(k.name(), String.valueOf(v))); dimTags.put("from", lastCountKey != null ? lastCountKey : "nascent"); lastCountKey = event; Id id = registry.createId(metricBase.name() + '.' + event) .withTags(metricBase.tags()) .withTags(dimTags); Gauge gauge = registry.gauge(id); synchronized (getLock(id)) { double current = gauge.value(); gauge.set(Double.isNaN(current) ? 1 : current + 1); } counts.put(event, gauge); } public double getCurrentActiveConns() { return counts.containsKey("active") ? counts.get("active").value() : 0.0; } public void decrement(String event) { Objects.requireNonNull(event); Gauge gauge = counts.remove(event); if (gauge == null) { // TODO(carl-mastrangelo): make this throw IllegalStateException after verifying this doesn't happen. logger.warn("Missing conn counter increment {}", event); return; } synchronized (getLock(gauge.id())) { // Noop gauges break this assertion in tests, but the type is package private. Check to make sure // the gauge has a value, or by implementation cannot have a value. assert !Double.isNaN(gauge.value()) || gauge.getClass().getName().equals("com.netflix.spectator.api.NoopGauge"); gauge.set(gauge.value() - 1); } } // This is here to pick the correct lock stripe. This avoids multiple threads synchronizing on the // same lock in the common case. This can go away once there is an atomic gauge update implemented // in spectator. private static Object getLock(Id id) { return locks[id.hashCode() & LOCK_MASK]; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/monitoring/ConnTimer.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; import com.netflix.config.DynamicBooleanProperty; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.time.Duration; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * A timer for connection stats. Not thread-safe. */ public final class ConnTimer { private static final DynamicBooleanProperty PRECISE_TIMING = new DynamicBooleanProperty("zuul.conn.precise_timing", false); private static final AttributeKey CONN_TIMER = AttributeKey.newInstance("zuul.conntimer"); private static final Duration MIN_CONN_TIMING = Duration.ofNanos(1024); private static final Duration MAX_CONN_TIMING = Duration.ofDays(366); private static final Attrs EMPTY = Attrs.newInstance(); private final Registry registry; private final Channel chan; // TODO(carl-mastrangelo): make this changeable. private final Id metricBase; @Nullable private final Id preciseMetricBase; private final Map timings = new LinkedHashMap<>(); private ConnTimer(Registry registry, Channel chan, Id metricBase) { this.registry = Objects.requireNonNull(registry); this.chan = Objects.requireNonNull(chan); this.metricBase = Objects.requireNonNull(metricBase); if (PRECISE_TIMING.get()) { preciseMetricBase = registry.createId(metricBase.name() + ".pct").withTags(metricBase.tags()); } else { preciseMetricBase = null; } } public static ConnTimer install(Channel chan, Registry registry, Id metricBase) { ConnTimer timer = new ConnTimer(registry, chan, metricBase); if (!chan.attr(CONN_TIMER).compareAndSet(null, timer)) { throw new IllegalStateException("pre-existing timer already present"); } return timer; } public static ConnTimer from(Channel chan) { Objects.requireNonNull(chan); ConnTimer timer = chan.attr(CONN_TIMER).get(); if (timer != null) { return timer; } if (chan.parent() != null && (timer = chan.parent().attr(CONN_TIMER).get()) != null) { return timer; } throw new IllegalStateException("no timer on channel"); } public void record(Long now, String event) { record(now, event, EMPTY); } public void record(Long now, String event, Attrs extraDimensions) { if (timings.containsKey(event)) { return; } Objects.requireNonNull(now); Objects.requireNonNull(event); Objects.requireNonNull(extraDimensions); Attrs connDims = chan.attr(Server.CONN_DIMENSIONS).get(); Map dimTags = new HashMap<>(connDims.size() + extraDimensions.size()); connDims.forEach((k, v) -> dimTags.put(k.name(), String.valueOf(v))); extraDimensions.forEach((k, v) -> dimTags.put(k.name(), String.valueOf(v))); // Note: this is effectively O(n^2) because it will be called for each event in the connection // setup. It should be bounded to at most 10 or so. timings.forEach((from, stamp) -> { long durationNanos = now - stamp; if (durationNanos == 0) { // This may happen if an event is double listed, or if the timer is not accurate enough to record // it. return; } registry.timer(buildId(metricBase, from, event, dimTags)).record(durationNanos, TimeUnit.NANOSECONDS); if (preciseMetricBase != null) { PercentileTimer.builder(registry) .withId(buildId(preciseMetricBase, from, event, dimTags)) .withRange(MIN_CONN_TIMING, MAX_CONN_TIMING) .build() .record(durationNanos, TimeUnit.NANOSECONDS); } }); timings.put(event, now); } private Id buildId(Id base, String from, String to, Map tags) { return registry.createId(metricBase.name() + '.' + from + '-' + to) .withTags(base.tags()) .withTags(tags); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/monitoring/MonitoringHelper.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; /** * Dummy implementations of CounterFactory, TracerFactory, and Tracer * @author mhawthorne */ public class MonitoringHelper { public static final void initMocks() { TracerFactory.initialize(new TracerFactoryImpl()); } private static final class TracerFactoryImpl extends TracerFactory { @Override public Tracer startMicroTracer(String name) { return new TracerImpl(); } } private static final class TracerImpl implements Tracer { @Override public void setName(String name) {} @Override public void stopAndLog() {} } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/monitoring/Tracer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; /** * Time based monitoring metric. * * @author mhawthorne */ public interface Tracer { /** * Stops and Logs a time based tracer * */ void stopAndLog(); /** * Sets the name for the time based tracer * * @param name a String value */ void setName(String name); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/monitoring/TracerFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; /** * Abstraction layer to provide time-based monitoring. * * @author mhawthorne */ public abstract class TracerFactory { private static TracerFactory INSTANCE; /** * sets a TracerFactory Implementation * * @param f a TracerFactory value */ public static final void initialize(TracerFactory f) { INSTANCE = f; } /** * Returns the singleton TracerFactory * * @return a TracerFactory value */ public static final TracerFactory instance() { if (INSTANCE == null) { throw new IllegalStateException(String.format("%s not initialized", TracerFactory.class.getSimpleName())); } return INSTANCE; } public abstract Tracer startMicroTracer(String name); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/ChannelUtils.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.Channel; public class ChannelUtils { public static String channelInfoForLogging(Channel ch) { if (ch == null) { return "null"; } String passport = CurrentPassport.fromChannel(ch).toString(); StringBuilder builder = new StringBuilder(256 + passport.length()); builder.append("Channel: ") .append(ch) .append(", active=") .append(ch.isActive()) .append(", open=") .append(ch.isOpen()) .append(", registered=") .append(ch.isRegistered()) .append(", writable=") .append(ch.isWritable()) .append(", Passport: ") .append(passport); return builder.toString(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/NettyRequestAttemptFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.exception.ErrorType; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.exception.OutboundException; import com.netflix.zuul.netty.connectionpool.OriginConnectException; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.origins.OriginConcurrencyExceededException; import io.netty.channel.unix.Errors; import io.netty.handler.codec.http2.Http2Exception.HeaderListSizeException; import io.netty.handler.timeout.ReadTimeoutException; import java.nio.channels.ClosedChannelException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class NettyRequestAttemptFactory { private static final Logger LOG = LoggerFactory.getLogger(NettyRequestAttemptFactory.class); public ErrorType mapNettyToOutboundErrorType(Throwable t) { if (t instanceof ReadTimeoutException) { return OutboundErrorType.READ_TIMEOUT; } if (t instanceof OriginConcurrencyExceededException) { return OutboundErrorType.ORIGIN_CONCURRENCY_EXCEEDED; } if (t instanceof OriginConnectException) { return ((OriginConnectException) t).getErrorType(); } if (t instanceof OutboundException) { return ((OutboundException) t).getOutboundErrorType(); } if (t instanceof Errors.NativeIoException && ((Errors.NativeIoException) t).expectedErr() == Errors.ERRNO_ECONNRESET_NEGATIVE) { // This is a "Connection reset by peer" which we see fairly often happening when Origin servers are // overloaded. LOG.warn("ERRNO_ECONNRESET_NEGATIVE mapped to RESET_CONNECTION", t); return OutboundErrorType.RESET_CONNECTION; } if (t instanceof ClosedChannelException) { return OutboundErrorType.RESET_CONNECTION; } if (t instanceof HeaderListSizeException) { return OutboundErrorType.HEADER_FIELDS_TOO_LARGE; } Throwable cause = t.getCause(); if (cause instanceof IllegalStateException && cause.getMessage().contains("server")) { LOG.warn("IllegalStateException mapped to NO_AVAILABLE_SERVERS", cause); return OutboundErrorType.NO_AVAILABLE_SERVERS; } return OutboundErrorType.OTHER; } public OutboundException mapNettyToOutboundException(Throwable t, SessionContext context) { if (t instanceof OutboundException) { return (OutboundException) t; } // Map this throwable to zuul's OutboundException. ErrorType errorType = mapNettyToOutboundErrorType(t); RequestAttempts attempts = RequestAttempts.getFromSessionContext(context); if (errorType == OutboundErrorType.OTHER) { return new OutboundException(errorType, attempts, t); } return new OutboundException(errorType, attempts); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/RequestCancelledEvent.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty; /** * User: michaels@netflix.com * Date: 4/13/17 * Time: 6:09 PM */ public class RequestCancelledEvent {} ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/SpectatorUtils.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty; import com.netflix.spectator.api.CompositeRegistry; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.api.Timer; public final class SpectatorUtils { private SpectatorUtils() {} public static Counter newCounter(String name, String id) { return Spectator.globalRegistry().counter(name, "id", id); } public static Counter newCounter(String name, String id, String... tags) { String[] allTags = getTagsWithId(id, tags); return Spectator.globalRegistry().counter(name, allTags); } public static Timer newTimer(String name, String id) { return Spectator.registry().timer(name, "id", id); } public static Timer newTimer(String name, String id, String... tags) { return Spectator.globalRegistry().timer(name, getTagsWithId(id, tags)); } public static T newGauge(String name, String id, T number) { CompositeRegistry registry = Spectator.globalRegistry(); Id gaugeId = registry.createId(name, "id", id); return registry.gauge(gaugeId, number); } public static T newGauge(String name, String id, T number, String... tags) { CompositeRegistry registry = Spectator.globalRegistry(); Id gaugeId = registry.createId(name, getTagsWithId(id, tags)); return registry.gauge(gaugeId, number); } private static String[] getTagsWithId(String id, String[] tags) { String[] allTags = new String[tags.length + 2]; System.arraycopy(tags, 0, allTags, 0, tags.length); allTags[allTags.length - 2] = "id"; allTags[allTags.length - 1] = id; return allTags; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/BasicRequestStat.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.google.common.base.Stopwatch; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.exception.ErrorType; import com.netflix.zuul.exception.OutboundErrorType; import java.util.concurrent.TimeUnit; /** * @author michaels */ public class BasicRequestStat implements RequestStat { private volatile boolean isFinished; private volatile Stopwatch stopwatch; public BasicRequestStat() { this.isFinished = false; this.stopwatch = Stopwatch.createStarted(); } @Override public RequestStat server(DiscoveryResult server) { return this; } @Override public boolean isFinished() { return isFinished; } @Override public long duration() { long ms = stopwatch.elapsed(TimeUnit.MILLISECONDS); return ms > 0 ? ms : 0; } @Override public void serviceUnavailable() { failAndSetErrorCode(OutboundErrorType.SERVICE_UNAVAILABLE); } @Override public void generalError() { failAndSetErrorCode(OutboundErrorType.OTHER); } @Override public void failAndSetErrorCode(ErrorType error) { // override to implement metric tracking } @Override public void updateWithHttpStatusCode(int httpStatusCode) { // override to implement metric tracking } @Override public void finalAttempt(boolean finalAttempt) {} @Override public boolean finishIfNotAlready() { if (isFinished) { return false; } stopwatch.stop(); publishMetrics(); isFinished = true; return true; } protected void publishMetrics() { // override to publish metrics here } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ClientChannelManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.EventLoop; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.util.concurrent.atomic.AtomicReference; /** * User: michaels@netflix.com * Date: 7/8/16 * Time: 12:36 PM */ public interface ClientChannelManager { void init(); boolean isAvailable(); int getInflightRequestsCount(); void shutdown(); default void gracefulShutdown() { shutdown(); } boolean release(PooledConnection conn); Promise acquire(EventLoop eventLoop); Promise acquire( EventLoop eventLoop, Object key, CurrentPassport passport, AtomicReference selectedServer, AtomicReference selectedHostAddr); boolean isCold(); boolean remove(PooledConnection conn); int getConnsInPool(); int getConnsInUse(); ConnectionPoolConfig getConfig(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ClientTimeoutHandler.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.AttributeKey; import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Client Timeout Handler * * Author: Arthur Gonigberg * Date: July 01, 2019 */ public final class ClientTimeoutHandler { private static final Logger LOG = LoggerFactory.getLogger(ClientTimeoutHandler.class); public static final AttributeKey ORIGIN_RESPONSE_READ_TIMEOUT = AttributeKey.newInstance("originResponseReadTimeout"); public static final class InboundHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { if (msg instanceof LastHttpContent) { LOG.debug( "[{}] Removing read timeout handler", ctx.channel().id()); PooledConnection.getFromChannel(ctx.channel()).removeReadTimeoutHandler(); } } finally { super.channelRead(ctx, msg); } } } public static final class OutboundHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { try { if (!(msg instanceof LastHttpContent)) { return; } Duration timeout = ctx.channel().attr(ORIGIN_RESPONSE_READ_TIMEOUT).get(); if (timeout != null) { promise.addListener(e -> { if (e.isSuccess()) { LOG.debug( "[{}] Adding read timeout handler: {}", ctx.channel().id(), timeout.toMillis()); PooledConnection.getFromChannel(ctx.channel()).startReadTimeoutHandler(timeout); } }); } } finally { super.write(ctx, msg, promise); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ConnectionPoolConfig.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.zuul.origins.OriginName; /** * Created by saroskar on 3/24/16. */ public interface ConnectionPoolConfig { /* Origin name from connection pool */ OriginName getOriginName(); /* Max number of requests per connection before it needs to be recycled */ int getMaxRequestsPerConnection(); /* Max connections per host */ int maxConnectionsPerHost(); int perServerWaterline(); /* Origin client TCP configuration options */ int getConnectTimeout(); /* number of milliseconds connection can stay idle in a connection pool before it is closed */ int getIdleTimeout(); int getTcpReceiveBufferSize(); int getTcpSendBufferSize(); int getNettyWriteBufferHighWaterMark(); int getNettyWriteBufferLowWaterMark(); boolean getTcpKeepAlive(); boolean getTcpNoDelay(); boolean getNettyAutoRead(); boolean isSecure(); boolean useIPAddrForServer(); default boolean isCloseOnCircuitBreakerEnabled() { return true; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ConnectionPoolConfigImpl.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import com.netflix.client.config.IClientConfigKey; import com.netflix.zuul.origins.OriginName; import java.util.Objects; /** * Created by saroskar on 3/24/16. */ public class ConnectionPoolConfigImpl implements ConnectionPoolConfig { static final int DEFAULT_BUFFER_SIZE = 32 * 1024; static final int DEFAULT_CONNECT_TIMEOUT = 500; static final int DEFAULT_IDLE_TIMEOUT = 60000; static final int DEFAULT_MAX_CONNS_PER_HOST = 50; static final int DEFAULT_PER_SERVER_WATERLINE = 4; static final int DEFAULT_MAX_REQUESTS_PER_CONNECTION = 1000; static final boolean DEFAULT_TCP_NO_DELAY = true; // TODO(argha-c): Document why these values were chosen, as opposed to defaults of 32k/64k static final int DEFAULT_WRITE_BUFFER_HIGH_WATER_MARK = 32 * 1024; static final int DEFAULT_WRITE_BUFFER_LOW_WATER_MARK = 8 * 1024; /** * NOTE that each eventloop has its own connection pool per host, and this is applied per event-loop. */ public static final IClientConfigKey PER_SERVER_WATERLINE = new CommonClientConfigKey<>("PerServerWaterline") {}; public static final IClientConfigKey CLOSE_ON_CIRCUIT_BREAKER = new CommonClientConfigKey<>("CloseOnCircuitBreaker") {}; public static final IClientConfigKey MAX_REQUESTS_PER_CONNECTION = new CommonClientConfigKey<>("MaxRequestsPerConnection") {}; public static final IClientConfigKey TCP_KEEP_ALIVE = new CommonClientConfigKey<>("TcpKeepAlive") {}; public static final IClientConfigKey TCP_NO_DELAY = new CommonClientConfigKey<>("TcpNoDelay") {}; public static final IClientConfigKey AUTO_READ = new CommonClientConfigKey<>("AutoRead") {}; public static final IClientConfigKey WRITE_BUFFER_HIGH_WATER_MARK = new CommonClientConfigKey<>("WriteBufferHighWaterMark") {}; public static final IClientConfigKey WRITE_BUFFER_LOW_WATER_MARK = new CommonClientConfigKey<>("WriteBufferLowWaterMark") {}; private final OriginName originName; private final IClientConfig clientConfig; public ConnectionPoolConfigImpl(OriginName originName, IClientConfig clientConfig) { this.originName = Objects.requireNonNull(originName, "originName"); this.clientConfig = clientConfig; } @Override public OriginName getOriginName() { return originName; } @Override public int getConnectTimeout() { return clientConfig.getPropertyAsInteger(IClientConfigKey.Keys.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT); } @Override public int getMaxRequestsPerConnection() { return clientConfig.getPropertyAsInteger(MAX_REQUESTS_PER_CONNECTION, DEFAULT_MAX_REQUESTS_PER_CONNECTION); } @Override public int maxConnectionsPerHost() { return clientConfig.getPropertyAsInteger( IClientConfigKey.Keys.MaxConnectionsPerHost, DEFAULT_MAX_CONNS_PER_HOST); } @Override public int perServerWaterline() { return clientConfig.getPropertyAsInteger(PER_SERVER_WATERLINE, DEFAULT_PER_SERVER_WATERLINE); } @Override public int getIdleTimeout() { return clientConfig.getPropertyAsInteger( IClientConfigKey.Keys.ConnIdleEvictTimeMilliSeconds, DEFAULT_IDLE_TIMEOUT); } @Override public boolean getTcpKeepAlive() { return clientConfig.getPropertyAsBoolean(TCP_KEEP_ALIVE, false); } @Override public boolean getTcpNoDelay() { return clientConfig.getPropertyAsBoolean(TCP_NO_DELAY, DEFAULT_TCP_NO_DELAY); } @Override public int getTcpReceiveBufferSize() { return clientConfig.getPropertyAsInteger(IClientConfigKey.Keys.ReceiveBufferSize, DEFAULT_BUFFER_SIZE); } @Override public int getTcpSendBufferSize() { return clientConfig.getPropertyAsInteger(IClientConfigKey.Keys.SendBufferSize, DEFAULT_BUFFER_SIZE); } @Override public int getNettyWriteBufferHighWaterMark() { return clientConfig.getPropertyAsInteger(WRITE_BUFFER_HIGH_WATER_MARK, DEFAULT_WRITE_BUFFER_HIGH_WATER_MARK); } @Override public int getNettyWriteBufferLowWaterMark() { return clientConfig.getPropertyAsInteger(WRITE_BUFFER_LOW_WATER_MARK, DEFAULT_WRITE_BUFFER_LOW_WATER_MARK); } @Override public boolean getNettyAutoRead() { return clientConfig.getPropertyAsBoolean(AUTO_READ, false); } @Override public boolean isSecure() { return clientConfig.getPropertyAsBoolean(IClientConfigKey.Keys.IsSecure, false); } @Override public boolean useIPAddrForServer() { return clientConfig.getPropertyAsBoolean(IClientConfigKey.Keys.UseIPAddrForServer, true); } @Override public boolean isCloseOnCircuitBreakerEnabled() { return clientConfig.getPropertyAsBoolean(CLOSE_ON_CIRCUIT_BREAKER, true); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ConnectionPoolHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import static com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.spectator.api.Spectator; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.origins.OriginName; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.handler.timeout.IdleStateEvent; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 6/23/16 * Time: 1:57 PM */ @ChannelHandler.Sharable public class ConnectionPoolHandler extends ChannelDuplexHandler { private static final Logger LOG = LoggerFactory.getLogger(ConnectionPoolHandler.class); private final ConnectionPoolMetrics metrics; private final OriginName originName; @Deprecated public ConnectionPoolHandler(OriginName originName) { this(ConnectionPoolMetrics.create(Objects.requireNonNull(originName), Spectator.globalRegistry())); } public ConnectionPoolHandler(ConnectionPoolMetrics metrics) { this.originName = metrics.originName(); this.metrics = metrics; } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // First let other handlers do their thing ... // super.userEventTriggered(ctx, evt); if (evt instanceof IdleStateEvent) { // Log some info about this. metrics.idleCounter().increment(); String msg = "Origin channel for origin - " + originName + " - idle timeout has fired. " + ChannelUtils.channelInfoForLogging(ctx.channel()); closeConnection(ctx, msg); } else if (evt instanceof CompleteEvent completeEvt) { // The HttpLifecycleChannelHandler instance will fire this event when either a response has finished being // written, or // the channel is no longer active or disconnected. // Return the connection to pool. CompleteReason reason = completeEvt.getReason(); if (reason == CompleteReason.SESSION_COMPLETE) { PooledConnection conn = PooledConnection.getFromChannel(ctx.channel()); if (conn != null) { if ("close".equalsIgnoreCase(getConnectionHeader(completeEvt))) { String msg = "Origin channel for origin - " + originName + " - completed because of expired keep-alive. " + ChannelUtils.channelInfoForLogging(ctx.channel()); metrics.headerCloseCounter().increment(); closeConnection(ctx, msg); } else { conn.setConnectionState(PooledConnection.ConnectionState.WRITE_READY); conn.release(); } } } else { String msg = "Origin channel for origin - " + originName + " - completed with reason " + reason.name() + ", " + ChannelUtils.channelInfoForLogging(ctx.channel()); closeConnection(ctx, msg); } } else if (evt instanceof SslCloseCompletionEvent event) { metrics.sslCloseCompletionCounter().increment(); String msg = "Origin channel for origin - " + originName + " - received SslCloseCompletionEvent " + event + ". " + ChannelUtils.channelInfoForLogging(ctx.channel()); closeConnection(ctx, msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // super.exceptionCaught(ctx, cause); metrics.errorCounter().increment(); String mesg = "Exception on Origin channel for origin - " + originName + ". " + ChannelUtils.channelInfoForLogging(ctx.channel()) + " - " + cause.getClass().getCanonicalName() + ": " + cause.getMessage(); closeConnection(ctx, mesg); if (LOG.isDebugEnabled()) { LOG.debug(mesg, cause); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // super.channelInactive(ctx); metrics.inactiveCounter().increment(); String msg = "Client channel for origin - " + originName + " - inactive event has fired. " + ChannelUtils.channelInfoForLogging(ctx.channel()); closeConnection(ctx, msg); } private void closeConnection(ChannelHandlerContext ctx, String msg) { PooledConnection conn = PooledConnection.getFromChannel(ctx.channel()); if (conn != null) { if (LOG.isDebugEnabled()) { msg = msg + " Closing the PooledConnection and releasing. conn={}"; LOG.debug(msg, conn); } flagCloseAndReleaseConnection(conn); } else { // If somehow we don't have a PooledConnection instance attached to this channel, then // close the channel directly. LOG.warn("{} But no PooledConnection attribute. So just closing Channel.", msg); ctx.close(); } } private void flagCloseAndReleaseConnection(PooledConnection pooledConnection) { if (pooledConnection.isInPool()) { pooledConnection.closeAndRemoveFromPool(); } else { pooledConnection.flagShouldClose(); pooledConnection.release(); } } private static String getConnectionHeader(CompleteEvent completeEvt) { HttpResponse response = completeEvt.getResponse(); if (response != null) { return response.headers().get(HttpHeaderNames.CONNECTION); } return null; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ConnectionPoolMetrics.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.spectator.api.patterns.PolledMeter; import com.netflix.zuul.origins.OriginName; import java.util.concurrent.atomic.AtomicInteger; /** * @author Justin Guerra * @since 2/26/25 */ public record ConnectionPoolMetrics( OriginName originName, Counter createNewConnCounter, Counter createConnSucceededCounter, Counter createConnFailedCounter, Counter closeConnCounter, Counter closeAbovePoolHighWaterMarkCounter, Counter closeExpiredConnLifetimeCounter, Counter requestConnCounter, Counter reuseConnCounter, Counter releaseConnCounter, Counter alreadyClosedCounter, Counter connTakenFromPoolIsNotOpen, Counter maxConnsPerHostExceededCounter, Counter closeWrtBusyConnCounter, Counter circuitBreakerClose, PercentileTimer connEstablishTimer, AtomicInteger connsInPool, AtomicInteger connsInUse, Counter idleCounter, Counter inactiveCounter, Counter errorCounter, Counter headerCloseCounter, Counter sslCloseCompletionCounter) { public static ConnectionPoolMetrics create(OriginName originName, Registry registry) { Counter createNewConnCounter = newCounter("connectionpool_create", originName, registry); Counter createConnSucceededCounter = newCounter("connectionpool_create_success", originName, registry); Counter createConnFailedCounter = newCounter("connectionpool_create_fail", originName, registry); Counter closeConnCounter = newCounter("connectionpool_close", originName, registry); Counter closeAbovePoolHighWaterMarkCounter = newCounter("connectionpool_closeAbovePoolHighWaterMark", originName, registry); Counter closeExpiredConnLifetimeCounter = newCounter("connectionpool_closeExpiredConnLifetime", originName, registry); Counter requestConnCounter = newCounter("connectionpool_request", originName, registry); Counter reuseConnCounter = newCounter("connectionpool_reuse", originName, registry); Counter releaseConnCounter = newCounter("connectionpool_release", originName, registry); Counter alreadyClosedCounter = newCounter("connectionpool_alreadyClosed", originName, registry); Counter connTakenFromPoolIsNotOpen = newCounter("connectionpool_fromPoolIsClosed", originName, registry); Counter maxConnsPerHostExceededCounter = newCounter("connectionpool_maxConnsPerHostExceeded", originName, registry); Counter closeWrtBusyConnCounter = newCounter("connectionpool_closeWrtBusyConnCounter", originName, registry); Counter circuitBreakerClose = newCounter("connectionpool_closeCircuitBreaker", originName, registry); Counter idleCounter = newCounter("connectionpool_idle", originName, registry); Counter inactiveCounter = newCounter("connectionpool_inactive", originName, registry); Counter errorCounter = newCounter("connectionpool_error", originName, registry); Counter headerCloseCounter = newCounter("connectionpool_headerClose", originName, registry); Counter sslCloseCompletionCounter = newCounter("connectionpool_sslClose", originName, registry); PercentileTimer connEstablishTimer = PercentileTimer.get( registry, registry.createId("connectionpool_createTiming", "id", originName.getMetricId())); AtomicInteger connsInPool = newGauge("connectionpool_inPool", originName, registry); AtomicInteger connsInUse = newGauge("connectionpool_inUse", originName, registry); return new ConnectionPoolMetrics( originName, createNewConnCounter, createConnSucceededCounter, createConnFailedCounter, closeConnCounter, closeAbovePoolHighWaterMarkCounter, closeExpiredConnLifetimeCounter, requestConnCounter, reuseConnCounter, releaseConnCounter, alreadyClosedCounter, connTakenFromPoolIsNotOpen, maxConnsPerHostExceededCounter, closeWrtBusyConnCounter, circuitBreakerClose, connEstablishTimer, connsInPool, connsInUse, idleCounter, inactiveCounter, errorCounter, headerCloseCounter, sslCloseCompletionCounter); } private static Counter newCounter(String metricName, OriginName originName, Registry registry) { return registry.counter(metricName, "id", originName.getMetricId()); } private static AtomicInteger newGauge(String metricName, OriginName originName, Registry registry) { return PolledMeter.using(registry) .withName(metricName) .withTag("id", originName.getMetricId()) .monitorValue(new AtomicInteger()); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/DefaultClientChannelManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.google.common.annotations.VisibleForTesting; import com.google.common.net.InetAddresses; import com.netflix.client.config.IClientConfig; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.discovery.DynamicServerResolver; import com.netflix.zuul.discovery.ResolverResult; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.netty.SpectatorUtils; import com.netflix.zuul.netty.insights.PassportStateHttpClientHandler; import com.netflix.zuul.netty.server.OriginResponseReceiver; import com.netflix.zuul.origins.OriginName; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.resolver.Resolver; import com.netflix.zuul.resolver.ResolverListener; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoop; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 7/8/16 * Time: 12:39 PM */ public class DefaultClientChannelManager implements ClientChannelManager { public static final String IDLE_STATE_HANDLER_NAME = "idleStateHandler"; private static final Logger LOG = LoggerFactory.getLogger(DefaultClientChannelManager.class); protected final Resolver dynamicServerResolver; protected final ConnectionPoolConfig connPoolConfig; protected final IClientConfig clientConfig; protected final Registry registry; protected final OriginName originName; protected final ConcurrentHashMap perServerPools; protected final ConnectionPoolMetrics metrics; protected NettyClientConnectionFactory clientConnFactory; protected OriginChannelInitializer channelInitializer; private volatile boolean shuttingDown = false; public DefaultClientChannelManager(OriginName originName, IClientConfig clientConfig, Registry registry) { this(originName, clientConfig, new DynamicServerResolver(clientConfig), registry); } public DefaultClientChannelManager( OriginName originName, IClientConfig clientConfig, Resolver resolver, Registry registry) { this.originName = Objects.requireNonNull(originName, "originName"); this.dynamicServerResolver = resolver; this.clientConfig = clientConfig; this.registry = registry; this.perServerPools = new ConcurrentHashMap<>(200); this.connPoolConfig = new ConnectionPoolConfigImpl(originName, this.clientConfig); this.metrics = ConnectionPoolMetrics.create(originName, registry); } @Override public void init() { dynamicServerResolver.setListener(new ServerPoolListener()); // Load channel initializer and conn factory. // We don't do this within the constructor because some subclass may not be initialized until post-construct. this.channelInitializer = createChannelInitializer(clientConfig, connPoolConfig, registry); this.clientConnFactory = createNettyClientConnectionFactory(connPoolConfig, channelInitializer); } protected OriginChannelInitializer createChannelInitializer( IClientConfig clientConfig, ConnectionPoolConfig connPoolConfig, Registry registry) { return new DefaultOriginChannelInitializer(connPoolConfig, registry); } protected NettyClientConnectionFactory createNettyClientConnectionFactory( ConnectionPoolConfig connPoolConfig, ChannelInitializer clientConnInitializer) { return new NettyClientConnectionFactory(connPoolConfig, clientConnInitializer); } @Override public ConnectionPoolConfig getConfig() { return connPoolConfig; } @Override public boolean isAvailable() { return dynamicServerResolver.hasServers(); } @Override public boolean isCold() { return false; } @Override public int getInflightRequestsCount() { return this.channelInitializer.getHttpMetricsHandler().getInflightRequestsCount(); } @Override public void shutdown() { this.shuttingDown = true; dynamicServerResolver.shutdown(); for (IConnectionPool pool : perServerPools.values()) { pool.shutdown(); } } /** * Gracefully shuts down a DefaultClientChannelManager by allowing in-flight requests to finish before closing the connections. * Idle connections in the connection pools are closed, and any connections associated with an in-flight request * will be closed upon trying to return the connection to the pool */ @Override public void gracefulShutdown() { LOG.info("Starting a graceful shutdown of {}", clientConfig.getClientName()); shuttingDown = true; dynamicServerResolver.shutdown(); perServerPools.values().forEach(IConnectionPool::drain); } @Override public boolean release(PooledConnection conn) { conn.stopRequestTimer(); metrics.releaseConnCounter().increment(); metrics.connsInUse().decrementAndGet(); DiscoveryResult discoveryResult = conn.getServer(); updateServerStatsOnRelease(conn); boolean released = false; if (conn.isShouldClose()) { // Close and discard the connection, as it has been flagged (possibly due to receiving a non-channel error // like a 503). conn.setInPool(false); conn.close(); LOG.debug( "[{}] closing conn flagged to be closed", conn.getChannel().id()); } else if (isConnectionExpired(conn.getUsageCount())) { conn.setInPool(false); conn.close(); metrics.closeExpiredConnLifetimeCounter().increment(); LOG.debug( "[{}] closing conn lifetime expired, usage: {}", conn.getChannel().id(), conn.getUsageCount()); } else if (connPoolConfig.isCloseOnCircuitBreakerEnabled() && discoveryResult.isCircuitBreakerTripped()) { LOG.debug( "[{}] closing conn, server circuit breaker tripped", conn.getChannel().id()); metrics.circuitBreakerClose().increment(); // Don't put conns for currently circuit-tripped servers back into the pool. conn.setInPool(false); conn.close(); } else if (!conn.isActive()) { LOG.debug("[{}] conn inactive, cleaning up", conn.getChannel().id()); // Connection is already closed, so discard. metrics.alreadyClosedCounter().increment(); // make sure to decrement OpenConnectionCounts conn.updateServerStats(); conn.setInPool(false); } else { releaseHandlers(conn); // Attempt to return connection to the pool. IConnectionPool pool = perServerPools.get(discoveryResult); if (pool != null) { released = pool.release(conn); } else { // The pool for this server no longer exists (maybe due to it falling out of // discovery). conn.setInPool(false); released = false; conn.close(); } LOG.debug("PooledConnection released: {}", conn); } return released; } protected boolean isConnectionExpired(long usageCount) { // if the connection has been around too long (i.e. too many requests), then close it // TODO(argha-c): Document what is a reasonable default here, and the class of origins that optimizes for return usageCount > connPoolConfig.getMaxRequestsPerConnection(); } protected void updateServerStatsOnRelease(PooledConnection conn) { DiscoveryResult discoveryResult = conn.getServer(); discoveryResult.decrementActiveRequestsCount(); discoveryResult.incrementNumRequests(); } protected void releaseHandlers(PooledConnection conn) { ChannelPipeline pipeline = conn.getChannel().pipeline(); removeHandlerFromPipeline(OriginResponseReceiver.CHANNEL_HANDLER_NAME, pipeline); // The Outbound handler is always after the inbound handler, so look for it. ChannelHandlerContext passportStateHttpClientHandlerCtx = pipeline.context(PassportStateHttpClientHandler.OutboundHandler.class); pipeline.addAfter( passportStateHttpClientHandlerCtx.name(), IDLE_STATE_HANDLER_NAME, new IdleStateHandler(0, 0, connPoolConfig.getIdleTimeout(), TimeUnit.MILLISECONDS)); } public static void removeHandlerFromPipeline(String handlerName, ChannelPipeline pipeline) { if (pipeline.get(handlerName) != null) { pipeline.remove(handlerName); } } @Override public boolean remove(PooledConnection conn) { if (conn == null) { return false; } if (!conn.isInPool()) { return false; } // Attempt to remove the connection from the pool. IConnectionPool pool = perServerPools.get(conn.getServer()); if (pool != null) { return pool.remove(conn); } else { // The pool for this server no longer exists (maybe due to it failing out of // discovery). conn.setInPool(false); metrics.connsInPool().decrementAndGet(); return false; } } @Override public Promise acquire(EventLoop eventLoop) { return acquire(eventLoop, null, CurrentPassport.create(), new AtomicReference<>(), new AtomicReference<>()); } @Override public Promise acquire( EventLoop eventLoop, @Nullable Object key, CurrentPassport passport, AtomicReference selectedServer, AtomicReference selectedHostAddr) { if (shuttingDown) { Promise promise = eventLoop.newPromise(); promise.setFailure(new IllegalStateException("ConnectionPool is shutting down now.")); return promise; } // Choose the next load-balanced server. DiscoveryResult chosenServer = dynamicServerResolver.resolve(key); // (argha-c): Always ensure the selected server is updated, since the call chain relies on this mutation. selectedServer.set(chosenServer); if (Objects.equals(chosenServer, DiscoveryResult.EMPTY)) { Promise promise = eventLoop.newPromise(); promise.setFailure( new OriginConnectException("No servers available", OutboundErrorType.NO_AVAILABLE_SERVERS)); return promise; } // Now get the connection-pool for this server. IConnectionPool pool = perServerPools.computeIfAbsent(chosenServer, s -> { SocketAddress finalServerAddr = pickAddress(chosenServer); ClientChannelManager clientChannelMgr = this; PooledConnectionFactory pcf = createPooledConnectionFactory( chosenServer, clientChannelMgr, metrics.closeConnCounter(), metrics.closeWrtBusyConnCounter()); // Create a new pool for this server. return createConnectionPool( chosenServer, finalServerAddr, clientConnFactory, pcf, connPoolConfig, clientConfig, metrics.createNewConnCounter(), metrics.createConnSucceededCounter(), metrics.createConnFailedCounter(), metrics.requestConnCounter(), metrics.reuseConnCounter(), metrics.connTakenFromPoolIsNotOpen(), metrics.closeAbovePoolHighWaterMarkCounter(), metrics.maxConnsPerHostExceededCounter(), metrics.connEstablishTimer(), metrics.connsInPool(), metrics.connsInUse()); }); return pool.acquire(eventLoop, passport, selectedHostAddr); } protected PooledConnectionFactory createPooledConnectionFactory( DiscoveryResult chosenServer, ClientChannelManager clientChannelMgr, Counter closeConnCounter, Counter closeWrtBusyConnCounter) { return ch -> new PooledConnection(ch, chosenServer, clientChannelMgr, closeConnCounter, closeWrtBusyConnCounter); } protected IConnectionPool createConnectionPool( DiscoveryResult discoveryResult, SocketAddress serverAddr, NettyClientConnectionFactory clientConnFactory, PooledConnectionFactory pcf, ConnectionPoolConfig connPoolConfig, IClientConfig clientConfig, Counter createNewConnCounter, Counter createConnSucceededCounter, Counter createConnFailedCounter, Counter requestConnCounter, Counter reuseConnCounter, Counter connTakenFromPoolIsNotOpen, Counter closeAbovePoolHighWaterMarkCounter, Counter maxConnsPerHostExceededCounter, PercentileTimer connEstablishTimer, AtomicInteger connsInPool, AtomicInteger connsInUse) { return new PerServerConnectionPool( discoveryResult, serverAddr, clientConnFactory, pcf, connPoolConfig, clientConfig, createNewConnCounter, createConnSucceededCounter, createConnFailedCounter, requestConnCounter, reuseConnCounter, connTakenFromPoolIsNotOpen, closeAbovePoolHighWaterMarkCounter, maxConnsPerHostExceededCounter, connEstablishTimer, connsInPool, connsInUse); } final class ServerPoolListener implements ResolverListener { @Override public void onChange(List removedSet) { if (!removedSet.isEmpty()) { LOG.debug( "Removing connection pools for missing servers. name = {}. {} servers gone.", originName, removedSet.size()); for (DiscoveryResult s : removedSet) { IConnectionPool pool = perServerPools.remove(s); if (pool != null) { pool.shutdown(); } } } } } @Override public int getConnsInPool() { return metrics.connsInPool().get(); } @Override public int getConnsInUse() { return metrics.connsInUse().get(); } protected ConcurrentHashMap getPerServerPools() { return perServerPools; } @VisibleForTesting static SocketAddress pickAddressInternal(ResolverResult chosenServer, @Nullable OriginName originName) { String rawHost; int port; rawHost = chosenServer.getHost(); port = chosenServer.getPort(); InetSocketAddress serverAddr; try { InetAddress ipAddr = InetAddresses.forString(rawHost); serverAddr = new InetSocketAddress(ipAddr, port); } catch (IllegalArgumentException e1) { LOG.warn("NettyClientConnectionFactory got an unresolved address, addr: {}", rawHost); Counter unresolvedDiscoveryHost = SpectatorUtils.newCounter( "unresolvedDiscoveryHost", originName == null ? "unknownOrigin" : originName.getTarget()); unresolvedDiscoveryHost.increment(); try { serverAddr = new InetSocketAddress(rawHost, port); } catch (RuntimeException e2) { e1.addSuppressed(e2); throw e1; } } return serverAddr; } /** * Given a server chosen from the load balancer, pick the appropriate address to connect to. */ protected SocketAddress pickAddress(DiscoveryResult chosenServer) { return pickAddressInternal(chosenServer, connPoolConfig.getOriginName()); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/DefaultOriginChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.netty.common.HttpClientLifecycleChannelHandler; import com.netflix.netty.common.metrics.HttpMetricsChannelHandler; import com.netflix.spectator.api.Registry; import com.netflix.zuul.netty.insights.PassportStateHttpClientHandler; import com.netflix.zuul.netty.insights.PassportStateOriginHandler; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import com.netflix.zuul.netty.ssl.ClientSslContextFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; /** * Default Origin Channel Initializer * * Author: Arthur Gonigberg * Date: December 01, 2017 */ public class DefaultOriginChannelInitializer extends OriginChannelInitializer { public static final String ORIGIN_NETTY_LOGGER = "originNettyLogger"; public static final String CONNECTION_POOL_HANDLER = "connectionPoolHandler"; private final ConnectionPoolConfig connectionPoolConfig; private final SslContext sslContext; protected final ConnectionPoolHandler connectionPoolHandler; protected final HttpMetricsChannelHandler httpMetricsHandler; protected final LoggingHandler nettyLogger; public DefaultOriginChannelInitializer(ConnectionPoolConfig connPoolConfig, Registry spectatorRegistry) { this.connectionPoolConfig = connPoolConfig; String niwsClientName = connectionPoolConfig.getOriginName().getNiwsClientName(); this.connectionPoolHandler = new ConnectionPoolHandler( ConnectionPoolMetrics.create(connPoolConfig.getOriginName(), spectatorRegistry)); this.httpMetricsHandler = new HttpMetricsChannelHandler(spectatorRegistry, "client", niwsClientName); this.nettyLogger = new LoggingHandler("zuul.origin.nettylog." + niwsClientName, LogLevel.INFO); this.sslContext = getClientSslContext(spectatorRegistry); } @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new PassportStateOriginHandler.InboundHandler()); pipeline.addLast(new PassportStateOriginHandler.OutboundHandler()); if (connectionPoolConfig.isSecure()) { pipeline.addLast("ssl", sslContext.newHandler(ch.alloc())); } pipeline.addLast( BaseZuulChannelInitializer.HTTP_CODEC_HANDLER_NAME, new HttpClientCodec( BaseZuulChannelInitializer.MAX_INITIAL_LINE_LENGTH.get(), BaseZuulChannelInitializer.MAX_HEADER_SIZE.get(), BaseZuulChannelInitializer.MAX_CHUNK_SIZE.get(), false, false)); pipeline.addLast(new PassportStateHttpClientHandler.InboundHandler()); pipeline.addLast(new PassportStateHttpClientHandler.OutboundHandler()); pipeline.addLast(ORIGIN_NETTY_LOGGER, nettyLogger); pipeline.addLast(httpMetricsHandler); addMethodBindingHandler(pipeline); pipeline.addLast(HttpClientLifecycleChannelHandler.INBOUND_CHANNEL_HANDLER); pipeline.addLast(HttpClientLifecycleChannelHandler.OUTBOUND_CHANNEL_HANDLER); pipeline.addLast(new ClientTimeoutHandler.InboundHandler()); pipeline.addLast(new ClientTimeoutHandler.OutboundHandler()); pipeline.addLast(CONNECTION_POOL_HANDLER, connectionPoolHandler); } /** * This method can be overridden to create your own custom SSL context * * @param spectatorRegistry metrics registry * @return Netty SslContext */ protected SslContext getClientSslContext(Registry spectatorRegistry) { return new ClientSslContextFactory(spectatorRegistry).getClientSslContext(); } /** * This method can be overridden to add your own MethodBinding handler for preserving thread locals or thread variables. * * This should be a handler that binds downstream channelRead and userEventTriggered with the * MethodBinding class. It should be added using the pipeline.addLast method. * * @param pipeline the channel pipeline */ protected void addMethodBindingHandler(ChannelPipeline pipeline) {} @Override public HttpMetricsChannelHandler getHttpMetricsHandler() { return httpMetricsHandler; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/IConnectionPool.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.EventLoop; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.util.concurrent.atomic.AtomicReference; /** * User: michaels@netflix.com * Date: 7/8/16 * Time: 1:10 PM */ public interface IConnectionPool { Promise acquire( EventLoop eventLoop, CurrentPassport passport, AtomicReference selectedHostAddr); boolean release(PooledConnection conn); boolean remove(PooledConnection conn); void shutdown(); default void drain() { shutdown(); } boolean isAvailable(); int getConnsInUse(); int getConnsInPool(); ConnectionPoolConfig getConfig(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/NettyClientConnectionFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.zuul.netty.server.Server; import com.netflix.zuul.passport.CurrentPassport; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoop; import io.netty.channel.WriteBufferWaterMark; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Objects; /** * Created by saroskar on 3/16/16. */ public class NettyClientConnectionFactory { private final ConnectionPoolConfig connPoolConfig; private final ChannelInitializer channelInitializer; public NettyClientConnectionFactory( ConnectionPoolConfig connPoolConfig, ChannelInitializer channelInitializer) { this.connPoolConfig = connPoolConfig; this.channelInitializer = channelInitializer; } public ChannelFuture connect( EventLoop eventLoop, SocketAddress socketAddress, CurrentPassport passport, IConnectionPool pool) { Objects.requireNonNull(socketAddress, "socketAddress"); if (socketAddress instanceof InetSocketAddress) { // This should be checked by the ClientConnectionManager assert !((InetSocketAddress) socketAddress).isUnresolved() : socketAddress; } Bootstrap bootstrap = new Bootstrap() .channel(Server.defaultOutboundChannelType.get()) .handler(channelInitializer) .group(eventLoop) .attr(CurrentPassport.CHANNEL_ATTR, passport) .attr(PerServerConnectionPool.CHANNEL_ATTR, pool) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connPoolConfig.getConnectTimeout()) .option(ChannelOption.SO_KEEPALIVE, connPoolConfig.getTcpKeepAlive()) .option(ChannelOption.TCP_NODELAY, connPoolConfig.getTcpNoDelay()) .option(ChannelOption.SO_SNDBUF, connPoolConfig.getTcpSendBufferSize()) .option(ChannelOption.SO_RCVBUF, connPoolConfig.getTcpReceiveBufferSize()) .option( ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark( connPoolConfig.getNettyWriteBufferLowWaterMark(), connPoolConfig.getNettyWriteBufferHighWaterMark())) .option(ChannelOption.AUTO_READ, connPoolConfig.getNettyAutoRead()) .remoteAddress(socketAddress); return bootstrap.connect(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/OriginChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.netty.common.metrics.HttpMetricsChannelHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; /** * Origin Channel Initializer * * Author: Arthur Gonigberg * Date: December 01, 2017 */ public abstract class OriginChannelInitializer extends ChannelInitializer { public abstract HttpMetricsChannelHandler getHttpMetricsHandler(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/OriginConnectException.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.zuul.exception.ErrorType; /** * Wrapper for exceptions failing to connect to origin with details on which server failed the attempt. */ public class OriginConnectException extends Exception { private final ErrorType errorType; public OriginConnectException(String message, ErrorType errorType) { // ensure this exception does not fill its stacktrace, this causes a 10x slowdown super(message, null, true, false); this.errorType = errorType; } public OriginConnectException(String message, Throwable cause, ErrorType errorType) { // ensure this exception does not fill its stacktrace, this causes a 10x slowdown super(message, cause, true, false); this.errorType = errorType; } public ErrorType getErrorType() { return errorType; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/PerServerConnectionPool.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.client.config.IClientConfig; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Timer; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoop; import io.netty.handler.codec.DecoderException; import io.netty.util.AttributeKey; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Deque; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 7/8/16 * Time: 1:09 PM */ public class PerServerConnectionPool implements IConnectionPool { private static final Logger LOG = LoggerFactory.getLogger(PerServerConnectionPool.class); public static final AttributeKey CHANNEL_ATTR = AttributeKey.newInstance("_connection_pool"); protected final ConcurrentHashMap> connectionsPerEventLoop = new ConcurrentHashMap<>(); protected final PooledConnectionFactory pooledConnectionFactory; protected final DiscoveryResult server; protected final SocketAddress serverAddr; protected final NettyClientConnectionFactory connectionFactory; protected final ConnectionPoolConfig config; protected final IClientConfig niwsClientConfig; protected final Counter createNewConnCounter; protected final Counter createConnSucceededCounter; protected final Counter createConnFailedCounter; protected final Counter requestConnCounter; protected final Counter reuseConnCounter; protected final Counter connTakenFromPoolIsNotOpen; protected final Counter maxConnsPerHostExceededCounter; protected final Counter closeAboveHighWaterMarkCounter; protected final Timer connEstablishTimer; protected final AtomicInteger connsInPool; protected final AtomicInteger connsInUse; /** * This is the count of connections currently in progress of being established. * They will only be added to connsInUse _after_ establishment has completed. */ protected final AtomicInteger connCreationsInProgress; protected volatile boolean draining; public PerServerConnectionPool( DiscoveryResult server, SocketAddress serverAddr, NettyClientConnectionFactory connectionFactory, PooledConnectionFactory pooledConnectionFactory, ConnectionPoolConfig config, IClientConfig niwsClientConfig, Counter createNewConnCounter, Counter createConnSucceededCounter, Counter createConnFailedCounter, Counter requestConnCounter, Counter reuseConnCounter, Counter connTakenFromPoolIsNotOpen, Counter closeAboveHighWaterMarkCounter, Counter maxConnsPerHostExceededCounter, Timer connEstablishTimer, AtomicInteger connsInPool, AtomicInteger connsInUse) { this.server = server; // Note: child classes can sometimes connect to different addresses than this.serverAddr = Objects.requireNonNull(serverAddr, "serverAddr"); this.connectionFactory = connectionFactory; this.pooledConnectionFactory = pooledConnectionFactory; this.config = config; this.niwsClientConfig = niwsClientConfig; this.createNewConnCounter = createNewConnCounter; this.createConnSucceededCounter = createConnSucceededCounter; this.createConnFailedCounter = createConnFailedCounter; this.requestConnCounter = requestConnCounter; this.reuseConnCounter = reuseConnCounter; this.connTakenFromPoolIsNotOpen = connTakenFromPoolIsNotOpen; this.closeAboveHighWaterMarkCounter = closeAboveHighWaterMarkCounter; this.maxConnsPerHostExceededCounter = maxConnsPerHostExceededCounter; this.connEstablishTimer = connEstablishTimer; this.connsInPool = connsInPool; this.connsInUse = connsInUse; this.connCreationsInProgress = new AtomicInteger(0); } @Override public ConnectionPoolConfig getConfig() { return this.config; } public IClientConfig getNiwsClientConfig() { return niwsClientConfig; } @Override public boolean isAvailable() { return !draining; } /** function to run when a connection is acquired before returning it to caller. */ protected void onAcquire(PooledConnection conn, CurrentPassport passport) { passport.setOnChannel(conn.getChannel()); removeIdleStateHandler(conn); conn.setInUse(); LOG.debug("PooledConnection acquired: {}", conn); } protected void removeIdleStateHandler(PooledConnection conn) { DefaultClientChannelManager.removeHandlerFromPipeline( DefaultClientChannelManager.IDLE_STATE_HANDLER_NAME, conn.getChannel().pipeline()); } @Override public Promise acquire( EventLoop eventLoop, CurrentPassport passport, AtomicReference selectedHostAddr) { if (draining) { throw new IllegalStateException("Attempt to acquire connection while draining"); } requestConnCounter.increment(); updateServerStatsOnAcquire(); Promise promise = eventLoop.newPromise(); // Try getting a connection from the pool. PooledConnection conn = tryGettingFromConnectionPool(eventLoop); if (conn != null) { // There was a pooled connection available, so use this one. reusePooledConnection(passport, selectedHostAddr, conn, promise); } else { // connection pool empty, create new connection using client connection factory. tryMakingNewConnection(eventLoop, promise, passport, selectedHostAddr); } return promise; } protected void reusePooledConnection( CurrentPassport passport, AtomicReference selectedHostAddr, PooledConnection conn, Promise promise) { conn.startRequestTimer(); conn.incrementUsageCount(); conn.getChannel().read(); onAcquire(conn, passport); initPooledConnection(conn, promise); selectedHostAddr.set(getSelectedHostString(serverAddr)); } protected void updateServerStatsOnAcquire() { server.incrementActiveRequestsCount(); } public PooledConnection tryGettingFromConnectionPool(EventLoop eventLoop) { PooledConnection conn; Deque connections = getPoolForEventLoop(eventLoop); while ((conn = connections.poll()) != null) { conn.setInPool(false); /* Check that the connection is still open. */ if (isValidFromPool(conn)) { reuseConnCounter.increment(); connsInUse.incrementAndGet(); connsInPool.decrementAndGet(); return conn; } else { connTakenFromPoolIsNotOpen.increment(); connsInPool.decrementAndGet(); conn.close(); } } return null; } protected boolean isValidFromPool(PooledConnection conn) { return conn.isActive() && conn.getChannel().isOpen(); } protected void initPooledConnection(PooledConnection conn, Promise promise) { // add custom init code by overriding this method promise.setSuccess(conn); } protected Deque getPoolForEventLoop(EventLoop eventLoop) { // We don't want to block under any circumstances, so can't use CHM.computeIfAbsent(). // Instead we accept the slight inefficiency of an unnecessary instantiation of a ConcurrentLinkedDeque. Deque pool = connectionsPerEventLoop.get(eventLoop); if (pool == null) { pool = new ConcurrentLinkedDeque<>(); connectionsPerEventLoop.putIfAbsent(eventLoop, pool); } return pool; } protected void tryMakingNewConnection( EventLoop eventLoop, Promise promise, CurrentPassport passport, AtomicReference selectedHostAddr) { if (!isWithinConnectionLimit(promise)) { return; } try { createNewConnCounter.increment(); connCreationsInProgress.incrementAndGet(); passport.add(PassportState.ORIGIN_CH_CONNECTING); selectedHostAddr.set(getSelectedHostString(serverAddr)); ChannelFuture cf = connectToServer(eventLoop, passport, serverAddr); if (cf.isDone()) { handleConnectCompletion(cf, promise, passport); } else { cf.addListener(future -> { try { handleConnectCompletion((ChannelFuture) future, promise, passport); } catch (Throwable e) { if (!promise.isDone()) { promise.setFailure(e); } LOG.warn( "Error creating new connection! origin={}, host={}", config.getOriginName(), server.getServerId()); } }); } } catch (Throwable e) { promise.setFailure(e); } } protected boolean isWithinConnectionLimit(Promise promise) { // Enforce MaxConnectionsPerHost config. int maxConnectionsPerHost = config.maxConnectionsPerHost(); int openAndOpeningConnectionCount = server.getOpenConnectionsCount() + connCreationsInProgress.get(); if (maxConnectionsPerHost != -1 && openAndOpeningConnectionCount >= maxConnectionsPerHost) { maxConnsPerHostExceededCounter.increment(); promise.setFailure(new OriginConnectException( "maxConnectionsPerHost=" + maxConnectionsPerHost + ", connectionsPerHost=" + openAndOpeningConnectionCount, OutboundErrorType.ORIGIN_SERVER_MAX_CONNS)); LOG.warn( "Unable to create new connection because at MaxConnectionsPerHost! maxConnectionsPerHost={}," + " connectionsPerHost={}, host={}origin={}", maxConnectionsPerHost, openAndOpeningConnectionCount, server.getServerId(), config.getOriginName()); return false; } return true; } protected ChannelFuture connectToServer(EventLoop eventLoop, CurrentPassport passport, SocketAddress serverAddr) { return connectionFactory.connect(eventLoop, serverAddr, passport, this); } protected void handleConnectCompletion( ChannelFuture cf, Promise callerPromise, CurrentPassport passport) { connCreationsInProgress.decrementAndGet(); updateServerStatsOnConnectCompletion(cf); if (cf.isSuccess()) { passport.add(PassportState.ORIGIN_CH_CONNECTED); createConnSucceededCounter.increment(); connsInUse.incrementAndGet(); createConnection(cf, callerPromise, passport); } else { createConnFailedCounter.increment(); // unwrap DecoderExceptions to get a better indication of why decoding failed // as decoding failures are not indicative of actual connection causes if (cf.cause() instanceof DecoderException de && de.getCause() != null) { callerPromise.setFailure(new OriginConnectException( de.getCause().getMessage(), de.getCause(), OutboundErrorType.CONNECT_ERROR)); } else { callerPromise.setFailure(new OriginConnectException( cf.cause().getMessage(), cf.cause(), OutboundErrorType.CONNECT_ERROR)); } } } protected void updateServerStatsOnConnectCompletion(ChannelFuture cf) { if (cf.isSuccess()) { server.incrementOpenConnectionsCount(); } else { server.incrementSuccessiveConnectionFailureCount(); server.addToFailureCount(); server.decrementActiveRequestsCount(); } } protected void createConnection( ChannelFuture cf, Promise callerPromise, CurrentPassport passport) { PooledConnection conn = pooledConnectionFactory.create(cf.channel()); conn.incrementUsageCount(); conn.startRequestTimer(); conn.getChannel().read(); onAcquire(conn, passport); callerPromise.setSuccess(conn); } @Override public boolean release(PooledConnection conn) { if (conn == null) { return false; } if (conn.isInPool()) { return false; } if (draining) { LOG.debug( "[{}] closing released connection during drain", conn.getChannel().id()); conn.getChannel().close(); return false; } // Get the eventloop for this channel. EventLoop eventLoop = conn.getChannel().eventLoop(); Deque connections = getPoolForEventLoop(eventLoop); CurrentPassport passport = CurrentPassport.fromChannel(conn.getChannel()); // Discard conn if already at least above waterline in the pool already for this server. if (isOverPerServerWaterline(connections.size())) { closeAboveHighWaterMarkCounter.increment(); conn.close(); conn.setInPool(false); return false; } // Attempt to return connection to the pool. else if (connections.offer(conn)) { conn.setInPool(true); connsInPool.incrementAndGet(); passport.add(PassportState.ORIGIN_CH_POOL_RETURNED); return true; } else { // If the pool is full, then close the conn and discard. conn.close(); conn.setInPool(false); return false; } } protected boolean isOverPerServerWaterline(int connectionsInPool) { int poolWaterline = config.perServerWaterline(); return poolWaterline > -1 && connectionsInPool >= poolWaterline; } @Override public boolean remove(PooledConnection conn) { if (conn == null) { return false; } if (!conn.isInPool()) { return false; } // Get the eventloop for this channel. EventLoop eventLoop = conn.getChannel().eventLoop(); // Attempt to remove connection from the pool. Deque connections = getPoolForEventLoop(eventLoop); if (connections.remove(conn)) { conn.setInPool(false); connsInPool.decrementAndGet(); return true; } else { return false; } } @Override public void shutdown() { for (Deque connections : connectionsPerEventLoop.values()) { for (PooledConnection conn : connections) { conn.close(); } } } @Override public void drain() { if (draining) { throw new IllegalStateException("Already draining"); } draining = true; connectionsPerEventLoop.forEach((eventLoop, v) -> drainIdleConnectionsOnEventLoop(eventLoop)); } @Override public int getConnsInPool() { return connsInPool.get(); } @Override public int getConnsInUse() { return connsInUse.get(); } @Nullable protected InetAddress getSelectedHostString(SocketAddress addr) { if (addr instanceof InetSocketAddress) { return ((InetSocketAddress) addr).getAddress(); } else { // If it's some other kind of address, just set it to empty return null; } } /** * Closes idle connections in the connection pool for a given EventLoop. The closing is performed on the EventLoop * thread since the connection pool is not thread safe. * * @param eventLoop - the event loop to drain */ void drainIdleConnectionsOnEventLoop(EventLoop eventLoop) { eventLoop.execute(() -> { Deque connections = connectionsPerEventLoop.get(eventLoop); if (connections == null) { return; } for (PooledConnection connection : connections) { // any connections in the Deque are idle since they are removed in tryGettingFromConnectionPool() connection.setInPool(false); LOG.debug("Closing connection {}", connection); connection.close(); connsInPool.decrementAndGet(); } }); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/PooledConnection.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.spectator.api.Counter; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelPipeline; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.util.AttributeKey; import java.time.Duration; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by saroskar on 3/15/16. */ public class PooledConnection { public static final AttributeKey CHANNEL_ATTR = AttributeKey.newInstance("_pooled_connection"); public static final String READ_TIMEOUT_HANDLER_NAME = "readTimeoutHandler"; private final DiscoveryResult server; private final Channel channel; private final ClientChannelManager channelManager; private final long creationTS; private final Counter closeConnCounter; private final Counter closeWrtBusyConnCounter; private static final Logger LOG = LoggerFactory.getLogger(PooledConnection.class); /** * Connection State */ public enum ConnectionState { /** * valid state in pool */ WRITE_READY, /** * Can not be put in pool */ WRITE_BUSY } private ConnectionState connectionState; private long usageCount = 0; private long reqStartTime; private boolean inPool = false; private boolean shouldClose = false; protected boolean released = false; public PooledConnection( Channel channel, DiscoveryResult server, ClientChannelManager channelManager, Counter closeConnCounter, Counter closeWrtBusyConnCounter) { this.channel = channel; this.server = server; this.channelManager = channelManager; this.creationTS = System.currentTimeMillis(); this.closeConnCounter = closeConnCounter; this.closeWrtBusyConnCounter = closeWrtBusyConnCounter; this.connectionState = ConnectionState.WRITE_READY; // Store ourself as an attribute on the underlying Channel. channel.attr(CHANNEL_ATTR).set(this); } public void setInUse() { this.connectionState = ConnectionState.WRITE_BUSY; this.released = false; } public void setConnectionState(ConnectionState state) { this.connectionState = state; } public static PooledConnection getFromChannel(Channel ch) { return ch.attr(CHANNEL_ATTR).get(); } public ConnectionPoolConfig getConfig() { return this.channelManager.getConfig(); } public DiscoveryResult getServer() { return server; } public Channel getChannel() { return channel; } public long getUsageCount() { return usageCount; } public void incrementUsageCount() { this.usageCount++; } public long getCreationTS() { return creationTS; } public long getAgeInMillis() { return System.currentTimeMillis() - creationTS; } public void startRequestTimer() { reqStartTime = System.nanoTime(); } public long stopRequestTimer() { long responseTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - reqStartTime); server.noteResponseTime((double) responseTime); return responseTime; } public boolean isActive() { return (channel.isActive() && channel.isRegistered()); } public boolean isInPool() { return inPool; } public void setInPool(boolean inPool) { this.inPool = inPool; } public boolean isShouldClose() { return shouldClose; } public void flagShouldClose() { this.shouldClose = true; } public ChannelFuture close() { server.decrementOpenConnectionsCount(); closeConnCounter.increment(); return channel.close(); } public void updateServerStats() { server.decrementOpenConnectionsCount(); server.stopPublishingStats(); } public ChannelFuture closeAndRemoveFromPool() { channelManager.remove(this); return this.close(); } public boolean release() { if (released) { return true; } if (isActive()) { if (connectionState != ConnectionState.WRITE_READY) { closeWrtBusyConnCounter.increment(); } } if (!isShouldClose() && connectionState != ConnectionState.WRITE_READY) { CurrentPassport passport = CurrentPassport.fromChannel(channel); LOG.info("Error - Attempt to put busy connection into the pool = {}, {}", this, passport); this.shouldClose = true; } // reset the connectionState connectionState = ConnectionState.WRITE_READY; released = true; return channelManager.release(this); } public void removeReadTimeoutHandler() { // Remove (and therefore destroy) the readTimeoutHandler when we release the // channel back to the pool. As don't want it timing-out when it's not in use. ChannelPipeline pipeline = getChannel().pipeline(); removeHandlerFromPipeline(READ_TIMEOUT_HANDLER_NAME, pipeline); } private void removeHandlerFromPipeline(String handlerName, ChannelPipeline pipeline) { if (pipeline.get(handlerName) != null) { pipeline.remove(handlerName); } } public void startReadTimeoutHandler(Duration readTimeout) { Channel channel = getChannel(); if (!channel.isActive()) { LOG.debug("Tried to start read timeout handler, but channel is not active"); return; } channel.pipeline() .addBefore( DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER, READ_TIMEOUT_HANDLER_NAME, new ReadTimeoutHandler(readTimeout.toMillis(), TimeUnit.MILLISECONDS)); } ConnectionState getConnectionState() { return connectionState; } boolean isReleased() { return released; } @Override public String toString() { return "PooledConnection{" + "channel=" + channel + ", usageCount=" + usageCount + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/PooledConnectionFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import io.netty.channel.Channel; /** * User: Mike Smith * Date: 7/9/16 * Time: 2:25 PM */ public interface PooledConnectionFactory { PooledConnection create(Channel ch); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/RequestStat.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.exception.ErrorType; /** * Request Stat * * Author: Arthur Gonigberg * Date: November 29, 2017 */ public interface RequestStat { String SESSION_CONTEXT_KEY = "niwsRequestStat"; static RequestStat putInSessionContext(RequestStat stat, SessionContext context) { context.put(SESSION_CONTEXT_KEY, stat); return stat; } static RequestStat getFromSessionContext(SessionContext context) { return (RequestStat) context.get(SESSION_CONTEXT_KEY); } RequestStat server(DiscoveryResult server); boolean isFinished(); long duration(); void serviceUnavailable(); void generalError(); void failAndSetErrorCode(ErrorType errorType); void updateWithHttpStatusCode(int httpStatusCode); void finalAttempt(boolean finalAttempt); boolean finishIfNotAlready(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/ZuulNettyExceptionMapper.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; /** * User: Mike Smith * Date: 7/13/16 * Time: 6:02 PM */ public class ZuulNettyExceptionMapper {} ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/BaseZuulFilterRunner.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.spectator.impl.Preconditions; import com.netflix.zuul.ExecutionStatus; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.Debug; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.filters.FilterError; import com.netflix.zuul.filters.FilterSyncType; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.SyncZuulFilter; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.SpectatorUtils; import com.netflix.zuul.netty.server.MethodBinding; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; import io.perfmark.Link; import io.perfmark.PerfMark; import io.perfmark.TaskCloseable; import jakarta.annotation.Nullable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.concurrent.ThreadSafe; import lombok.Getter; import lombok.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observer; import rx.functions.Action0; import rx.functions.Action1; /** * Subclasses of this class are supposed to be thread safe * * Created by saroskar on 5/18/17. */ @ThreadSafe public abstract class BaseZuulFilterRunner implements FilterRunner { private final FilterUsageNotifier usageNotifier; @Getter private final FilterRunner nextStage; private final String RUNNING_FILTER_IDX_SESSION_CTX_KEY; private final String AWAITING_BODY_FLAG_SESSION_CTX_KEY; private static final Logger logger = LoggerFactory.getLogger(BaseZuulFilterRunner.class); private static final CachedDynamicIntProperty FILTER_EXCESSIVE_EXEC_TIME = new CachedDynamicIntProperty("zuul.filters.excessive.execTime", 500); private final Registry registry; private final Id filterExcessiveTimerId; private final FilterConstraints filterConstraints; protected BaseZuulFilterRunner( FilterType filterType, FilterUsageNotifier usageNotifier, FilterRunner nextStage, FilterConstraints filterConstraints, Registry registry) { this.usageNotifier = Preconditions.checkNotNull(usageNotifier, "filter usage notifier"); this.nextStage = nextStage; this.RUNNING_FILTER_IDX_SESSION_CTX_KEY = filterType + "RunningFilterIndex"; this.AWAITING_BODY_FLAG_SESSION_CTX_KEY = filterType + "IsAwaitingBody"; this.registry = registry; this.filterExcessiveTimerId = registry.createId("zuul.request.timing.filterExcessive"); this.filterConstraints = filterConstraints; } @NonNull public static ChannelHandlerContext getChannelHandlerContext(ZuulMessage mesg) { return (ChannelHandlerContext) mesg.getContext().get(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT); } protected final AtomicInteger initRunningFilterIndex(I zuulMesg) { AtomicInteger idx = new AtomicInteger(0); zuulMesg.getContext().put(RUNNING_FILTER_IDX_SESSION_CTX_KEY, idx); return idx; } protected final AtomicInteger getRunningFilterIndex(I zuulMesg) { SessionContext ctx = zuulMesg.getContext(); return (AtomicInteger) Preconditions.checkNotNull(ctx.get(RUNNING_FILTER_IDX_SESSION_CTX_KEY), "runningFilterIndex"); } protected final boolean isFilterAwaitingBody(SessionContext context) { return context.containsKey(AWAITING_BODY_FLAG_SESSION_CTX_KEY); } protected final void setFilterAwaitingBody(I zuulMesg, boolean flag) { if (flag) { zuulMesg.getContext().put(AWAITING_BODY_FLAG_SESSION_CTX_KEY, Boolean.TRUE); } else { zuulMesg.getContext().remove(AWAITING_BODY_FLAG_SESSION_CTX_KEY); } } protected final void invokeNextStage(O zuulMesg, HttpContent chunk) { if (nextStage != null) { try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".invokeNextStageChunk")) { addPerfMarkTags(zuulMesg); nextStage.filter(zuulMesg, chunk); } } else { // Next stage is Netty channel handler try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".fireChannelReadChunk")) { addPerfMarkTags(zuulMesg); ChannelHandlerContext channelHandlerContext = getChannelHandlerContext(zuulMesg); if (!channelHandlerContext.channel().isActive()) { zuulMesg.getContext().cancel(); zuulMesg.disposeBufferedBody(); SpectatorUtils.newCounter( "zuul.filterChain.chunk.hanging", zuulMesg.getClass().getSimpleName()) .increment(); } else { channelHandlerContext.fireChannelRead(chunk); } } } } protected final void invokeNextStage(O zuulMesg) { if (nextStage != null) { try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".invokeNextStage")) { addPerfMarkTags(zuulMesg); nextStage.filter(zuulMesg); } } else { // Next stage is Netty channel handler try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".fireChannelRead")) { addPerfMarkTags(zuulMesg); ChannelHandlerContext channelHandlerContext = getChannelHandlerContext(zuulMesg); if (!channelHandlerContext.channel().isActive()) { zuulMesg.getContext().cancel(); zuulMesg.disposeBufferedBody(); SpectatorUtils.newCounter( "zuul.filterChain.message.hanging", zuulMesg.getClass().getSimpleName()) .increment(); } else { channelHandlerContext.fireChannelRead(zuulMesg); } } } } protected final void addPerfMarkTags(ZuulMessage inMesg) { HttpRequestInfo req = null; if (inMesg instanceof HttpRequestInfo) { req = (HttpRequestInfo) inMesg; } if (inMesg instanceof HttpResponseMessage msg) { req = msg.getOutboundRequest(); PerfMark.attachTag("statuscode", msg.getStatus()); } if (req != null) { PerfMark.attachTag("path", req, HttpRequestInfo::getPath); PerfMark.attachTag("originalhost", req, HttpRequestInfo::getOriginalHost); } PerfMark.attachTag("uuid", inMesg, m -> m.getContext().getUUID()); } protected final FilterExecutionResult executeFilter(ZuulFilter filter, I inMesg) { long startTime = System.nanoTime(); ZuulMessage snapshot = inMesg.getContext().debugRouting() ? inMesg.clone() : null; try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".filter")) { addPerfMarkTags(inMesg); ExecutionStatus executionStatus = checkFilterPreconditions(filter, inMesg); if (executionStatus != null) { recordFilterCompletion(executionStatus, filter, startTime, inMesg, snapshot); return FilterExecutionResult.completed(filter.getDefaultOutput(inMesg)); } if (!isMessageBodyReadyForFilter(filter, inMesg)) { setFilterAwaitingBody(inMesg, true); logger.debug( "Filter {} waiting for body, UUID {}", filter.filterName(), inMesg.getContext().getUUID()); return FilterExecutionResult.pending(); } setFilterAwaitingBody(inMesg, false); if (snapshot != null) { Debug.addRoutingDebug( inMesg.getContext(), "Filter " + filter.filterType().toString() + " " + filter.filterOrder() + " " + filter.filterName()); } // run body contents accumulated so far through this filter inMesg.runBufferedBodyContentThroughFilter(filter); if (filter.getSyncType() == FilterSyncType.SYNC) { return executeSyncFilter((SyncZuulFilter) filter, inMesg, startTime, snapshot); } return executeAsyncFilter(filter, inMesg, startTime, snapshot); } catch (Throwable t) { O outMesg = handleFilterException(inMesg, filter, t); outMesg.finishBufferedBodyIfIncomplete(); recordFilterCompletion(ExecutionStatus.FAILED, filter, startTime, inMesg, snapshot); return FilterExecutionResult.completed(outMesg); } } @Nullable private ExecutionStatus checkFilterPreconditions(ZuulFilter filter, I inMesg) { if (filter.filterType() == FilterType.INBOUND && inMesg.getContext().shouldSendErrorResponse()) { // Pass request down the pipeline, all the way to error endpoint if error response needs to be generated return ExecutionStatus.SKIPPED; } try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".shouldSkipFilter")) { if (shouldSkipFilter(inMesg, filter)) { return ExecutionStatus.SKIPPED; } } if (filter.isDisabled()) { return ExecutionStatus.DISABLED; } return null; } /** * Execute a SyncZuulFilter apply on the current event loop thread. */ private FilterExecutionResult executeSyncFilter( SyncZuulFilter filter, I inMesg, long startTime, ZuulMessage snapshot) { O outMesg; try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".apply")) { addPerfMarkTags(inMesg); outMesg = filter.apply(inMesg); } recordFilterCompletion(ExecutionStatus.SUCCESS, filter, startTime, inMesg, snapshot); return FilterExecutionResult.completed((outMesg != null) ? outMesg : filter.getDefaultOutput(inMesg)); } /** * Execute a ZuulFilter's async apply, subscribing to resume the filter chain once the observable completes. */ private FilterExecutionResult executeAsyncFilter( ZuulFilter filter, I inMesg, long startTime, ZuulMessage snapshot) { FilterChainResumer resumer; filter.incrementConcurrency(); try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".applyAsync")) { Link nettyToSchedulerLink = PerfMark.linkOut(); resumer = new FilterChainResumer(inMesg, filter, snapshot, startTime); filter.applyAsync(inMesg) .doOnSubscribe(() -> { try (TaskCloseable ignored2 = PerfMark.traceTask(filter, f -> f.filterName() + ".onSubscribeAsync")) { PerfMark.linkIn(nettyToSchedulerLink); } }) .doOnNext(resumer.onNextStarted(nettyToSchedulerLink)) .doOnError(resumer.onErrorStarted(nettyToSchedulerLink)) .doOnCompleted(resumer.onCompletedStarted(nettyToSchedulerLink)) .observeOn(new EventExecutorScheduler( getChannelHandlerContext(inMesg).executor())) .doOnUnsubscribe(resumer::decrementConcurrency) .subscribe(resumer); } catch (Throwable t) { filter.decrementConcurrency(); throw t; } return FilterExecutionResult.pending(); } /** * This is typically set by a filter when wanting to reject a request and also reduce load on the server by * not processing anymore filterChain */ protected final boolean shouldSkipFilter(I inMesg, ZuulFilter filter) { if (filter.filterType() == FilterType.ENDPOINT) { // Endpoints may not be skipped return false; } SessionContext zuulCtx = inMesg.getContext(); if (zuulCtx.shouldStopFilterProcessing() && !filter.overrideStopFilterProcessing()) { return true; } if (zuulCtx.isCancelled()) { return true; } if (filterConstraints.isConstrained(inMesg, filter)) { return true; } return !filter.shouldFilter(inMesg); } private boolean isMessageBodyReadyForFilter(ZuulFilter filter, I inMesg) { return inMesg.hasCompleteBody() || !filter.needsBodyBuffered(inMesg); } protected O handleFilterException(I inMesg, ZuulFilter filter, Throwable ex) { inMesg.getContext().setError(ex); if (filter.filterType() == FilterType.ENDPOINT) { inMesg.getContext().setShouldSendErrorResponse(true); } recordFilterError(inMesg, filter, ex); return filter.getDefaultOutput(inMesg); } protected void recordFilterError(I inMesg, ZuulFilter filter, Throwable t) { // Add a log statement for this exception. String errorMsg = "Filter Exception: filter=" + filter.filterName() + ", request-info=" + inMesg.getInfoForLogging() + ", msg=" + String.valueOf(t.getMessage()); if (t instanceof ZuulException && !((ZuulException) t).shouldLogAsError()) { logger.warn(errorMsg); } else { logger.error(errorMsg, t); } // Store this filter error for possible future use. But we still continue with next filter in the chain. SessionContext zuulCtx = inMesg.getContext(); zuulCtx.getFilterErrors() .add(new FilterError(filter.filterName(), filter.filterType().toString(), t)); if (zuulCtx.debugRouting()) { Debug.addRoutingDebug( zuulCtx, "Running Filter failed " + filter.filterName() + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + t.getMessage()); } } protected void recordFilterCompletion( ExecutionStatus status, ZuulFilter filter, long startTime, ZuulMessage zuulMesg, ZuulMessage startSnapshot) { SessionContext zuulCtx = zuulMesg.getContext(); long execTimeNs = System.nanoTime() - startTime; long execTimeMs = execTimeNs / 1_000_000L; if (execTimeMs >= FILTER_EXCESSIVE_EXEC_TIME.get()) { zuulCtx.setEventProperty("filter_execution_time_exceeded", true); registry.timer(filterExcessiveTimerId .withTag("id", filter.filterName()) .withTag("status", status.name())) .record(execTimeMs, TimeUnit.MILLISECONDS); } // Record the execution summary in context. switch (status) { case FAILED: if (logger.isDebugEnabled()) { zuulCtx.addFilterExecutionSummary(filter.filterName(), ExecutionStatus.FAILED.name(), execTimeMs); } break; case SUCCESS: if (logger.isDebugEnabled()) { zuulCtx.addFilterExecutionSummary(filter.filterName(), ExecutionStatus.SUCCESS.name(), execTimeMs); } if (startSnapshot != null) { // debugRouting == true Debug.addRoutingDebug( zuulCtx, "Filter {" + filter.filterName() + " TYPE:" + filter.filterType().toString() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTimeMs + "ms"); Debug.compareContextState(filter.filterName(), zuulCtx, startSnapshot.getContext()); } break; default: break; } logger.debug( "Filter {} completed with status {}, UUID {}", filter.filterName(), status.name(), zuulMesg.getContext().getUUID()); // Notify configured listener. usageNotifier.notify(filter, status); } protected void handleException(ZuulMessage zuulMesg, String filterName, Exception ex) { HttpRequestInfo zuulReq = null; if (zuulMesg instanceof HttpRequestMessage) { zuulReq = (HttpRequestMessage) zuulMesg; } else if (zuulMesg instanceof HttpResponseMessage) { zuulReq = ((HttpResponseMessage) zuulMesg).getInboundRequest(); } String path = (zuulReq != null) ? zuulReq.getPathAndQuery() : "-"; String method = (zuulReq != null) ? zuulReq.getMethod() : "-"; String errMesg = "Error with filter: " + filterName + ", path: " + path + ", method: " + method; logger.error(errMesg, ex); getChannelHandlerContext(zuulMesg).fireExceptionCaught(ex); } protected abstract void resume(O zuulMesg); protected MethodBinding methodBinding(ZuulMessage zuulMesg) { return MethodBinding.NO_OP_BINDING; } protected void resumeInBindingContext(O zuulMesg, String filterName) { try { methodBinding(zuulMesg).bind(() -> resume(zuulMesg)); } catch (Exception ex) { handleException(zuulMesg, filterName, ex); } } /** * FilterExecutionResult indicates if the filter is still processing a request, such as waiting * on an async filter execution or for a full body to buffer, or has completed. */ protected sealed interface FilterExecutionResult { record Complete(@Nullable O message) implements FilterExecutionResult {} record Pending() implements FilterExecutionResult {} Pending pending = new Pending<>(); @SuppressWarnings("unchecked") static FilterExecutionResult pending() { return (FilterExecutionResult) pending; } static FilterExecutionResult completed(O message) { return new Complete<>(message); } } private final class FilterChainResumer implements Observer { private final I inMesg; private final ZuulFilter filter; private final long startTime; private final ZuulMessage snapshot; private final AtomicBoolean concurrencyDecremented; private final AtomicReference onNextLinkOut = new AtomicReference<>(); private final AtomicReference onErrorLinkOut = new AtomicReference<>(); private final AtomicReference onCompletedLinkOut = new AtomicReference<>(); // no synchronization needed since onNext and onCompleted are always called on the same thread private O outMesg; public FilterChainResumer(I inMesg, ZuulFilter filter, ZuulMessage snapshot, long startTime) { this.inMesg = Preconditions.checkNotNull(inMesg, "input message"); this.filter = Preconditions.checkNotNull(filter, "filter"); this.snapshot = snapshot; this.startTime = startTime; this.concurrencyDecremented = new AtomicBoolean(false); } void decrementConcurrency() { if (concurrencyDecremented.compareAndSet(false, true)) { filter.decrementConcurrency(); } } @Override public void onNext(O outMesg) { try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".onNextAsync")) { PerfMark.linkIn(onNextLinkOut.get()); addPerfMarkTags(inMesg); this.outMesg = outMesg; } catch (Exception e) { decrementConcurrency(); handleException(inMesg, filter.filterName(), e); } } @Override public void onError(Throwable ex) { try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".onErrorAsync")) { PerfMark.linkIn(onErrorLinkOut.get()); decrementConcurrency(); recordFilterCompletion(ExecutionStatus.FAILED, filter, startTime, inMesg, snapshot); O outMesg = handleFilterException(inMesg, filter, ex); resumeInBindingContext(outMesg, filter.filterName()); } catch (Exception e) { handleException(inMesg, filter.filterName(), e); } } @Override public void onCompleted() { try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".onCompletedAsync")) { PerfMark.linkIn(onCompletedLinkOut.get()); decrementConcurrency(); if (outMesg == null) { outMesg = filter.getDefaultOutput(inMesg); } recordFilterCompletion(ExecutionStatus.SUCCESS, filter, startTime, inMesg, snapshot); resumeInBindingContext(outMesg, filter.filterName()); } catch (Exception e) { handleException(inMesg, filter.filterName(), e); } } private Action1 onNextStarted(Link onNextLinkIn) { return o -> { try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".onNext")) { PerfMark.linkIn(onNextLinkIn); onNextLinkOut.compareAndSet(null, PerfMark.linkOut()); } }; } private Action1 onErrorStarted(Link onErrorLinkIn) { return t -> { try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".onError")) { PerfMark.linkIn(onErrorLinkIn); onErrorLinkOut.compareAndSet(null, PerfMark.linkOut()); } }; } private Action0 onCompletedStarted(Link onCompletedLinkIn) { return () -> { try (TaskCloseable ignored = PerfMark.traceTask(filter, f -> f.filterName() + ".onCompleted")) { PerfMark.linkIn(onCompletedLinkIn); onCompletedLinkOut.compareAndSet(null, PerfMark.linkOut()); } }; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/EventExecutorScheduler.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import io.netty.util.concurrent.EventExecutor; import java.util.Objects; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.NotImplementedException; import rx.Scheduler; import rx.Subscription; import rx.functions.Action0; import rx.internal.schedulers.ScheduledAction; import rx.subscriptions.Subscriptions; /** * A custom {@link Scheduler} for use with a {@link EventExecutor} that * 1) Ensures that every action is run on the EventExecutor thread * 2) avoids the unnecessary executions that occur from using {@link rx.internal.schedulers.ExecutorScheduler} if already * executing on the correct thread * * Should only be used with {@link io.netty.channel.SingleThreadEventLoop} * * @author Justin Guerra * @since 5/19/25 */ public class EventExecutorScheduler extends Scheduler { private final EventExecutor executor; public EventExecutorScheduler(EventExecutor executor) { this.executor = Objects.requireNonNull(executor); } @Override public Worker createWorker() { return new Worker() { private volatile boolean unsubscribed; @Override public void unsubscribe() { unsubscribed = true; } @Override public boolean isUnsubscribed() { return unsubscribed; } @Override public Subscription schedule(Action0 action) { if (executor.inEventLoop()) { action.call(); return Subscriptions.unsubscribed(); } else { ScheduledAction sa = new ScheduledAction(action); executor.execute(sa); return sa; } } @Override public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { throw new NotImplementedException(); } }; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/FilterConstraints.java ================================================ /* * Copyright 2026 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import com.netflix.zuul.FilterConstraint; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.ZuulMessage; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.jspecify.annotations.NullMarked; /** * Class responsible for checking {@link FilterConstraint}. * Register this class with custom constraints by using {@link com.netflix.zuul.netty.server.ZuulDependencyKeys#filterConstraints} * in {@link com.netflix.zuul.netty.server.BaseZuulChannelInitializer} * * @author Justin Guerra * @since 1/9/26 */ @NullMarked public class FilterConstraints { @SuppressWarnings("unchecked") private static final Class[] NO_CONSTRAINTS = new Class[0]; private final Map, FilterConstraint> lookup; private final Map[]> filterConstraints; public FilterConstraints(List constraints) { this.lookup = constraints.stream().collect(Collectors.toUnmodifiableMap(FilterConstraint::getClass, c -> c)); this.filterConstraints = new ConcurrentHashMap<>(); } /** * Checks if any {@link FilterConstraint}'s are active for the given msg */ public boolean isConstrained(ZuulMessage msg, ZuulFilter filter) { Class[] constraints = filterConstraints.computeIfAbsent(filter.getClass().getName(), f -> { Class[] filterConstraints = filter.constraints(); return filterConstraints != null ? filterConstraints : NO_CONSTRAINTS; }); for (Class constraint : constraints) { FilterConstraint filterConstraint = lookup.get(constraint); if (filterConstraint != null && filterConstraint.isConstrained(msg)) { return true; } } return false; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/FilterRunner.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import com.netflix.zuul.message.ZuulMessage; import io.netty.handler.codec.http.HttpContent; /** * Created by saroskar on 5/18/17. */ public interface FilterRunner { void filter(I zuulMesg); void filter(I zuulMesg, HttpContent chunk); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/ZuulEndPointRunner.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import com.google.common.base.Strings; import com.netflix.config.DynamicStringProperty; import com.netflix.netty.common.ByteBufUtil; import com.netflix.spectator.api.Registry; import com.netflix.spectator.impl.Preconditions; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.Endpoint; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.SyncZuulFilterAdapter; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.filters.endpoint.EndpointLifecycle; import com.netflix.zuul.filters.endpoint.MissingEndpointHandlingFilter; import com.netflix.zuul.filters.endpoint.ProxyEndpoint; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import com.netflix.zuul.netty.server.MethodBinding; import io.netty.handler.codec.http.HttpContent; import io.netty.util.ReferenceCountUtil; import io.perfmark.PerfMark; import io.perfmark.TaskCloseable; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is supposed to be thread safe and hence should not have any non final member variables * Created by saroskar on 5/18/17. */ @ThreadSafe public class ZuulEndPointRunner extends BaseZuulFilterRunner { private final FilterLoader filterLoader; private static final Logger logger = LoggerFactory.getLogger(ZuulEndPointRunner.class); public static final String PROXY_ENDPOINT_FILTER_NAME = ProxyEndpoint.class.getCanonicalName(); public static final DynamicStringProperty DEFAULT_ERROR_ENDPOINT = new DynamicStringProperty("zuul.filters.error.default", "endpoint.ErrorResponse"); public ZuulEndPointRunner( FilterUsageNotifier usageNotifier, FilterLoader filterLoader, FilterRunner respFilters, FilterConstraints filterConstraints, Registry registry) { super(FilterType.ENDPOINT, usageNotifier, respFilters, filterConstraints, registry); this.filterLoader = filterLoader; } @Nullable public static ZuulFilter getEndpoint( @Nullable HttpRequestMessage zuulReq) { if (zuulReq != null) { return zuulReq.getContext().get(CommonContextKeys.ZUUL_ENDPOINT); } return null; } protected ZuulFilter getEndpoint( String endpointName, HttpRequestMessage zuulRequest) { SessionContext zuulCtx = zuulRequest.getContext(); if (zuulCtx.getStaticResponse() != null) { return STATIC_RESPONSE_ENDPOINT; } if (endpointName == null) { return new MissingEndpointHandlingFilter("NO_ENDPOINT_NAME"); } if (endpointName.equals(PROXY_ENDPOINT_FILTER_NAME)) { return newProxyEndpoint(zuulRequest); } Endpoint filter = getEndpointFilter(endpointName); if (filter == null) { return new MissingEndpointHandlingFilter(endpointName); } return filter; } public static void setEndpoint( HttpRequestMessage zuulReq, ZuulFilter endpoint) { zuulReq.getContext().put(CommonContextKeys.ZUUL_ENDPOINT, endpoint); } @Override public void filter(HttpRequestMessage zuulReq) { if (zuulReq.getContext().isCancelled()) { PerfMark.event(getClass().getName(), "filterCancelled"); zuulReq.disposeBufferedBody(); logger.debug("Request was cancelled, UUID {}", zuulReq.getContext().getUUID()); return; } String endpointName = getEndPointName(zuulReq.getContext()); try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".filter")) { Preconditions.checkNotNull(zuulReq, "input message"); addPerfMarkTags(zuulReq); ZuulFilter endpoint = getEndpoint(endpointName, zuulReq); logger.debug( "Got endpoint {}, UUID {}", endpoint.filterName(), zuulReq.getContext().getUUID()); setEndpoint(zuulReq, endpoint); FilterExecutionResult result = executeFilter(endpoint, zuulReq); if (result instanceof FilterExecutionResult.Complete(HttpResponseMessage message) && !(endpoint instanceof EndpointLifecycle)) { // EdgeProxyEndpoint calls invokeNextStage internally logger.debug( "Endpoint calling invokeNextStage, UUID {}", zuulReq.getContext().getUUID()); invokeNextStage(message); } } catch (Exception ex) { handleException(zuulReq, endpointName, ex); } } @Override public void filter(HttpRequestMessage zuulReq, HttpContent chunk) { if (zuulReq.getContext().isCancelled()) { chunk.release(); return; } String endpointName = "-"; try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".filterChunk")) { addPerfMarkTags(zuulReq); ZuulFilter endpoint = Preconditions.checkNotNull(getEndpoint(zuulReq), "endpoint"); endpointName = endpoint.filterName(); ByteBufUtil.touch(chunk, "Endpoint processing chunk, ZuulMessage: ", zuulReq); HttpContent newChunk = endpoint.processContentChunk(zuulReq, chunk); if (newChunk != null) { ByteBufUtil.touch(newChunk, "Endpoint buffering newChunk, ZuulMessage: ", zuulReq); // Endpoints do not directly forward content chunks to next stage in the filter chain. zuulReq.bufferBodyContents(newChunk); // deallocate original chunk if necessary if (newChunk != chunk) { chunk.release(); } if (isFilterAwaitingBody(zuulReq.getContext()) && zuulReq.hasCompleteBody() && !(endpoint instanceof EndpointLifecycle)) { // whole body has arrived, resume filter chain ByteBufUtil.touch(newChunk, "Endpoint body complete, resume chain, ZuulMessage: ", zuulReq); FilterExecutionResult result = executeFilter(endpoint, zuulReq); if (result instanceof FilterExecutionResult.Complete(HttpResponseMessage message)) { invokeNextStage(message); } } } } catch (Exception ex) { ReferenceCountUtil.safeRelease(chunk); handleException(zuulReq, endpointName, ex); } } @Override protected void resume(HttpResponseMessage zuulMesg) { try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".resume")) { if (zuulMesg.getContext().isCancelled()) { return; } invokeNextStage(zuulMesg); } } protected String getEndPointName(SessionContext zuulCtx) { if (zuulCtx.shouldSendErrorResponse()) { zuulCtx.setShouldSendErrorResponse(false); zuulCtx.setErrorResponseSent(true); String errEndPointName = zuulCtx.getErrorEndpoint(); return Strings.isNullOrEmpty(errEndPointName) ? DEFAULT_ERROR_ENDPOINT.get() : errEndPointName; } else { return zuulCtx.getEndpoint(); } } /** * Override to inject your own proxy endpoint implementation * * @param zuulRequest - the request message * @return the proxy endpoint */ protected ZuulFilter newProxyEndpoint(HttpRequestMessage zuulRequest) { return new ProxyEndpoint( zuulRequest, getChannelHandlerContext(zuulRequest), getNextStage(), MethodBinding.NO_OP_BINDING); } protected Endpoint getEndpointFilter(String endpointName) { return (Endpoint) filterLoader.getFilterByNameAndType(endpointName, FilterType.ENDPOINT); } protected static final ZuulFilter STATIC_RESPONSE_ENDPOINT = new SyncZuulFilterAdapter() { @Override public HttpResponseMessage apply(HttpRequestMessage request) { HttpResponseMessage resp = request.getContext().getStaticResponse(); resp.finishBufferedBodyIfIncomplete(); return resp; } @Override public String filterName() { return "StaticResponseEndpoint"; } @Override public HttpResponseMessage getDefaultOutput(HttpRequestMessage input) { return HttpResponseMessageImpl.defaultErrorResponse(input); } }; } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/ZuulFilterChainHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import com.google.common.base.Preconditions; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import com.netflix.netty.common.HttpRequestReadTimeoutEvent; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.filters.endpoint.EndpointLifecycle; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.netty.RequestCancelledEvent; import com.netflix.zuul.netty.SpectatorUtils; import com.netflix.zuul.netty.server.ClientRequestReceiver; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.unix.Errors; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.timeout.IdleStateEvent; import io.netty.util.ReferenceCountUtil; import java.nio.channels.ClosedChannelException; import javax.net.ssl.SSLException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by saroskar on 5/18/17. */ public class ZuulFilterChainHandler extends ChannelInboundHandlerAdapter { private final ZuulFilterChainRunner requestFilterChain; private final ZuulFilterChainRunner responseFilterChain; private HttpRequestMessage zuulRequest; private static final Logger logger = LoggerFactory.getLogger(ZuulFilterChainHandler.class); public ZuulFilterChainHandler( ZuulFilterChainRunner requestFilterChain, ZuulFilterChainRunner responseFilterChain) { this.requestFilterChain = Preconditions.checkNotNull(requestFilterChain, "request filter chain"); this.responseFilterChain = Preconditions.checkNotNull(responseFilterChain, "response filter chain"); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequestMessage) { zuulRequest = (HttpRequestMessage) msg; // Replace NETTY_SERVER_CHANNEL_HANDLER_CONTEXT in SessionContext SessionContext zuulCtx = zuulRequest.getContext(); zuulCtx.put(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT, ctx); requestFilterChain.filter(zuulRequest); } else if ((msg instanceof HttpContent) && (zuulRequest != null)) { requestFilterChain.filter(zuulRequest, (HttpContent) msg); } else { logger.debug( "Received unrecognized message type. {}", msg.getClass().getName()); ReferenceCountUtil.release(msg); } } @Override public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof CompleteEvent completeEvent) { fireEndpointFinish( completeEvent.getReason() != HttpLifecycleChannelHandler.CompleteReason.SESSION_COMPLETE, ctx); } else if (evt instanceof HttpRequestReadTimeoutEvent) { sendResponse(ZuulStatusCategory.FAILURE_CLIENT_TIMEOUT, 408, ctx); } else if (evt instanceof IdleStateEvent) { sendResponse(ZuulStatusCategory.FAILURE_LOCAL_IDLE_TIMEOUT, 504, ctx); } else if (evt instanceof RequestCancelledEvent) { if (zuulRequest != null) { zuulRequest.getContext().cancel(); StatusCategoryUtils.storeStatusCategoryIfNotAlreadyFailure( zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_CANCELLED); } fireEndpointFinish(true, ctx); ctx.close(); } super.userEventTriggered(ctx, evt); } private void sendResponse(StatusCategory statusCategory, int status, ChannelHandlerContext ctx) { if (zuulRequest == null) { ctx.close(); } else { SessionContext zuulCtx = zuulRequest.getContext(); zuulRequest.getContext().cancel(); StatusCategoryUtils.storeStatusCategoryIfNotAlreadyFailure(zuulCtx, statusCategory); HttpResponseMessage zuulResponse = new HttpResponseMessageImpl(zuulCtx, zuulRequest, status); Headers headers = zuulResponse.getHeaders(); headers.add("Connection", "close"); headers.add("Content-Length", "0"); zuulResponse.finishBufferedBodyIfIncomplete(); responseFilterChain.filter(zuulResponse); fireEndpointFinish(true, ctx); } } protected HttpRequestMessage getZuulRequest() { return zuulRequest; } protected void fireEndpointFinish(boolean error, ChannelHandlerContext ctx) { // make sure filter chain is not left hanging finishResponseFilters(ctx); ZuulFilter endpoint = ZuulEndPointRunner.getEndpoint(zuulRequest); if (endpoint instanceof EndpointLifecycle lifecycleEndpoint) { lifecycleEndpoint.finish(error); } zuulRequest = null; } private void finishResponseFilters(ChannelHandlerContext ctx) { // check if there are any response filters awaiting a buffered body if (zuulRequest != null && responseFilterChain.isFilterAwaitingBody(zuulRequest.getContext())) { HttpResponseMessage zuulResponse = ctx.channel().attr(ClientRequestReceiver.ATTR_ZUUL_RESP).get(); if (zuulResponse != null) { // fire a last content into the filter chain to unblock any filters awaiting a buffered body responseFilterChain.filter(zuulResponse, new DefaultLastHttpContent()); SpectatorUtils.newCounter( "zuul.filterChain.bodyBuffer.hanging", zuulRequest.getContext().getRouteVIP()) .increment(); } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof SSLException) { logger.debug("SSL exception not handled in filter chain", cause); } else { logger.error( "zuul filter chain handler caught exception on channel: {}", ChannelUtils.channelInfoForLogging(ctx.channel()), cause); } if (zuulRequest != null && !isClientChannelClosed(cause)) { SessionContext zuulCtx = zuulRequest.getContext(); zuulCtx.setError(cause); zuulCtx.setShouldSendErrorResponse(true); sendResponse(ZuulStatusCategory.FAILURE_LOCAL, 500, ctx); } else { fireEndpointFinish(true, ctx); ctx.close(); } } // Race condition: channel.isActive() did not catch // channel close..resulting in an i/o exception private boolean isClientChannelClosed(Throwable cause) { if (cause instanceof ClosedChannelException || cause instanceof Errors.NativeIoException) { logger.error("ZuulFilterChainHandler::isClientChannelClosed - IO Exception"); return true; } return false; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/filter/ZuulFilterChainRunner.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import com.netflix.netty.common.ByteBufUtil; import com.netflix.spectator.api.Registry; import com.netflix.spectator.impl.Preconditions; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.handler.codec.http.HttpContent; import io.netty.util.ReferenceCountUtil; import io.perfmark.PerfMark; import io.perfmark.TaskCloseable; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.concurrent.ThreadSafe; /** * This class is supposed to be thread safe * Created by saroskar on 5/17/17. */ @ThreadSafe public class ZuulFilterChainRunner extends BaseZuulFilterRunner { private final ZuulFilter[] filters; public ZuulFilterChainRunner( ZuulFilter[] zuulFilters, FilterUsageNotifier usageNotifier, FilterRunner nextStage, FilterConstraints filterConstraints, Registry registry) { super(zuulFilters[0].filterType(), usageNotifier, nextStage, filterConstraints, registry); this.filters = zuulFilters; } public ZuulFilterChainRunner( ZuulFilter[] zuulFilters, FilterUsageNotifier usageNotifier, FilterConstraints filterConstraints, Registry registry) { this(zuulFilters, usageNotifier, null, filterConstraints, registry); } @Override public void filter(T inMesg) { try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".filter")) { addPerfMarkTags(inMesg); runFilters(inMesg, initRunningFilterIndex(inMesg)); } } @Override public void filter(T inMesg, HttpContent chunk) { String filterName = "-"; try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".filterChunk")) { addPerfMarkTags(inMesg); Preconditions.checkNotNull(inMesg, "input message"); AtomicInteger runningFilterIdx = getRunningFilterIndex(inMesg); int limit = runningFilterIdx.get(); for (int i = 0; i < limit; i++) { ZuulFilter filter = filters[i]; filterName = filter.filterName(); if (!filter.isDisabled() && !shouldSkipFilter(inMesg, filter)) { ByteBufUtil.touch(chunk, "Filter runner processing chunk, filter: ", filterName); HttpContent newChunk = filter.processContentChunk(inMesg, chunk); if (newChunk == null) { // Filter wants to break the chain and stop propagating this chunk any further return; } // deallocate original chunk if necessary if ((newChunk != chunk) && (chunk.refCnt() > 0)) { ByteBufUtil.touch(chunk, "Filter runner processing newChunk, filter: ", filterName); chunk.release(chunk.refCnt()); } chunk = newChunk; } } if (limit >= filters.length) { // Filter chain has run to end, pass down the channel pipeline ByteBufUtil.touch(chunk, "Filter runner chain complete, message: ", inMesg); invokeNextStage(inMesg, chunk); } else { ByteBufUtil.touch(chunk, "Filter runner buffering chunk, message: ", inMesg); inMesg.bufferBodyContents(chunk); boolean isAwaitingBody = isFilterAwaitingBody(inMesg.getContext()); // Record passport states for start and end of buffering bodies. if (isAwaitingBody) { CurrentPassport passport = CurrentPassport.fromSessionContext(inMesg.getContext()); if (inMesg.hasCompleteBody()) { if (inMesg instanceof HttpRequestMessage) { passport.addIfNotAlready(PassportState.FILTERS_INBOUND_BUF_END); } else if (inMesg instanceof HttpResponseMessage) { passport.addIfNotAlready(PassportState.FILTERS_OUTBOUND_BUF_END); } } else { if (inMesg instanceof HttpRequestMessage) { passport.addIfNotAlready(PassportState.FILTERS_INBOUND_BUF_START); } else if (inMesg instanceof HttpResponseMessage) { passport.addIfNotAlready(PassportState.FILTERS_OUTBOUND_BUF_START); } } } if (isAwaitingBody && inMesg.hasCompleteBody()) { // whole body has arrived, resume filter chain ByteBufUtil.touch(chunk, "Filter body complete, resume chain, ZuulMessage: ", inMesg); runFilters(inMesg, runningFilterIdx); } } } catch (Exception ex) { ReferenceCountUtil.safeRelease(chunk); handleException(inMesg, filterName, ex); } } @Override protected void resume(T inMesg) { try (TaskCloseable ignored = PerfMark.traceTask(this, s -> s.getClass().getSimpleName() + ".resume")) { AtomicInteger runningFilterIdx = getRunningFilterIndex(inMesg); runningFilterIdx.incrementAndGet(); runFilters(inMesg, runningFilterIdx); } } private final void runFilters(T mesg, AtomicInteger runningFilterIdx) { T inMesg = mesg; String filterName = "-"; try { Preconditions.checkNotNull(mesg, "Input message"); int i = runningFilterIdx.get(); while (i < filters.length) { ZuulFilter filter = filters[i]; filterName = filter.filterName(); FilterExecutionResult result = executeFilter(filter, inMesg); if (result instanceof FilterExecutionResult.Pending) { return; } if (result instanceof FilterExecutionResult.Complete(T message)) { inMesg = message; } i = runningFilterIdx.incrementAndGet(); } // Filter chain has reached its end, pass result to the next stage invokeNextStage(inMesg); } catch (Exception ex) { handleException(inMesg, filterName, ex); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/insights/PassportLoggingHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import com.netflix.config.CachedDynamicLongProperty; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.netty.common.metrics.HttpMetricsChannelHandler; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.monitoring.ConnCounter; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.netty.server.ClientRequestReceiver; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import com.netflix.zuul.passport.StartAndEnd; import com.netflix.zuul.stats.status.StatusCategoryUtils; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 2/28/17 * Time: 5:41 PM */ @ChannelHandler.Sharable public class PassportLoggingHandler extends ChannelInboundHandlerAdapter { private static final Logger LOG = LoggerFactory.getLogger(PassportLoggingHandler.class); private static final CachedDynamicLongProperty WARN_REQ_PROCESSING_TIME_NS = new CachedDynamicLongProperty("zuul.passport.log.request.time.threshold", 1000 * 1000 * 1000); // 1000 ms private static final CachedDynamicLongProperty WARN_RESP_PROCESSING_TIME_NS = new CachedDynamicLongProperty("zuul.passport.log.response.time.threshold", 1000 * 1000 * 1000); // 1000 ms private final Counter incompleteProxySessionCounter; public PassportLoggingHandler(Registry spectatorRegistry) { incompleteProxySessionCounter = spectatorRegistry.counter("server.http.session.incomplete"); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { try { super.userEventTriggered(ctx, evt); } finally { if (evt instanceof HttpLifecycleChannelHandler.CompleteEvent) { try { logPassport(ctx.channel()); } catch (Exception e) { LOG.error("Error logging passport info after request completed!", e); } } } } private void logPassport(Channel channel) { // Collect attributes. CurrentPassport passport = CurrentPassport.fromChannel(channel); HttpRequestMessage request = ClientRequestReceiver.getRequestFromChannel(channel); HttpResponseMessage response = ClientRequestReceiver.getResponseFromChannel(channel); SessionContext ctx = request == null ? null : request.getContext(); String topLevelRequestId = getRequestId(channel, ctx); // Do some debug logging of the Passport. if (LOG.isDebugEnabled()) { LOG.debug( "State after complete. , current-server-conns = {}, current-http-reqs = {}, status = {}, nfstatus" + " = {}, toplevelid = {}, req = {}, passport = {}", ConnCounter.from(channel).getCurrentActiveConns(), HttpMetricsChannelHandler.getInflightRequestCountFromChannel(channel), (response == null ? getRequestId(channel, ctx) : response.getStatus()), String.valueOf(StatusCategoryUtils.getStatusCategory(ctx)), topLevelRequestId, request.getInfoForLogging(), String.valueOf(passport)); } // Some logging of session states if certain criteria match: if (LOG.isInfoEnabled()) { if (passport.wasProxyAttempt()) { if (passport.findStateBackwards(PassportState.OUT_RESP_LAST_CONTENT_SENDING) == null) { incompleteProxySessionCounter.increment(); LOG.info( "Incorrect final state! toplevelid = {}, {}", topLevelRequestId, ChannelUtils.channelInfoForLogging(channel)); } } if (!passport.wasProxyAttempt()) { if (ctx != null && !isHealthcheckRequest(request)) { // Why did we fail to attempt to proxy this request? RequestAttempts attempts = RequestAttempts.getFromSessionContext(ctx); LOG.debug( "State after complete. , context-error = {}, current-http-reqs = {}, toplevelid = {}, req" + " = {}, attempts = {}, passport = {}", String.valueOf(ctx.getError()), HttpMetricsChannelHandler.getInflightRequestCountFromChannel(channel), topLevelRequestId, request.getInfoForLogging(), String.valueOf(attempts), String.valueOf(passport)); } } StartAndEnd inReqToOutResp = passport.findFirstStartAndLastEndStates( PassportState.IN_REQ_HEADERS_RECEIVED, PassportState.OUT_REQ_LAST_CONTENT_SENT); if (passport.calculateTimeBetween(inReqToOutResp) > WARN_REQ_PROCESSING_TIME_NS.get()) { LOG.info( "Request processing took longer than threshold! toplevelid = {}, {}", topLevelRequestId, ChannelUtils.channelInfoForLogging(channel)); } StartAndEnd inRespToOutResp = passport.findLastStartAndFirstEndStates( PassportState.IN_RESP_HEADERS_RECEIVED, PassportState.OUT_RESP_LAST_CONTENT_SENT); if (passport.calculateTimeBetween(inRespToOutResp) > WARN_RESP_PROCESSING_TIME_NS.get()) { LOG.info( "Response processing took longer than threshold! toplevelid = {}, {}", topLevelRequestId, ChannelUtils.channelInfoForLogging(channel)); } } } protected boolean isHealthcheckRequest(HttpRequestMessage req) { return req.getPath().equals("/healthcheck"); } protected String getRequestId(Channel channel, SessionContext ctx) { return ctx == null ? "-" : ctx.getUUID(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/insights/PassportStateHttpClientHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; /** * User: Mike Smith * Date: 9/24/16 * Time: 2:41 PM */ public final class PassportStateHttpClientHandler { private static CurrentPassport passport(ChannelHandlerContext ctx) { return CurrentPassport.fromChannel(ctx.channel()); } public static final class InboundHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { CurrentPassport passport = passport(ctx); if (msg instanceof HttpResponse) { passport.add(PassportState.IN_RESP_HEADERS_RECEIVED); } if (msg instanceof LastHttpContent) { passport.add(PassportState.IN_RESP_LAST_CONTENT_RECEIVED); } else if (msg instanceof HttpContent) { passport.add(PassportState.IN_RESP_CONTENT_RECEIVED); } } finally { super.channelRead(ctx, msg); } } } public static final class OutboundHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { try { CurrentPassport passport = passport(ctx); if (msg instanceof HttpRequest) { passport.add(PassportState.OUT_REQ_HEADERS_SENDING); promise.addListener(new PassportStateListener( passport, PassportState.OUT_REQ_HEADERS_SENT, PassportState.OUT_REQ_HEADERS_ERROR_SENDING)); } if (msg instanceof LastHttpContent) { passport.add(PassportState.OUT_REQ_LAST_CONTENT_SENDING); promise.addListener(new PassportStateListener( passport, PassportState.OUT_REQ_LAST_CONTENT_SENT, PassportState.OUT_REQ_LAST_CONTENT_ERROR_SENDING)); } else if (msg instanceof HttpContent) { passport.add(PassportState.OUT_REQ_CONTENT_SENDING); promise.addListener(new PassportStateListener( passport, PassportState.OUT_REQ_CONTENT_SENT, PassportState.OUT_REQ_CONTENT_ERROR_SENDING)); } } finally { super.write(ctx, msg, promise); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/insights/PassportStateHttpServerHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; /** * User: Mike Smith * Date: 9/24/16 * Time: 2:41 PM */ public final class PassportStateHttpServerHandler { private static CurrentPassport passport(ChannelHandlerContext ctx) { return CurrentPassport.fromChannel(ctx.channel()); } public static final class InboundHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // Get existing passport or create new if none already. CurrentPassport passport = passport(ctx); if (msg instanceof HttpRequest) { // If the current passport for this channel already contains an inbound http request, then // we know it's used, so discard and create a new one. // NOTE: we do this because we want to include the initial conn estab + ssl handshake into the passport // of the 1st request on a channel, but not on subsequent requests. if (passport.findState(PassportState.IN_REQ_HEADERS_RECEIVED) != null) { passport = CurrentPassport.createForChannel(ctx.channel()); } passport.add(PassportState.IN_REQ_HEADERS_RECEIVED); } if (msg instanceof LastHttpContent) { passport.add(PassportState.IN_REQ_LAST_CONTENT_RECEIVED); } else if (msg instanceof HttpContent) { passport.add(PassportState.IN_REQ_CONTENT_RECEIVED); } super.channelRead(ctx, msg); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { try { super.userEventTriggered(ctx, evt); } finally { if (evt instanceof HttpLifecycleChannelHandler.CompleteEvent) { CurrentPassport.clearFromChannel(ctx.channel()); } } } } public static final class OutboundHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { CurrentPassport passport = passport(ctx); // Set into the SENDING state. if (msg instanceof HttpResponse) { passport.add(PassportState.OUT_RESP_HEADERS_SENDING); promise.addListener(new PassportStateListener( passport, PassportState.OUT_RESP_HEADERS_SENT, PassportState.OUT_RESP_HEADERS_ERROR_SENDING)); } if (msg instanceof LastHttpContent) { passport.add(PassportState.OUT_RESP_LAST_CONTENT_SENDING); promise.addListener(new PassportStateListener( passport, PassportState.OUT_RESP_LAST_CONTENT_SENT, PassportState.OUT_RESP_LAST_CONTENT_ERROR_SENDING)); } else if (msg instanceof HttpContent) { passport.add(PassportState.OUT_RESP_CONTENT_SENDING); promise.addListener(new PassportStateListener( passport, PassportState.OUT_RESP_CONTENT_SENT, PassportState.OUT_RESP_CONTENT_ERROR_SENDING)); } // Continue with the write. super.write(ctx, msg, promise); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/insights/PassportStateListener.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; public class PassportStateListener implements GenericFutureListener> { private final CurrentPassport passport; private final PassportState successState; private final PassportState failState; public PassportStateListener(CurrentPassport passport, PassportState successState) { this.passport = passport; this.successState = successState; this.failState = null; } public PassportStateListener(CurrentPassport passport, PassportState successState, PassportState failState) { this.passport = passport; this.successState = successState; this.failState = failState; } @Override public void operationComplete(Future future) throws Exception { if (future.isSuccess()) { passport.add(successState); } else { if (failState != null) { // only capture a single failure state event, // as sending content errors will fire for all content chunks, // and we only need the first one passport.addIfNotAlready(failState); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/insights/PassportStateOriginHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import java.net.SocketAddress; /** * User: Mike Smith * Date: 9/24/16 * Time: 2:41 PM */ public final class PassportStateOriginHandler { private static CurrentPassport passport(ChannelHandlerContext ctx) { return CurrentPassport.fromChannel(ctx.channel()); } public static final class InboundHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { passport(ctx).add(PassportState.ORIGIN_CH_ACTIVE); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { passport(ctx).add(PassportState.ORIGIN_CH_INACTIVE); super.channelInactive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { passport(ctx).add(PassportState.ORIGIN_CH_EXCEPTION); super.exceptionCaught(ctx, cause); } } public static final class OutboundHandler extends ChannelOutboundHandlerAdapter { @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { passport(ctx).add(PassportState.ORIGIN_CH_DISCONNECT); super.disconnect(ctx, promise); } @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { passport(ctx).add(PassportState.ORIGIN_CH_CLOSE); super.close(ctx, promise); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { passport(ctx).add(PassportState.ORIGIN_CH_EXCEPTION); super.exceptionCaught(ctx, cause); } @Override public void connect( ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception { // We would prefer to set this passport state here, but if we do then it will be run _after_ the http // request // has actually been written to the channel. Because another listener is added before this one. // So instead we have to add this listener in the PerServerConnectionPool.handleConnectCompletion() method // instead. // passport.add(PassportState.ORIGIN_CH_CONNECTING); // promise.addListener(new PassportStateListener(passport, PassportState.ORIGIN_CH_CONNECTED)); super.connect(ctx, remoteAddress, localAddress, promise); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/insights/ServerStateHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.channel.unix.Errors; import io.netty.handler.timeout.IdleStateEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: Mike Smith Date: 9/24/16 Time: 2:41 PM */ public final class ServerStateHandler { private static final Logger logger = LoggerFactory.getLogger(ServerStateHandler.class); private static CurrentPassport passport(ChannelHandlerContext ctx) { return CurrentPassport.fromChannel(ctx.channel()); } public static final class InboundHandler extends ChannelInboundHandlerAdapter { private final Registry registry; private final Counter totalConnections; private final Counter connectionClosed; private final Counter connectionErrors; public InboundHandler(Registry registry, String metricId) { this.registry = registry; this.totalConnections = registry.counter("server.connections.connect", "id", metricId); this.connectionClosed = registry.counter("server.connections.close", "id", metricId); this.connectionErrors = registry.counter("server.connections.errors", "id", metricId); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { totalConnections.increment(); passport(ctx).add(PassportState.SERVER_CH_ACTIVE); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { connectionClosed.increment(); passport(ctx).add(PassportState.SERVER_CH_INACTIVE); super.channelInactive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { connectionErrors.increment(); registry.counter( "server.connection.exception.inbound", "handler", "ServerStateHandler.InboundHandler", "id", cause.getClass().getSimpleName()) .increment(); passport(ctx).add(PassportState.SERVER_CH_EXCEPTION); logger.info("Connection error on Inbound", cause); super.exceptionCaught(ctx, cause); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { CurrentPassport passport = CurrentPassport.fromChannel(ctx.channel()); if (passport != null) { passport.add(PassportState.SERVER_CH_IDLE_TIMEOUT); } } super.userEventTriggered(ctx, evt); } } public static final class OutboundHandler extends ChannelOutboundHandlerAdapter { private final Registry registry; public OutboundHandler(Registry registry) { this.registry = registry; } @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { passport(ctx).add(PassportState.SERVER_CH_CLOSE); super.close(ctx, promise); } @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { passport(ctx).add(PassportState.SERVER_CH_DISCONNECT); super.disconnect(ctx, promise); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { passport(ctx).add(PassportState.SERVER_CH_EXCEPTION); if (cause instanceof Errors.NativeIoException) { logger.debug("PassportStateServerHandler Outbound NativeIoException", cause); registry.counter( "server.connection.exception.outbound", "handler", "ServerStateHandler.OutboundHandler", "id", cause.getClass().getSimpleName()) .increment(); } else { super.exceptionCaught(ctx, cause); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/ratelimiting/NullChannelHandlerProvider.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ratelimiting; import io.netty.channel.ChannelHandler; import jakarta.inject.Provider; import jakarta.inject.Singleton; @Singleton public class NullChannelHandlerProvider implements Provider { @Override public ChannelHandler get() { return null; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/BaseServerStartup.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.google.errorprone.annotations.ForOverride; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.config.ChainedDynamicProperty; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicIntProperty; import com.netflix.discovery.EurekaClient; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.ChannelConfigValue; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.RequestCompleteHandler; import com.netflix.zuul.context.SessionContextDecorator; import com.netflix.zuul.netty.ratelimiting.NullChannelHandlerProvider; import io.netty.channel.ChannelInitializer; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.ssl.SslContext; import io.netty.util.AsyncMapping; import io.netty.util.concurrent.GlobalEventExecutor; import jakarta.inject.Inject; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Map; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class BaseServerStartup { protected static final Logger LOG = LoggerFactory.getLogger(BaseServerStartup.class); protected final ServerStatusManager serverStatusManager; protected final Registry registry; @SuppressWarnings("unused") // force initialization protected final DirectMemoryMonitor directMemoryMonitor; protected final EventLoopGroupMetrics eventLoopGroupMetrics; protected final EventLoopConfig eventLoopConfig; protected final EurekaClient discoveryClient; protected final ApplicationInfoManager applicationInfoManager; protected final AccessLogPublisher accessLogPublisher; protected final SessionContextDecorator sessionCtxDecorator; protected final RequestCompleteHandler reqCompleteHandler; protected final FilterLoader filterLoader; protected final FilterUsageNotifier usageNotifier; private Map> addrsToChannelInitializers; private ClientConnectionsShutdown clientConnectionsShutdown; private Server server; @Inject public BaseServerStartup( ServerStatusManager serverStatusManager, FilterLoader filterLoader, SessionContextDecorator sessionCtxDecorator, FilterUsageNotifier usageNotifier, RequestCompleteHandler reqCompleteHandler, Registry registry, DirectMemoryMonitor directMemoryMonitor, EventLoopGroupMetrics eventLoopGroupMetrics, EventLoopConfig eventLoopConfig, EurekaClient discoveryClient, ApplicationInfoManager applicationInfoManager, AccessLogPublisher accessLogPublisher) { this.serverStatusManager = serverStatusManager; this.registry = registry; this.directMemoryMonitor = directMemoryMonitor; this.eventLoopGroupMetrics = eventLoopGroupMetrics; this.eventLoopConfig = eventLoopConfig; this.discoveryClient = discoveryClient; this.applicationInfoManager = applicationInfoManager; this.accessLogPublisher = accessLogPublisher; this.sessionCtxDecorator = sessionCtxDecorator; this.reqCompleteHandler = reqCompleteHandler; this.filterLoader = filterLoader; this.usageNotifier = usageNotifier; } public Server server() { return server; } @Inject public void init() throws Exception { ChannelGroup clientChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); clientConnectionsShutdown = new ClientConnectionsShutdown(clientChannels, GlobalEventExecutor.INSTANCE, discoveryClient); addrsToChannelInitializers = chooseAddrsAndChannels(clientChannels); server = new Server( registry, serverStatusManager, addrsToChannelInitializers, clientConnectionsShutdown, eventLoopGroupMetrics, eventLoopConfig); } // TODO(carl-mastrangelo): remove this after 2.1.7 /** * Use {@link #chooseAddrsAndChannels(ChannelGroup)} instead. */ @Deprecated protected Map choosePortsAndChannels(ChannelGroup clientChannels) { throw new UnsupportedOperationException("unimplemented"); } @ForOverride protected Map> chooseAddrsAndChannels(ChannelGroup clientChannels) { @SuppressWarnings("unchecked") // Channel init map has the wrong generics and we can't fix without api breakage. Map> portMap = (Map>) (Map) choosePortsAndChannels(clientChannels); return Server.convertPortMap(portMap); } protected ChannelConfig defaultChannelDependencies(String listenAddressName) { ChannelConfig channelDependencies = new ChannelConfig(); addChannelDependencies(channelDependencies, listenAddressName); return channelDependencies; } protected ChannelConfig defaultChannelDependencies(ListenerSpec listenerSpec) { ChannelConfig channelDependencies = new ChannelConfig(); addChannelDependencies(channelDependencies, listenerSpec); return channelDependencies; } protected void addChannelDependencies( ChannelConfig channelDeps, @SuppressWarnings("unused") String listenAddressName) { // listenAddressName may be overridden by subclasse channelDeps.set(ZuulDependencyKeys.registry, registry); channelDeps.set(ZuulDependencyKeys.applicationInfoManager, applicationInfoManager); channelDeps.set(ZuulDependencyKeys.serverStatusManager, serverStatusManager); channelDeps.set(ZuulDependencyKeys.accessLogPublisher, accessLogPublisher); channelDeps.set(ZuulDependencyKeys.sessionCtxDecorator, sessionCtxDecorator); channelDeps.set(ZuulDependencyKeys.requestCompleteHandler, reqCompleteHandler); Counter httpRequestHeadersReadTimeoutCounter = registry.counter("server.http.request.headers.read.timeout"); channelDeps.set(ZuulDependencyKeys.httpRequestHeadersReadTimeoutCounter, httpRequestHeadersReadTimeoutCounter); PercentileTimer httpRequestHeadersReadTimer = PercentileTimer.get(registry, registry.createId("server.http.request.headers.read.timer")); channelDeps.set(ZuulDependencyKeys.httpRequestHeadersReadTimer, httpRequestHeadersReadTimer); Counter httpRequestReadTimeoutCounter = registry.counter("server.http.request.read.timeout"); channelDeps.set(ZuulDependencyKeys.httpRequestReadTimeoutCounter, httpRequestReadTimeoutCounter); channelDeps.set(ZuulDependencyKeys.filterLoader, filterLoader); channelDeps.set(ZuulDependencyKeys.filterUsageNotifier, usageNotifier); channelDeps.set(ZuulDependencyKeys.eventLoopGroupMetrics, eventLoopGroupMetrics); channelDeps.set(ZuulDependencyKeys.sslClientCertCheckChannelHandlerProvider, new NullChannelHandlerProvider()); channelDeps.set(ZuulDependencyKeys.rateLimitingChannelHandlerProvider, new NullChannelHandlerProvider()); } protected void addChannelDependencies( ChannelConfig channelDeps, @SuppressWarnings("unused") ListenerSpec listenerSpec) { // listenerSpec may be overridden by subclasses channelDeps.set(ZuulDependencyKeys.registry, registry); channelDeps.set(ZuulDependencyKeys.applicationInfoManager, applicationInfoManager); channelDeps.set(ZuulDependencyKeys.serverStatusManager, serverStatusManager); channelDeps.set(ZuulDependencyKeys.accessLogPublisher, accessLogPublisher); channelDeps.set(ZuulDependencyKeys.sessionCtxDecorator, sessionCtxDecorator); channelDeps.set(ZuulDependencyKeys.requestCompleteHandler, reqCompleteHandler); Counter httpRequestHeadersReadTimeoutCounter = registry.counter("server.http.request.headers.read.timeout"); channelDeps.set(ZuulDependencyKeys.httpRequestHeadersReadTimeoutCounter, httpRequestHeadersReadTimeoutCounter); PercentileTimer httpRequestHeadersReadTimer = PercentileTimer.get(registry, registry.createId("server.http.request.headers.read.timer")); channelDeps.set(ZuulDependencyKeys.httpRequestHeadersReadTimer, httpRequestHeadersReadTimer); Counter httpRequestReadTimeoutCounter = registry.counter("server.http.request.read.timeout"); channelDeps.set(ZuulDependencyKeys.httpRequestReadTimeoutCounter, httpRequestReadTimeoutCounter); channelDeps.set(ZuulDependencyKeys.filterLoader, filterLoader); channelDeps.set(ZuulDependencyKeys.filterUsageNotifier, usageNotifier); channelDeps.set(ZuulDependencyKeys.eventLoopGroupMetrics, eventLoopGroupMetrics); channelDeps.set(ZuulDependencyKeys.sslClientCertCheckChannelHandlerProvider, new NullChannelHandlerProvider()); channelDeps.set(ZuulDependencyKeys.rateLimitingChannelHandlerProvider, new NullChannelHandlerProvider()); } /** * First looks for a property specific to the named listen address of the form - * "server.${addrName}.${propertySuffix}". If none found, then looks for a server-wide property of the form - * "server.${propertySuffix}". If that is also not found, then returns the specified default value. */ public static int chooseIntChannelProperty(String listenAddressName, String propertySuffix, int defaultValue) { String globalPropertyName = "server." + propertySuffix; String listenAddressPropertyName = "server." + listenAddressName + "." + propertySuffix; Integer value = new DynamicIntProperty(listenAddressPropertyName, -999).get(); if (value == -999) { value = new DynamicIntProperty(globalPropertyName, -999).get(); if (value == -999) { value = defaultValue; } } return value; } public static boolean chooseBooleanChannelProperty( String listenAddressName, String propertySuffix, boolean defaultValue) { String globalPropertyName = "server." + propertySuffix; String listenAddressPropertyName = "server." + listenAddressName + "." + propertySuffix; Boolean value = new ChainedDynamicProperty.DynamicBooleanPropertyThatSupportsNull( listenAddressPropertyName, null) .get(); if (value == null) { value = new DynamicBooleanProperty(globalPropertyName, defaultValue) .getDynamicProperty() .getBoolean(); if (value == null) { value = defaultValue; } } return value; } public static ChannelConfig defaultChannelConfig(String listenAddressName) { ChannelConfig config = new ChannelConfig(); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxConnections, chooseIntChannelProperty( listenAddressName, "connection.max", CommonChannelConfigKeys.maxConnections.defaultValue()))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxRequestsPerConnection, chooseIntChannelProperty(listenAddressName, "connection.max.requests", 20000))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxRequestsPerConnectionInBrownout, chooseIntChannelProperty( listenAddressName, "connection.max.requests.brownout", CommonChannelConfigKeys.maxRequestsPerConnectionInBrownout.defaultValue()))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.connectionExpiry, chooseIntChannelProperty( listenAddressName, "connection.expiry", CommonChannelConfigKeys.connectionExpiry.defaultValue()))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.httpRequestReadTimeout, chooseIntChannelProperty( listenAddressName, "http.request.read.timeout", CommonChannelConfigKeys.httpRequestReadTimeout.defaultValue()))); int connectionIdleTimeout = chooseIntChannelProperty( listenAddressName, "connection.idle.timeout", CommonChannelConfigKeys.idleTimeout.defaultValue()); config.add(new ChannelConfigValue<>(CommonChannelConfigKeys.idleTimeout, connectionIdleTimeout)); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.serverTimeout, new ServerTimeout(connectionIdleTimeout))); // For security, default to NEVER allowing XFF/Proxy headers from client. config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER)); config.set(CommonChannelConfigKeys.withProxyProtocol, true); config.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.connCloseDelay, chooseIntChannelProperty( listenAddressName, "connection.close.delay", CommonChannelConfigKeys.connCloseDelay.defaultValue()))); return config; } public static void addHttp2DefaultConfig(ChannelConfig config, String listenAddressName) { config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxConcurrentStreams, chooseIntChannelProperty( listenAddressName, "http2.max.concurrent.streams", CommonChannelConfigKeys.maxConcurrentStreams.defaultValue()))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.initialWindowSize, chooseIntChannelProperty( listenAddressName, "http2.initialwindowsize", CommonChannelConfigKeys.initialWindowSize.defaultValue()))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxHttp2HeaderTableSize, chooseIntChannelProperty(listenAddressName, "http2.maxheadertablesize", 65536))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxHttp2HeaderListSize, chooseIntChannelProperty(listenAddressName, "http2.maxheaderlistsize", 32768))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.http2EncoderMaxResetFrames, chooseIntChannelProperty( listenAddressName, "http2.maxResetFrames", CommonChannelConfigKeys.http2EncoderMaxResetFrames.defaultValue()))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.http2EncoderMaxResetFramesWindow, chooseIntChannelProperty( listenAddressName, "http2.maxResetFramesWindow", CommonChannelConfigKeys.http2EncoderMaxResetFramesWindow.defaultValue()))); // Override this to a lower value, as we'll be using ELB TCP listeners for h2, and therefore the connection // is direct from each device rather than shared in an ELB pool. config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.maxRequestsPerConnection, chooseIntChannelProperty(listenAddressName, "connection.max.requests", 4000))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.http2AllowGracefulDelayed, chooseBooleanChannelProperty(listenAddressName, "connection.close.graceful.delayed.allow", true))); config.add(new ChannelConfigValue<>( CommonChannelConfigKeys.http2SwallowUnknownExceptionsOnConnClose, chooseBooleanChannelProperty(listenAddressName, "connection.close.swallow.unknown.exceptions", false))); } // TODO(carl-mastrangelo): remove this after 2.1.7 /** * Use {@link #logAddrConfigured(SocketAddress)} instead. */ @Deprecated protected void logPortConfigured(int port) { logAddrConfigured(new InetSocketAddress(port)); } // TODO(carl-mastrangelo): remove this after 2.1.7 /** * Use {@link #logAddrConfigured(SocketAddress, ServerSslConfig)} instead. */ @Deprecated protected void logPortConfigured(int port, ServerSslConfig serverSslConfig) { logAddrConfigured(new InetSocketAddress(port), serverSslConfig); } // TODO(carl-mastrangelo): remove this after 2.1.7 /** * Use {@link #logAddrConfigured(SocketAddress, AsyncMapping)} instead. */ @Deprecated protected void logPortConfigured(int port, AsyncMapping sniMapping) { logAddrConfigured(new InetSocketAddress(port), sniMapping); } protected final void logAddrConfigured(SocketAddress socketAddress) { LOG.info("Configured address: {}", socketAddress); } protected final void logAddrConfigured(SocketAddress socketAddress, @Nullable ServerSslConfig serverSslConfig) { String msg = "Configured address: " + socketAddress; if (serverSslConfig != null) { msg = msg + " with SSL config: " + serverSslConfig; } LOG.info(msg); } protected final void logAddrConfigured( SocketAddress socketAddress, @Nullable AsyncMapping sniMapping) { String msg = "Configured address: " + socketAddress; if (sniMapping != null) { msg = msg + " with SNI config: " + sniMapping; } LOG.info(msg); } protected final void logSecureAddrConfigured(SocketAddress socketAddress, @Nullable Object securityConfig) { LOG.info("Configured address: {} with security config {}", socketAddress, securityConfig); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/BaseZuulChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.google.common.base.Preconditions; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.netty.common.CloseOnIdleStateHandler; import com.netflix.netty.common.Http1ConnectionCloseHandler; import com.netflix.netty.common.Http1ConnectionExpiryHandler; import com.netflix.netty.common.HttpRequestReadTimeoutHandler; import com.netflix.netty.common.HttpServerLifecycleChannelHandler; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.SslExceptionsHandler; import com.netflix.netty.common.accesslog.AccessLogChannelHandler; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.metrics.HttpBodySizeRecordingChannelHandler; import com.netflix.netty.common.metrics.HttpMetricsChannelHandler; import com.netflix.netty.common.metrics.PerEventLoopMetricsChannelHandler; import com.netflix.netty.common.proxyprotocol.ElbProxyProtocolChannelHandler; import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler; import com.netflix.netty.common.throttle.MaxInboundConnectionsHandler; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.RequestCompleteHandler; import com.netflix.zuul.context.SessionContextDecorator; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.filters.passport.InboundPassportStampingFilter; import com.netflix.zuul.filters.passport.OutboundPassportStampingFilter; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.filter.FilterConstraints; import com.netflix.zuul.netty.filter.FilterRunner; import com.netflix.zuul.netty.filter.ZuulEndPointRunner; import com.netflix.zuul.netty.filter.ZuulFilterChainHandler; import com.netflix.zuul.netty.filter.ZuulFilterChainRunner; import com.netflix.zuul.netty.insights.PassportLoggingHandler; import com.netflix.zuul.netty.insights.PassportStateHttpServerHandler; import com.netflix.zuul.netty.insights.ServerStateHandler; import com.netflix.zuul.netty.server.ssl.SslHandshakeInfoHandler; import com.netflix.zuul.netty.timeouts.HttpHeadersTimeoutHandler; import com.netflix.zuul.passport.PassportState; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.AttributeKey; import java.util.List; import java.util.SortedSet; import java.util.concurrent.TimeUnit; /** * User: Mike Smith * Date: 3/5/16 * Time: 6:26 PM */ public abstract class BaseZuulChannelInitializer extends ChannelInitializer { public static final String HTTP_CODEC_HANDLER_NAME = "codec"; public static final AttributeKey ATTR_CHANNEL_CONFIG = AttributeKey.newInstance("channel_config"); protected static final LoggingHandler nettyLogger = new LoggingHandler("zuul.server.nettylog", LogLevel.INFO); public static final CachedDynamicIntProperty MAX_INITIAL_LINE_LENGTH = new CachedDynamicIntProperty("server.http.decoder.maxInitialLineLength", 16384); public static final CachedDynamicIntProperty MAX_HEADER_SIZE = new CachedDynamicIntProperty("server.http.decoder.maxHeaderSize", 32768); public static final CachedDynamicIntProperty MAX_CHUNK_SIZE = new CachedDynamicIntProperty("server.http.decoder.maxChunkSize", 32768); public static final CachedDynamicBooleanProperty HTTP_REQUEST_HEADERS_READ_TIMEOUT_ENABLED = new CachedDynamicBooleanProperty("server.http.request.headers.read.timeout.enabled", false); public static final CachedDynamicIntProperty HTTP_REQUEST_HEADERS_READ_TIMEOUT = new CachedDynamicIntProperty("server.http.request.headers.read.timeout", 10000); /** * The port that the server intends to listen on. Subclasses should NOT use this field, as it may not be set, and * may differ from the actual listening port. For example: * *

    *
  • When binding the server to port `0`, the actual port will be different from the one provided here. *
  • If there is no port (such as in a LocalSocket, or DomainSocket), the port number may be `-1`. *
* *

Instead, subclasses should read the local address on channel initialization, and decide to take action then. */ @Deprecated protected final int port; protected final String metricId; protected final ChannelConfig channelConfig; protected final ChannelConfig channelDependencies; protected final int idleTimeout; protected final int httpRequestReadTimeout; protected final int maxRequestsPerConnection; protected final int maxRequestsPerConnectionInBrownout; protected final int connectionExpiry; protected final int maxConnections; protected final Registry registry; protected final HttpMetricsChannelHandler httpMetricsHandler; protected final PerEventLoopMetricsChannelHandler.Connections perEventLoopConnectionMetricsHandler; protected final PerEventLoopMetricsChannelHandler.HttpRequests perEventLoopRequestsMetricsHandler; protected final MaxInboundConnectionsHandler maxConnectionsHandler; protected final AccessLogPublisher accessLogPublisher; protected final PassportLoggingHandler passportLoggingHandler; protected final boolean withProxyProtocol; protected final StripUntrustedProxyHeadersHandler stripInboundProxyHeadersHandler; // TODO // protected final HttpRequestThrottleChannelHandler requestThrottleHandler; protected final ChannelHandler rateLimitingChannelHandler; protected final ChannelHandler sslClientCertCheckChannelHandler; // protected final RequestRejectedChannelHandler requestRejectedChannelHandler; protected final SessionContextDecorator sessionContextDecorator; protected final RequestCompleteHandler requestCompleteHandler; protected final Counter httpRequestHeadersReadTimeoutCounter; protected final PercentileTimer httpRequestHeadersReadTimer; protected final Counter httpRequestReadTimeoutCounter; protected final FilterLoader filterLoader; protected final FilterUsageNotifier filterUsageNotifier; protected final SourceAddressChannelHandler sourceAddressChannelHandler; protected final FilterConstraints filterConstraints; /** A collection of all the active channels that we can use to things like graceful shutdown */ protected final ChannelGroup channels; /** * After calling this method, child classes should not reference {@link #port} any more. */ protected BaseZuulChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this(-1, metricId, channelConfig, channelDependencies, channels); } /** * Call {@link #BaseZuulChannelInitializer(String, ChannelConfig, ChannelConfig, ChannelGroup)} instead. */ @Deprecated protected BaseZuulChannelInitializer( int port, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this(port, String.valueOf(port), channelConfig, channelDependencies, channels); } private BaseZuulChannelInitializer( int port, String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this.port = port; Preconditions.checkNotNull(metricId, "metricId"); this.metricId = metricId; this.channelConfig = channelConfig; this.channelDependencies = channelDependencies; this.channels = channels; this.accessLogPublisher = channelDependencies.get(ZuulDependencyKeys.accessLogPublisher); this.withProxyProtocol = channelConfig.get(CommonChannelConfigKeys.withProxyProtocol); this.idleTimeout = channelConfig.get(CommonChannelConfigKeys.idleTimeout); this.httpRequestReadTimeout = channelConfig.get(CommonChannelConfigKeys.httpRequestReadTimeout); this.registry = channelDependencies.get(ZuulDependencyKeys.registry); this.httpMetricsHandler = new HttpMetricsChannelHandler(registry, "server", "http-" + metricId); EventLoopGroupMetrics eventLoopGroupMetrics = channelDependencies.get(ZuulDependencyKeys.eventLoopGroupMetrics); PerEventLoopMetricsChannelHandler perEventLoopMetricsHandler = new PerEventLoopMetricsChannelHandler(eventLoopGroupMetrics); this.perEventLoopConnectionMetricsHandler = perEventLoopMetricsHandler.new Connections(); this.perEventLoopRequestsMetricsHandler = perEventLoopMetricsHandler.new HttpRequests(); this.maxConnections = channelConfig.get(CommonChannelConfigKeys.maxConnections); this.maxConnectionsHandler = new MaxInboundConnectionsHandler(registry, metricId, maxConnections); this.maxRequestsPerConnection = channelConfig.get(CommonChannelConfigKeys.maxRequestsPerConnection); this.maxRequestsPerConnectionInBrownout = channelConfig.get(CommonChannelConfigKeys.maxRequestsPerConnectionInBrownout); this.connectionExpiry = channelConfig.get(CommonChannelConfigKeys.connectionExpiry); StripUntrustedProxyHeadersHandler.AllowWhen allowProxyHeadersWhen = channelConfig.get(CommonChannelConfigKeys.allowProxyHeadersWhen); this.stripInboundProxyHeadersHandler = new StripUntrustedProxyHeadersHandler(allowProxyHeadersWhen); this.rateLimitingChannelHandler = channelDependencies .get(ZuulDependencyKeys.rateLimitingChannelHandlerProvider) .get(); this.sslClientCertCheckChannelHandler = channelDependencies .get(ZuulDependencyKeys.sslClientCertCheckChannelHandlerProvider) .get(); this.passportLoggingHandler = new PassportLoggingHandler(registry); this.sessionContextDecorator = channelDependencies.get(ZuulDependencyKeys.sessionCtxDecorator); this.requestCompleteHandler = channelDependencies.get(ZuulDependencyKeys.requestCompleteHandler); this.httpRequestHeadersReadTimeoutCounter = channelDependencies.get(ZuulDependencyKeys.httpRequestHeadersReadTimeoutCounter); this.httpRequestHeadersReadTimer = channelDependencies.get(ZuulDependencyKeys.httpRequestHeadersReadTimer); this.httpRequestReadTimeoutCounter = channelDependencies.get(ZuulDependencyKeys.httpRequestReadTimeoutCounter); this.filterLoader = channelDependencies.get(ZuulDependencyKeys.filterLoader); this.filterUsageNotifier = channelDependencies.get(ZuulDependencyKeys.filterUsageNotifier); FilterConstraints filterConstraints = channelDependencies.get(ZuulDependencyKeys.filterConstraints); this.filterConstraints = filterConstraints != null ? filterConstraints : new FilterConstraints(List.of()); this.sourceAddressChannelHandler = new SourceAddressChannelHandler(); } protected void storeChannel(Channel ch) { this.channels.add(ch); // Also add the ChannelConfig as an attribute on each channel. So interested filters/channel-handlers can // introspect // and potentially act differently based on the config. ch.attr(ATTR_CHANNEL_CONFIG).set(channelConfig); } protected void addPassportHandler(ChannelPipeline pipeline) { pipeline.addLast(new ServerStateHandler.InboundHandler(registry, "http-" + metricId)); pipeline.addLast(new ServerStateHandler.OutboundHandler(registry)); } protected void addTcpRelatedHandlers(ChannelPipeline pipeline) { pipeline.addLast(sourceAddressChannelHandler); pipeline.addLast(perEventLoopConnectionMetricsHandler); new ElbProxyProtocolChannelHandler(registry, withProxyProtocol).addProxyProtocol(pipeline); pipeline.addLast(maxConnectionsHandler); } protected void addHttp1Handlers(ChannelPipeline pipeline) { pipeline.addLast(HTTP_CODEC_HANDLER_NAME, createHttpServerCodec()); pipeline.addLast(new Http1ConnectionCloseHandler()); pipeline.addLast( "conn_expiry_handler", new Http1ConnectionExpiryHandler( maxRequestsPerConnection, maxRequestsPerConnectionInBrownout, connectionExpiry)); } protected HttpServerCodec createHttpServerCodec() { return new HttpServerCodec(MAX_INITIAL_LINE_LENGTH.get(), MAX_HEADER_SIZE.get(), MAX_CHUNK_SIZE.get(), false); } protected void addHttpRelatedHandlers(ChannelPipeline pipeline) { pipeline.addLast(new HttpHeadersTimeoutHandler.InboundHandler( HTTP_REQUEST_HEADERS_READ_TIMEOUT_ENABLED::get, HTTP_REQUEST_HEADERS_READ_TIMEOUT::get, httpRequestHeadersReadTimeoutCounter, httpRequestHeadersReadTimer)); pipeline.addLast(new PassportStateHttpServerHandler.InboundHandler()); pipeline.addLast(new PassportStateHttpServerHandler.OutboundHandler()); if (httpRequestReadTimeout > -1) { HttpRequestReadTimeoutHandler.addLast( pipeline, httpRequestReadTimeout, TimeUnit.MILLISECONDS, httpRequestReadTimeoutCounter); } pipeline.addLast(new HttpServerLifecycleChannelHandler.HttpServerLifecycleInboundChannelHandler()); pipeline.addLast(new HttpServerLifecycleChannelHandler.HttpServerLifecycleOutboundChannelHandler()); pipeline.addLast(new HttpBodySizeRecordingChannelHandler.InboundChannelHandler()); pipeline.addLast(new HttpBodySizeRecordingChannelHandler.OutboundChannelHandler()); pipeline.addLast(httpMetricsHandler); pipeline.addLast(perEventLoopRequestsMetricsHandler); if (accessLogPublisher != null) { pipeline.addLast(new AccessLogChannelHandler.AccessLogInboundChannelHandler(accessLogPublisher)); pipeline.addLast(new AccessLogChannelHandler.AccessLogOutboundChannelHandler()); } pipeline.addLast(stripInboundProxyHeadersHandler); if (rateLimitingChannelHandler != null) { pipeline.addLast(rateLimitingChannelHandler); } // pipeline.addLast(requestRejectedChannelHandler); } protected void addTimeoutHandlers(ChannelPipeline pipeline) { pipeline.addLast(new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS)); pipeline.addLast(new CloseOnIdleStateHandler(registry, metricId)); } protected void addSslInfoHandlers(ChannelPipeline pipeline, boolean isSSlFromIntermediary) { pipeline.addLast("ssl_info", new SslHandshakeInfoHandler(registry, isSSlFromIntermediary)); pipeline.addLast("ssl_exceptions", new SslExceptionsHandler(registry)); } protected void addSslClientCertChecks(ChannelPipeline pipeline) { if (channelConfig.get(ZuulDependencyKeys.SSL_CLIENT_CERT_CHECK_REQUIRED)) { if (this.sslClientCertCheckChannelHandler == null) { throw new IllegalArgumentException("A sslClientCertCheckChannelHandler is required!"); } pipeline.addLast(this.sslClientCertCheckChannelHandler); } } protected void addZuulHandlers(ChannelPipeline pipeline) { pipeline.addLast("logger", nettyLogger); pipeline.addLast(new ClientRequestReceiver(sessionContextDecorator)); pipeline.addLast(passportLoggingHandler); addZuulFilterChainHandler(pipeline); pipeline.addLast(new ClientResponseWriter(requestCompleteHandler, registry)); } protected void addZuulFilterChainHandler(ChannelPipeline pipeline) { ZuulFilter[] responseFilters = getFilters( new OutboundPassportStampingFilter(PassportState.FILTERS_OUTBOUND_START), new OutboundPassportStampingFilter(PassportState.FILTERS_OUTBOUND_END)); // response filter chain ZuulFilterChainRunner responseFilterChain = getFilterChainRunner(responseFilters, filterUsageNotifier); // endpoint | response filter chain FilterRunner endPoint = getEndpointRunner(responseFilterChain, filterUsageNotifier, filterLoader); ZuulFilter[] requestFilters = getFilters( new InboundPassportStampingFilter(PassportState.FILTERS_INBOUND_START), new InboundPassportStampingFilter(PassportState.FILTERS_INBOUND_END)); // request filter chain | end point | response filter chain ZuulFilterChainRunner requestFilterChain = getFilterChainRunner(requestFilters, filterUsageNotifier, endPoint); pipeline.addLast(new ZuulFilterChainHandler(requestFilterChain, responseFilterChain)); } protected ZuulEndPointRunner getEndpointRunner( ZuulFilterChainRunner responseFilterChain, FilterUsageNotifier filterUsageNotifier, FilterLoader filterLoader) { return new ZuulEndPointRunner( filterUsageNotifier, filterLoader, responseFilterChain, filterConstraints, registry); } protected ZuulFilterChainRunner getFilterChainRunner( ZuulFilter[] filters, FilterUsageNotifier filterUsageNotifier) { return new ZuulFilterChainRunner<>(filters, filterUsageNotifier, filterConstraints, registry); } protected ZuulFilterChainRunner getFilterChainRunner( ZuulFilter[] filters, FilterUsageNotifier filterUsageNotifier, FilterRunner filterRunner) { return new ZuulFilterChainRunner<>(filters, filterUsageNotifier, filterRunner, filterConstraints, registry); } @SuppressWarnings("unchecked") // For the conversion from getFiltersByType. It's not safe, sorry. public ZuulFilter[] getFilters(ZuulFilter start, ZuulFilter stop) { SortedSet> zuulFilters = filterLoader.getFiltersByType(start.filterType()); ZuulFilter[] filters = new ZuulFilter[zuulFilters.size() + 2]; filters[0] = start; int i = 1; for (ZuulFilter filter : zuulFilters) { // TODO(carl-mastrangelo): find some way to make this cast not needed. filters[i++] = (ZuulFilter) filter; } filters[filters.length - 1] = stop; return filters; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ClientConnectionsShutdown.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.netflix.appinfo.InstanceInfo; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicIntProperty; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.StatusChangeEvent; import com.netflix.netty.common.ConnectionCloseChannelAttributes; import com.netflix.netty.common.ConnectionCloseType; import io.netty.channel.Channel; import io.netty.channel.ChannelPromise; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.ChannelGroupFuture; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * TODO: Change this class to be an instance per-port. * So that then the configuration can be different per-port, which is need for the combined FTL/Cloud clusters. *

* User: michaels@netflix.com * Date: 3/6/17 * Time: 12:36 PM */ public class ClientConnectionsShutdown { private static final Logger LOG = LoggerFactory.getLogger(ClientConnectionsShutdown.class); private static final DynamicBooleanProperty ENABLED = new DynamicBooleanProperty("server.outofservice.connections.shutdown", false); private static final DynamicIntProperty DELAY_AFTER_OUT_OF_SERVICE_MS = new DynamicIntProperty("server.outofservice.connections.delay", 2000); private static final DynamicIntProperty GRACEFUL_CLOSE_TIMEOUT = new DynamicIntProperty("server.outofservice.close.timeout", 30); public enum ShutdownType { OUT_OF_SERVICE, SHUTDOWN } private final ChannelGroup channels; private final EventExecutor executor; private final EurekaClient discoveryClient; public ClientConnectionsShutdown(ChannelGroup channels, EventExecutor executor, EurekaClient discoveryClient) { this.channels = channels; this.executor = executor; this.discoveryClient = discoveryClient; if (discoveryClient != null) { initDiscoveryListener(); } } private void initDiscoveryListener() { this.discoveryClient.registerEventListener(event -> { if (event instanceof StatusChangeEvent sce) { LOG.info("Received {}", sce); if (sce.getPreviousStatus() == InstanceInfo.InstanceStatus.UP && (sce.getStatus() == InstanceInfo.InstanceStatus.OUT_OF_SERVICE || sce.getStatus() == InstanceInfo.InstanceStatus.DOWN)) { // TODO - Also should stop accepting any new client connections now too? // Schedule to gracefully close all the client connections. if (ENABLED.get()) { executor.schedule( () -> gracefullyShutdownClientChannels(ShutdownType.OUT_OF_SERVICE), DELAY_AFTER_OUT_OF_SERVICE_MS.get(), TimeUnit.MILLISECONDS); } } } }); } public Promise gracefullyShutdownClientChannels() { return gracefullyShutdownClientChannels(ShutdownType.SHUTDOWN); } Promise gracefullyShutdownClientChannels(ShutdownType shutdownType) { // Mark all active connections to be closed after next response sent. LOG.warn("Flagging CLOSE_AFTER_RESPONSE on {} client channels.", channels.size()); // racy situation if new connections are still coming in, but any channels created after newCloseFuture will // be closed during the force close stage ChannelGroupFuture closeFuture = channels.newCloseFuture(); for (Channel channel : channels) { flagChannelForClose(channel, shutdownType); } LOG.info("Setting up scheduled task for {} with shutdownType: {}", closeFuture, shutdownType); Promise promise = executor.newPromise(); Runnable cancelTimeoutTask; if (shutdownType == ShutdownType.SHUTDOWN) { ScheduledFuture timeoutTask = executor.schedule( () -> { LOG.warn("Force closing remaining {} active client channels.", channels.size()); channels.close().addListener(future -> { if (!future.isSuccess()) { LOG.error("Failed to close all connections", future.cause()); } if (!promise.isDone()) { promise.setSuccess(null); } }); }, GRACEFUL_CLOSE_TIMEOUT.get(), TimeUnit.SECONDS); cancelTimeoutTask = () -> { if (!timeoutTask.isDone()) { LOG.info("Timeout task canceled before completion."); // close happened before the timeout timeoutTask.cancel(false); } }; } else { cancelTimeoutTask = () -> {}; } closeFuture.addListener(future -> { LOG.info("CloseFuture completed successfully: {}", future.isSuccess()); cancelTimeoutTask.run(); promise.setSuccess(null); }); return promise; } protected void flagChannelForClose(Channel channel, ShutdownType shutdownType) { ConnectionCloseType.setForChannel(channel, ConnectionCloseType.DELAYED_GRACEFUL); ChannelPromise closePromise = channel.pipeline().newPromise(); channel.attr(ConnectionCloseChannelAttributes.CLOSE_AFTER_RESPONSE).set(closePromise); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ClientRequestReceiver.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import static com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.ssl.SslHandshakeInfo; import com.netflix.netty.common.throttle.RejectionUtils; import com.netflix.spectator.api.Spectator; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.Debug; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.context.SessionContextDecorator; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.netty.server.http2.Http2OrHttpHandler; import com.netflix.zuul.netty.server.ssl.SslHandshakeInfoHandler; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import com.netflix.zuul.util.HttpUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.unix.Errors; import io.netty.handler.codec.haproxy.HAProxyMessage; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; import io.perfmark.PerfMark; import io.perfmark.TaskCloseable; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map.Entry; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLException; import lombok.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by saroskar on 1/6/17. */ public class ClientRequestReceiver extends ChannelDuplexHandler { public static final AttributeKey ATTR_ZUUL_REQ = AttributeKey.newInstance("_zuul_request"); public static final AttributeKey ATTR_ZUUL_RESP = AttributeKey.newInstance("_zuul_response"); public static final AttributeKey ATTR_LAST_CONTENT_RECEIVED = AttributeKey.newInstance("_last_content_received"); private static final Logger LOG = LoggerFactory.getLogger(ClientRequestReceiver.class); private static final String SCHEME_HTTP = "http"; private static final String SCHEME_HTTPS = "https"; // via @stephenhay https://mathiasbynens.be/demo/url-regex, groups added // group 1: scheme, group 2: domain, group 3: path+query private static final Pattern URL_REGEX = Pattern.compile("^(https?)://([^\\s/$.?#].[^\\s/]*)([^\\s]*)$"); private final SessionContextDecorator decorator; private HttpRequestMessage zuulRequest; private HttpRequest clientRequest; public ClientRequestReceiver(SessionContextDecorator decorator) { this.decorator = decorator; } public static HttpRequestMessage getRequestFromChannel(Channel ch) { return ch.attr(ATTR_ZUUL_REQ).get(); } public static HttpResponseMessage getResponseFromChannel(Channel ch) { return ch.attr(ATTR_ZUUL_RESP).get(); } public static boolean isLastContentReceivedForChannel(Channel ch) { Boolean value = ch.attr(ATTR_LAST_CONTENT_RECEIVED).get(); return value == null ? false : value; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try (TaskCloseable ignore = PerfMark.traceTask("CRR.channelRead")) { channelReadInternal(ctx, msg); } } private void channelReadInternal(ChannelHandlerContext ctx, Object msg) { // Flag that we have now received the LastContent for this request from the client. // This is needed for ClientResponseReceiver to know whether it's yet safe to start writing // a response to the client channel. if (msg instanceof LastHttpContent) { ctx.channel().attr(ATTR_LAST_CONTENT_RECEIVED).set(Boolean.TRUE); } if (msg instanceof HttpRequest) { clientRequest = (HttpRequest) msg; zuulRequest = buildZuulHttpRequest(clientRequest, ctx); // Handle invalid HTTP requests. if (clientRequest.decoderResult().isFailure()) { String clientIp = Objects.requireNonNullElse(getClientIp(ctx.channel()), "unknown"); LOG.warn( "Invalid http request. clientRequest = {}, clientIp = {}, info = {}", clientRequest, clientIp, ChannelUtils.channelInfoForLogging(ctx.channel()), clientRequest.decoderResult().cause()); StatusCategoryUtils.setStatusCategory( zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST, "Invalid request provided: Decode failure"); RejectionUtils.rejectByClosingConnection( ctx, ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST, "decodefailure", clientRequest, null); return; } else if (zuulRequest.hasBody() && zuulRequest.getBodyLength() > zuulRequest.getMaxBodySize()) { String errorMsg = "Request too large. " + "clientRequest = " + clientRequest.toString() + ", uri = " + String.valueOf(clientRequest.uri()) + ", info = " + ChannelUtils.channelInfoForLogging(ctx.channel()); ZuulException ze = new ZuulException(errorMsg); ze.setStatusCode(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE.code()); StatusCategoryUtils.setStatusCategory( zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST, "Invalid request provided: Request body size " + zuulRequest.getBodyLength() + " is above limit of " + zuulRequest.getMaxBodySize()); zuulRequest.getContext().setError(ze); zuulRequest.getContext().setShouldSendErrorResponse(true); } else if (zuulRequest .getHeaders() .getAll(HttpHeaderNames.HOST.toString()) .size() > 1) { LOG.debug( "Multiple Host headers. clientRequest = {} , uri = {}, info = {}", clientRequest, clientRequest.uri(), ChannelUtils.channelInfoForLogging(ctx.channel())); ZuulException ze = new ZuulException("Multiple Host headers"); ze.setStatusCode(HttpResponseStatus.BAD_REQUEST.code()); StatusCategoryUtils.setStatusCategory( zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST, "Invalid request provided: Multiple Host headers"); zuulRequest.getContext().setError(ze); zuulRequest.getContext().setShouldSendErrorResponse(true); } handleExpect100Continue(ctx, clientRequest); // Send the request down the filter pipeline ctx.fireChannelRead(zuulRequest); } else if (msg instanceof HttpContent) { if ((zuulRequest != null) && !zuulRequest.getContext().isCancelled()) { ctx.fireChannelRead(msg); } else { // We already sent response for this request, these are laggard request body chunks that are still // arriving ReferenceCountUtil.release(msg); } } else if (msg instanceof HAProxyMessage) { // do nothing, should already be handled by ElbProxyProtocolHandler LOG.debug("Received HAProxyMessage for Proxy Protocol IP: {}", ((HAProxyMessage) msg).sourceAddress()); ReferenceCountUtil.release(msg); } else { LOG.debug("Received unrecognized message type. {}", msg.getClass().getName()); ReferenceCountUtil.release(msg); } } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof CompleteEvent) { CompleteReason reason = ((CompleteEvent) evt).getReason(); if (zuulRequest != null) { zuulRequest.getContext().cancel(); zuulRequest.disposeBufferedBody(); CurrentPassport passport = CurrentPassport.fromSessionContext(zuulRequest.getContext()); if ((passport != null) && (passport.findState(PassportState.OUT_RESP_LAST_CONTENT_SENT) == null)) { // Only log this state if the response does not seem to have completed normally. passport.add(PassportState.IN_REQ_CANCELLED); } } if (reason == CompleteReason.INACTIVE && zuulRequest != null) { this.handleClientChannelInactiveEvent(zuulRequest); } if (reason == CompleteReason.PIPELINE_REJECT && zuulRequest != null) { StatusCategoryUtils.setStatusCategory( zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_PIPELINE_REJECT); } if (reason != CompleteReason.SESSION_COMPLETE && zuulRequest != null) { SessionContext zuulCtx = zuulRequest.getContext(); if (clientRequest != null) { if (LOG.isInfoEnabled()) { // With http/2, the netty codec closes/completes the stream immediately after writing the // lastcontent // of response to the channel, which causes this CompleteEvent to fire before we have cleaned up // state. But // that's ok, so don't log in that case. if (Objects.equals(zuulRequest.getProtocol(), "HTTP/2")) { LOG.debug( "Client {} request UUID {} to {} completed with reason = {}, {}", clientRequest.method(), zuulCtx.getUUID(), clientRequest.uri(), reason.name(), ChannelUtils.channelInfoForLogging(ctx.channel())); } } } if (zuulCtx.debugRequest()) { LOG.debug("Endpoint = {}", zuulCtx.getEndpoint()); dumpDebugInfo(Debug.getRequestDebug(zuulCtx)); dumpDebugInfo(Debug.getRoutingDebug(zuulCtx)); } } if (zuulRequest == null) { Spectator.globalRegistry() .counter("zuul.client.complete.null", "reason", String.valueOf(reason)) .increment(); } clientRequest = null; zuulRequest = null; } super.userEventTriggered(ctx, evt); if (evt instanceof CompleteEvent) { Channel channel = ctx.channel(); channel.attr(ATTR_ZUUL_REQ).set(null); channel.attr(ATTR_ZUUL_RESP).set(null); channel.attr(ATTR_LAST_CONTENT_RECEIVED).set(null); } } /** * Handle the event when the client channel is moved to inactive, * generally this occurs if the client cancels the request. */ protected void handleClientChannelInactiveEvent(@NonNull HttpRequestMessage zuulRequest) { // client closed connection prematurely StatusCategoryUtils.setStatusCategory(zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_CANCELLED); } private static void dumpDebugInfo(List debugInfo) { debugInfo.forEach((dbg) -> LOG.debug(dbg)); } private void handleExpect100Continue(ChannelHandlerContext ctx, HttpRequest req) { if (HttpUtil.is100ContinueExpected(req)) { PerfMark.event("CRR.handleExpect100Continue"); ChannelFuture f = ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); f.addListener((s) -> { if (!s.isSuccess()) { throw new ZuulException(s.cause(), "Failed while writing 100-continue response", true); } }); // Remove the Expect: 100-Continue header from request as we don't want to proxy it downstream. req.headers().remove(HttpHeaderNames.EXPECT); zuulRequest.getHeaders().remove(HttpHeaderNames.EXPECT.toString()); } } // Build a ZuulMessage from the netty request. private HttpRequestMessage buildZuulHttpRequest(HttpRequest nativeRequest, ChannelHandlerContext clientCtx) { PerfMark.attachTag("path", nativeRequest, HttpRequest::uri); // Setup the context for this request. SessionContext context; if (decorator != null) { // Optionally decorate the context. SessionContext tempContext = new SessionContext(); // Store the netty channel in SessionContext. tempContext.set(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT, clientCtx); context = decorator.decorate(tempContext); // We expect the UUID is present after decoration PerfMark.attachTag("uuid", context, SessionContext::getUUID); } else { context = new SessionContext(); } // Get the client IP (ignore XFF headers at this point, as that can be app specific). Channel channel = clientCtx.channel(); String clientIp = getClientIp(channel); // This is the only way I found to get the port of the request with netty... int port = channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).get(); String serverName = channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_ADDRESS) .get(); SocketAddress clientDestinationAddress = channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).get(); InetSocketAddress proxyProtocolDestinationAddress = channel.attr( SourceAddressChannelHandler.ATTR_PROXY_PROTOCOL_DESTINATION_ADDRESS) .get(); if (proxyProtocolDestinationAddress != null) { context.set(CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS, proxyProtocolDestinationAddress); } // Store info about the SSL handshake if applicable, and choose the http scheme. String scheme = SCHEME_HTTP; SslHandshakeInfo sslHandshakeInfo = channel.attr(SslHandshakeInfoHandler.ATTR_SSL_INFO).get(); if (sslHandshakeInfo != null) { context.set(CommonContextKeys.SSL_HANDSHAKE_INFO, sslHandshakeInfo); scheme = SCHEME_HTTPS; } // Decide if this is HTTP/1 or HTTP/2. String protocol = channel.attr(Http2OrHttpHandler.PROTOCOL_NAME).get(); if (protocol == null) { protocol = nativeRequest.protocolVersion().text(); } // Strip off the query from the path. String path = parsePath(nativeRequest.uri()); // Setup the req/resp message objects. HttpRequestMessage request = new HttpRequestMessageImpl( context, protocol, nativeRequest.method().asciiName().toString().toLowerCase(Locale.ROOT), path, copyQueryParams(nativeRequest), copyHeaders(nativeRequest), clientIp, scheme, port, serverName, clientDestinationAddress, false); // Try to decide if this request has a body or not based on the headers (as we won't yet have // received any of the content). // NOTE that we also later may override this if it is Chunked encoding, but we receive // a LastHttpContent without any prior HttpContent's. if (HttpUtils.hasChunkedTransferEncodingHeader(request) || HttpUtils.hasNonZeroContentLengthHeader(request)) { request.setHasBody(true); } // Store this original request info for future reference (ie. for metrics and access logging purposes). request.storeInboundRequest(); // Store the netty request for use later. context.set(CommonContextKeys.NETTY_HTTP_REQUEST, nativeRequest); // Store zuul request on netty channel for later use. channel.attr(ATTR_ZUUL_REQ).set(request); if (nativeRequest instanceof DefaultFullHttpRequest) { ByteBuf chunk = ((DefaultFullHttpRequest) nativeRequest).content(); request.bufferBodyContents(new DefaultLastHttpContent(chunk)); } return request; } protected String getClientIp(Channel channel) { return channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get(); } private String parsePath(String uri) { String path; try { URI uriObject = new URI(uri); uriObject = uriObject.normalize(); path = uriObject.getRawPath(); if (path == null) { // If we have an opaque URI, match existing behavior of using the URI as the path. return uri; } while (path.startsWith("/..")) { path = path.substring(3); } return path; } catch (URISyntaxException ex) { LOG.debug("URI syntax error", ex); } // manual path parsing // relative uri if (uri.startsWith("/")) { path = uri; } else { Matcher m = URL_REGEX.matcher(uri); // absolute uri if (m.matches()) { String match = m.group(3); if (match == null) { // in case of no match, default to existing behavior path = uri; } else { path = match; } } // unknown value else { // in case of unknown value, default to existing behavior path = uri; } } int queryIndex = path.indexOf('?'); if (queryIndex > -1) { path = path.substring(0, queryIndex); } while (path.startsWith("/..")) { path = path.substring(3); } return path; } private static Headers copyHeaders(HttpRequest req) { Headers headers = new Headers(req.headers().size()); for (Iterator> it = req.headers().iteratorAsString(); it.hasNext(); ) { Entry header = it.next(); headers.add(header.getKey(), header.getValue()); } return headers; } public static HttpQueryParams copyQueryParams(HttpRequest nativeRequest) { String uri = nativeRequest.uri(); int queryStart = uri.indexOf('?'); String query = queryStart == -1 ? null : uri.substring(queryStart + 1); return HttpQueryParams.parse(query); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { try (TaskCloseable ignored = PerfMark.traceTask("CRR.write")) { if (msg instanceof HttpResponse) { promise.addListener((future) -> { if (!future.isSuccess()) { fireWriteError("response headers", future.cause(), ctx); } }); super.write(ctx, msg, promise); } else if (msg instanceof HttpContent) { promise.addListener((future) -> { if (!future.isSuccess()) { fireWriteError("response content", future.cause(), ctx); } }); super.write(ctx, msg, promise); } else { // should never happen ReferenceCountUtil.release(msg); throw new ZuulException( "Attempt to write invalid content type to client: " + msg.getClass().getSimpleName(), true); } } } private void fireWriteError(String requestPart, Throwable cause, ChannelHandlerContext ctx) { String errMesg = String.format("Error writing %s to client", requestPart); if (cause instanceof java.nio.channels.ClosedChannelException || cause instanceof Errors.NativeIoException || cause instanceof SSLException || (cause.getCause() != null && cause.getCause() instanceof SSLException) || isStreamCancelled(cause)) { LOG.debug("{} - client connection is closed.", errMesg); if (zuulRequest != null) { zuulRequest.getContext().cancel(); StatusCategoryUtils.storeStatusCategoryIfNotAlreadyFailure( zuulRequest.getContext(), ZuulStatusCategory.FAILURE_CLIENT_CANCELLED); } } else { LOG.error(errMesg, cause); ctx.fireExceptionCaught(new ZuulException(cause, errMesg, true)); } } private boolean isStreamCancelled(Throwable cause) { // Detect if the stream is cancelled or closed. // If the stream was closed before the write occurred, then netty flags it with INTERNAL_ERROR code. if (cause instanceof Http2Exception.StreamException) { Http2Exception http2Exception = (Http2Exception) cause; if (http2Exception.error() == Http2Error.INTERNAL_ERROR) { return true; } } return false; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ClientResponseWriter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import static com.netflix.netty.common.HttpLifecycleChannelHandler.StartEvent; import com.google.common.annotations.VisibleForTesting; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.RequestCompleteHandler; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.message.Header; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpContent; 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.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.util.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by saroskar on 2/26/17. */ public class ClientResponseWriter extends ChannelInboundHandlerAdapter { private static final Registry NOOP_REGISTRY = new NoopRegistry(); private final RequestCompleteHandler requestCompleteHandler; private final Counter responseBeforeReceivedLastContentCounter; // state private boolean isHandlingRequest; private boolean startedSendingResponseToClient; private boolean closeConnection; // data private HttpResponseMessage zuulResponse; private static final Logger logger = LoggerFactory.getLogger(ClientResponseWriter.class); public ClientResponseWriter(RequestCompleteHandler requestCompleteHandler) { this(requestCompleteHandler, NOOP_REGISTRY); } public ClientResponseWriter(RequestCompleteHandler requestCompleteHandler, Registry registry) { this.requestCompleteHandler = requestCompleteHandler; this.responseBeforeReceivedLastContentCounter = registry.counter("server.http.requests.responseBeforeReceivedLastContent"); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Channel channel = ctx.channel(); if (msg instanceof HttpResponseMessage resp) { if (skipProcessing(resp)) { return; } if (!isHandlingRequest || startedSendingResponseToClient) { /* This can happen if we are already in the process of streaming response back to client OR NOT within active request/response cycle and something like IDLE or Request Read timeout occurs. In that case we have no way to recover other than closing the socket and cleaning up resources used by BOTH responses. */ resp.disposeBufferedBody(); if (zuulResponse != null) { zuulResponse.disposeBufferedBody(); } ctx.close(); // This will trigger CompleteEvent if one is needed return; } startedSendingResponseToClient = true; zuulResponse = resp; if ("close".equalsIgnoreCase(zuulResponse.getHeaders().getFirst("Connection"))) { closeConnection = true; } channel.attr(ClientRequestReceiver.ATTR_ZUUL_RESP).set(zuulResponse); if (channel.isActive()) { // track if response is being written before receiving LastContent for requests with a body if (!ClientRequestReceiver.isLastContentReceivedForChannel(channel) && !shouldAllowPreemptiveResponse(channel) && zuulResponse.getInboundRequest().hasBody()) { responseBeforeReceivedLastContentCounter.increment(); logger.warn( "Writing response to client channel before have received the LastContent of request! {}," + " {}", zuulResponse.getInboundRequest().getInfoForLogging(), ChannelUtils.channelInfoForLogging(channel)); } // Write out and flush the response to the client channel. channel.write(buildHttpResponse(zuulResponse)); writeBufferedBodyContent(zuulResponse, channel); channel.flush(); } else { resp.disposeBufferedBody(); channel.close(); } } else if (msg instanceof HttpContent chunk) { if (channel.isActive()) { channel.writeAndFlush(chunk); } else { chunk.release(); channel.close(); } } else { // should never happen ReferenceCountUtil.release(msg); throw new ZuulException("Received invalid message from origin", true); } } protected boolean shouldAllowPreemptiveResponse(Channel channel) { // If the request timed-out while being read, then there won't have been any LastContent, but that's ok because // the connection will have to be discarded anyway. StatusCategory status = StatusCategoryUtils.getStatusCategory(ClientRequestReceiver.getRequestFromChannel(channel)); return status == ZuulStatusCategory.FAILURE_CLIENT_TIMEOUT; } protected boolean skipProcessing(HttpResponseMessage resp) { // override if you need to skip processing of response return false; } private static void writeBufferedBodyContent(HttpResponseMessage zuulResponse, Channel channel) { zuulResponse.getBodyContents().forEach(chunk -> channel.write(chunk.retain())); } private HttpResponse buildHttpResponse(HttpResponseMessage zuulResp) { HttpRequestInfo zuulRequest = zuulResp.getInboundRequest(); HttpVersion responseHttpVersion; String inboundProtocol = zuulRequest.getProtocol(); if (inboundProtocol.startsWith("HTTP/1")) { responseHttpVersion = HttpVersion.valueOf(inboundProtocol); } else { // Default to 1.1. We do this to cope with HTTP/2 inbound requests. responseHttpVersion = HttpVersion.HTTP_1_1; } // Create the main http response to send, with body. DefaultHttpResponse nativeResponse = new DefaultHttpResponse( responseHttpVersion, HttpResponseStatus.valueOf(zuulResp.getStatus()), false, false); // Now set all of the response headers - note this is a multi-set in keeping with HTTP semantics HttpHeaders nativeHeaders = nativeResponse.headers(); for (Header entry : zuulResp.getHeaders().entries()) { nativeHeaders.add(entry.getKey(), entry.getValue()); } // Netty does not automatically add Content-Length or Transfer-Encoding: chunked. So we add here if missing. if (!HttpUtil.isContentLengthSet(nativeResponse) && !HttpUtil.isTransferEncodingChunked(nativeResponse)) { nativeResponse.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); } HttpRequest nativeReq = (HttpRequest) zuulResp.getContext().get(CommonContextKeys.NETTY_HTTP_REQUEST); if (!closeConnection && HttpUtil.isKeepAlive(nativeReq)) { HttpUtil.setKeepAlive(nativeResponse, true); } else { // Send a Connection: close response header (only needed for HTTP/1.0 but no harm in doing for 1.1 too). nativeResponse.headers().set("Connection", "close"); } // TODO - temp hack for http/2 handling. if (nativeReq.headers().contains(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text())) { String streamId = nativeReq.headers().get(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); nativeResponse.headers().set(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); } return nativeResponse; } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof StartEvent) { isHandlingRequest = true; startedSendingResponseToClient = false; closeConnection = false; zuulResponse = null; } else if (evt instanceof CompleteEvent) { HttpResponse response = ((CompleteEvent) evt).getResponse(); if (response != null) { if ("close".equalsIgnoreCase(response.headers().get("Connection"))) { closeConnection = true; } } if (zuulResponse != null) { zuulResponse.disposeBufferedBody(); } // Do all the post-completion metrics and logging. handleComplete(ctx.channel()); // Choose to either close the connection, or prepare it for next use. CompleteEvent completeEvent = (CompleteEvent) evt; CompleteReason reason = completeEvent.getReason(); if (reason == CompleteReason.SESSION_COMPLETE || reason == CompleteReason.INACTIVE) { if (!closeConnection) { // Start reading next request over HTTP 1.1 persistent connection ctx.channel().read(); } else { ctx.close(); } } else { if (isHandlingRequest) { logger.debug( "Received complete event while still handling the request. With reason: {} -- {}", reason.name(), ChannelUtils.channelInfoForLogging(ctx.channel())); } ctx.close(); } isHandlingRequest = false; } else if (evt instanceof IdleStateEvent) { logger.debug("Received IdleStateEvent."); } else { logger.debug("ClientResponseWriter Received event {}", evt); } } private void handleComplete(Channel channel) { try { if (isHandlingRequest) { completeMetrics(channel, zuulResponse); // Notify requestComplete listener if configured. HttpRequestMessage zuulRequest = ClientRequestReceiver.getRequestFromChannel(channel); if ((requestCompleteHandler != null) && (zuulRequest != null)) { requestCompleteHandler.handle(zuulRequest.getInboundRequest(), zuulResponse); } zuulResponse = null; } } catch (Throwable ex) { logger.error("Error in RequestCompleteHandler.", ex); } } protected void completeMetrics(Channel channel, HttpResponseMessage zuulResponse) { // override for recording complete metrics } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { int status = 500; if (cause instanceof ZuulException ze) { status = ze.getStatusCode(); logger.error( "Exception caught in ClientResponseWriter for channel {} ", ChannelUtils.channelInfoForLogging(ctx.channel()), cause); } else if (cause instanceof ReadTimeoutException) { logger.debug("Read timeout for channel {} ", ChannelUtils.channelInfoForLogging(ctx.channel()), cause); status = 504; } else { logger.error("Exception caught in ClientResponseWriter: ", cause); } if (isHandlingRequest && !startedSendingResponseToClient && ctx.channel().isActive()) { HttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(status)); ctx.writeAndFlush(httpResponse).addListener(ChannelFutureListener.CLOSE); startedSendingResponseToClient = true; } else { ctx.close(); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); ctx.close(); } @VisibleForTesting HttpResponseMessage getZuulResponse() { return zuulResponse; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/DefaultEventLoopConfig.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.netflix.config.DynamicIntProperty; import jakarta.inject.Singleton; /** * Event loop configuration for the Zuul server. * By default, it configures a single acceptor thread with workers = logical cores available. */ @Singleton public class DefaultEventLoopConfig implements EventLoopConfig { private static final DynamicIntProperty ACCEPTOR_THREADS = new DynamicIntProperty("zuul.server.netty.threads.acceptor", 1); private static final DynamicIntProperty WORKER_THREADS = new DynamicIntProperty("zuul.server.netty.threads.worker", -1); private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); private final int eventLoopCount; private final int acceptorCount; public DefaultEventLoopConfig() { eventLoopCount = WORKER_THREADS.get() > 0 ? WORKER_THREADS.get() : PROCESSOR_COUNT; acceptorCount = ACCEPTOR_THREADS.get(); } public DefaultEventLoopConfig(int eventLoopCount, int acceptorCount) { this.eventLoopCount = eventLoopCount; this.acceptorCount = acceptorCount; } @Override public int eventLoopCount() { return eventLoopCount; } @Override public int acceptorCount() { return acceptorCount; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/DirectMemoryMonitor.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.netflix.config.DynamicIntProperty; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.patterns.PolledMeter; import io.netty.util.internal.PlatformDependent; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.time.Duration; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 4/29/16 * Time: 10:23 AM */ @Singleton public final class DirectMemoryMonitor { private static final Logger LOG = LoggerFactory.getLogger(DirectMemoryMonitor.class); private static final String PROP_PREFIX = "zuul.directmemory"; private static final DynamicIntProperty TASK_DELAY_PROP = new DynamicIntProperty(PROP_PREFIX + ".task.delay", 10); private final ScheduledExecutorService service; @Inject public DirectMemoryMonitor(Registry registry) { service = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("dmm-%d") .build()); PolledMeter.using(registry) .withName(PROP_PREFIX + ".reserved") .withDelay(Duration.ofSeconds(TASK_DELAY_PROP.get())) .scheduleOn(service) .monitorValue(DirectMemoryMonitor.class, DirectMemoryMonitor::getReservedMemory); PolledMeter.using(registry) .withName(PROP_PREFIX + ".max") .withDelay(Duration.ofSeconds(TASK_DELAY_PROP.get())) .scheduleOn(service) .monitorValue(DirectMemoryMonitor.class, DirectMemoryMonitor::getMaxMemory); } public DirectMemoryMonitor() { // no-op constructor this.service = null; } private static double getReservedMemory(Object discard) { try { return PlatformDependent.usedDirectMemory(); } catch (Throwable e) { LOG.warn("Error in DirectMemoryMonitor task.", e); } return -1; } private static double getMaxMemory(Object discard) { try { return PlatformDependent.maxDirectMemory(); } catch (Throwable e) { LOG.warn("Error in DirectMemoryMonitor task.", e); } return -1; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/EventLoopConfig.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; public interface EventLoopConfig { int eventLoopCount(); int acceptorCount(); /** * specifies the backlog (accept queue) size to use */ default int getBacklogSize() { return 128; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/Http1MutualSslChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.zuul.netty.ssl.SslContextFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import javax.net.ssl.SSLException; /** * User: michaels@netflix.com * Date: 1/31/17 * Time: 11:43 PM */ public class Http1MutualSslChannelInitializer extends BaseZuulChannelInitializer { private final SslContextFactory sslContextFactory; private final SslContext sslContext; private final boolean isSSlFromIntermediary; /** * Use {@link #Http1MutualSslChannelInitializer(String, ChannelConfig, ChannelConfig, ChannelGroup)} instead. */ @Deprecated public Http1MutualSslChannelInitializer( int port, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this(String.valueOf(port), channelConfig, channelDependencies, channels); } public Http1MutualSslChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { super(metricId, channelConfig, channelDependencies, channels); this.isSSlFromIntermediary = channelConfig.get(CommonChannelConfigKeys.isSSlFromIntermediary); this.sslContextFactory = channelConfig.get(CommonChannelConfigKeys.sslContextFactory); try { sslContext = sslContextFactory.createBuilderForServer().build(); } catch (SSLException e) { throw new RuntimeException("Error configuring SslContext!", e); } // Enable TLS Session Tickets support. sslContextFactory.enableSessionTickets(sslContext); // Setup metrics tracking the OpenSSL stats. sslContextFactory.configureOpenSslStatsMetrics(sslContext, metricId); } @Override protected void initChannel(Channel ch) throws Exception { SslHandler sslHandler = sslContext.newHandler(ch.alloc()); sslHandler.engine().setEnabledProtocols(sslContextFactory.getProtocols()); // Configure our pipeline of ChannelHandlerS. ChannelPipeline pipeline = ch.pipeline(); storeChannel(ch); addTimeoutHandlers(pipeline); addPassportHandler(pipeline); addTcpRelatedHandlers(pipeline); pipeline.addLast("ssl", sslHandler); addSslInfoHandlers(pipeline, isSSlFromIntermediary); addSslClientCertChecks(pipeline); addHttp1Handlers(pipeline); addHttpRelatedHandlers(pipeline); addZuulHandlers(pipeline); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ListenerSpec.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; /* * @author Argha C * @since 10/2/24 */ import java.net.SocketAddress; import java.util.Objects; /** * A specification of an address to listen on. */ public record ListenerSpec(String addressName, boolean defaultAddressEnabled, SocketAddress defaultAddressValue) { public ListenerSpec { Objects.requireNonNull(addressName, "addressName"); Objects.requireNonNull(defaultAddressValue, "defaultAddressValue"); } /** * The fast property name that indicates if this address is enabled. This is used when overriding * {@link #defaultAddressEnabled}. */ public String addressEnabledPropertyName() { return "zuul.server." + addressName + ".enabled"; } /** * The fast property to override the default port for the address name */ @Deprecated public String portPropertyName() { return "zuul.server.port." + addressName; } /** * The fast property to override the default address name */ public String addressPropertyName() { return "zuul.server.addr." + addressName; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/MethodBinding.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import java.util.concurrent.Callable; import java.util.function.BiConsumer; /** * Utility used for binding context variables or thread variables, depending on requirements. * * Author: Arthur Gonigberg * Date: November 29, 2017 */ public class MethodBinding { private final BiConsumer boundMethod; private final Callable bindingContextExtractor; public static MethodBinding NO_OP_BINDING = new MethodBinding<>((r, t) -> {}, () -> null); public MethodBinding(BiConsumer boundMethod, Callable bindingContextExtractor) { this.boundMethod = boundMethod; this.bindingContextExtractor = bindingContextExtractor; } public void bind(Runnable method) throws Exception { T bindingContext = bindingContextExtractor.call(); if (bindingContext == null) { method.run(); } else { boundMethod.accept(method, bindingContext); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/NamedSocketAddress.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import java.net.SocketAddress; import java.util.Objects; import javax.annotation.CheckReturnValue; public final class NamedSocketAddress extends SocketAddress { private final String name; private final SocketAddress delegate; public NamedSocketAddress(String name, SocketAddress delegate) { this.name = Objects.requireNonNull(name); this.delegate = Objects.requireNonNull(delegate); } public String name() { return name; } public SocketAddress unwrap() { return delegate; } @CheckReturnValue public NamedSocketAddress withNewSocket(SocketAddress delegate) { return new NamedSocketAddress(this.name, delegate); } @Override public String toString() { return "NamedSocketAddress{" + "name='" + name + '\'' + ", delegate=" + delegate + '}'; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } NamedSocketAddress that = (NamedSocketAddress) o; return Objects.equals(name, that.name) && Objects.equals(delegate, that.delegate); } @Override public int hashCode() { return Objects.hash(name, delegate); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/OriginResponseReceiver.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.exception.OutboundException; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.filters.endpoint.ProxyEndpoint; import com.netflix.zuul.message.Header; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.netty.connectionpool.OriginConnectException; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; import io.perfmark.PerfMark; import io.perfmark.TaskCloseable; import java.io.IOException; import java.util.Locale; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by saroskar on 1/18/17. */ public class OriginResponseReceiver extends ChannelDuplexHandler { protected volatile ProxyEndpoint edgeProxy; private static final Logger logger = LoggerFactory.getLogger(OriginResponseReceiver.class); private static final AttributeKey SSL_HANDSHAKE_UNSUCCESS_FROM_ORIGIN_THROWABLE = AttributeKey.newInstance("_ssl_handshake_from_origin_throwable"); private static final AttributeKey SSL_CLOSE_NOTIFY_SEEN = AttributeKey.newInstance("_ssl_close_notify_seen"); public static final String CHANNEL_HANDLER_NAME = "_origin_response_receiver"; public OriginResponseReceiver(ProxyEndpoint edgeProxy) { this.edgeProxy = edgeProxy; } public void unlinkFromClientRequest() { edgeProxy = null; } @Override public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try (TaskCloseable a = PerfMark.traceTask("ORR.channelRead")) { channelReadInternal(ctx, msg, true); } } protected void channelReadInternal(ChannelHandlerContext ctx, Object msg, boolean triggerRead) throws Exception { if (msg instanceof HttpResponse) { if (edgeProxy != null) { edgeProxy.responseFromOrigin((HttpResponse) msg); } else if (ReferenceCountUtil.refCnt(msg) > 0) { // this handles the case of a DefaultFullHttpResponse that could have content that needs to be released ReferenceCountUtil.safeRelease(msg); } if (triggerRead) { ctx.channel().read(); } } else if (msg instanceof HttpContent chunk) { if (edgeProxy != null) { edgeProxy.invokeNext(chunk); } else { ReferenceCountUtil.safeRelease(chunk); } if (triggerRead) { ctx.channel().read(); } } else { // should never happen ReferenceCountUtil.release(msg); Exception error = new IllegalStateException("Received invalid message from origin"); if (edgeProxy != null) { edgeProxy.errorFromOrigin(error); } ctx.fireExceptionCaught(error); } } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof CompleteEvent completeEvent) { CompleteReason reason = completeEvent.getReason(); if ((reason != CompleteReason.SESSION_COMPLETE) && (edgeProxy != null)) { if (reason == CompleteReason.CLOSE && Objects.equals( ctx.channel().attr(SSL_CLOSE_NOTIFY_SEEN).get(), Boolean.TRUE)) { logger.warn( "Origin request completed with close, after getting a SslCloseCompletionEvent event: {}", ChannelUtils.channelInfoForLogging(ctx.channel())); edgeProxy.errorFromOrigin(new OriginConnectException( "Origin connection close_notify", OutboundErrorType.CLOSE_NOTIFY_CONNECTION)); } else { logger.error( "Origin request completed with reason other than COMPLETE: {}, {}", reason.name(), ChannelUtils.channelInfoForLogging(ctx.channel())); ZuulException ze = new ZuulException("CompleteEvent", reason.name(), true); edgeProxy.errorFromOrigin(ze); } } // First let this event propagate along the pipeline, before cleaning vars from the channel. // See channelWrite() where these vars are first set onto the channel. try { super.userEventTriggered(ctx, evt); } finally { postCompleteHook(ctx, evt); } } else if (evt instanceof SslHandshakeCompletionEvent && !((SslHandshakeCompletionEvent) evt).isSuccess()) { Throwable cause = ((SslHandshakeCompletionEvent) evt).cause(); ctx.channel().attr(SSL_HANDSHAKE_UNSUCCESS_FROM_ORIGIN_THROWABLE).set(cause); } else if (evt instanceof IdleStateEvent) { if (edgeProxy != null) { logger.error( "Origin request received IDLE event: {}", ChannelUtils.channelInfoForLogging(ctx.channel())); edgeProxy.errorFromOrigin( new OutboundException(OutboundErrorType.READ_TIMEOUT, edgeProxy.getRequestAttempts())); } super.userEventTriggered(ctx, evt); } else if (evt instanceof SslCloseCompletionEvent) { logger.debug("Received SslCloseCompletionEvent on {}", ChannelUtils.channelInfoForLogging(ctx.channel())); ctx.channel().attr(SSL_CLOSE_NOTIFY_SEEN).set(true); super.userEventTriggered(ctx, evt); } else { super.userEventTriggered(ctx, evt); } } /** * Override to add custom post complete functionality * * @param ctx - channel handler context * @param evt - netty event * @throws Exception */ protected void postCompleteHook(ChannelHandlerContext ctx, Object evt) throws Exception {} private HttpRequest buildOriginHttpRequest(HttpRequestMessage zuulRequest) { String method = zuulRequest.getMethod().toUpperCase(Locale.ROOT); String uri = pathAndQueryString(zuulRequest); customRequestProcessing(zuulRequest); DefaultHttpRequest nettyReq = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri, false); // Copy headers across. for (Header h : zuulRequest.getHeaders().entries()) { nettyReq.headers().add(h.getKey(), h.getValue()); } return nettyReq; } /** * Override to add custom modifications to the request before it goes out * * @param headers */ protected void customRequestProcessing(HttpRequestMessage headers) {} private static String pathAndQueryString(HttpRequestMessage request) { // parsing the params cleans up any empty/null params using the logic of the HttpQueryParams class HttpQueryParams cleanParams = HttpQueryParams.parse(request.getQueryParams().toEncodedString()); String cleanQueryStr = cleanParams.toEncodedString(); if (cleanQueryStr == null || cleanQueryStr.isEmpty()) { return request.getPath(); } else { return request.getPath() + "?" + cleanParams.toEncodedString(); } } @Override public final void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { try (TaskCloseable ignore = PerfMark.traceTask("ORR.writeInternal")) { writeInternal(ctx, msg, promise); } } private void writeInternal(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (!ctx.channel().isActive()) { ReferenceCountUtil.release(msg); return; } if (msg instanceof HttpRequestMessage) { promise.addListener((future) -> { if (!future.isSuccess()) { Throwable cause = ctx.channel() .attr(SSL_HANDSHAKE_UNSUCCESS_FROM_ORIGIN_THROWABLE) .get(); if (cause != null) { // Set the specific SSL handshake error if the handlers have already caught them ctx.channel() .attr(SSL_HANDSHAKE_UNSUCCESS_FROM_ORIGIN_THROWABLE) .set(null); fireWriteError("request headers", cause, ctx); logger.debug( "SSLException is overridden by SSLHandshakeException caught in handler level. Original" + " SSL exception message: ", future.cause()); } else { fireWriteError("request headers", future.cause(), ctx); } } }); HttpRequestMessage zuulReq = (HttpRequestMessage) msg; preWriteHook(ctx, zuulReq); super.write(ctx, buildOriginHttpRequest(zuulReq), promise); } else if (msg instanceof HttpContent) { promise.addListener((future) -> { if (!future.isSuccess()) { fireWriteError("request content chunk", future.cause(), ctx); } }); super.write(ctx, msg, promise); } else { // should never happen ReferenceCountUtil.release(msg); throw new ZuulException("Received invalid message from client", true); } } /** * Override to add custom pre-write functionality * * @param ctx channel handler context * @param zuulReq request message to modify */ protected void preWriteHook(ChannelHandlerContext ctx, HttpRequestMessage zuulReq) {} private void fireWriteError(String requestPart, Throwable cause, ChannelHandlerContext ctx) { String errMesg = "Error while proxying " + requestPart + " to origin "; if (edgeProxy != null) { ProxyEndpoint ep = edgeProxy; edgeProxy = null; errMesg += ep.getOrigin().getName(); ep.errorFromOrigin(cause); } ctx.fireExceptionCaught(new ZuulException(cause, errMesg, true)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (edgeProxy != null) { if (cause instanceof ReadTimeoutException) { edgeProxy.getPassport().add(PassportState.ORIGIN_CH_READ_TIMEOUT); logger.debug( "read timeout on origin channel {} ", ChannelUtils.channelInfoForLogging(ctx.channel()), cause); } else if (cause instanceof IOException) { edgeProxy.getPassport().add(PassportState.ORIGIN_CH_IO_EX); logger.debug( "I/O error on origin channel {} ", ChannelUtils.channelInfoForLogging(ctx.channel()), cause); } else { logger.error("Error from Origin connection:", cause); } edgeProxy.errorFromOrigin(cause); } ctx.fireExceptionCaught(cause); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (edgeProxy != null) { logger.debug("Origin channel inactive. channel-info={}", ChannelUtils.channelInfoForLogging(ctx.channel())); OriginConnectException ex = new OriginConnectException("Origin server inactive", OutboundErrorType.RESET_CONNECTION); edgeProxy.errorFromOrigin(ex); } super.channelInactive(ctx); ctx.close(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/Server.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.netflix.appinfo.InstanceInfo; import com.netflix.config.DynamicBooleanProperty; import com.netflix.netty.common.CategorizedThreadFactory; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.api.patterns.PolledMeter; import com.netflix.zuul.Attrs; import com.netflix.zuul.monitoring.ConnCounter; import com.netflix.zuul.monitoring.ConnTimer; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocatorMetric; import io.netty.buffer.ByteBufAllocatorMetricProvider; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.IoHandlerFactory; import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.ServerChannel; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollChannelOption; import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueueIoHandler; import io.netty.channel.kqueue.KQueueServerSocketChannel; import io.netty.channel.kqueue.KQueueSocketChannel; import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.uring.IoUring; import io.netty.channel.uring.IoUringIoHandler; import io.netty.channel.uring.IoUringServerSocketChannel; import io.netty.channel.uring.IoUringSocketChannel; import io.netty.util.AttributeKey; import io.netty.util.concurrent.DefaultEventExecutorChooserFactory; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.ThreadPerTaskExecutor; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * NOTE: Shout-out to LittleProxy which was great as a reference. * * User: michaels * Date: 11/8/14 * Time: 8:39 PM */ public class Server { /** * This field is effectively a noop, as Epoll is enabled automatically if available. This can be disabled by * using the {@link #FORCE_NIO} property. */ @Deprecated public static final DynamicBooleanProperty USE_EPOLL = new DynamicBooleanProperty("zuul.server.netty.socket.epoll", false); /** * If {@code true}, The Zuul server will avoid autodetecting the transport type and use the default Java NIO * transport. */ private static final DynamicBooleanProperty FORCE_NIO = new DynamicBooleanProperty("zuul.server.netty.socket.force_nio", false); private static final DynamicBooleanProperty FORCE_IO_URING = new DynamicBooleanProperty("zuul.server.netty.socket.force_io_uring", false); private static final Logger LOG = LoggerFactory.getLogger(Server.class); private static final DynamicBooleanProperty MANUAL_DISCOVERY_STATUS = new DynamicBooleanProperty("zuul.server.netty.manual.discovery.status", true); private final Thread jvmShutdownHook; private final Registry registry; private ServerGroup serverGroup; private final ClientConnectionsShutdown clientConnectionsShutdown; private final ServerStatusManager serverStatusManager; private final Map> addressesToInitializers; /** * Unlike the above, the socket addresses in this map are the *bound* addresses, rather than the requested ones. */ private final Map addressesToChannels = new LinkedHashMap<>(); private final EventLoopConfig eventLoopConfig; private final Map acceptCountersByPort = new ConcurrentHashMap<>(); /** * This is a hack to expose the channel type to the origin channel. It is NOT API stable and should not be * referenced by non-Zuul code. */ @Deprecated public static final AtomicReference> defaultOutboundChannelType = new AtomicReference<>(); /** * Use {@link #Server(Registry, ServerStatusManager, Map, ClientConnectionsShutdown, EventLoopGroupMetrics, * EventLoopConfig)} * instead. */ @SuppressWarnings("rawtypes") @Deprecated public Server( Map portsToChannelInitializers, ServerStatusManager serverStatusManager, ClientConnectionsShutdown clientConnectionsShutdown, EventLoopGroupMetrics eventLoopGroupMetrics) { this( portsToChannelInitializers, serverStatusManager, clientConnectionsShutdown, eventLoopGroupMetrics, new DefaultEventLoopConfig()); } /** * Use {@link #Server(Registry, ServerStatusManager, Map, ClientConnectionsShutdown, EventLoopGroupMetrics, * EventLoopConfig)} * instead. */ @SuppressWarnings({"unchecked", "rawtypes" }) // Channel init map has the wrong generics and we can't fix without api breakage. @Deprecated public Server( Map portsToChannelInitializers, ServerStatusManager serverStatusManager, ClientConnectionsShutdown clientConnectionsShutdown, EventLoopGroupMetrics eventLoopGroupMetrics, EventLoopConfig eventLoopConfig) { this( Spectator.globalRegistry(), serverStatusManager, convertPortMap((Map>) (Map) portsToChannelInitializers), clientConnectionsShutdown, eventLoopGroupMetrics, eventLoopConfig); } public Server( Registry registry, ServerStatusManager serverStatusManager, Map> addressesToInitializers, ClientConnectionsShutdown clientConnectionsShutdown, EventLoopGroupMetrics eventLoopGroupMetrics, EventLoopConfig eventLoopConfig) { this( registry, serverStatusManager, addressesToInitializers, clientConnectionsShutdown, eventLoopGroupMetrics, eventLoopConfig, null); } public Server( Registry registry, ServerStatusManager serverStatusManager, Map> addressesToInitializers, ClientConnectionsShutdown clientConnectionsShutdown, EventLoopGroupMetrics eventLoopGroupMetrics, EventLoopConfig eventLoopConfig, Thread jvmShutdownHook) { this.registry = Objects.requireNonNull(registry); this.addressesToInitializers = Collections.unmodifiableMap(new LinkedHashMap<>(addressesToInitializers)); this.serverStatusManager = Preconditions.checkNotNull(serverStatusManager, "serverStatusManager"); this.clientConnectionsShutdown = Preconditions.checkNotNull(clientConnectionsShutdown, "clientConnectionsShutdown"); this.eventLoopConfig = Preconditions.checkNotNull(eventLoopConfig, "eventLoopConfig"); this.jvmShutdownHook = jvmShutdownHook != null ? jvmShutdownHook : new Thread(this::stop, "Zuul-JVM-shutdown-hook"); } public void stop() { LOG.info("Shutting down Zuul."); serverGroup.stop(); LOG.info("Completed zuul shutdown."); } public void start() { if (jvmShutdownHook != null) { Runtime.getRuntime().addShutdownHook(jvmShutdownHook); } serverGroup = new ServerGroup("Salamander", eventLoopConfig.acceptorCount(), eventLoopConfig.eventLoopCount()); serverGroup.initializeTransport(); List allBindFutures = new ArrayList<>(addressesToInitializers.size()); // Setup each of the channel initializers on requested ports. for (Map.Entry> entry : addressesToInitializers.entrySet()) { NamedSocketAddress requestedNamedAddr = entry.getKey(); ChannelFuture nettyServerFuture = setupServerBootstrap(requestedNamedAddr, entry.getValue()); Channel chan = nettyServerFuture.channel(); addressesToChannels.put(requestedNamedAddr.withNewSocket(chan.localAddress()), chan); allBindFutures.add(nettyServerFuture); } // All channels should share a single ByteBufAllocator instance. // Add metrics to monitor that allocator's memory usage. if (!allBindFutures.isEmpty()) { ByteBufAllocator alloc = allBindFutures.get(0).channel().alloc(); if (alloc instanceof ByteBufAllocatorMetricProvider) { ByteBufAllocatorMetric metrics = ((ByteBufAllocatorMetricProvider) alloc).metric(); PolledMeter.using(registry) .withId(registry.createId("zuul.nettybuffermem.live", "type", "heap")) .monitorValue(metrics, ByteBufAllocatorMetric::usedHeapMemory); PolledMeter.using(registry) .withId(registry.createId("zuul.nettybuffermem.live", "type", "direct")) .monitorValue(metrics, ByteBufAllocatorMetric::usedDirectMemory); } } } public final void awaitTermination() throws InterruptedException { for (Channel chan : addressesToChannels.values()) { chan.closeFuture().sync(); } } public final List getListeningAddresses() { if (serverGroup == null) { throw new IllegalStateException("Server has not been started"); } return Collections.unmodifiableList(new ArrayList<>(addressesToChannels.keySet())); } @VisibleForTesting public void waitForEachEventLoop() throws InterruptedException, ExecutionException { for (EventExecutor exec : serverGroup.clientToProxyWorkerPool) { exec.submit(() -> { // Do nothing. }) .get(); } } private ChannelFuture setupServerBootstrap( NamedSocketAddress listenAddress, ChannelInitializer channelInitializer) { ServerBootstrap serverBootstrap = new ServerBootstrap().group(serverGroup.clientToProxyBossPool, serverGroup.clientToProxyWorkerPool); LOG.info("Proxy listening with {}", serverGroup.channelType); serverBootstrap.channel(serverGroup.channelType); serverBootstrap.option(ChannelOption.SO_BACKLOG, eventLoopConfig.getBacklogSize()); serverBootstrap.childOption(ChannelOption.SO_LINGER, -1); serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true); serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); // Apply transport specific socket options. for (Map.Entry, ?> optionEntry : serverGroup.transportChannelOptions.entrySet()) { applyServerOption(serverBootstrap, optionEntry.getKey(), optionEntry.getValue()); } serverBootstrap.handler(new NewConnHandler()); serverBootstrap.childHandler(channelInitializer); serverBootstrap.validate(); LOG.info("Binding to : {}", listenAddress); if (MANUAL_DISCOVERY_STATUS.get()) { // Flag status as UP just before binding to the port. serverStatusManager.localStatus(InstanceInfo.InstanceStatus.UP); } // Bind and start to accept incoming connections. ChannelFuture bindFuture = serverBootstrap.bind(listenAddress.unwrap()); try { return bindFuture.sync(); } catch (Exception e) { // sync() sneakily throws a checked Exception, but doesn't declare it. This can happen if there is a bind // failure, which is typically an IOException. Just chain it and rethrow. throw new RuntimeException("Failed to bind on addr " + listenAddress, e); } } /** * Override for metrics or informational purposes * * @param clientToProxyBossPool - acceptor pool * @param clientToProxyWorkerPool - worker pool */ public void postEventLoopCreationHook( EventLoopGroup clientToProxyBossPool, EventLoopGroup clientToProxyWorkerPool) {} private final class ServerGroup { /** A name for this ServerGroup to use in naming threads. */ private final String name; private final int acceptorThreads; private final int workerThreads; private EventLoopGroup clientToProxyBossPool; private EventLoopGroup clientToProxyWorkerPool; private Class channelType; private Map, ?> transportChannelOptions; private volatile boolean stopped = false; private ServerGroup(String name, int acceptorThreads, int workerThreads) { this.name = name; this.acceptorThreads = acceptorThreads; this.workerThreads = workerThreads; Thread.setDefaultUncaughtExceptionHandler((t, e) -> LOG.error("Uncaught throwable", e)); } private void initializeTransport() { Map, Object> extraOptions = new HashMap<>(); boolean useNio = FORCE_NIO.get(); boolean useIoUring = FORCE_IO_URING.get(); final IoHandlerFactory handlerFactory; if (useIoUring && ioUringIsAvailable()) { channelType = IoUringServerSocketChannel.class; defaultOutboundChannelType.set(IoUringSocketChannel.class); handlerFactory = IoUringIoHandler.newFactory(); } else if (!useNio && epollIsAvailable()) { channelType = EpollServerSocketChannel.class; defaultOutboundChannelType.set(EpollSocketChannel.class); handlerFactory = EpollIoHandler.newFactory(); extraOptions.put(EpollChannelOption.TCP_DEFER_ACCEPT, -1); } else if (!useNio && kqueueIsAvailable()) { channelType = KQueueServerSocketChannel.class; defaultOutboundChannelType.set(KQueueSocketChannel.class); handlerFactory = KQueueIoHandler.newFactory(); } else { channelType = NioServerSocketChannel.class; defaultOutboundChannelType.set(NioSocketChannel.class); handlerFactory = NioIoHandler.newFactory(); } clientToProxyBossPool = new MultiThreadIoEventLoopGroup( acceptorThreads, new CategorizedThreadFactory(name + "-ClientToZuulAcceptor"), handlerFactory); ThreadFactory workerThreadFactory = new CategorizedThreadFactory(name + "-ClientToZuulWorker"); Executor workerExecutor = new ThreadPerTaskExecutor(workerThreadFactory); clientToProxyWorkerPool = new MultiThreadIoEventLoopGroup( workerThreads, workerExecutor, DefaultEventExecutorChooserFactory.INSTANCE, handlerFactory); transportChannelOptions = Collections.unmodifiableMap(extraOptions); postEventLoopCreationHook(clientToProxyBossPool, clientToProxyWorkerPool); } private synchronized void stop() { LOG.info("Shutting down"); if (stopped) { LOG.info("Already stopped"); return; } if (MANUAL_DISCOVERY_STATUS.get()) { // Flag status as down. // that we can flag to return DOWN here (would that then update Discovery? or still be a delay?) serverStatusManager.localStatus(InstanceInfo.InstanceStatus.DOWN); } // Shutdown each of the client connections (blocks until complete). // NOTE: ClientConnectionsShutdown can also be configured to gracefully close connections when the // discovery status changes to DOWN. So if it has been configured that way, then this will be an additional // call to gracefullyShutdownClientChannels(), which will be a noop. clientConnectionsShutdown.gracefullyShutdownClientChannels().syncUninterruptibly(); LOG.info("Shutting down event loops"); List allEventLoopGroups = new ArrayList<>(); allEventLoopGroups.add(clientToProxyBossPool); allEventLoopGroups.add(clientToProxyWorkerPool); for (EventLoopGroup group : allEventLoopGroups) { group.shutdownGracefully(); } for (EventLoopGroup group : allEventLoopGroups) { try { group.awaitTermination(20, TimeUnit.SECONDS); } catch (InterruptedException ie) { LOG.warn("Interrupted while shutting down event loop"); } } stopped = true; LOG.info("Done shutting down"); } } /* * Keys should be a short string usable in metrics. */ public static final AttributeKey CONN_DIMENSIONS = AttributeKey.newInstance("zuulconndimensions"); private final class NewConnHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Long now = System.nanoTime(); Channel child = (Channel) msg; int localPort = child.localAddress() instanceof InetSocketAddress localAddr ? localAddr.getPort() : -1; acceptCountersByPort .computeIfAbsent( localPort, p -> registry.counter( registry.createId("zuul.conn.acceptor.accepts", "port", String.valueOf(p)))) .increment(); child.attr(CONN_DIMENSIONS).set(Attrs.newInstance()); ConnTimer timer = ConnTimer.install(child, registry, registry.createId("zuul.conn.client.timing")); timer.record(now, "ACCEPT"); ConnCounter.install(child, registry, registry.createId("zuul.conn.client.current")); super.channelRead(ctx, msg); } } static Map> convertPortMap( Map> portsToChannelInitializers) { Map> addrsToInitializers = new LinkedHashMap<>(portsToChannelInitializers.size()); for (Map.Entry> portToInitializer : portsToChannelInitializers.entrySet()) { int portNumber = portToInitializer.getKey(); addrsToInitializers.put( new NamedSocketAddress("port" + portNumber, new InetSocketAddress(portNumber)), portToInitializer.getValue()); } return Collections.unmodifiableMap(addrsToInitializers); } @SuppressWarnings("unchecked") private static void applyServerOption(ServerBootstrap bootstrap, ChannelOption key, Object value) { bootstrap.option(key, (T) value); } private static boolean epollIsAvailable() { boolean available; try { available = Epoll.isAvailable(); } catch (NoClassDefFoundError e) { LOG.debug("Epoll is unavailable, skipping", e); return false; } catch (RuntimeException | Error e) { LOG.warn("Epoll is unavailable, skipping", e); return false; } if (!available) { LOG.debug("Epoll is unavailable, skipping", Epoll.unavailabilityCause()); } return available; } private static boolean ioUringIsAvailable() { boolean available; try { available = IoUring.isAvailable(); } catch (NoClassDefFoundError e) { LOG.debug("io_uring is unavailable, skipping", e); return false; } catch (RuntimeException | Error e) { LOG.warn("io_uring is unavailable, skipping", e); return false; } if (!available) { LOG.debug("io_uring is unavailable, skipping", IoUring.unavailabilityCause()); } return available; } private static boolean kqueueIsAvailable() { boolean available; try { available = KQueue.isAvailable(); } catch (NoClassDefFoundError e) { LOG.debug("KQueue is unavailable, skipping", e); return false; } catch (RuntimeException | Error e) { LOG.warn("KQueue is unavailable, skipping", e); return false; } if (!available) { LOG.debug("KQueue is unavailable, skipping", KQueue.unavailabilityCause()); } return available; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ServerTimeout.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; public class ServerTimeout { private final int connectionIdleTimeout; public ServerTimeout(int connectionIdleTimeout) { this.connectionIdleTimeout = connectionIdleTimeout; } public int connectionIdleTimeout() { return connectionIdleTimeout; } public int defaultRequestExpiryTimeout() { // Note this is the timeout for the inbound request to zuul, not for each outbound attempt. // It needs to align with the inbound connection idle timeout and/or the ELB idle timeout. So we // set it here to 1 sec less than that. return connectionIdleTimeout > 1000 ? connectionIdleTimeout - 1000 : 1000; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/SocketAddressProperty.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.google.common.annotations.VisibleForTesting; import com.netflix.config.StringDerivedProperty; import io.netty.channel.unix.DomainSocketAddress; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Locale; import java.util.concurrent.Callable; import java.util.function.Supplier; import javax.annotation.Nullable; /** * This class expresses an address that Zuul can bind to. Similar to {@link * com.netflix.config.DynamicStringMapProperty} this class uses a similar key=value syntax, but only supports a single * pair. * *

To use this class, set a bind type such as {@link BindType#ANY} and assign it a port number like {@code 7001}. * Sample usage: *

    *
  • {@code =7001} - equivalent to {@code ANY=7001}
  • *
  • {@code ANY=7001} Binds on all IP addresses and IP stack for port 7001
  • *
  • {@code IPV4_ANY=7001} Binds on all IPv4 address 0.0.0.0 for port 7001
  • *
  • {@code IPV6_ANY=7001} Binds on all IPv6 address :: for port 7001
  • *
  • {@code ANY_LOCAL=7001} Binds on localhost for all IP stacks for port 7001
  • *
  • {@code IPV4_LOCAL=7001} Binds on IPv4 localhost (127.0.0.1) for port 7001
  • *
  • {@code IPV6_LOCAL=7001} Binds on IPv6 localhost (::1) for port 7001
  • *
  • {@code UDS=/var/run/zuul.sock} Binds a domain socket at /var/run/zuul.sock
  • *
* *

Note that the local IPv4 binds only work for {@code 127.0.0.1}, and not any other loopback addresses. Currently, * all IP stack specific bind types only "prefer" a stack; it is up to the OS and the JVM to pick the the final * address. */ public final class SocketAddressProperty extends StringDerivedProperty { public enum BindType { /** * Supports any IP stack, for a given port. This is the default behaviour. This also indicates that the * caller doesn't prefer a given IP stack. */ ANY, /** * Binds on IPv4 {@code 0.0.0.0} address. */ IPV4_ANY(() -> InetAddress.getByAddress("0.0.0.0", new byte[] {0, 0, 0, 0})), /** * Binds on IPv6 {@code ::} address. */ IPV6_ANY(() -> InetAddress.getByAddress("::", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), /** * Binds on any local address. This indicates that the caller doesn't prefer a given IP stack. */ ANY_LOCAL(InetAddress::getLoopbackAddress), /** * Binds on the IPv4 {@code 127.0.0.1} localhost address. */ IPV4_LOCAL(() -> InetAddress.getByAddress("localhost", new byte[] {127, 0, 0, 1})), /** * Binds on the IPv6 {@code ::1} localhost address. */ IPV6_LOCAL(() -> InetAddress.getByAddress("localhost", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})), /** * Binds on the Unix Domain Socket path. */ UDS, ; @SuppressWarnings("ImmutableEnumChecker") // Hopes and prayers that addressSupplier returns a constant. @Nullable private final Supplier addressSupplier; BindType() { addressSupplier = null; } BindType(Callable addressFn) { this.addressSupplier = () -> { try { return addressFn.call(); } catch (Exception e) { throw new RuntimeException(e); } }; } } @VisibleForTesting static final class Decoder implements com.google.common.base.Function { static final Decoder INSTANCE = new Decoder(); @Override public SocketAddress apply(String input) { if (input == null || input.isEmpty()) { throw new IllegalArgumentException("Invalid address"); } int equalsPosition = input.indexOf('='); if (equalsPosition == -1) { throw new IllegalArgumentException("Invalid address " + input); } String rawBindType = equalsPosition != 0 ? input.substring(0, equalsPosition) : BindType.ANY.name(); BindType bindType = BindType.valueOf(rawBindType.toUpperCase(Locale.ROOT)); String rawAddress = input.substring(equalsPosition + 1); int port; switch (bindType) { case ANY: // fallthrough case IPV4_ANY: // fallthrough case IPV6_ANY: // fallthrough case ANY_LOCAL: // fallthrough case IPV4_LOCAL: // fallthrough case IPV6_LOCAL: // fallthrough try { port = Integer.parseInt(rawAddress); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid Port " + input, e); } break; case UDS: port = -1; break; default: throw new AssertionError("Missed cased: " + bindType); } switch (bindType) { case ANY: return new InetSocketAddress(port); case IPV4_ANY: // fallthrough case IPV6_ANY: // fallthrough case ANY_LOCAL: // fallthrough case IPV4_LOCAL: // fallthrough case IPV6_LOCAL: // fallthrough return new InetSocketAddress(bindType.addressSupplier.get(), port); case UDS: return new DomainSocketAddress(rawAddress); } throw new AssertionError("Missed cased: " + bindType); } } public SocketAddressProperty(String propName, SocketAddress defaultValue) { super(propName, defaultValue, Decoder.INSTANCE); } public SocketAddressProperty(String propName, String defaultValue) { this(propName, Decoder.INSTANCE.apply(defaultValue)); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ZuulDependencyKeys.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.discovery.EurekaClient; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.channel.config.ChannelConfigKey; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.RequestCompleteHandler; import com.netflix.zuul.context.SessionContextDecorator; import com.netflix.zuul.netty.filter.FilterConstraints; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import io.netty.channel.ChannelHandler; import jakarta.inject.Provider; /** * User: michaels@netflix.com * Date: 2/9/17 * Time: 9:35 AM */ public class ZuulDependencyKeys { public static final ChannelConfigKey accessLogPublisher = new ChannelConfigKey<>("accessLogPublisher"); public static final ChannelConfigKey eventLoopGroupMetrics = new ChannelConfigKey<>("eventLoopGroupMetrics"); public static final ChannelConfigKey registry = new ChannelConfigKey<>("registry"); public static final ChannelConfigKey sessionCtxDecorator = new ChannelConfigKey<>("sessionCtxDecorator"); public static final ChannelConfigKey requestCompleteHandler = new ChannelConfigKey<>("requestCompleteHandler"); public static final ChannelConfigKey httpRequestHeadersReadTimeoutCounter = new ChannelConfigKey<>("httpRequestHeadersReadTimeoutCounter"); public static final ChannelConfigKey httpRequestHeadersReadTimer = new ChannelConfigKey<>("httpRequestHeadersReadTimer"); public static final ChannelConfigKey httpRequestReadTimeoutCounter = new ChannelConfigKey<>("httpRequestReadTimeoutCounter"); public static final ChannelConfigKey filterLoader = new ChannelConfigKey<>("filterLoader"); public static final ChannelConfigKey filterUsageNotifier = new ChannelConfigKey<>("filterUsageNotifier"); public static final ChannelConfigKey discoveryClient = new ChannelConfigKey<>("discoveryClient"); public static final ChannelConfigKey applicationInfoManager = new ChannelConfigKey<>("applicationInfoManager"); public static final ChannelConfigKey serverStatusManager = new ChannelConfigKey<>("serverStatusManager"); public static final ChannelConfigKey SSL_CLIENT_CERT_CHECK_REQUIRED = new ChannelConfigKey<>("requiresSslClientCertCheck", false); public static final ChannelConfigKey> rateLimitingChannelHandlerProvider = new ChannelConfigKey<>("rateLimitingChannelHandlerProvider"); public static final ChannelConfigKey> sslClientCertCheckChannelHandlerProvider = new ChannelConfigKey<>("sslClientCertCheckChannelHandlerProvider"); public static final ChannelConfigKey pushConnectionRegistry = new ChannelConfigKey<>("pushConnectionRegistry"); public static final ChannelConfigKey filterConstraints = new ChannelConfigKey<>("filterConstraints"); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ZuulServerChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import com.netflix.netty.common.channel.config.ChannelConfig; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; /** * User: Mike Smith * Date: 3/5/16 * Time: 6:44 PM */ public class ZuulServerChannelInitializer extends BaseZuulChannelInitializer { public ZuulServerChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { super(metricId, channelConfig, channelDependencies, channels); } /** * Use {@link #ZuulServerChannelInitializer(String, ChannelConfig, ChannelConfig, ChannelGroup)} instead. */ @Deprecated public ZuulServerChannelInitializer( int port, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this(String.valueOf(port), channelConfig, channelDependencies, channels); } @Override protected void initChannel(Channel ch) throws Exception { // Configure our pipeline of ChannelHandlerS. ChannelPipeline pipeline = ch.pipeline(); storeChannel(ch); addTimeoutHandlers(pipeline); addPassportHandler(pipeline); addTcpRelatedHandlers(pipeline); addHttp1Handlers(pipeline); addHttpRelatedHandlers(pipeline); addZuulHandlers(pipeline); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/DummyChannelHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; /** * Dummy Channel Handler * * Author: Arthur Gonigberg * Date: December 15, 2017 */ public class DummyChannelHandler implements ChannelHandler { @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception {} @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {} @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {} } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2Configuration.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import com.netflix.zuul.netty.ssl.SslContextFactory; 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 javax.net.ssl.SSLException; public class Http2Configuration { public static SslContext configureSSL(SslContextFactory sslContextFactory, String metricId) { SslContextBuilder builder = sslContextFactory.createBuilderForServer(); String[] supportedProtocols = new String[] {ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1}; ApplicationProtocolConfig apn = new ApplicationProtocolConfig( ApplicationProtocolConfig.Protocol.ALPN, // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, supportedProtocols); SslContext sslContext; try { sslContext = builder.applicationProtocolConfig(apn).build(); } catch (SSLException e) { throw new RuntimeException("Error configuring SslContext with ALPN!", e); } // Enable TLS Session Tickets support. sslContextFactory.enableSessionTickets(sslContext); // Setup metrics tracking the OpenSSL stats. sslContextFactory.configureOpenSslStatsMetrics(sslContext, metricId); return sslContext; } /** * This is meant to be use in cases where the server wishes not to advertise h2 as part of ALPN. */ public static SslContext configureSSLWithH2Disabled(SslContextFactory sslContextFactory, String host) { ApplicationProtocolConfig apn = new ApplicationProtocolConfig( ApplicationProtocolConfig.Protocol.ALPN, // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_1_1); SslContext sslContext; try { sslContext = sslContextFactory .createBuilderForServer() .applicationProtocolConfig(apn) .build(); } catch (SSLException e) { throw new RuntimeException("Error configuring SslContext with ALPN!", e); } sslContextFactory.enableSessionTickets(sslContext); sslContextFactory.configureOpenSslStatsMetrics(sslContext, host); return sslContext; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2ConnectionErrorHandler.java ================================================ /** * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import com.netflix.zuul.netty.SpectatorUtils; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http2.Http2Exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Logs and tracks connection errors. The actual * sending of the go-away and closing the connection is handled by netty in {@link io.netty.handler.codec.http2.Http2ConnectionHandler} * onConnectionError * * See also, {@link com.netflix.netty.common.channel.config.CommonChannelConfigKeys#http2CatchConnectionErrors} * @author Justin Guerra * @since 11/14/23 */ public class Http2ConnectionErrorHandler extends ChannelInboundHandlerAdapter { private static final Logger LOG = LoggerFactory.getLogger(Http2ConnectionErrorHandler.class); @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof Http2Exception http2Exception) { LOG.debug("Received Http/2 connection error", cause); SpectatorUtils.newCounter( "server.connection.http2.connection.exception", http2Exception.error().name()) .increment(); } else { ctx.fireExceptionCaught(cause); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2ContentLengthEnforcingHandler.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http2.DefaultHttp2ResetFrame; import io.netty.handler.codec.http2.Http2Error; import io.netty.util.ReferenceCountUtil; import java.util.List; /** * Validates that HTTP/2 request content-length headers are consistent with the actual body. * This class is only suitable for use on HTTP/2 child channels. */ public final class Http2ContentLengthEnforcingHandler extends ChannelInboundHandlerAdapter { private static final long UNSET_CONTENT_LENGTH = -1; private long expectedContentLength = UNSET_CONTENT_LENGTH; private long seenContentLength; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest req && !validateRequest(req)) { rejectAndRelease(ctx, msg); return; } if (msg instanceof HttpContent httpContent && !validateContent(httpContent)) { rejectAndRelease(ctx, msg); return; } if (msg instanceof LastHttpContent && !validateEndOfStream()) { rejectAndRelease(ctx, msg); return; } super.channelRead(ctx, msg); } private boolean validateRequest(HttpRequest req) { List lengthHeaders = req.headers().getAll(HttpHeaderNames.CONTENT_LENGTH); if (lengthHeaders.size() > 1) { return false; } if (lengthHeaders.size() == 1) { try { expectedContentLength = Long.parseLong(lengthHeaders.getFirst()); } catch (NumberFormatException e) { return false; } if (expectedContentLength < 0) { return false; } } return isContentLengthUnset() || !HttpUtil.isTransferEncodingChunked(req); } private boolean validateContent(HttpContent httpContent) { incrementSeenContent(httpContent.content().readableBytes()); return isContentLengthUnset() || seenContentLength <= expectedContentLength; } private boolean validateEndOfStream() { return isContentLengthUnset() || seenContentLength == expectedContentLength; } private void rejectAndRelease(ChannelHandlerContext ctx, Object msg) { // TODO(carl-mastrangelo): this is not right, but meh. Fix this to return a proper 400. ctx.writeAndFlush(new DefaultHttp2ResetFrame(Http2Error.PROTOCOL_ERROR)); ReferenceCountUtil.safeRelease(msg); } private boolean isContentLengthUnset() { return expectedContentLength == UNSET_CONTENT_LENGTH; } private void incrementSeenContent(int length) { seenContentLength = Math.addExact(seenContentLength, length); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2OrHttpHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.http2.DynamicHttp2FrameLogger; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import com.netflix.zuul.netty.server.psk.TlsPskHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.UniformStreamByteDistributor; import io.netty.handler.logging.LogLevel; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.util.AttributeKey; import java.util.function.Consumer; /** * Http2 Or Http Handler *

* Author: Arthur Gonigberg * Date: December 15, 2017 */ public class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler { public static final AttributeKey PROTOCOL_NAME = AttributeKey.valueOf("protocol_name"); public static final String PROTOCOL_HTTP_1_1 = "HTTP/1.1"; public static final String PROTOCOL_HTTP_2 = "HTTP/2"; private static final String FALLBACK_APPLICATION_PROTOCOL = ApplicationProtocolNames.HTTP_1_1; private static final DynamicHttp2FrameLogger FRAME_LOGGER = new DynamicHttp2FrameLogger(LogLevel.DEBUG, Http2FrameCodec.class); private final ChannelHandler http2StreamHandler; private final int maxConcurrentStreams; private final int initialWindowSize; private final long maxHeaderTableSize; private final long maxHeaderListSize; private final boolean catchConnectionErrors; private final boolean connectProtocolEnabled; // controls the number of rst frames that will be sent to a client before closing the connection private final int maxEncoderRstFrames; private final int maxEncoderRstFramesWindow; private final Consumer addHttpHandlerFn; public Http2OrHttpHandler( ChannelHandler http2StreamHandler, ChannelConfig channelConfig, Consumer addHttpHandlerFn) { super(FALLBACK_APPLICATION_PROTOCOL); this.http2StreamHandler = http2StreamHandler; this.maxConcurrentStreams = channelConfig.get(CommonChannelConfigKeys.maxConcurrentStreams); this.initialWindowSize = channelConfig.get(CommonChannelConfigKeys.initialWindowSize); this.maxHeaderTableSize = channelConfig.get(CommonChannelConfigKeys.maxHttp2HeaderTableSize); this.maxHeaderListSize = channelConfig.get(CommonChannelConfigKeys.maxHttp2HeaderListSize); this.catchConnectionErrors = channelConfig.get(CommonChannelConfigKeys.http2CatchConnectionErrors); this.maxEncoderRstFrames = channelConfig.get(CommonChannelConfigKeys.http2EncoderMaxResetFrames); this.maxEncoderRstFramesWindow = channelConfig.get(CommonChannelConfigKeys.http2EncoderMaxResetFramesWindow); this.connectProtocolEnabled = channelConfig.get(CommonChannelConfigKeys.http2ConnectProtocolEnabled); this.addHttpHandlerFn = addHttpHandlerFn; } /** * this method is inspired by ApplicationProtocolNegotiationHandler.userEventTriggered */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof SslHandshakeCompletionEvent handshakeEvent) { if (handshakeEvent.isSuccess()) { TlsPskHandler tlsPskHandler = ctx.channel().pipeline().get(TlsPskHandler.class); if (tlsPskHandler != null) { // PSK mode try { String tlsPskApplicationProtocol = tlsPskHandler.getApplicationProtocol(); configurePipeline( ctx, tlsPskApplicationProtocol != null ? tlsPskApplicationProtocol : FALLBACK_APPLICATION_PROTOCOL); } catch (Throwable cause) { exceptionCaught(ctx, cause); } finally { // Handshake failures are handled in exceptionCaught(...). if (handshakeEvent.isSuccess()) { removeSelfIfPresent(ctx); } } } else { // non PSK mode super.userEventTriggered(ctx, evt); } } else { // handshake failures // TODO sunnys - handle PSK handshake failures super.userEventTriggered(ctx, evt); } } else { super.userEventTriggered(ctx, evt); } } @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { if (protocol.equals(ApplicationProtocolNames.HTTP_2)) { ctx.channel().attr(PROTOCOL_NAME).set(PROTOCOL_HTTP_2); configureHttp2(ctx.pipeline()); return; } if (protocol.equals(ApplicationProtocolNames.HTTP_1_1)) { ctx.channel().attr(PROTOCOL_NAME).set(PROTOCOL_HTTP_1_1); configureHttp1(ctx.pipeline()); return; } throw new IllegalStateException("unknown protocol: " + protocol); } private void configureHttp2(ChannelPipeline pipeline) { // setup the initial stream settings for the server to use. Http2Settings settings = new Http2Settings() .maxConcurrentStreams(maxConcurrentStreams) .initialWindowSize(initialWindowSize) .headerTableSize(maxHeaderTableSize) .maxHeaderListSize(maxHeaderListSize) .connectProtocolEnabled(connectProtocolEnabled); Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forServer() .frameLogger(FRAME_LOGGER) .initialSettings(settings) .validateHeaders(true) .encoderEnforceMaxRstFramesPerWindow(maxEncoderRstFrames, maxEncoderRstFramesWindow) .build(); Http2Connection conn = frameCodec.connection(); // Use the uniform byte distributor until https://github.com/netty/netty/issues/10525 is fixed. conn.remote() .flowController(new DefaultHttp2RemoteFlowController(conn, new UniformStreamByteDistributor(conn))); Http2MultiplexHandler multiplexHandler = new Http2MultiplexHandler(http2StreamHandler); // The frame codec MUST be in the pipeline. pipeline.addBefore("codec_placeholder", null, frameCodec); pipeline.replace("codec_placeholder", BaseZuulChannelInitializer.HTTP_CODEC_HANDLER_NAME, multiplexHandler); if (catchConnectionErrors) { pipeline.addLast(new Http2ConnectionErrorHandler()); } } private void configureHttp1(ChannelPipeline pipeline) { addHttpHandlerFn.accept(pipeline); } private void removeSelfIfPresent(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); if (!ctx.isRemoved()) { pipeline.remove(this); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2ResetFrameHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import com.netflix.zuul.netty.RequestCancelledEvent; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http2.Http2ResetFrame; import io.netty.util.ReferenceCountUtil; /** * User: michaels@netflix.com * Date: 4/13/17 * Time: 6:02 PM */ @ChannelHandler.Sharable public class Http2ResetFrameHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof Http2ResetFrame) { // Inform zuul to cancel the request. ctx.fireUserEventTriggered(new RequestCancelledEvent()); ReferenceCountUtil.safeRelease(msg); } else { super.channelRead(ctx, msg); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2SslChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import com.google.common.base.Preconditions; import com.netflix.netty.common.Http2ConnectionCloseHandler; import com.netflix.netty.common.Http2ConnectionExpiryHandler; import com.netflix.netty.common.SwallowSomeHttp2ExceptionsHandler; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.Http2MetricsChannelHandlers; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.zuul.logging.Http2FrameLoggingPerClientIpHandler; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import com.netflix.zuul.netty.ssl.SslContextFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: Mike Smith * Date: 3/5/16 * Time: 5:41 PM */ public final class Http2SslChannelInitializer extends BaseZuulChannelInitializer { private static final Logger LOG = LoggerFactory.getLogger(Http2SslChannelInitializer.class); private static final DummyChannelHandler DUMMY_HANDLER = new DummyChannelHandler(); private final ServerSslConfig serverSslConfig; private final SslContext sslContext; private final boolean isSSlFromIntermediary; private final SwallowSomeHttp2ExceptionsHandler swallowSomeHttp2ExceptionsHandler; private final String http2SslMetricId; /** * Use {@link #Http2SslChannelInitializer(String, ChannelConfig, ChannelConfig, ChannelGroup)} instead. */ @Deprecated public Http2SslChannelInitializer( int port, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this(String.valueOf(port), channelConfig, channelDependencies, channels); } public Http2SslChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { super(metricId, channelConfig, channelDependencies, channels); this.http2SslMetricId = Preconditions.checkNotNull(metricId, "metricId"); this.swallowSomeHttp2ExceptionsHandler = new SwallowSomeHttp2ExceptionsHandler(registry); this.serverSslConfig = channelConfig.get(CommonChannelConfigKeys.serverSslConfig); this.isSSlFromIntermediary = channelConfig.get(CommonChannelConfigKeys.isSSlFromIntermediary); SslContextFactory sslContextFactory = channelConfig.get(CommonChannelConfigKeys.sslContextFactory); sslContext = Http2Configuration.configureSSL(sslContextFactory, metricId); } @Override protected void initChannel(Channel ch) { SslHandler sslHandler = sslContext.newHandler(ch.alloc()); sslHandler.engine().setEnabledProtocols(serverSslConfig.getProtocols()); // SSLParameters sslParameters = new SSLParameters(); // AlgorithmConstraints algoConstraints = new AlgorithmConstraints(); // sslParameters.setAlgorithmConstraints(algoConstraints); // sslParameters.setUseCipherSuitesOrder(true); // sslHandler.engine().setSSLParameters(sslParameters); if (LOG.isDebugEnabled()) { LOG.debug( "ssl protocols supported: {}", String.join(", ", sslHandler.engine().getSupportedProtocols())); LOG.debug( "ssl protocols enabled: {}", String.join(", ", sslHandler.engine().getEnabledProtocols())); LOG.debug( "ssl ciphers supported: {}", String.join(", ", sslHandler.engine().getSupportedCipherSuites())); LOG.debug( "ssl ciphers enabled: {}", String.join(", ", sslHandler.engine().getEnabledCipherSuites())); } // Configure our pipeline of ChannelHandlerS. ChannelPipeline pipeline = ch.pipeline(); storeChannel(ch); addTimeoutHandlers(pipeline); addPassportHandler(pipeline); addTcpRelatedHandlers(pipeline); pipeline.addLast(new Http2FrameLoggingPerClientIpHandler()); pipeline.addLast("ssl", sslHandler); addSslInfoHandlers(pipeline, isSSlFromIntermediary); addSslClientCertChecks(pipeline); Http2MetricsChannelHandlers http2MetricsChannelHandlers = new Http2MetricsChannelHandlers(registry, "server", "http2-" + http2SslMetricId); Http2ConnectionCloseHandler connectionCloseHandler = new Http2ConnectionCloseHandler(registry); Http2ConnectionExpiryHandler connectionExpiryHandler = new Http2ConnectionExpiryHandler( maxRequestsPerConnection, maxRequestsPerConnectionInBrownout, connectionExpiry); pipeline.addLast( "http2CodecSwapper", new Http2OrHttpHandler( new Http2StreamInitializer( ch, this::http1Handlers, http2MetricsChannelHandlers, connectionCloseHandler, connectionExpiryHandler), channelConfig, cp -> { http1Codec(cp); http1Handlers(cp); })); pipeline.addLast("codec_placeholder", DUMMY_HANDLER); pipeline.addLast(swallowSomeHttp2ExceptionsHandler); } private void http1Handlers(ChannelPipeline pipeline) { addHttpRelatedHandlers(pipeline); addZuulHandlers(pipeline); } private void http1Codec(ChannelPipeline pipeline) { pipeline.replace("codec_placeholder", HTTP_CODEC_HANDLER_NAME, createHttpServerCodec()); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2StreamErrorHandler.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http2.DefaultHttp2ResetFrame; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; /** * Author: Susheel Aroskar * Date: 5/7/2018 */ @ChannelHandler.Sharable public class Http2StreamErrorHandler extends ChannelInboundHandlerAdapter { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof Http2Exception.StreamException streamEx) { ctx.writeAndFlush(new DefaultHttp2ResetFrame(streamEx.error())); } else if (cause instanceof DecoderException) { ctx.writeAndFlush(new DefaultHttp2ResetFrame(Http2Error.PROTOCOL_ERROR)); } else { super.exceptionCaught(ctx, cause); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2StreamHeaderCleaner.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpRequest; /** * The Http2ServerDowngrader currently is always incorrectly setting the "x-http2-stream-id" * header to "0", which is confusing. And as we don't actually need it and the other "x-http2-" headers, we * strip them out here to avoid the confusion. * * Hopefully in a future netty release that header value will be correct and we can then * stop doing this. Although potentially we _never_ want to pass these downstream to origins .... ? */ @ChannelHandler.Sharable public class Http2StreamHeaderCleaner extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest req) { for (String name : req.headers().names()) { if (name.startsWith("x-http2-")) { req.headers().remove(name); } } } super.channelRead(ctx, msg); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/http2/Http2StreamInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import com.netflix.netty.common.Http2ConnectionCloseHandler; import com.netflix.netty.common.Http2ConnectionExpiryHandler; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.metrics.Http2MetricsChannelHandlers; import com.netflix.netty.common.proxyprotocol.HAProxyMessageChannelHandler; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import com.netflix.zuul.netty.server.Server; import com.netflix.zuul.netty.server.ssl.SslHandshakeInfoHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; import io.netty.util.AttributeKey; import java.util.Set; import java.util.function.Consumer; /** * TODO - can this be done when we create the Http2StreamChannelBootstrap instead now? */ @ChannelHandler.Sharable public class Http2StreamInitializer extends ChannelInboundHandlerAdapter { private static final Set> ATTRIBUTES_TO_COPY = Set.of( SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS, SourceAddressChannelHandler.ATTR_LOCAL_INET_ADDR, SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS, SourceAddressChannelHandler.ATTR_REMOTE_ADDR, SourceAddressChannelHandler.ATTR_SOURCE_INET_ADDR, SourceAddressChannelHandler.ATTR_SERVER_LOCAL_ADDRESS, SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT, SourceAddressChannelHandler.ATTR_PROXY_PROTOCOL_DESTINATION_ADDRESS, Http2OrHttpHandler.PROTOCOL_NAME, SslHandshakeInfoHandler.ATTR_SSL_INFO, HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE, HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION, HAProxyMessageChannelHandler.ATTR_HAPROXY_CUSTOM_TLVS, BaseZuulChannelInitializer.ATTR_CHANNEL_CONFIG, Server.CONN_DIMENSIONS); private static final Http2StreamHeaderCleaner http2StreamHeaderCleaner = new Http2StreamHeaderCleaner(); private static final Http2ResetFrameHandler http2ResetFrameHandler = new Http2ResetFrameHandler(); private static final Http2StreamErrorHandler http2StreamErrorHandler = new Http2StreamErrorHandler(); private final Channel parent; private final Consumer addHttpHandlerFn; private final Http2MetricsChannelHandlers http2MetricsChannelHandlers; private final Http2ConnectionCloseHandler connectionCloseHandler; private final Http2ConnectionExpiryHandler connectionExpiryHandler; public Http2StreamInitializer( Channel parent, Consumer addHttpHandlerFn, Http2MetricsChannelHandlers http2MetricsChannelHandlers, Http2ConnectionCloseHandler connectionCloseHandler, Http2ConnectionExpiryHandler connectionExpiryHandler) { this.parent = parent; this.addHttpHandlerFn = addHttpHandlerFn; this.http2MetricsChannelHandlers = http2MetricsChannelHandlers; this.connectionCloseHandler = connectionCloseHandler; this.connectionExpiryHandler = connectionExpiryHandler; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { copyAttrsFromParentChannel(this.parent, ctx.channel()); addHttp2MetricsHandlers(ctx.pipeline()); addHttp2StreamSpecificHandlers(ctx.pipeline()); addHttpHandlerFn.accept(ctx.pipeline()); ctx.pipeline().remove(this); } protected void addHttp2StreamSpecificHandlers(ChannelPipeline pipeline) { pipeline.addLast("h2_max_requests_per_conn", connectionExpiryHandler); pipeline.addLast("h2_conn_close", connectionCloseHandler); pipeline.addLast(http2ResetFrameHandler); pipeline.addLast("h2_downgrader", new Http2StreamFrameToHttpObjectCodec(true)); pipeline.addLast(http2StreamErrorHandler); pipeline.addLast(http2StreamHeaderCleaner); pipeline.addLast(new Http2ContentLengthEnforcingHandler()); } protected void addHttp2MetricsHandlers(ChannelPipeline pipeline) { pipeline.addLast("h2_metrics_inbound", http2MetricsChannelHandlers.inbound()); pipeline.addLast("h2_metrics_outbound", http2MetricsChannelHandlers.outbound()); } protected void copyAttrsFromParentChannel(Channel parent, Channel child) { for (AttributeKey key : ATTRIBUTES_TO_COPY) { copyAttributesFromParentChannel(parent, child, key); } } protected void copyAttributesFromParentChannel(Channel parent, Channel child, AttributeKey key) { child.attr(key).set(parent.attr(key).get()); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/ClientPSKIdentityInfo.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; import java.util.List; public record ClientPSKIdentityInfo(List clientPSKIdentity) {} ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/ExternalTlsPskProvider.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; public interface ExternalTlsPskProvider { byte[] provide(byte[] clientPskIdentity, byte[] clientRandom) throws PskCreationFailureException; } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/PskCreationFailureException.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; public class PskCreationFailureException extends Exception { public enum TlsAlertMessage { /** * The server does not recognize the (client) PSK identity */ unknown_psk_identity, /** * The (client) PSK identity existed but the key was incorrect */ decrypt_error, } private final TlsAlertMessage tlsAlertMessage; public PskCreationFailureException(TlsAlertMessage tlsAlertMessage, String message) { super(message); this.tlsAlertMessage = tlsAlertMessage; } public PskCreationFailureException(TlsAlertMessage tlsAlertMessage, String message, Throwable cause) { super(message, cause); this.tlsAlertMessage = tlsAlertMessage; } public TlsAlertMessage getTlsAlertMessage() { return tlsAlertMessage; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/TlsPskDecoder.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import java.util.List; import org.bouncycastle.tls.TlsFatalAlert; public class TlsPskDecoder extends ByteToMessageDecoder { private final TlsPskServerProtocol tlsPskServerProtocol; public TlsPskDecoder(TlsPskServerProtocol tlsPskServerProtocol) { this.tlsPskServerProtocol = tlsPskServerProtocol; } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { byte[] bytesRead = in.hasArray() ? in.array() : TlsPskUtils.readDirect(in); try { tlsPskServerProtocol.offerInput(bytesRead); } catch (TlsFatalAlert tlsFatalAlert) { writeOutputIfAvailable(ctx); ctx.fireUserEventTriggered(new SslHandshakeCompletionEvent(tlsFatalAlert)); ctx.close(); return; } writeOutputIfAvailable(ctx); int appDataAvailable = tlsPskServerProtocol.getAvailableInputBytes(); if (appDataAvailable > 0) { byte[] appData = new byte[appDataAvailable]; tlsPskServerProtocol.readInput(appData, 0, appDataAvailable); out.add(Unpooled.wrappedBuffer(appData)); } } private void writeOutputIfAvailable(ChannelHandlerContext ctx) { int availableOutputBytes = tlsPskServerProtocol.getAvailableOutputBytes(); // output is available immediately (handshake not complete), pipe that back to the client right away if (availableOutputBytes != 0) { byte[] outputBytes = new byte[availableOutputBytes]; tlsPskServerProtocol.readOutput(outputBytes, 0, availableOutputBytes); ctx.writeAndFlush(Unpooled.wrappedBuffer(outputBytes)) .addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/TlsPskHandler.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; import com.netflix.spectator.api.Registry; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; import java.security.SecureRandom; import java.util.Map; import java.util.Set; import javax.net.ssl.SSLSession; import org.bouncycastle.tls.CipherSuite; import org.bouncycastle.tls.ProtocolName; import org.bouncycastle.tls.crypto.impl.jcajce.JcaTlsCryptoProvider; public class TlsPskHandler extends ChannelDuplexHandler { public static final Map SUPPORTED_TLS_PSK_CIPHER_SUITE_MAP = Map.of( CipherSuite.TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", CipherSuite.TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384"); public static final AttributeKey CLIENT_PSK_IDENTITY_ATTRIBUTE_KEY = AttributeKey.newInstance("_client_psk_identity_info"); public static final SecureRandom secureRandom = new SecureRandom(); private final Registry registry; private final ExternalTlsPskProvider externalTlsPskProvider; private final Set supportedApplicationProtocols; private final TlsPskServerProtocol tlsPskServerProtocol; private ZuulPskServer tlsPskServer; public TlsPskHandler( Registry registry, ExternalTlsPskProvider externalTlsPskProvider, Set supportedApplicationProtocols) { super(); this.registry = registry; this.externalTlsPskProvider = externalTlsPskProvider; this.supportedApplicationProtocols = supportedApplicationProtocols; this.tlsPskServerProtocol = new TlsPskServerProtocol(); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (!(msg instanceof ByteBuf byteBufMsg)) { ReferenceCountUtil.safeRelease(msg); promise.setFailure( new IllegalStateException("Failed to write message on the channel. Message is not a ByteBuf")); return; } byte[] appDataBytes = TlsPskUtils.getAppDataBytesAndRelease(byteBufMsg); tlsPskServerProtocol.writeApplicationData(appDataBytes, 0, appDataBytes.length); int availableOutputBytes = tlsPskServerProtocol.getAvailableOutputBytes(); if (availableOutputBytes != 0) { byte[] outputBytes = new byte[availableOutputBytes]; tlsPskServerProtocol.readOutput(outputBytes, 0, availableOutputBytes); ctx.writeAndFlush(Unpooled.wrappedBuffer(outputBytes), promise) .addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); } } @Override public void handlerAdded(ChannelHandlerContext ctx) { ctx.pipeline().addBefore(ctx.name(), "tls_psk_handler", new TlsPskDecoder(tlsPskServerProtocol)); } @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { tlsPskServer = new ZuulPskServer( new JcaTlsCryptoProvider().create(secureRandom), registry, externalTlsPskProvider, ctx, supportedApplicationProtocols); tlsPskServerProtocol.accept(tlsPskServer); super.channelRegistered(ctx); } /** * Returns the name of the current application-level protocol. * Returns: * the protocol name or null if application-level protocol has not been negotiated */ public String getApplicationProtocol() { return tlsPskServer != null ? tlsPskServer.getApplicationProtocol() : null; } public SSLSession getSession() { return tlsPskServerProtocol != null ? tlsPskServerProtocol.getSSLSession() : null; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/TlsPskServerProtocol.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; import java.security.Principal; import java.security.cert.Certificate; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSessionContext; import javax.security.cert.X509Certificate; import org.bouncycastle.tls.TlsServerProtocol; public class TlsPskServerProtocol extends TlsServerProtocol { public SSLSession getSSLSession() { return new SSLSession() { @Override public byte[] getId() { return tlsSession.getSessionID(); } @Override public SSLSessionContext getSessionContext() { return null; } @Override public long getCreationTime() { return 0; } @Override public long getLastAccessedTime() { return 0; } @Override public void invalidate() {} @Override public boolean isValid() { return !isClosed(); } @Override public void putValue(String name, Object value) {} @Override public Object getValue(String name) { return null; } @Override public void removeValue(String name) {} @Override public String[] getValueNames() { return new String[0]; } @Override public Certificate[] getPeerCertificates() { return new Certificate[0]; } @Override public Certificate[] getLocalCertificates() { return new Certificate[0]; } @Override @SuppressWarnings("removal") public X509Certificate[] getPeerCertificateChain() { return new X509Certificate[0]; } @Override public Principal getPeerPrincipal() { return null; } @Override public Principal getLocalPrincipal() { return null; } @Override public String getCipherSuite() { return TlsPskHandler.SUPPORTED_TLS_PSK_CIPHER_SUITE_MAP.get( getContext().getSecurityParameters().getCipherSuite()); } @Override public String getProtocol() { return getContext().getServerVersion().getName(); } @Override public String getPeerHost() { return null; } @Override public int getPeerPort() { return 0; } @Override public int getPacketBufferSize() { return 0; } @Override public int getApplicationBufferSize() { return 0; } }; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/TlsPskUtils.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; import io.netty.buffer.ByteBuf; import io.netty.util.ReferenceCountUtil; class TlsPskUtils { protected static byte[] readDirect(ByteBuf byteBufMsg) { int length = byteBufMsg.readableBytes(); byte[] dest = new byte[length]; byteBufMsg.readBytes(dest); return dest; } protected static byte[] getAppDataBytesAndRelease(ByteBuf byteBufMsg) { byte[] appDataBytes = byteBufMsg.hasArray() ? byteBufMsg.array() : TlsPskUtils.readDirect(byteBufMsg); ReferenceCountUtil.safeRelease(byteBufMsg); return appDataBytes; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/psk/ZuulPskServer.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.psk; import com.google.common.primitives.Bytes; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Timer; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.util.AttributeKey; import java.io.IOException; import java.util.Hashtable; import java.util.List; import java.util.Set; import java.util.Vector; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import lombok.SneakyThrows; import org.bouncycastle.tls.AbstractTlsServer; import org.bouncycastle.tls.AlertDescription; import org.bouncycastle.tls.AlertLevel; import org.bouncycastle.tls.BasicTlsPSKExternal; import org.bouncycastle.tls.CipherSuite; import org.bouncycastle.tls.PRFAlgorithm; import org.bouncycastle.tls.ProtocolName; import org.bouncycastle.tls.ProtocolVersion; import org.bouncycastle.tls.PskIdentity; import org.bouncycastle.tls.TlsCredentials; import org.bouncycastle.tls.TlsFatalAlert; import org.bouncycastle.tls.TlsPSKExternal; import org.bouncycastle.tls.TlsUtils; import org.bouncycastle.tls.crypto.TlsCrypto; import org.bouncycastle.tls.crypto.TlsSecret; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ZuulPskServer extends AbstractTlsServer { private static final Logger LOGGER = LoggerFactory.getLogger(ZuulPskServer.class); public static final AttributeKey TLS_HANDSHAKE_USING_EXTERNAL_PSK = AttributeKey.newInstance("_tls_handshake_using_external_psk"); private static class PSKTimings { private final Timer handshakeCompleteTimer; private Long handshakeStartTime; PSKTimings(Registry registry) { handshakeCompleteTimer = registry.timer("zuul.psk.handshake.complete.time"); } public void recordHandshakeStarting() { handshakeStartTime = System.nanoTime(); } public void recordHandshakeComplete() { handshakeCompleteTimer.record(System.nanoTime() - handshakeStartTime, TimeUnit.NANOSECONDS); } } private final PSKTimings pskTimings; private final ExternalTlsPskProvider externalTlsPskProvider; private final ChannelHandlerContext ctx; private final Set supportedApplicationProtocols; public ZuulPskServer( TlsCrypto crypto, Registry registry, ExternalTlsPskProvider externalTlsPskProvider, ChannelHandlerContext ctx, Set supportedApplicationProtocols) { super(crypto); this.pskTimings = new PSKTimings(registry); this.externalTlsPskProvider = externalTlsPskProvider; this.ctx = ctx; this.supportedApplicationProtocols = supportedApplicationProtocols; } @Override public TlsCredentials getCredentials() { return null; } @Override protected Vector getProtocolNames() { Vector protocolNames = new Vector(); if (supportedApplicationProtocols != null) { supportedApplicationProtocols.forEach(protocolNames::addElement); } return protocolNames; } @Override public void notifyHandshakeBeginning() throws IOException { pskTimings.recordHandshakeStarting(); this.ctx.channel().attr(TLS_HANDSHAKE_USING_EXTERNAL_PSK).set(false); // TODO: sunnys - handshake timeouts super.notifyHandshakeBeginning(); } @Override public void notifyHandshakeComplete() throws IOException { pskTimings.recordHandshakeComplete(); this.ctx.channel().attr(TLS_HANDSHAKE_USING_EXTERNAL_PSK).set(true); super.notifyHandshakeComplete(); ctx.fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS); } @Override protected ProtocolVersion[] getSupportedVersions() { return ProtocolVersion.TLSv13.only(); } @Override protected int[] getSupportedCipherSuites() { return TlsUtils.getSupportedCipherSuites( getCrypto(), TlsPskHandler.SUPPORTED_TLS_PSK_CIPHER_SUITE_MAP.keySet().stream() .mapToInt(Number::intValue) .toArray()); } @Override public ProtocolVersion getServerVersion() throws IOException { return super.getServerVersion(); } /** * TODO: Ask BC folks to see if getExternalPSK can throw a checked exception * https://github.com/bcgit/bc-java/issues/1673 * We are using SneakyThrows here because getExternalPSK is an override and we can't have throws in the method signature * and we dont want to catch and wrap in RuntimeException. * SneakyThrows allows up to compile and it will throw the exception at runtime. */ @Override @SneakyThrows public TlsPSKExternal getExternalPSK(Vector clientPskIdentities) { byte[] clientPskIdentity = ((PskIdentity) clientPskIdentities.get(0)).getIdentity(); byte[] psk; try { this.ctx .channel() .attr(TlsPskHandler.CLIENT_PSK_IDENTITY_ATTRIBUTE_KEY) .set(new ClientPSKIdentityInfo(List.copyOf(Bytes.asList(clientPskIdentity)))); psk = externalTlsPskProvider.provide( clientPskIdentity, this.context.getSecurityParametersHandshake().getClientRandom()); } catch (PskCreationFailureException e) { throw switch (e.getTlsAlertMessage()) { case unknown_psk_identity -> new TlsFatalAlert(AlertDescription.unknown_psk_identity, "Unknown or null client PSk identity"); case decrypt_error -> new TlsFatalAlert(AlertDescription.decrypt_error, "Invalid or expired client PSk identity"); }; } TlsSecret pskTlsSecret = getCrypto().createSecret(psk); int prfAlgorithm = getPRFAlgorithm13(getSelectedCipherSuite()); return new BasicTlsPSKExternal(clientPskIdentity, pskTlsSecret, prfAlgorithm); } @Override public void notifyAlertRaised(short alertLevel, short alertDescription, String message, Throwable cause) { super.notifyAlertRaised(alertLevel, alertDescription, message, cause); Consumer loggerFunc = (alertLevel == AlertLevel.fatal) ? LOGGER::error : LOGGER::debug; loggerFunc.accept("TLS/PSK server raised alert: " + AlertLevel.getText(alertLevel) + ", " + AlertDescription.getText(alertDescription)); if (message != null) { loggerFunc.accept("> " + message); } if (cause != null) { LOGGER.error("TLS/PSK alert stacktrace", cause); } if (alertDescription == AlertDescription.close_notify) { ctx.fireUserEventTriggered(SslCloseCompletionEvent.SUCCESS); } } @Override public void notifyAlertReceived(short alertLevel, short alertDescription) { Consumer loggerFunc = (alertLevel == AlertLevel.fatal) ? LOGGER::error : LOGGER::debug; loggerFunc.accept("TLS 1.3 PSK server received alert: " + AlertLevel.getText(alertLevel) + ", " + AlertDescription.getText(alertDescription)); } @Override public void processClientExtensions(Hashtable clientExtensions) throws IOException { if (context.getSecurityParametersHandshake().getClientRandom() == null) { throw new TlsFatalAlert(AlertDescription.internal_error); } super.processClientExtensions(clientExtensions); } @Override public Hashtable getServerExtensions() throws IOException { if (context.getSecurityParametersHandshake().getServerRandom() == null) { throw new TlsFatalAlert(AlertDescription.internal_error); } return super.getServerExtensions(); } @Override public void getServerExtensionsForConnection(Hashtable serverExtensions) throws IOException { if (context.getSecurityParametersHandshake().getServerRandom() == null) { throw new TlsFatalAlert(AlertDescription.internal_error); } super.getServerExtensionsForConnection(serverExtensions); } public String getApplicationProtocol() { ProtocolName protocolName = context.getSecurityParametersConnection().getApplicationProtocol(); if (protocolName != null) { return protocolName.getUtf8Decoding(); } return null; } private static int getPRFAlgorithm13(int cipherSuite) { return switch (cipherSuite) { case CipherSuite.TLS_AES_128_CCM_SHA256, CipherSuite.TLS_AES_128_CCM_8_SHA256, CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_CHACHA20_POLY1305_SHA256 -> PRFAlgorithm.tls13_hkdf_sha256; case CipherSuite.TLS_AES_256_GCM_SHA384 -> PRFAlgorithm.tls13_hkdf_sha384; case CipherSuite.TLS_SM4_CCM_SM3, CipherSuite.TLS_SM4_GCM_SM3 -> PRFAlgorithm.tls13_hkdf_sm3; default -> -1; }; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushAuthHandler.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import com.google.common.base.Strings; import com.netflix.zuul.message.http.Cookies; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; 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.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import java.util.List; import java.util.Locale; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Author: Susheel Aroskar * Date: 5/11/18 */ @ChannelHandler.Sharable public abstract class PushAuthHandler extends SimpleChannelInboundHandler { private final String pushConnectionPath; private final String originDomain; public static final String NAME = "push_auth_handler"; private static final Logger logger = LoggerFactory.getLogger(PushAuthHandler.class); public PushAuthHandler(String pushConnectionPath, String originDomain) { this.pushConnectionPath = pushConnectionPath; this.originDomain = originDomain; } public final void sendHttpResponse(HttpRequest req, ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status); resp.headers().add("Content-Length", "0"); boolean closeConn = (!Objects.equals(status, HttpResponseStatus.OK) || !HttpUtil.isKeepAlive(req)); if (closeConn) { resp.headers().add(HttpHeaderNames.CONNECTION, "Close"); } ChannelFuture cf = ctx.channel().writeAndFlush(resp); if (closeConn) { cf.addListener(ChannelFutureListener.CLOSE); } } @Override protected final void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) { if (!Objects.equals(req.method(), HttpMethod.GET)) { sendHttpResponse(req, ctx, HttpResponseStatus.METHOD_NOT_ALLOWED); return; } String path = req.uri(); if (Objects.equals(path, "/healthcheck")) { sendHttpResponse(req, ctx, HttpResponseStatus.OK); } else if (pushConnectionPath.equals(path)) { // CSRF protection if (isInvalidOrigin(req)) { sendHttpResponse(req, ctx, HttpResponseStatus.BAD_REQUEST); } else if (isDelayedAuth(req, ctx)) { // client auth will happen later, continue with WebSocket upgrade handshake ctx.fireChannelRead(req.retain()); } else { PushUserAuth authEvent = doAuth(req, ctx); if (authEvent.isSuccess()) { ctx.fireChannelRead(req.retain()); // continue with WebSocket upgrade handshake ctx.fireUserEventTriggered(authEvent); } else { logger.warn("Auth failed: {}", authEvent.statusCode()); sendHttpResponse(req, ctx, HttpResponseStatus.valueOf(authEvent.statusCode())); } } } else { sendHttpResponse(req, ctx, HttpResponseStatus.NOT_FOUND); } } protected boolean isInvalidOrigin(FullHttpRequest req) { String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (origin == null || !origin.toLowerCase(Locale.ROOT).endsWith(originDomain)) { logger.error("Invalid Origin header {} in WebSocket upgrade request", origin); return true; } return false; } protected final Cookies parseCookies(FullHttpRequest req) { Cookies cookies = new Cookies(); String cookieStr = req.headers().get(HttpHeaderNames.COOKIE); if (!Strings.isNullOrEmpty(cookieStr)) { List decoded = ServerCookieDecoder.LAX.decodeAll(cookieStr); decoded.forEach(cookies::add); } return cookies; } /** * @return true if Auth credentials will be provided later, for example in first WebSocket frame sent */ protected abstract boolean isDelayedAuth(FullHttpRequest req, ChannelHandlerContext ctx); protected abstract PushUserAuth doAuth(FullHttpRequest req, ChannelHandlerContext ctx); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushChannelInitializer.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; /** * Author: Susheel Aroskar * Date: 5/15/18 */ public abstract class PushChannelInitializer extends BaseZuulChannelInitializer { /** * Use {@link #PushChannelInitializer(String, ChannelConfig, ChannelConfig, ChannelGroup)} instead. */ @Deprecated public PushChannelInitializer( int port, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { this(String.valueOf(port), channelConfig, channelDependencies, channels); } protected PushChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { super(metricId, channelConfig, channelDependencies, channels); } @Override protected void addHttp1Handlers(ChannelPipeline pipeline) { pipeline.addLast( HTTP_CODEC_HANDLER_NAME, new HttpServerCodec(MAX_INITIAL_LINE_LENGTH.get(), MAX_HEADER_SIZE.get(), MAX_CHUNK_SIZE.get(), false)); pipeline.addLast(new HttpObjectAggregator(8192)); } @Override protected void addHttpRelatedHandlers(ChannelPipeline pipeline) { pipeline.addLast(stripInboundProxyHeadersHandler); } @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); storeChannel(ch); addTcpRelatedHandlers(pipeline); addHttp1Handlers(pipeline); addHttpRelatedHandlers(pipeline); addPushHandlers(pipeline); } protected abstract void addPushHandlers(ChannelPipeline pipeline); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushClientProtocolHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * Author: Susheel Aroskar * Date: 11/2/2018 */ public class PushClientProtocolHandler extends ChannelInboundHandlerAdapter { protected PushUserAuth authEvent; protected boolean isAuthenticated() { return (authEvent != null && authEvent.isSuccess()); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof PushUserAuth) { authEvent = (PushUserAuth) evt; } super.userEventTriggered(ctx, evt); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushConnection.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import com.google.common.base.Charsets; import com.netflix.config.CachedDynamicIntProperty; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; /** * Author: Susheel Aroskar * Date: */ public class PushConnection { private final PushProtocol pushProtocol; private final ChannelHandlerContext ctx; private String secureToken; // Token bucket implementation state. private double tkBktAllowance; private long tkBktLastCheckTime; public static final CachedDynamicIntProperty TOKEN_BUCKET_RATE = new CachedDynamicIntProperty("zuul.push.tokenBucket.rate", 3); public static final CachedDynamicIntProperty TOKEN_BUCKET_WINDOW = new CachedDynamicIntProperty("zuul.push.tokenBucket.window.millis", 2000); public PushConnection(PushProtocol pushProtocol, ChannelHandlerContext ctx) { this.pushProtocol = pushProtocol; this.ctx = ctx; tkBktAllowance = TOKEN_BUCKET_RATE.get(); tkBktLastCheckTime = System.currentTimeMillis(); } public String getSecureToken() { return secureToken; } public void setSecureToken(String secureToken) { this.secureToken = secureToken; } /** * Implementation of TokenBucket algorithm to do rate limiting: http://stackoverflow.com/a/668327 * @return true if should be rate limited, false if it is OK to send the message */ public synchronized boolean isRateLimited() { double rate = TOKEN_BUCKET_RATE.get(); double window = TOKEN_BUCKET_WINDOW.get(); long current = System.currentTimeMillis(); double timePassed = current - tkBktLastCheckTime; tkBktLastCheckTime = current; tkBktAllowance = tkBktAllowance + timePassed * (rate / window); if (tkBktAllowance > rate) { tkBktAllowance = rate; // cap max to rate } if (tkBktAllowance < 1.0) { return true; } tkBktAllowance = tkBktAllowance - 1.0; return false; } public ChannelFuture sendPushMessage(ByteBuf mesg) { return pushProtocol.sendPushMessage(ctx, mesg); } public ChannelFuture sendPushMessage(String mesg) { return sendPushMessage(Unpooled.copiedBuffer(mesg, Charsets.UTF_8)); } public ChannelFuture sendPing() { return pushProtocol.sendPing(ctx); } public void closeConnection(WebSocketCloseStatus status, String message) { ctx.writeAndFlush(new CloseWebSocketFrame(status, message)).addListener(ChannelFutureListener.CLOSE); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushConnectionRegistry.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; /** * Maintains client identity to web socket or SSE channel mapping. * * Created by saroskar on 9/26/16. */ @Singleton public class PushConnectionRegistry { private final ConcurrentMap clientPushConnectionMap; private final SecureRandom secureTokenGenerator; @Inject public PushConnectionRegistry() { clientPushConnectionMap = new ConcurrentHashMap<>(1024 * 32); secureTokenGenerator = new SecureRandom(); } @Nullable public PushConnection get(String clientId) { return clientPushConnectionMap.get(clientId); } public List getAll() { return new ArrayList<>(clientPushConnectionMap.values()); } public Map getAllEntries() { return Collections.unmodifiableMap(clientPushConnectionMap); } public String mintNewSecureToken() { byte[] tokenBuffer = new byte[15]; secureTokenGenerator.nextBytes(tokenBuffer); return Base64.getUrlEncoder().encodeToString(tokenBuffer); } public void put(String clientId, PushConnection pushConnection) { pushConnection.setSecureToken(mintNewSecureToken()); clientPushConnectionMap.put(clientId, pushConnection); } public PushConnection remove(String clientId) { PushConnection pc = clientPushConnectionMap.remove(clientId); return pc; } public int size() { return clientPushConnectionMap.size(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushMessageFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; /** * Author: Susheel Aroskar * Date: 11/2/2018 */ public abstract class PushMessageFactory { public final void sendErrorAndClose(ChannelHandlerContext ctx, int statusCode, String reasonText) { ctx.writeAndFlush(serverClosingConnectionMessage(statusCode, reasonText)) .addListener(ChannelFutureListener.CLOSE); } /** * Application level protocol for asking client to close connection * @return WebSocketFrame which when sent to client will cause it to close the WebSocket */ protected abstract Object goAwayMessage(); /** * Message server sends to the client just before it force closes connection from its side */ protected abstract Object serverClosingConnectionMessage(int statusCode, String reasonText); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushMessageSender.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import com.google.common.base.Strings; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; 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.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.ReferenceCountUtil; import jakarta.inject.Inject; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Serves "/push" URL that is used by the backend to POST push messages to a given Zuul instance. This URL handler * MUST BE accessible ONLY from RFC 1918 private internal network space (10.0.0.0 or 172.16.0.0) to guarantee that * external applications/agents cannot push messages to your client. In AWS this can typically be achieved using * correctly configured security groups. * * Author: Susheel Aroskar * Date: 5/14/18 */ @ChannelHandler.Sharable public abstract class PushMessageSender extends SimpleChannelInboundHandler { private final PushConnectionRegistry pushConnectionRegistry; public static final String SECURE_TOKEN_HEADER_NAME = "X-Zuul.push.secure.token"; private static final Logger logger = LoggerFactory.getLogger(PushMessageSender.class); @Inject public PushMessageSender(PushConnectionRegistry pushConnectionRegistry) { this.pushConnectionRegistry = pushConnectionRegistry; } protected void sendHttpResponse( ChannelHandlerContext ctx, FullHttpRequest request, HttpResponseStatus status, PushUserAuth userAuth) { FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status); resp.headers().add("Content-Length", "0"); ChannelFuture cf = ctx.channel().writeAndFlush(resp); if (!HttpUtil.isKeepAlive(request)) { cf.addListener(ChannelFutureListener.CLOSE); } logPushEvent(request, status, userAuth); } protected boolean verifySecureToken(FullHttpRequest request, PushConnection conn) { String secureToken = request.headers().get(SECURE_TOKEN_HEADER_NAME); if (Strings.isNullOrEmpty(secureToken)) { // caller is not asking to verify secure token return true; } return secureToken.equals(conn.getSecureToken()); } @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { if (!request.decoderResult().isSuccess()) { sendHttpResponse(ctx, request, HttpResponseStatus.BAD_REQUEST, null); return; } String path = request.uri(); if (path == null) { sendHttpResponse(ctx, request, HttpResponseStatus.BAD_REQUEST, null); return; } if (path.endsWith("/push")) { logPushAttempt(); HttpMethod method = request.method(); if (!Objects.equals(method, HttpMethod.POST) && !Objects.equals(method, HttpMethod.GET)) { sendHttpResponse(ctx, request, HttpResponseStatus.METHOD_NOT_ALLOWED, null); return; } PushUserAuth userAuth = getPushUserAuth(request); if (!userAuth.isSuccess()) { sendHttpResponse(ctx, request, HttpResponseStatus.UNAUTHORIZED, userAuth); logNoIdentity(); return; } PushConnection pushConn = pushConnectionRegistry.get(userAuth.getClientIdentity()); if (pushConn == null) { sendHttpResponse(ctx, request, HttpResponseStatus.NOT_FOUND, userAuth); logClientNotConnected(); return; } if (!verifySecureToken(request, pushConn)) { sendHttpResponse(ctx, request, HttpResponseStatus.FORBIDDEN, userAuth); logSecurityTokenVerificationFail(); return; } if (Objects.equals(method, HttpMethod.GET)) { // client only checking if particular CID + ESN is connected to this instance sendHttpResponse(ctx, request, HttpResponseStatus.OK, userAuth); return; } if (pushConn.isRateLimited()) { sendHttpResponse(ctx, request, HttpResponseStatus.SERVICE_UNAVAILABLE, userAuth); logRateLimited(); return; } ByteBuf body = request.content().retain(); if (body.readableBytes() <= 0) { sendHttpResponse(ctx, request, HttpResponseStatus.NO_CONTENT, userAuth); // Because we are not passing the body to the pushConn (who would normally handle destroying), // we need to release it here. ReferenceCountUtil.release(body); return; } logPushEventBody(request, body); ChannelFuture clientFuture = pushConn.sendPushMessage(body); clientFuture.addListener(cf -> { HttpResponseStatus status; if (cf.isSuccess()) { logPushSuccess(); status = HttpResponseStatus.OK; } else { logPushError(cf.cause()); status = HttpResponseStatus.INTERNAL_SERVER_ERROR; } sendHttpResponse(ctx, request, status, userAuth); }); } else { // Last handler in the chain sendHttpResponse(ctx, request, HttpResponseStatus.BAD_REQUEST, null); } } protected void logPushAttempt() { logger.debug("pushing notification"); } protected void logNoIdentity() { logger.debug("push notification missing identity"); } protected void logClientNotConnected() { logger.debug("push notification, client not connected"); } protected void logPushSuccess() { logger.debug("push notification success"); } protected void logPushError(Throwable t) { logger.debug("pushing notification error", t); } protected void logRateLimited() { logger.warn("Push message was rejected because of the rate limiting"); } protected void logSecurityTokenVerificationFail() { logger.warn("Push secure token verification failed"); } protected void logPushEvent(FullHttpRequest request, HttpResponseStatus status, PushUserAuth userAuth) { logger.debug("Push notification status: {}, auth: {}", status.code(), userAuth != null ? userAuth : "-"); } protected void logPushEventBody(FullHttpRequest request, ByteBuf body) { logger.debug("push event body"); } protected abstract PushUserAuth getPushUserAuth(FullHttpRequest request); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushMessageSenderInitializer.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; /** * Author: Susheel Aroskar * Date: 5/16/18 */ public abstract class PushMessageSenderInitializer extends ChannelInitializer { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); addPushMessageHandlers(pipeline); } protected abstract void addPushMessageHandlers(ChannelPipeline pipeline); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushProtocol.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import com.google.common.base.Charsets; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; /** * Created by saroskar on 10/10/16. */ public enum PushProtocol { WEBSOCKET { @Override // The alternative object for HANDSHAKE_COMPLETE is not publicly visible, so disable deprecation warnings. In // the future, it may be possible to not fire this even and remove the suppression. @SuppressWarnings("deprecation") public Object getHandshakeCompleteEvent() { return WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE; } @Override public String getPath() { return "/ws"; } @Override public ChannelFuture sendPushMessage(ChannelHandlerContext ctx, ByteBuf mesg) { TextWebSocketFrame wsf = new TextWebSocketFrame(mesg); return ctx.channel().writeAndFlush(wsf); } @Override public ChannelFuture sendPing(ChannelHandlerContext ctx) { return ctx.channel().writeAndFlush(new PingWebSocketFrame()); } @Override public Object goAwayMessage() { return new TextWebSocketFrame("_CLOSE_"); } @Override public Object serverClosingConnectionMessage(int statusCode, String reasonText) { return new CloseWebSocketFrame(statusCode, reasonText); } }, SSE { private static final String SSE_HANDSHAKE_COMPLETE_EVENT = "sse_handshake_complete"; @Override public Object getHandshakeCompleteEvent() { return SSE_HANDSHAKE_COMPLETE_EVENT; } @Override public String getPath() { return "/sse"; } private static final String SSE_PREAMBLE = "event: push\r\ndata: "; private static final String SSE_TERMINATION = "\r\n\r\n"; @Override public ChannelFuture sendPushMessage(ChannelHandlerContext ctx, ByteBuf mesg) { ByteBuf newBuff = ctx.alloc().buffer(); newBuff.ensureWritable(SSE_PREAMBLE.length()); newBuff.writeCharSequence(SSE_PREAMBLE, Charsets.UTF_8); newBuff.ensureWritable(mesg.writableBytes()); newBuff.writeBytes(mesg); newBuff.ensureWritable(SSE_TERMINATION.length()); newBuff.writeCharSequence(SSE_TERMINATION, Charsets.UTF_8); mesg.release(); return ctx.channel().writeAndFlush(newBuff); } private static final String SSE_PING = "event: ping\r\ndata: ping\r\n\r\n"; @Override public ChannelFuture sendPing(ChannelHandlerContext ctx) { ByteBuf newBuff = ctx.alloc().buffer(); newBuff.ensureWritable(SSE_PING.length()); newBuff.writeCharSequence(SSE_PING, Charsets.UTF_8); return ctx.channel().writeAndFlush(newBuff); } @Override public Object goAwayMessage() { return "event: goaway\r\ndata: _CLOSE_\r\n\r\n"; } @Override public Object serverClosingConnectionMessage(int statusCode, String reasonText) { return "event: close\r\ndata: " + statusCode + " " + reasonText + "\r\n\r\n"; } }; public final void sendErrorAndClose(ChannelHandlerContext ctx, int statusCode, String reasonText) { Object mesg = serverClosingConnectionMessage(statusCode, reasonText); ctx.writeAndFlush(mesg).addListener(ChannelFutureListener.CLOSE); } public abstract Object getHandshakeCompleteEvent(); public abstract String getPath(); public abstract ChannelFuture sendPushMessage(ChannelHandlerContext ctx, ByteBuf mesg); public abstract ChannelFuture sendPing(ChannelHandlerContext ctx); /** * Application level protocol for asking client to close connection * @return WebSocketFrame which when sent to client will cause it to close the WebSocket */ public abstract Object goAwayMessage(); /** * Message server sends to the client just before it force closes connection from its side */ public abstract Object serverClosingConnectionMessage(int statusCode, String reasonText); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushRegistrationHandler.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import com.google.common.annotations.VisibleForTesting; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.config.CachedDynamicIntProperty; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.util.concurrent.ScheduledFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Author: Susheel Aroskar * Date: 5/14/18 */ public class PushRegistrationHandler extends ChannelInboundHandlerAdapter { protected final PushConnectionRegistry pushConnectionRegistry; protected final PushProtocol pushProtocol; /* Identity */ protected volatile PushUserAuth authEvent; /* state */ protected final AtomicBoolean destroyed; private ChannelHandlerContext ctx; private volatile PushConnection pushConnection; private final List> scheduledFutures; public static final CachedDynamicIntProperty PUSH_REGISTRY_TTL = new CachedDynamicIntProperty("zuul.push.registry.ttl.seconds", 30 * 60); public static final CachedDynamicIntProperty RECONNECT_DITHER = new CachedDynamicIntProperty("zuul.push.reconnect.dither.seconds", 3 * 60); public static final CachedDynamicIntProperty UNAUTHENTICATED_CONN_TTL = new CachedDynamicIntProperty("zuul.push.noauth.ttl.seconds", 8); public static final CachedDynamicIntProperty CLIENT_CLOSE_GRACE_PERIOD = new CachedDynamicIntProperty("zuul.push.client.close.grace.period", 4); public static final CachedDynamicBooleanProperty KEEP_ALIVE_ENABLED = new CachedDynamicBooleanProperty("zuul.push.keepalive.enabled", true); public static final CachedDynamicIntProperty KEEP_ALIVE_INTERVAL = new CachedDynamicIntProperty("zuul.push.keepalive.interval.seconds", 3 * 60); private static final Logger logger = LoggerFactory.getLogger(PushRegistrationHandler.class); public PushRegistrationHandler(PushConnectionRegistry pushConnectionRegistry, PushProtocol pushProtocol) { this.pushConnectionRegistry = pushConnectionRegistry; this.pushProtocol = pushProtocol; this.destroyed = new AtomicBoolean(); this.scheduledFutures = Collections.synchronizedList(new ArrayList<>()); } protected final boolean isAuthenticated() { return (authEvent != null && authEvent.isSuccess()); } protected void tearDown() { if (!destroyed.getAndSet(true)) { if (authEvent != null) { // We should only remove the PushConnection entry from the registry if it's still this pushConnection. String clientID = authEvent.getClientIdentity(); PushConnection savedPushConnection = pushConnectionRegistry.get(clientID); if (savedPushConnection != null && savedPushConnection == pushConnection) { pushConnectionRegistry.remove(authEvent.getClientIdentity()); logger.debug("Removed connection from registry for {}", authEvent); } logger.debug("Closing connection for {}", authEvent); } } scheduledFutures.forEach(f -> f.cancel(false)); scheduledFutures.clear(); } @Override public final void channelInactive(ChannelHandlerContext ctx) throws Exception { tearDown(); super.channelInactive(ctx); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.error("Exception caught, closing push channel for {}", authEvent, cause); ctx.close(); } protected final void forceCloseConnectionFromServerSide() { if (!destroyed.get()) { logger.debug("server forcing close connection"); pushProtocol.sendErrorAndClose(ctx, 1000, "Server closed connection"); } } private void closeIfNotAuthenticated() { if (!isAuthenticated()) { logger.debug( "Closing connection because it is still unauthenticated after {} seconds.", UNAUTHENTICATED_CONN_TTL.get()); forceCloseConnectionFromServerSide(); } } private void requestClientToCloseConnection() { if (ctx.channel().isActive()) { // Application level protocol for asking client to close connection ctx.writeAndFlush(pushProtocol.goAwayMessage()); // Force close connection if client doesn't close in reasonable time after we made request scheduledFutures.add(ctx.executor() .schedule( this::forceCloseConnectionFromServerSide, CLIENT_CLOSE_GRACE_PERIOD.get(), TimeUnit.SECONDS)); } else { forceCloseConnectionFromServerSide(); } } protected void keepAlive() { if (KEEP_ALIVE_ENABLED.get()) { ctx.writeAndFlush(new PingWebSocketFrame()); } } private int ditheredReconnectDeadline() { int dither = ThreadLocalRandom.current().nextInt(RECONNECT_DITHER.get()); return PUSH_REGISTRY_TTL.get() - dither - CLIENT_CLOSE_GRACE_PERIOD.get(); } @Override public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { this.ctx = ctx; if (!destroyed.get()) { if (evt == pushProtocol.getHandshakeCompleteEvent()) { pushConnection = new PushConnection(pushProtocol, ctx); // Unauthenticated connection, wait for small amount of time for a client to send auth token in // a first web socket frame, otherwise close connection ctx.executor() .schedule(this::closeIfNotAuthenticated, UNAUTHENTICATED_CONN_TTL.get(), TimeUnit.SECONDS); logger.debug("WebSocket handshake complete."); } else if (evt instanceof PushUserAuth) { authEvent = (PushUserAuth) evt; if (authEvent.isSuccess() && (pushConnection != null)) { logger.debug("registering client {}", authEvent); ctx.pipeline().remove(PushAuthHandler.NAME); registerClient(ctx, authEvent, pushConnection, pushConnectionRegistry); logger.debug("Authentication complete {}", authEvent); } else { logger.error( "Push registration failed: Auth success={}, WS handshake success={}", authEvent.isSuccess(), pushConnection != null); if (pushConnection != null) { pushProtocol.sendErrorAndClose(ctx, 1008, "Auth failed"); } } } } super.userEventTriggered(ctx, evt); } protected int getKeepAliveInterval() { return KEEP_ALIVE_INTERVAL.get(); } /** * Register authenticated client - represented by PushAuthEvent - with PushConnectionRegistry of this instance. * * For all but really simplistic case - basically anything other than a single node push cluster, You'd most likely * need some sort of off-box, partitioned, global registration registry that keeps track of which client is connected * to which push server instance. You should override this default implementation for such cases and register your * client with your global registry in addition to local push connection registry that is limited to this JVM instance * Make sure such a registration is done in strictly non-blocking fashion lest you will block Netty event loop * decimating your throughput. * * A typical arrangement is to use something like Memcached or redis cluster sharded by client connection key and * to use blocking Memcached/redis driver in a background thread-pool to do the actual registration so that Netty * event loop doesn't block */ protected void registerClient( ChannelHandlerContext ctx, PushUserAuth authEvent, PushConnection conn, PushConnectionRegistry registry) { registry.put(authEvent.getClientIdentity(), conn); // Make client reconnect after ttl seconds by closing this connection to limit stickiness of the client scheduledFutures.add(ctx.executor() .schedule(this::requestClientToCloseConnection, ditheredReconnectDeadline(), TimeUnit.SECONDS)); if (KEEP_ALIVE_ENABLED.get()) { scheduledFutures.add(ctx.executor() .scheduleWithFixedDelay( this::keepAlive, getKeepAliveInterval(), getKeepAliveInterval(), TimeUnit.SECONDS)); } } @VisibleForTesting PushConnection getPushConnection() { return pushConnection; } @VisibleForTesting List> getScheduledFutures() { return scheduledFutures; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/push/PushUserAuth.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; public interface PushUserAuth { boolean isSuccess(); int statusCode(); String getClientIdentity(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/server/ssl/SslHandshakeInfoHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.ssl; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.netflix.config.DynamicBooleanProperty; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.ssl.SslHandshakeInfo; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Tag; import com.netflix.zuul.netty.ChannelUtils; import com.netflix.zuul.netty.server.psk.ClientPSKIdentityInfo; import com.netflix.zuul.netty.server.psk.TlsPskHandler; import com.netflix.zuul.netty.server.psk.ZuulPskServer; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SniCompletionEvent; import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.util.AttributeKey; import java.nio.channels.ClosedChannelException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Stores info about the client and server's SSL certificates in the context, after a successful handshake. *

* User: michaels@netflix.com Date: 3/29/16 Time: 10:48 AM */ public class SslHandshakeInfoHandler extends ChannelInboundHandlerAdapter { public static final AttributeKey ATTR_SSL_INFO = AttributeKey.newInstance("_ssl_handshake_info"); // optionally set by an outbound handler that parses the TLS ServerHello key_share extension public static final AttributeKey ATTR_SSL_NAMED_GROUP = AttributeKey.newInstance("_ssl_named_group"); private static final Logger logger = LoggerFactory.getLogger(SslHandshakeInfoHandler.class); // extracts reason string from SSL errors formatted in the open ssl style // error:[error code]:[library name]:OPENSSL_internal:[reason string] // see https://github.com/google/boringssl/blob/d206f3db6ac2b74e8949ddd9947b94a5424d6a1d/include/openssl/err.h#L231 private static final Pattern OPEN_SSL_PATTERN = Pattern.compile("OPENSSL_internal:(.+)"); static DynamicBooleanProperty SNI_LOGGING_ENABLED = new DynamicBooleanProperty("zuul.ssl.handshake.snilogging.enabled", false); private final Registry spectatorRegistry; private final boolean isSSlFromIntermediary; public SslHandshakeInfoHandler(Registry spectatorRegistry, boolean isSSlFromIntermediary) { this.spectatorRegistry = Preconditions.checkNotNull(spectatorRegistry); this.isSSlFromIntermediary = isSSlFromIntermediary; } @VisibleForTesting SslHandshakeInfoHandler() { spectatorRegistry = new NoopRegistry(); isSSlFromIntermediary = false; } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof SslHandshakeCompletionEvent) { try { SslHandshakeCompletionEvent sslEvent = (SslHandshakeCompletionEvent) evt; if (sslEvent.isSuccess()) { CurrentPassport.fromChannel(ctx.channel()).add(PassportState.SERVER_CH_SSL_HANDSHAKE_COMPLETE); SSLSession session = getSSLSession(ctx); if (session == null) { logger.warn("Error getting the SSL handshake info. SSLSession is null"); return; } ClientAuth clientAuth = whichClientAuthEnum(ctx); Certificate serverCert = null; X509Certificate peerCert = null; if ((clientAuth == ClientAuth.REQUIRE || clientAuth == ClientAuth.OPTIONAL) && session.getPeerCertificates() != null && session.getPeerCertificates().length > 0) { peerCert = (X509Certificate) session.getPeerCertificates()[0]; } if (session.getLocalCertificates() != null && session.getLocalCertificates().length > 0) { serverCert = session.getLocalCertificates()[0]; } // if attribute is true, then true. If null or false then false boolean tlsHandshakeUsingExternalPSK = Objects.equals( ctx.channel() .attr(ZuulPskServer.TLS_HANDSHAKE_USING_EXTERNAL_PSK) .get(), Boolean.TRUE); ClientPSKIdentityInfo clientPSKIdentityInfo = ctx.channel() .attr(TlsPskHandler.CLIENT_PSK_IDENTITY_ATTRIBUTE_KEY) .get(); String requestedSni = "none"; try { List serverNames = ((ExtendedSSLSession) session).getRequestedServerNames(); if (serverNames != null) { requestedSni = serverNames.stream() .filter(sni -> sni instanceof SNIHostName) .findFirst() .map(sni -> ((SNIHostName) sni).getAsciiName()) .orElse("none"); } } catch (Exception e) { logger.warn("Error getting the request server names.", e); } SslHandshakeInfo info = SslHandshakeInfo.builder() .requestedSni(requestedSni) .isOfIntermediary(isSSlFromIntermediary) .protocol(session.getProtocol()) .cipherSuite(session.getCipherSuite()) .namedGroup(ctx.channel().attr(ATTR_SSL_NAMED_GROUP).get()) .clientAuthRequirement(clientAuth) .serverCertificate(serverCert) .clientCertificate(peerCert) .usingExternalPSK(tlsHandshakeUsingExternalPSK) .clientPSKIdentityInfo(clientPSKIdentityInfo) .build(); ctx.channel().attr(ATTR_SSL_INFO).set(info); // Metrics. incrementCounters(sslEvent, info); logger.debug("Successful SSL Handshake: {}", info); } else { String clientIP = ctx.channel() .attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS) .get(); Throwable cause = sslEvent.cause(); PassportState passportState = CurrentPassport.fromChannel(ctx.channel()).getState(); if (cause instanceof ClosedChannelException && (passportState == PassportState.SERVER_CH_INACTIVE || passportState == PassportState.SERVER_CH_IDLE_TIMEOUT)) { // Either client closed the connection without/before having completed a handshake, or // the connection idle timed-out before handshake. // NOTE: we were seeing a lot of these in prod and can repro by just telnetting to port and then // closing terminal // without sending anything. // So don't treat these as SSL handshake failures. logger.debug( "Client closed connection or it idle timed-out without doing an ssl handshake. ," + " client_ip = {}, channel_info = {}", clientIP, ChannelUtils.channelInfoForLogging(ctx.channel())); } else if (cause instanceof SSLException && cause.getMessage().contains("handshake timed out")) { logger.debug( "Client timed-out doing the ssl handshake. , client_ip = {}, channel_info = {}", clientIP, ChannelUtils.channelInfoForLogging(ctx.channel())); } else if (cause instanceof SSLException && cause.getMessage().contains("failure when writing TLS control frames")) { // This can happen if the ClientHello is sent followed by an RST packet, before we can respond. logger.debug( "Client terminated handshake early., client_ip = {}, channel_info = {}", clientIP, ChannelUtils.channelInfoForLogging(ctx.channel())); } else { if (logger.isDebugEnabled()) { String msg = "Unsuccessful SSL Handshake: " + sslEvent + ", client_ip = " + clientIP + ", channel_info = " + ChannelUtils.channelInfoForLogging(ctx.channel()) + ", error = " + cause; if (cause instanceof ClosedChannelException) { logger.debug(msg); } else { logger.debug(msg, cause); } } SslHandshakeInfo info = null; SSLSession session = getSSLSession(ctx); if (session != null) { List serverNames = ((ExtendedSSLSession) session).getRequestedServerNames(); String requestedSni = serverNames.stream() .filter(sni -> sni instanceof SNIHostName) .findFirst() .map(sni -> ((SNIHostName) sni).getAsciiName()) .orElse("none"); info = SslHandshakeInfo.builder() .requestedSni(requestedSni) .isOfIntermediary(isSSlFromIntermediary) .build(); } incrementCounters(sslEvent, info); } } } catch (Throwable e) { logger.warn("Error getting the SSL handshake info.", e); } finally { // Now remove this handler from the pipeline as no longer needed once the ssl handshake has completed. ctx.pipeline().remove(this); } } else if (evt instanceof SslCloseCompletionEvent) { // TODO - increment a separate metric for this event? } else if (evt instanceof SniCompletionEvent) { logger.debug("SNI Parsing Complete: {}", evt); SniCompletionEvent sniCompletionEvent = (SniCompletionEvent) evt; if (sniCompletionEvent.isSuccess()) { spectatorRegistry.counter("zuul.sni.parse.success").increment(); } else { Throwable cause = sniCompletionEvent.cause(); spectatorRegistry .counter("zuul.sni.parse.failure", "cause", cause != null ? cause.getMessage() : "UNKNOWN") .increment(); } } super.userEventTriggered(ctx, evt); } private SSLSession getSSLSession(ChannelHandlerContext ctx) { SslHandler sslhandler = ctx.channel().pipeline().get(SslHandler.class); if (sslhandler != null) { return sslhandler.engine().getSession(); } TlsPskHandler tlsPskHandler = ctx.channel().pipeline().get(TlsPskHandler.class); if (tlsPskHandler != null) { return tlsPskHandler.getSession(); } return null; } private ClientAuth whichClientAuthEnum(ChannelHandlerContext ctx) { SslHandler sslhandler = ctx.channel().pipeline().get(SslHandler.class); if (sslhandler == null) { return ClientAuth.NONE; } ClientAuth clientAuth; if (sslhandler.engine().getNeedClientAuth()) { clientAuth = ClientAuth.REQUIRE; } else if (sslhandler.engine().getWantClientAuth()) { clientAuth = ClientAuth.OPTIONAL; } else { clientAuth = ClientAuth.NONE; } return clientAuth; } private void incrementCounters( SslHandshakeCompletionEvent sslHandshakeCompletionEvent, SslHandshakeInfo handshakeInfo) { try { List tagList = new ArrayList<>(); if (sslHandshakeCompletionEvent.isSuccess()) { tagList.add(Tag.of( "protocol", handshakeInfo.getProtocol().isEmpty() ? "unknown" : handshakeInfo.getProtocol())); tagList.add(Tag.of( "ciphersuite", handshakeInfo.getCipherSuite().isEmpty() ? "unknown" : handshakeInfo.getCipherSuite())); tagList.add(Tag.of("clientauth", String.valueOf(handshakeInfo.getClientAuthRequirement()))); tagList.add(Tag.of("namedgroup", Objects.requireNonNullElse(handshakeInfo.getNamedGroup(), "unknown"))); } else { tagList.add(Tag.of("failure_cause", getFailureCause(sslHandshakeCompletionEvent.cause()))); } tagList.add(Tag.of("success", String.valueOf(sslHandshakeCompletionEvent.isSuccess()))); if (SNI_LOGGING_ENABLED.get()) { tagList.add(Tag.of("sni", handshakeInfo.getRequestedSni())); } spectatorRegistry.counter("server.ssl.handshake", tagList).increment(); } catch (Exception e) { logger.error("Error incrementing counters for SSL handshake!", e); } } @VisibleForTesting String getFailureCause(Throwable throwable) { String message = throwable.getMessage(); if (message == null) { return throwable.toString(); } Matcher matcher = OPEN_SSL_PATTERN.matcher(message); return matcher.find() ? matcher.group(1) : message; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/ssl/BaseSslContextFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ssl; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.errorprone.annotations.ForOverride; import com.netflix.config.DynamicBooleanProperty; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.patterns.PolledMeter; import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.OpenSslContextOption; import io.netty.handler.ssl.OpenSslSessionStats; import io.netty.handler.ssl.ReferenceCountedOpenSslContext; 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 java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Base64; import java.util.Enumeration; import java.util.List; import java.util.Objects; import java.util.function.ToDoubleFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 3/4/16 * Time: 4:00 PM */ public class BaseSslContextFactory implements SslContextFactory { private static final Logger LOG = LoggerFactory.getLogger(BaseSslContextFactory.class); private static final DynamicBooleanProperty ALLOW_USE_OPENSSL = new DynamicBooleanProperty("zuul.ssl.openssl.allow", true); // matches Netty's OpenSSL defaults (@see io.netty.handler.ssl.OpenSsl) private static final String[] DEFAULT_NAMED_GROUPS = {"x25519", "secp256r1", "secp384r1", "secp521r1"}; static { // Install BouncyCastle provider. java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } protected final Registry spectatorRegistry; protected final ServerSslConfig serverSslConfig; public BaseSslContextFactory(Registry spectatorRegistry, ServerSslConfig serverSslConfig) { this.spectatorRegistry = Objects.requireNonNull(spectatorRegistry); this.serverSslConfig = Objects.requireNonNull(serverSslConfig); } @Override public SslContextBuilder createBuilderForServer() { try { List trustedCerts = getTrustedX509Certificates(); SslProvider sslProvider = chooseSslProvider(); LOG.debug("Using SslProvider of type {}", sslProvider.name()); SslContextBuilder builder = newBuilderForServer() .ciphers(getCiphers(), getCiphersFilter()) .sessionTimeout(serverSslConfig.getSessionTimeout()) .sslProvider(sslProvider) .option(OpenSslContextOption.GROUPS, getNamedGroups()); if (serverSslConfig.getClientAuth() != null && trustedCerts != null && !trustedCerts.isEmpty()) { builder = builder.trustManager(trustedCerts.toArray(new X509Certificate[0])) .clientAuth(serverSslConfig.getClientAuth()); } return builder; } catch (Exception e) { throw new RuntimeException("Error configuring SslContext!", e); } } /** * This function is meant to call the correct overload of {@code SslContextBuilder.forServer()}. It should not * apply any other customization. */ @ForOverride protected SslContextBuilder newBuilderForServer() throws IOException { LOG.debug("Using certChainFile {}", serverSslConfig.getCertChainFile()); try (InputStream keyInput = getKeyInputStream(); InputStream certChainInput = new FileInputStream(serverSslConfig.getCertChainFile())) { return SslContextBuilder.forServer(certChainInput, keyInput); } } @Override public void enableSessionTickets(SslContext sslContext) { // TODO } @Override public void configureOpenSslStatsMetrics(SslContext sslContext, String sslContextId) { // Setup metrics tracking the OpenSSL stats. if (sslContext instanceof ReferenceCountedOpenSslContext) { OpenSslSessionStats stats = ((ReferenceCountedOpenSslContext) sslContext) .sessionContext() .stats(); openSslStatGauge(stats, sslContextId, "accept", OpenSslSessionStats::accept); openSslStatGauge(stats, sslContextId, "accept_good", OpenSslSessionStats::acceptGood); openSslStatGauge(stats, sslContextId, "accept_renegotiate", OpenSslSessionStats::acceptRenegotiate); openSslStatGauge(stats, sslContextId, "number", OpenSslSessionStats::number); openSslStatGauge(stats, sslContextId, "connect", OpenSslSessionStats::connect); openSslStatGauge(stats, sslContextId, "connect_good", OpenSslSessionStats::connectGood); openSslStatGauge(stats, sslContextId, "connect_renegotiate", OpenSslSessionStats::connectRenegotiate); openSslStatGauge(stats, sslContextId, "hits", OpenSslSessionStats::hits); openSslStatGauge(stats, sslContextId, "cb_hits", OpenSslSessionStats::cbHits); openSslStatGauge(stats, sslContextId, "misses", OpenSslSessionStats::misses); openSslStatGauge(stats, sslContextId, "timeouts", OpenSslSessionStats::timeouts); openSslStatGauge(stats, sslContextId, "cache_full", OpenSslSessionStats::cacheFull); openSslStatGauge(stats, sslContextId, "ticket_key_fail", OpenSslSessionStats::ticketKeyFail); openSslStatGauge(stats, sslContextId, "ticket_key_new", OpenSslSessionStats::ticketKeyNew); openSslStatGauge(stats, sslContextId, "ticket_key_renew", OpenSslSessionStats::ticketKeyRenew); openSslStatGauge(stats, sslContextId, "ticket_key_resume", OpenSslSessionStats::ticketKeyResume); } } private void openSslStatGauge( OpenSslSessionStats stats, String sslContextId, String statName, ToDoubleFunction value) { Id id = spectatorRegistry.createId("server.ssl.stats", "id", sslContextId, "stat", statName); PolledMeter.using(spectatorRegistry).withId(id).monitorValue(stats, value); LOG.debug("Registered spectator gauge - {}", id.name()); } public static SslProvider chooseSslProvider() { // Use openssl only if available and has ALPN support (ie. version > 1.0.2). SslProvider sslProvider; if (ALLOW_USE_OPENSSL.get() && OpenSsl.isAvailable() && SslProvider.isAlpnSupported(SslProvider.OPENSSL)) { sslProvider = SslProvider.OPENSSL; } else { sslProvider = SslProvider.JDK; } return sslProvider; } public ServerSslConfig getServerSslConfig() { return serverSslConfig; } @Override public String[] getProtocols() { return serverSslConfig.getProtocols(); } @Override public List getCiphers() throws NoSuchAlgorithmException { return serverSslConfig.getCiphers(); } protected CipherSuiteFilter getCiphersFilter() { return SupportedCipherSuiteFilter.INSTANCE; } protected String[] getNamedGroups() { return DEFAULT_NAMED_GROUPS; } protected List getTrustedX509Certificates() throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException { ArrayList trustedCerts = new ArrayList<>(); // Add the certificates from the JKS truststore - ie. the CA's of the client cert that peer Zuul's will use. if (serverSslConfig.getClientAuth() == ClientAuth.REQUIRE || serverSslConfig.getClientAuth() == ClientAuth.OPTIONAL) { // Get the encrypted bytes of the truststore password. byte[] trustStorePwdBytes; if (serverSslConfig.getClientAuthTrustStorePassword() != null) { trustStorePwdBytes = Base64.getDecoder().decode(serverSslConfig.getClientAuthTrustStorePassword()); } else if (serverSslConfig.getClientAuthTrustStorePasswordFile() != null) { trustStorePwdBytes = Files.readAllBytes( serverSslConfig.getClientAuthTrustStorePasswordFile().toPath()); } else { throw new IllegalArgumentException( "Must specify either ClientAuthTrustStorePassword or ClientAuthTrustStorePasswordFile!"); } // Decrypt the truststore password. String trustStorePassword = getTruststorePassword(trustStorePwdBytes); boolean dumpDecryptedTrustStorePassword = false; if (dumpDecryptedTrustStorePassword) { LOG.debug("X509Cert Trust Store Password {}", trustStorePassword); } KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load( new FileInputStream(serverSslConfig.getClientAuthTrustStoreFile()), trustStorePassword.toCharArray()); Enumeration aliases = trustStore.aliases(); while (aliases.hasMoreElements()) { X509Certificate cert = (X509Certificate) trustStore.getCertificate(aliases.nextElement()); trustedCerts.add(cert); } } return trustedCerts; } /** * Can be overridden to implement your own decryption scheme. * */ protected String getTruststorePassword(byte[] trustStorePwdBytes) { return new String(trustStorePwdBytes, UTF_8).trim(); } /** * Can be overridden to implement your own decryption scheme. */ protected InputStream getKeyInputStream() throws IOException { return new FileInputStream(serverSslConfig.getKeyFile()); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/ssl/ClientSslContextFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ssl; import com.netflix.config.DynamicBooleanProperty; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.spectator.api.Registry; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Client Ssl Context Factory * * Author: Arthur Gonigberg * Date: May 14, 2018 */ public final class ClientSslContextFactory extends BaseSslContextFactory { private static final DynamicBooleanProperty ENABLE_CLIENT_TLS13 = new DynamicBooleanProperty("com.netflix.zuul.netty.ssl.enable_tls13", false); private static final Logger log = LoggerFactory.getLogger(ClientSslContextFactory.class); private static final ServerSslConfig DEFAULT_CONFIG = ServerSslConfig.builder() .protocols(maybeAddTls13(ENABLE_CLIENT_TLS13.get(), "TLSv1.2")) .ciphers(ServerSslConfig.getDefaultCiphers()) .build(); public ClientSslContextFactory(Registry spectatorRegistry) { super(spectatorRegistry, DEFAULT_CONFIG); } public ClientSslContextFactory(Registry spectatorRegistry, ServerSslConfig serverSslConfig) { super(spectatorRegistry, serverSslConfig); } public SslContext getClientSslContext() { try { return SslContextBuilder.forClient() .sslProvider(chooseSslProvider()) .ciphers(getCiphers(), getCiphersFilter()) .protocols(getProtocols()) .build(); } catch (Exception e) { log.error("Error loading SslContext client request.", e); throw new RuntimeException("Error configuring SslContext for client request!", e); } } static String[] maybeAddTls13(boolean enableTls13, String... defaultProtocols) { if (enableTls13) { String[] protocols = new String[defaultProtocols.length + 1]; System.arraycopy(defaultProtocols, 0, protocols, 1, defaultProtocols.length); protocols[0] = "TLSv1.3"; return protocols; } else { return defaultProtocols; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/ssl/SslContextFactory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ssl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import java.security.NoSuchAlgorithmException; import java.util.List; /** * User: michaels@netflix.com * Date: 11/8/16 * Time: 1:01 PM */ public interface SslContextFactory { SslContextBuilder createBuilderForServer(); String[] getProtocols(); List getCiphers() throws NoSuchAlgorithmException; void enableSessionTickets(SslContext sslContext); void configureOpenSslStatsMetrics(SslContext sslContext, String sslContextId); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/timeouts/HttpHeadersTimeoutHandler.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.timeouts; import com.google.common.annotations.VisibleForTesting; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.histogram.PercentileTimer; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpMessage; import io.netty.util.AttributeKey; import io.netty.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; import java.util.function.IntSupplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HttpHeadersTimeoutHandler { private static final Logger LOG = LoggerFactory.getLogger(HttpHeadersTimeoutHandler.class); @VisibleForTesting static final AttributeKey> HTTP_HEADERS_READ_TIMEOUT_FUTURE = AttributeKey.newInstance("httpHeadersReadTimeoutFuture"); @VisibleForTesting static final AttributeKey HTTP_HEADERS_READ_START_TIME = AttributeKey.newInstance("httpHeadersReadStartTime"); public static class InboundHandler extends ChannelInboundHandlerAdapter { private final BooleanSupplier httpHeadersReadTimeoutEnabledSupplier; private final IntSupplier httpHeadersReadTimeoutSupplier; private final Counter httpHeadersReadTimeoutCounter; private final PercentileTimer httpHeadersReadTimer; private boolean closed = false; public InboundHandler( BooleanSupplier httpHeadersReadTimeoutEnabledSupplier, IntSupplier httpHeadersReadTimeoutSupplier, Counter httpHeadersReadTimeoutCounter, PercentileTimer httpHeadersReadTimer) { this.httpHeadersReadTimeoutEnabledSupplier = httpHeadersReadTimeoutEnabledSupplier; this.httpHeadersReadTimeoutSupplier = httpHeadersReadTimeoutSupplier; this.httpHeadersReadTimeoutCounter = httpHeadersReadTimeoutCounter; this.httpHeadersReadTimer = httpHeadersReadTimer; } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { try { ctx.channel().attr(HTTP_HEADERS_READ_START_TIME).set(System.nanoTime()); if (!httpHeadersReadTimeoutEnabledSupplier.getAsBoolean()) return; int timeout = httpHeadersReadTimeoutSupplier.getAsInt(); ctx.channel() .attr(HTTP_HEADERS_READ_TIMEOUT_FUTURE) .set(ctx.executor() .schedule( () -> { if (!closed) { ctx.close(); // triggers channelInactive -> destroy closed = true; if (httpHeadersReadTimeoutCounter != null) httpHeadersReadTimeoutCounter.increment(); LOG.debug( "[{}] HTTP headers read timeout handler timed out", ctx.channel().id()); } return null; }, timeout, TimeUnit.MILLISECONDS)); LOG.debug( "[{}] Adding HTTP headers read timeout handler: {}", ctx.channel().id(), timeout); } finally { super.channelActive(ctx); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { if (msg instanceof HttpMessage) { Long readStartTime = ctx.channel().attr(HTTP_HEADERS_READ_START_TIME).get(); if (httpHeadersReadTimer != null && readStartTime != null) httpHeadersReadTimer.record(System.nanoTime() - readStartTime, TimeUnit.NANOSECONDS); ctx.pipeline().remove(this); // triggers handlerRemoved -> destroy } } finally { super.channelRead(ctx, msg); } } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { destroy(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { destroy(ctx); super.channelInactive(ctx); } private void destroy(ChannelHandlerContext ctx) { ScheduledFuture future = ctx.channel().attr(HTTP_HEADERS_READ_TIMEOUT_FUTURE).get(); if (future != null) { future.cancel(false); ctx.channel().attr(HTTP_HEADERS_READ_TIMEOUT_FUTURE).set(null); ctx.channel().attr(HTTP_HEADERS_READ_START_TIME).set(null); LOG.debug( "[{}] Removing HTTP headers read timeout handler", ctx.channel().id()); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/netty/timeouts/OriginTimeoutManager.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.timeouts; import com.google.common.annotations.VisibleForTesting; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfig; import com.netflix.config.DynamicLongProperty; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.origins.NettyOrigin; import java.time.Duration; import java.util.Objects; import java.util.Optional; import javax.annotation.Nullable; /** * Origin Timeout Manager * * @author Arthur Gonigberg * @since February 24, 2021 */ public class OriginTimeoutManager { private final NettyOrigin origin; public OriginTimeoutManager(NettyOrigin origin) { this.origin = Objects.requireNonNull(origin); } @VisibleForTesting static final DynamicLongProperty MAX_OUTBOUND_READ_TIMEOUT_MS = new DynamicLongProperty( "zuul.origin.readtimeout.max", Duration.ofSeconds(90).toMillis()); /** * Derives the read timeout from the configuration. This implementation prefers the longer of either the origin * timeout or the request timeout. *

* This method can also be used to validate timeout and deadline boundaries and throw exceptions as needed. If * extending this method to do validation, you should extend {@link com.netflix.zuul.exception.OutboundException} * and set the appropriate {@link com.netflix.zuul.exception.ErrorType}. * * @param request the request. * @param attemptNum the attempt number, starting at 1. */ public Duration computeReadTimeout(HttpRequestMessage request, int attemptNum) { IClientConfig clientConfig = getRequestClientConfig(request); Long originTimeout = getOriginReadTimeout(); Long requestTimeout = getRequestReadTimeout(clientConfig); long computedTimeout; if (originTimeout == null && requestTimeout == null) { computedTimeout = MAX_OUTBOUND_READ_TIMEOUT_MS.get(); } else if (originTimeout == null || requestTimeout == null) { computedTimeout = originTimeout == null ? requestTimeout : originTimeout; } else { // return the stricter (i.e. lower) of the two timeouts computedTimeout = Math.min(originTimeout, requestTimeout); } // enforce max timeout upperbound return Duration.ofMillis(Math.min(computedTimeout, MAX_OUTBOUND_READ_TIMEOUT_MS.get())); } /** * This method will create a new client config or retrieve the existing one from the current request. * * @param zuulRequest - the request * @return the config */ protected IClientConfig getRequestClientConfig(HttpRequestMessage zuulRequest) { IClientConfig overriddenClientConfig = zuulRequest.getContext().get(CommonContextKeys.REST_CLIENT_CONFIG); if (overriddenClientConfig == null) { overriddenClientConfig = new DefaultClientConfigImpl(); zuulRequest.getContext().put(CommonContextKeys.REST_CLIENT_CONFIG, overriddenClientConfig); } return overriddenClientConfig; } /** * This method makes the assumption that the timeout is a numeric value */ @Nullable private Long getRequestReadTimeout(IClientConfig clientConfig) { return Optional.ofNullable(clientConfig.get(CommonClientConfigKey.ReadTimeout)) .map(Long::valueOf) .orElse(null); } /** * This method makes the assumption that the timeout is a numeric value */ @Nullable private Long getOriginReadTimeout() { return Optional.ofNullable(origin.getClientConfig().get(CommonClientConfigKey.ReadTimeout)) .map(Long::valueOf) .orElse(null); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/niws/RequestAttempt.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.niws; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.netflix.appinfo.AmazonInfo; import com.netflix.appinfo.InstanceInfo; import com.netflix.client.config.IClientConfig; import com.netflix.client.config.IClientConfigKey; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.discovery.SimpleMetaInfo; import com.netflix.zuul.exception.OutboundException; import com.netflix.zuul.netty.connectionpool.OriginConnectException; import io.netty.handler.timeout.ReadTimeoutException; import java.net.InetAddress; import java.util.Locale; import javax.net.ssl.SSLHandshakeException; /** * User: michaels@netflix.com * Date: 9/2/14 * Time: 2:52 PM */ public class RequestAttempt { private static final ObjectMapper JACKSON_MAPPER = new ObjectMapper(); private int attempt; private int status; private long duration; private String cause; private String error; private String exceptionType; private String app; private String asg; private String instanceId; private String host; private int port; private String ipAddress; private String vip; private String region; private String availabilityZone; private long readTimeout; private int connectTimeout; private int maxRetries; public RequestAttempt( int attemptNumber, InstanceInfo server, InetAddress serverAddr, String targetVip, String chosenWarmupLB, int status, String error, String exceptionType, int readTimeout, int connectTimeout, int maxRetries) { if (attemptNumber < 1) { throw new IllegalArgumentException("Attempt number must be greater than 0! - " + attemptNumber); } this.attempt = attemptNumber; this.vip = targetVip; if (server != null) { this.app = server.getAppName().toLowerCase(Locale.ROOT); this.asg = server.getASGName(); this.instanceId = server.getInstanceId(); this.host = server.getHostName(); this.port = server.getPort(); // If targetVip is null, then try to use the actual server's vip. if (targetVip == null) { this.vip = server.getVIPAddress(); } if (server.getDataCenterInfo() instanceof AmazonInfo) { this.availabilityZone = ((AmazonInfo) server.getDataCenterInfo()).getMetadata().get("availability-zone"); // HACK - get region by just removing the last char from zone. String az = getAvailabilityZone(); if (az != null && az.length() > 0) { this.region = az.substring(0, az.length() - 1); } } } if (serverAddr != null) { ipAddress = serverAddr.getHostAddress(); } this.status = status; this.error = error; this.exceptionType = exceptionType; this.readTimeout = readTimeout; this.connectTimeout = connectTimeout; this.maxRetries = maxRetries; } public RequestAttempt( DiscoveryResult server, InetAddress serverAddr, IClientConfig clientConfig, int attemptNumber, int readTimeout) { this.status = -1; this.attempt = attemptNumber; this.readTimeout = readTimeout; if (server != null && !server.equals(DiscoveryResult.EMPTY)) { this.host = server.getHost(); this.port = server.getPort(); this.availabilityZone = server.getZone(); if (server.isDiscoveryEnabled()) { this.app = server.getAppName().toLowerCase(Locale.ROOT); this.asg = server.getASGName(); this.instanceId = server.getServerId(); this.host = server.getHost(); this.port = server.getPort(); this.vip = server.getTarget(); this.availabilityZone = server.getAvailabilityZone(); } else { SimpleMetaInfo metaInfo = server.getMetaInfo(); if (metaInfo != null) { this.asg = metaInfo.getServerGroup(); this.vip = metaInfo.getServiceIdForDiscovery(); this.instanceId = metaInfo.getInstanceId(); } } // HACK - get region by just removing the last char from zone. if (availabilityZone != null && availabilityZone.length() > 0) { region = availabilityZone.substring(0, availabilityZone.length() - 1); } } if (serverAddr != null) { ipAddress = serverAddr.getHostAddress(); } if (clientConfig != null) { this.connectTimeout = clientConfig.get(IClientConfigKey.Keys.ConnectTimeout); } } private RequestAttempt() {} public void complete(int responseStatus, long durationMs, Throwable exception) { if (responseStatus > -1) { setStatus(responseStatus); } this.duration = durationMs; if (exception != null) { setException(exception); } } public int getAttempt() { return attempt; } public String getVip() { return vip; } public int getStatus() { return this.status; } public long getDuration() { return this.duration; } public String getCause() { return cause; } public String getError() { return error; } public String getApp() { return app; } public String getAsg() { return asg; } public String getInstanceId() { return instanceId; } public String getHost() { return host; } public int getPort() { return port; } public String getIpAddress() { return ipAddress; } public String getRegion() { return region; } public String getAvailabilityZone() { return availabilityZone; } public String getExceptionType() { return exceptionType; } public long getReadTimeout() { return readTimeout; } public int getConnectTimeout() { return connectTimeout; } public int getMaxRetries() { return maxRetries; } public void setStatus(int status) { this.status = status; } public void setError(String error) { this.error = error; } public void setExceptionType(String exceptionType) { this.exceptionType = exceptionType; } public void setApp(String app) { this.app = app; } public void setAsg(String asg) { this.asg = asg; } public void setInstanceId(String instanceId) { this.instanceId = instanceId; } public void setHost(String host) { this.host = host; } public void setPort(int port) { this.port = port; } public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } public void setVip(String vip) { this.vip = vip; } public void setRegion(String region) { this.region = region; } public void setAvailabilityZone(String availabilityZone) { this.availabilityZone = availabilityZone; } public void setReadTimeout(long readTimeout) { this.readTimeout = readTimeout; } public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; } public void setException(Throwable t) { if (t != null) { if (t instanceof ReadTimeoutException) { error = "READ_TIMEOUT"; exceptionType = t.getClass().getSimpleName(); } else if (t instanceof OriginConnectException oce) { if (oce.getErrorType() != null) { error = oce.getErrorType().toString(); } else { error = "ORIGIN_CONNECT_ERROR"; } Throwable oceCause = oce.getCause(); // unwrap ssl handshake exceptions to emit the underlying handshake failure causes if (oceCause instanceof SSLHandshakeException sslHandshakeException && sslHandshakeException.getCause() != null) { oceCause = sslHandshakeException.getCause(); } if (oceCause != null) { exceptionType = oce.getCause().getClass().getSimpleName(); cause = oceCause.getMessage(); } else { exceptionType = oce.getClass().getSimpleName(); } } else if (t instanceof OutboundException obe) { error = obe.getOutboundErrorType().toString(); exceptionType = OutboundException.class.getSimpleName(); } else if (t instanceof SSLHandshakeException) { error = t.getMessage(); exceptionType = t.getClass().getSimpleName(); cause = t.getCause().getMessage(); } else { error = t.getMessage(); exceptionType = t.getClass().getSimpleName(); // for unexpected exceptions, just capture the first line of the stacktrace // otherwise we risk large stacktraces in memory and metrics systems StackTraceElement[] stackTraceElements = t.getStackTrace(); if (stackTraceElements.length > 0) { cause = stackTraceElements[0].toString(); } } } } public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } @Override public String toString() { try { return JACKSON_MAPPER.writeValueAsString(toJsonNode()); } catch (JsonProcessingException e) { throw new RuntimeException("Error serializing RequestAttempt!", e); } } public ObjectNode toJsonNode() { ObjectNode root = JACKSON_MAPPER.createObjectNode(); root.put("status", status); root.put("duration", duration); root.put("attempt", attempt); putNullableAttribute(root, "error", error); putNullableAttribute(root, "cause", cause); putNullableAttribute(root, "exceptionType", exceptionType); putNullableAttribute(root, "region", region); putNullableAttribute(root, "availabilityZone", availabilityZone); putNullableAttribute(root, "asg", asg); putNullableAttribute(root, "instanceId", instanceId); putNullableAttribute(root, "vip", vip); putNullableAttribute(root, "ipAddress", ipAddress); if (port > 0) { root.put("port", port); } if (status < 1) { root.put("readTimeout", readTimeout); root.put("connectTimeout", connectTimeout); } return root; } private static ObjectNode putNullableAttribute(ObjectNode node, String name, String value) { if (value != null) { node.put(name, value); } return node; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/niws/RequestAttempts.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.niws; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import java.io.IOException; import java.util.ArrayList; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: michaels@netflix.com * Date: 6/25/15 * Time: 1:03 PM */ public class RequestAttempts extends ArrayList { private static final Logger LOG = LoggerFactory.getLogger(RequestAttempts.class); private static final ObjectMapper JACKSON_MAPPER = new ObjectMapper(); public RequestAttempts() { super(); } @Nullable public RequestAttempt getFinalAttempt() { if (size() > 0) { return get(size() - 1); } else { return null; } } public static RequestAttempts getFromSessionContext(SessionContext ctx) { return ctx.get(CommonContextKeys.REQUEST_ATTEMPTS); } public static RequestAttempts parse(String attemptsJson) throws IOException { return JACKSON_MAPPER.readValue(attemptsJson, RequestAttempts.class); } public String toJSON() { ArrayNode array = JACKSON_MAPPER.createArrayNode(); for (RequestAttempt attempt : this) { array.add(attempt.toJsonNode()); } try { return JACKSON_MAPPER.writeValueAsString(array); } catch (JsonProcessingException e) { throw new RuntimeException("Error serializing RequestAttempts!", e); } } @Override public String toString() { try { return toJSON(); } catch (Throwable e) { LOG.error(e.getMessage(), e); return ""; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/BasicNettyOrigin.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfig; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.exception.ErrorType; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.NettyRequestAttemptFactory; import com.netflix.zuul.netty.SpectatorUtils; import com.netflix.zuul.netty.connectionpool.ClientChannelManager; import com.netflix.zuul.netty.connectionpool.DefaultClientChannelManager; import com.netflix.zuul.netty.connectionpool.PooledConnection; import com.netflix.zuul.niws.RequestAttempt; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import io.netty.channel.EventLoop; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * Netty Origin basic implementation that can be used for most apps, with the more complex methods having no-op * implementations. * * Author: Arthur Gonigberg * Date: December 01, 2017 */ public class BasicNettyOrigin implements NettyOrigin { private final OriginName originName; private final Registry registry; private final IClientConfig config; private final ClientChannelManager clientChannelManager; private final NettyRequestAttemptFactory requestAttemptFactory; private final AtomicInteger concurrentRequests; private final Counter rejectedRequests; private final CachedDynamicIntProperty concurrencyMax; private final CachedDynamicBooleanProperty concurrencyProtectionEnabled; public BasicNettyOrigin(OriginName originName, Registry registry) { this.originName = Objects.requireNonNull(originName, "originName"); this.registry = registry; this.config = setupClientConfig(originName); this.clientChannelManager = createClientChannelManager(originName, config, registry); this.clientChannelManager.init(); this.requestAttemptFactory = new NettyRequestAttemptFactory(); String niwsClientName = getName().getNiwsClientName(); this.concurrentRequests = SpectatorUtils.newGauge("zuul.origin.concurrent.requests", niwsClientName, new AtomicInteger(0)); this.rejectedRequests = SpectatorUtils.newCounter("zuul.origin.rejected.requests", niwsClientName); this.concurrencyMax = new CachedDynamicIntProperty("zuul.origin." + niwsClientName + ".concurrency.max.requests", 200); this.concurrencyProtectionEnabled = new CachedDynamicBooleanProperty( "zuul.origin." + niwsClientName + ".concurrency.protect.enabled", true); } protected IClientConfig setupClientConfig(OriginName originName) { // Get the NIWS properties for this Origin. IClientConfig niwsClientConfig = DefaultClientConfigImpl.getClientConfigWithDefaultValues(originName.getNiwsClientName()); niwsClientConfig.set(CommonClientConfigKey.ClientClassName, originName.getNiwsClientName()); niwsClientConfig.loadProperties(originName.getNiwsClientName()); return niwsClientConfig; } /** * Factory method to create the ClientChannelManager. * Override this method in subclasses to provide a custom ClientChannelManager implementation. * * @param originName the origin name * @param config the client configuration * @param registry the Spectator registry * @return a ClientChannelManager instance */ protected ClientChannelManager createClientChannelManager( OriginName originName, IClientConfig config, Registry registry) { return new DefaultClientChannelManager(originName, config, registry); } @Override public OriginName getName() { return originName; } @Override public boolean isAvailable() { return clientChannelManager.isAvailable(); } @Override public boolean isCold() { return clientChannelManager.isCold(); } @Override public Promise connectToOrigin( HttpRequestMessage zuulReq, EventLoop eventLoop, int attemptNumber, CurrentPassport passport, AtomicReference chosenServer, AtomicReference chosenHostAddr) { return clientChannelManager.acquire(eventLoop, null, passport, chosenServer, chosenHostAddr); } @Override public int getMaxRetriesForRequest(SessionContext context) { return config.get(CommonClientConfigKey.MaxAutoRetriesNextServer, 0); } @Override public RequestAttempt newRequestAttempt( DiscoveryResult server, InetAddress serverAddr, SessionContext zuulCtx, int attemptNum) { return new RequestAttempt( server, serverAddr, config, attemptNum, config.get(CommonClientConfigKey.ReadTimeout)); } @Override public String getIpAddrFromServer(DiscoveryResult discoveryResult) { Optional ipAddr = discoveryResult.getIPAddr(); return ipAddr.isPresent() ? ipAddr.get() : null; } @Override public IClientConfig getClientConfig() { return config; } @Override public Registry getSpectatorRegistry() { return registry; } @Override public void recordFinalError(HttpRequestMessage requestMsg, Throwable throwable) { if (throwable == null) { return; } SessionContext zuulCtx = requestMsg.getContext(); // Choose StatusCategory based on the ErrorType. ErrorType et = requestAttemptFactory.mapNettyToOutboundErrorType(throwable); StatusCategory nfs = et.getStatusCategory(); StatusCategoryUtils.setStatusCategory(zuulCtx, nfs); StatusCategoryUtils.setOriginStatusCategory(zuulCtx, nfs); zuulCtx.setError(throwable); } @Override public void recordFinalResponse(HttpResponseMessage resp) { if (resp != null) { SessionContext zuulCtx = resp.getContext(); // Store the status code of final attempt response. int originStatusCode = resp.getStatus(); zuulCtx.put(CommonContextKeys.ORIGIN_STATUS, originStatusCode); // Mark origin StatusCategory based on http status code. StatusCategory originNfs = ZuulStatusCategory.SUCCESS; if (originStatusCode == 503) { originNfs = ZuulStatusCategory.FAILURE_ORIGIN_THROTTLED; } else if (StatusCategoryUtils.isResponseHttpErrorStatus(originStatusCode)) { originNfs = ZuulStatusCategory.FAILURE_ORIGIN; } StatusCategoryUtils.setOriginStatusCategory(zuulCtx, originNfs); // Choose the zuul StatusCategory based on the origin one... // ... but only if existing one has not already been set to a non-success value. StatusCategoryUtils.storeStatusCategoryIfNotAlreadyFailure(zuulCtx, originNfs); } } @Override public void preRequestChecks(HttpRequestMessage zuulRequest) { if (concurrencyProtectionEnabled.get() && concurrentRequests.get() > concurrencyMax.get()) { rejectedRequests.increment(); throw new OriginConcurrencyExceededException(getName()); } concurrentRequests.incrementAndGet(); } @Override public void recordProxyRequestEnd() { concurrentRequests.decrementAndGet(); } /* Not required for basic operation */ @Override public double getErrorPercentage() { return 0; } @Override public double getErrorAllPercentage() { return 0; } @Override public void onRequestExecutionStart(HttpRequestMessage zuulReq) {} @Override public void onRequestStartWithServer(HttpRequestMessage zuulReq, DiscoveryResult discoveryResult, int attemptNum) {} @Override public void onRequestExceptionWithServer( HttpRequestMessage zuulReq, DiscoveryResult discoveryResult, int attemptNum, Throwable t) {} @Override public void onRequestExecutionSuccess( HttpRequestMessage zuulReq, HttpResponseMessage zuulResp, DiscoveryResult discoveryResult, int attemptNum) {} @Override public void onRequestExecutionFailed( HttpRequestMessage zuulReq, DiscoveryResult discoveryResult, int attemptNum, Throwable t) {} @Override public void adjustRetryPolicyIfNeeded(HttpRequestMessage zuulRequest) {} @Override public void recordSuccessResponse() {} } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/BasicNettyOriginManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.spectator.api.Registry; import com.netflix.zuul.context.SessionContext; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.concurrent.ConcurrentHashMap; /** * Basic Netty Origin Manager that most apps can use. This can also serve as a useful template for creating more * complex origin managers. * * Author: Arthur Gonigberg * Date: November 30, 2017 */ @Singleton public class BasicNettyOriginManager implements OriginManager { private final Registry registry; private final ConcurrentHashMap originMappings; @Inject public BasicNettyOriginManager(Registry registry) { this.registry = registry; this.originMappings = new ConcurrentHashMap<>(); } @Override public BasicNettyOrigin getOrigin(OriginName originName, String uri, SessionContext ctx) { return originMappings.computeIfAbsent(originName, n -> createOrigin(originName, uri, ctx)); } @Override public BasicNettyOrigin createOrigin(OriginName originName, String uri, SessionContext ctx) { return new BasicNettyOrigin(originName, registry); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/InstrumentedOrigin.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.zuul.message.http.HttpRequestMessage; /** * User: michaels@netflix.com * Date: 10/8/14 * Time: 6:15 PM */ public interface InstrumentedOrigin extends Origin { double getErrorPercentage(); double getErrorAllPercentage(); void adjustRetryPolicyIfNeeded(HttpRequestMessage zuulRequest); void preRequestChecks(HttpRequestMessage zuulRequest); void recordSuccessResponse(); void recordProxyRequestEnd(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/NettyOrigin.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.client.config.IClientConfig; import com.netflix.spectator.api.Registry; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.connectionpool.PooledConnection; import com.netflix.zuul.niws.RequestAttempt; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.EventLoop; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.util.concurrent.atomic.AtomicReference; /** * Netty Origin interface for integrating cleanly with the ProxyEndpoint state management class. * * Author: Arthur Gonigberg * Date: November 29, 2017 */ public interface NettyOrigin extends InstrumentedOrigin { Promise connectToOrigin( HttpRequestMessage zuulReq, EventLoop eventLoop, int attemptNumber, CurrentPassport passport, AtomicReference chosenServer, AtomicReference chosenHostAddr); int getMaxRetriesForRequest(SessionContext context); void onRequestExecutionStart(HttpRequestMessage zuulReq); void onRequestStartWithServer(HttpRequestMessage zuulReq, DiscoveryResult discoveryResult, int attemptNum); void onRequestExceptionWithServer( HttpRequestMessage zuulReq, DiscoveryResult discoveryResult, int attemptNum, Throwable t); void onRequestExecutionSuccess( HttpRequestMessage zuulReq, HttpResponseMessage zuulResp, DiscoveryResult discoveryResult, int attemptNum); void onRequestExecutionFailed( HttpRequestMessage zuulReq, DiscoveryResult discoveryResult, int attemptNum, Throwable t); void recordFinalError(HttpRequestMessage requestMsg, Throwable throwable); void recordFinalResponse(HttpResponseMessage resp); RequestAttempt newRequestAttempt( DiscoveryResult server, InetAddress serverAddr, SessionContext zuulCtx, int attemptNum); String getIpAddrFromServer(DiscoveryResult server); IClientConfig getClientConfig(); Registry getSpectatorRegistry(); default void originRetryPolicyAdjustmentIfNeeded(HttpRequestMessage zuulReq, HttpResponse nettyResponse) {} } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/Origin.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; /** * User: michaels@netflix.com * Date: 5/11/15 * Time: 3:14 PM */ public interface Origin { OriginName getName(); boolean isAvailable(); boolean isCold(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/OriginConcurrencyExceededException.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.zuul.stats.status.ZuulStatusCategory; public class OriginConcurrencyExceededException extends OriginThrottledException { public OriginConcurrencyExceededException(OriginName originName) { super( originName, "Max concurrent requests on origin exceeded", ZuulStatusCategory.FAILURE_LOCAL_THROTTLED_ORIGIN_CONCURRENCY); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/OriginManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.zuul.context.SessionContext; /** * User: michaels@netflix.com * Date: 5/11/15 * Time: 3:15 PM */ public interface OriginManager { T getOrigin(OriginName originName, String uri, SessionContext ctx); T createOrigin(OriginName originName, String uri, SessionContext ctx); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/OriginName.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.zuul.util.VipUtils; import java.util.Locale; import java.util.Objects; /** * An Origin Name is a tuple of a target to connect to, an authority to use for connecting, and an NIWS client name * used for configuration of an Origin. These fields are semi independent, but are usually used in proximity to each * other, and thus makes sense to group them together. * *

The {@code target} represents the string used to look up the origin during name resolution. Currently, this is * a {@code VIP}, which is passed to a Eureka name resolved. In the future, other targets will be supported, such as * DNS. * *

The {@code authority} represents who we plan to connect to. In the case of TLS / SSL connections, this can be * used to verify the remote endpoint. It is not specified what the format is, but it may not be null. In the case * of VIPs, which are frequently used with AWS clusters, this is the Application Name. * *

The {@code NIWS Client Name} is a legacy construct, which is used to configure an origin. When the origin is * created, the NIWS client name can be used as a key into a configuration mapping to decide how the Origin should * behave. By default, the VIP is the same for the NIWS client name, but it can be different in scenarios where * multiple connections to the same origin are needed. Additionally, the NIWS client name also functions as a key * in metrics. */ public final class OriginName { /** * The NIWS client name of the origin. This is typically used in metrics and for configuration of NIWS * {@link com.netflix.client.config.IClientConfig} objects. */ private final String niwsClientName; /** * This should not be used in {@link #equals} or {@link #hashCode} as it is already covered by * {@link #niwsClientName}. */ private final String metricId; /** * The target to connect to, used for name resolution. This is typically the VIP. */ private final String target; /** * The authority of this origin. Usually this is the Application name of origin. It is primarily * used for establishing a secure connection, as well as logging. */ private final String authority; /** * @deprecated use {@link #fromVipAndApp(String, String)} */ @Deprecated public static OriginName fromVip(String vip) { return fromVipAndApp(vip, VipUtils.extractUntrustedAppNameFromVIP(vip)); } /** * @deprecated use {@link #fromVipAndApp(String, String, String)} */ @Deprecated public static OriginName fromVip(String vip, String niwsClientName) { return fromVipAndApp(vip, VipUtils.extractUntrustedAppNameFromVIP(vip), niwsClientName); } /** * Constructs an OriginName with a target and authority from the vip and app name. The vip is used as the NIWS * client name, which is frequently used for configuration. */ public static OriginName fromVipAndApp(String vip, String appName) { return fromVipAndApp(vip, appName, vip); } /** * Constructs an OriginName with a target, authority, and NIWS client name. The NIWS client name can be different * from the vip in cases where custom configuration for an Origin is needed. */ public static OriginName fromVipAndApp(String vip, String appName, String niwsClientName) { return new OriginName(vip, appName, niwsClientName); } private OriginName(String target, String authority, String niwsClientName) { this.target = Objects.requireNonNull(target, "target"); this.authority = Objects.requireNonNull(authority, "authority"); this.niwsClientName = Objects.requireNonNull(niwsClientName, "niwsClientName"); this.metricId = niwsClientName.toLowerCase(Locale.ROOT); } /** * This is typically the VIP for the given Origin. */ public String getTarget() { return target; } /** * Returns the niwsClientName. This is normally used for interaction with NIWS, and should be used without prior * knowledge that the value will be used in NIWS libraries. */ public String getNiwsClientName() { return niwsClientName; } /** * Returns the identifier for this this metric name. This may be different than any of the other * fields; currently it is equivalent to the lowercased {@link #getNiwsClientName()}. */ public String getMetricId() { return metricId; } /** * Returns the Authority of this origin. This is used for establishing secure connections. May be absent * if the authority is not trusted. */ public String getAuthority() { return authority; } @Override public boolean equals(Object o) { if (!(o instanceof OriginName)) { return false; } OriginName that = (OriginName) o; return Objects.equals(niwsClientName, that.niwsClientName) && Objects.equals(target, that.target) && Objects.equals(authority, that.authority); } @Override public int hashCode() { return Objects.hash(niwsClientName, target, authority); } @Override public String toString() { return "OriginName{" + "niwsClientName='" + niwsClientName + '\'' + ", target='" + target + '\'' + ", authority='" + authority + '\'' + '}'; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/origins/OriginThrottledException.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.stats.status.StatusCategory; import java.util.Objects; public abstract class OriginThrottledException extends ZuulException { private final OriginName originName; private final StatusCategory statusCategory; public OriginThrottledException(OriginName originName, String msg, StatusCategory statusCategory) { // Ensure this exception does not fill its stacktrace as causes too much load. super(msg + ", origin=" + originName, true); this.originName = Objects.requireNonNull(originName, "originName"); this.statusCategory = statusCategory; this.setStatusCode(503); } public OriginName getOriginName() { return originName; } public StatusCategory getStatusCategory() { return statusCategory; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/passport/CurrentPassport.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.passport; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ticker; import com.google.common.collect.Sets; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Spectator; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.ConcurrentModificationException; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CurrentPassport { protected static final Logger logger = LoggerFactory.getLogger(CurrentPassport.class); private static final CachedDynamicBooleanProperty COUNT_STATES = new CachedDynamicBooleanProperty("zuul.passport.count.enabled", false); public static final AttributeKey CHANNEL_ATTR = AttributeKey.newInstance("_current_passport"); private static final Ticker SYSTEM_TICKER = Ticker.systemTicker(); private static final Set CONTENT_STATES = Sets.newHashSet( PassportState.IN_REQ_CONTENT_RECEIVED, PassportState.IN_RESP_CONTENT_RECEIVED, PassportState.OUT_REQ_CONTENT_SENDING, PassportState.OUT_REQ_CONTENT_SENT, PassportState.OUT_RESP_CONTENT_SENDING, PassportState.OUT_RESP_CONTENT_SENT); private static final CachedDynamicBooleanProperty CONTENT_STATE_ENABLED = new CachedDynamicBooleanProperty("zuul.passport.state.content.enabled", false); private final Ticker ticker; private final ArrayDeque history; private final HashSet statesAdded; private final long creationTimeSinceEpochMs; private final IntrospectiveReentrantLock historyLock = new IntrospectiveReentrantLock(); private final Unlocker unlocker = new Unlocker(); private final class Unlocker implements AutoCloseable { @Override public void close() { historyLock.unlock(); } } private static final class IntrospectiveReentrantLock extends ReentrantLock { @Override protected Thread getOwner() { return super.getOwner(); } } private Unlocker lock() { boolean locked = false; if ((historyLock.isLocked() && !historyLock.isHeldByCurrentThread()) || !(locked = historyLock.tryLock())) { Thread owner = historyLock.getOwner(); String ownerStack = String.valueOf(owner != null ? Arrays.asList(owner.getStackTrace()) : historyLock); logger.warn( "CurrentPassport already locked!, other={}, self={}", ownerStack, Thread.currentThread(), new ConcurrentModificationException()); } if (!locked) { historyLock.lock(); } return unlocker; } CurrentPassport() { this(SYSTEM_TICKER); } @VisibleForTesting public CurrentPassport(Ticker ticker) { this.ticker = ticker; this.history = new ArrayDeque<>(); this.statesAdded = new HashSet<>(); this.creationTimeSinceEpochMs = System.currentTimeMillis(); } public static CurrentPassport create() { if (COUNT_STATES.get()) { return new CountingCurrentPassport(); } return new CurrentPassport(); } public static CurrentPassport fromSessionContext(SessionContext ctx) { return ctx.get(CommonContextKeys.PASSPORT); } public static CurrentPassport createForChannel(Channel ch) { CurrentPassport passport = create(); passport.setOnChannel(ch); return passport; } public static CurrentPassport fromChannel(Channel ch) { CurrentPassport passport = fromChannelOrNull(ch); if (passport == null) { passport = create(); ch.attr(CHANNEL_ATTR).set(passport); } return passport; } public static CurrentPassport fromChannelOrNull(Channel ch) { return ch.attr(CHANNEL_ATTR).get(); } public void setOnChannel(Channel ch) { ch.attr(CHANNEL_ATTR).set(this); } public static void clearFromChannel(Channel ch) { ch.attr(CHANNEL_ATTR).set(null); } public PassportState getState() { try (Unlocker ignored = lock()) { PassportItem passportItem = history.peekLast(); return passportItem != null ? passportItem.getState() : null; } } @VisibleForTesting public Deque getHistory() { try (Unlocker ignored = lock()) { // best effort, but doesn't actually protect anything return history; } } public void add(PassportState state) { if (!CONTENT_STATE_ENABLED.get()) { if (CONTENT_STATES.contains(state)) { // Discard. return; } } try (Unlocker ignored = lock()) { history.addLast(new PassportItem(state, now())); } statesAdded.add(state); } public void addIfNotAlready(PassportState state) { if (!statesAdded.contains(state)) { add(state); } } public long calculateTimeBetweenFirstAnd(PassportState endState) { long startTime = firstTime(); try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == endState) { return item.getTime() - startTime; } } } return now() - startTime; } /** * NOTE: This is NOT nanos since epoch. It's just since an arbitrary point in time. So only use relatively. */ public long firstTime() { try (Unlocker ignored = lock()) { return history.getFirst().getTime(); } } public long creationTimeSinceEpochMs() { return creationTimeSinceEpochMs; } public long calculateTimeBetween(StartAndEnd sae) { if (sae.startNotFound() || sae.endNotFound()) { return 0; } return sae.endTime - sae.startTime; } public long calculateTimeBetweenButIfNoEndThenUseNow(StartAndEnd sae) { if (sae.startNotFound()) { return 0; } // If no end state found, then default to now. if (sae.endNotFound()) { sae.endTime = now(); } return sae.endTime - sae.startTime; } public StartAndEnd findStartAndEndStates(PassportState startState, PassportState endState) { StartAndEnd sae = new StartAndEnd(); try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == startState) { sae.startTime = item.getTime(); } else if (item.getState() == endState) { sae.endTime = item.getTime(); } } } return sae; } public StartAndEnd findFirstStartAndLastEndStates(PassportState startState, PassportState endState) { StartAndEnd sae = new StartAndEnd(); try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (sae.startNotFound() && item.getState() == startState) { sae.startTime = item.getTime(); } else if (item.getState() == endState) { sae.endTime = item.getTime(); } } } return sae; } public StartAndEnd findLastStartAndFirstEndStates(PassportState startState, PassportState endState) { StartAndEnd sae = new StartAndEnd(); try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == startState) { sae.startTime = item.getTime(); } else if (sae.endNotFound() && item.getState() == endState) { sae.endTime = item.getTime(); } } } return sae; } public List findEachPairOf(PassportState startState, PassportState endState) { ArrayList items = new ArrayList<>(); StartAndEnd currentPair = null; try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == startState) { if (currentPair == null) { currentPair = new StartAndEnd(); currentPair.startTime = item.getTime(); } } else if (item.getState() == endState) { if (currentPair != null) { currentPair.endTime = item.getTime(); items.add(currentPair); currentPair = null; } } } } return items; } public PassportItem findState(PassportState state) { try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == state) { return item; } } } return null; } public PassportItem findStateBackwards(PassportState state) { try (Unlocker ignored = lock()) { Iterator itr = history.descendingIterator(); while (itr.hasNext()) { PassportItem item = (PassportItem) itr.next(); if (item.getState() == state) { return item; } } } return null; } public List findStates(PassportState state) { ArrayList items = new ArrayList<>(); try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == state) { items.add(item); } } } return items; } public List findTimes(PassportState state) { long startTick = firstTime(); ArrayList items = new ArrayList<>(); try (Unlocker ignored = lock()) { for (PassportItem item : history) { if (item.getState() == state) { items.add(item.getTime() - startTick); } } } return items; } public boolean wasProxyAttempt() { // If an attempt was made to send outbound request headers on this session, then assume it was an // attempt to proxy. return findState(PassportState.OUT_REQ_HEADERS_SENDING) != null; } private long now() { return ticker.read(); } @Override public String toString() { try (Unlocker ignored = lock()) { long startTime = history.size() > 0 ? firstTime() : 0; long now = now(); StringBuilder sb = new StringBuilder(); sb.append("CurrentPassport {"); sb.append("start_ms=").append(creationTimeSinceEpochMs()).append(", "); sb.append('['); for (PassportItem item : history) { sb.append('+') .append(item.getTime() - startTime) .append('=') .append(item.getState().name()) .append(", "); } sb.append('+').append(now - startTime).append('=').append("NOW"); sb.append(']'); sb.append('}'); return sb.toString(); } } @VisibleForTesting public static CurrentPassport parseFromToString(String text) { CurrentPassport passport = null; Pattern ptn = Pattern.compile("CurrentPassport \\{start_ms=\\d+, \\[(.*)\\]\\}"); Pattern ptnState = Pattern.compile("^\\+(\\d+)=(.+)$"); Matcher m = ptn.matcher(text); if (m.matches()) { String[] stateStrs = m.group(1).split(", ", -1); MockTicker ticker = new MockTicker(); passport = new CurrentPassport(ticker); try (Unlocker ignored = passport.lock()) { for (String stateStr : stateStrs) { Matcher stateMatch = ptnState.matcher(stateStr); if (stateMatch.matches()) { String stateName = stateMatch.group(2); if (stateName.equals("NOW")) { long startTime = passport.history.size() > 0 ? passport.firstTime() : 0; long now = Long.parseLong(stateMatch.group(1)) + startTime; ticker.setNow(now); } else { PassportState state = PassportState.valueOf(stateName); PassportItem item = new PassportItem(state, Long.parseLong(stateMatch.group(1))); passport.history.add(item); } } } } } return passport; } private static class MockTicker extends Ticker { private long now = -1; @Override public long read() { if (now == -1) { throw new IllegalStateException(); } return now; } public void setNow(long now) { this.now = now; } } } class CountingCurrentPassport extends CurrentPassport { private static final Counter IN_REQ_HEADERS_RECEIVED_CNT = createCounter("in_req_hdrs_rec"); private static final Counter IN_REQ_LAST_CONTENT_RECEIVED_CNT = createCounter("in_req_last_cont_rec"); private static final Counter IN_RESP_HEADERS_RECEIVED_CNT = createCounter("in_resp_hdrs_rec"); private static final Counter IN_RESP_LAST_CONTENT_RECEIVED_CNT = createCounter("in_resp_last_cont_rec"); private static final Counter OUT_REQ_HEADERS_SENT_CNT = createCounter("out_req_hdrs_sent"); private static final Counter OUT_REQ_LAST_CONTENT_SENT_CNT = createCounter("out_req_last_cont_sent"); private static final Counter OUT_RESP_HEADERS_SENT_CNT = createCounter("out_resp_hdrs_sent"); private static final Counter OUT_RESP_LAST_CONTENT_SENT_CNT = createCounter("out_resp_last_cont_sent"); private static Counter createCounter(String name) { return Spectator.globalRegistry().counter("zuul.passport." + name); } public CountingCurrentPassport() { super(); incrementStateCounter(getState()); } @Override public void add(PassportState state) { super.add(state); incrementStateCounter(state); } private void incrementStateCounter(PassportState state) { switch (state) { case IN_REQ_HEADERS_RECEIVED: IN_REQ_HEADERS_RECEIVED_CNT.increment(); break; case IN_REQ_LAST_CONTENT_RECEIVED: IN_REQ_LAST_CONTENT_RECEIVED_CNT.increment(); break; case OUT_REQ_HEADERS_SENT: OUT_REQ_HEADERS_SENT_CNT.increment(); break; case OUT_REQ_LAST_CONTENT_SENT: OUT_REQ_LAST_CONTENT_SENT_CNT.increment(); break; case IN_RESP_HEADERS_RECEIVED: IN_RESP_HEADERS_RECEIVED_CNT.increment(); break; case IN_RESP_LAST_CONTENT_RECEIVED: IN_RESP_LAST_CONTENT_RECEIVED_CNT.increment(); break; case OUT_RESP_HEADERS_SENT: OUT_RESP_HEADERS_SENT_CNT.increment(); break; case OUT_RESP_LAST_CONTENT_SENT: OUT_RESP_LAST_CONTENT_SENT_CNT.increment(); break; default: logger.debug("Not incrementing any state counter for state {}", state); break; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/passport/PassportItem.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.passport; public class PassportItem { private final long time; private final PassportState state; public PassportItem(PassportState state, long time) { this.time = time; this.state = state; } public long getTime() { return time; } public PassportState getState() { return state; } @Override public String toString() { return time + "=" + state; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/passport/PassportState.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.passport; public enum PassportState { IN_REQ_HEADERS_RECEIVED, IN_REQ_CONTENT_RECEIVED, IN_REQ_LAST_CONTENT_RECEIVED, IN_REQ_REJECTED, IN_REQ_READ_TIMEOUT, IN_REQ_CANCELLED, OUT_REQ_HEADERS_SENDING, OUT_REQ_HEADERS_SENT, OUT_REQ_HEADERS_ERROR_SENDING, OUT_REQ_CONTENT_SENDING, OUT_REQ_CONTENT_SENT, OUT_REQ_CONTENT_ERROR_SENDING, OUT_REQ_LAST_CONTENT_SENDING, OUT_REQ_LAST_CONTENT_SENT, OUT_REQ_LAST_CONTENT_ERROR_SENDING, IN_RESP_HEADERS_RECEIVED, IN_RESP_CONTENT_RECEIVED, IN_RESP_LAST_CONTENT_RECEIVED, OUT_RESP_HEADERS_SENDING, OUT_RESP_HEADERS_SENT, OUT_RESP_HEADERS_ERROR_SENDING, OUT_RESP_CONTENT_SENDING, OUT_RESP_CONTENT_SENT, OUT_RESP_CONTENT_ERROR_SENDING, OUT_RESP_LAST_CONTENT_SENDING, OUT_RESP_LAST_CONTENT_SENT, OUT_RESP_LAST_CONTENT_ERROR_SENDING, FILTERS_INBOUND_START, FILTERS_INBOUND_END, FILTERS_OUTBOUND_START, FILTERS_OUTBOUND_END, FILTERS_INBOUND_BUF_START, FILTERS_INBOUND_BUF_END, FILTERS_OUTBOUND_BUF_START, FILTERS_OUTBOUND_BUF_END, ORIGIN_CONN_ACQUIRE_START, ORIGIN_CONN_ACQUIRE_END, ORIGIN_CONN_ACQUIRE_FAILED, MISC_IO_START, MISC_IO_STOP, SERVER_CH_DISCONNECT, SERVER_CH_CLOSE, SERVER_CH_EXCEPTION, SERVER_CH_IDLE_TIMEOUT, SERVER_CH_ACTIVE, SERVER_CH_INACTIVE, SERVER_CH_THROTTLING, SERVER_CH_REJECTING, SERVER_CH_SSL_HANDSHAKE_COMPLETE, ORIGIN_CH_CONNECTING, ORIGIN_CH_CONNECTED, ORIGIN_CH_DISCONNECT, ORIGIN_CH_CLOSE, ORIGIN_CH_EXCEPTION, ORIGIN_CH_ACTIVE, ORIGIN_CH_INACTIVE, ORIGIN_CH_IDLE_TIMEOUT, ORIGIN_CH_POOL_RETURNED, ORIGIN_CH_READ_TIMEOUT, ORIGIN_CH_IO_EX, ORIGIN_RETRY_START, } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/passport/StartAndEnd.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.passport; public class StartAndEnd { long startTime = -1; long endTime = -1; public long getStart() { return startTime; } public long getEnd() { return endTime; } boolean startNotFound() { return startTime == -1; } boolean endNotFound() { return endTime == -1; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/plugins/Tracer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.plugins; import com.netflix.spectator.api.Spectator; import com.netflix.zuul.monitoring.TracerFactory; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.TimeUnit; /** * Plugin to hook up Servo Tracers * * @author Mikey Cohen * Date: 4/10/13 * Time: 4:51 PM */ public class Tracer extends TracerFactory { @Override public com.netflix.zuul.monitoring.Tracer startMicroTracer(String name) { return new SpectatorTracer(name); } class SpectatorTracer implements com.netflix.zuul.monitoring.Tracer { private String name; private final long start; private SpectatorTracer(String name) { this.name = name; start = System.nanoTime(); } @Override public void stopAndLog() { Spectator.globalRegistry() .timer(name, "hostname", getHostName(), "ip", getIp()) .record(System.nanoTime() - start, TimeUnit.NANOSECONDS); } @Override public void setName(String name) { this.name = name; } } private static String getHostName() { return (loadAddress() != null) ? loadAddress().getHostName() : "unkownHost"; } private static String getIp() { return (loadAddress() != null) ? loadAddress().getHostAddress() : "unknownHost"; } private static InetAddress loadAddress() { try { return InetAddress.getLocalHost(); } catch (UnknownHostException e) { return null; } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/AmazonInfoHolder.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.netflix.appinfo.AmazonInfo; /** * Builds and caches an AmazonInfo instance in memory. * * @author mhawthorne */ public class AmazonInfoHolder { private static final AmazonInfo INFO = AmazonInfo.Builder.newBuilder().autoBuild("eureka"); public static final AmazonInfo getInfo() { return INFO; } private AmazonInfoHolder() {} } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/BasicRequestMetricsPublisher.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.netflix.zuul.context.SessionContext; /** * User: michaels@netflix.com * Date: 6/4/15 * Time: 4:22 PM */ public class BasicRequestMetricsPublisher implements RequestMetricsPublisher { @Override public void collectAndPublish(SessionContext context) { // Record metrics here. } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/ErrorStatsData.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.api.patterns.PolledMeter; import com.netflix.zuul.stats.monitoring.NamedCount; import java.util.concurrent.atomic.AtomicLong; /** * Implementation of a Named counter to monitor and count error causes by route. Route is a defined zuul concept to * categorize requests into buckets. By default this is the first segment of the uri * @author Mikey Cohen * Date: 2/23/12 * Time: 4:16 PM */ public class ErrorStatsData implements NamedCount { private final String id; private final String errorCause; private final AtomicLong count = new AtomicLong(); /** * create a counter by route and cause of error * @param route * @param cause */ public ErrorStatsData(String route, String cause) { if (route == null || route.equals("")) { route = "UNKNOWN"; } id = route + "_" + cause; this.errorCause = cause; Registry registry = Spectator.globalRegistry(); PolledMeter.using(registry) .withId(registry.createId("zuul.ErrorStatsData", "ID", id)) .monitorValue(this, ErrorStatsData::getCount); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || !(o instanceof ErrorStatsData)) { return false; } ErrorStatsData that = (ErrorStatsData) o; return !(errorCause != null ? !errorCause.equals(that.errorCause) : that.errorCause != null); } @Override public int hashCode() { return errorCause != null ? errorCause.hashCode() : 0; } /** * increments the counter */ public void update() { count.incrementAndGet(); } @Override public String getName() { return id; } @Override public long getCount() { return count.get(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/ErrorStatsManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.netflix.zuul.stats.monitoring.MonitorRegistry; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Manager to handle Error Statistics * @author Mikey Cohen * Date: 2/23/12 * Time: 4:16 PM */ public class ErrorStatsManager { ConcurrentHashMap> routeMap = new ConcurrentHashMap>(); static final ErrorStatsManager INSTANCE = new ErrorStatsManager(); /** * * @return Singleton */ public static ErrorStatsManager getManager() { return INSTANCE; } /** * * @param route * @param cause * @return data structure for holding count information for a route and cause */ public ErrorStatsData getStats(String route, String cause) { Map map = routeMap.get(route); if (map == null) { return null; } return map.get(cause); } /** * updates count for the given route and error cause * @param route * @param cause */ public void putStats(String route, String cause) { if (route == null) { route = "UNKNOWN_ROUTE"; } route = route.replace("/", "_"); ConcurrentHashMap statsMap = routeMap.get(route); if (statsMap == null) { statsMap = new ConcurrentHashMap(); routeMap.putIfAbsent(route, statsMap); } ErrorStatsData sd = statsMap.get(cause); if (sd == null) { sd = new ErrorStatsData(route, cause); ErrorStatsData sd1 = statsMap.putIfAbsent(cause, sd); if (sd1 != null) { sd = sd1; } else { MonitorRegistry.getInstance().registerObject(sd); } } sd.update(); } public static class UnitTest {} } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/NamedCountingMonitor.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.api.patterns.PolledMeter; import com.netflix.zuul.stats.monitoring.MonitorRegistry; import com.netflix.zuul.stats.monitoring.NamedCount; import java.util.concurrent.atomic.AtomicLong; /** * Simple Epic counter with a name and a count. * * @author mhawthorne */ public class NamedCountingMonitor implements NamedCount { private final String name; private final AtomicLong count = new AtomicLong(); public NamedCountingMonitor(String name) { this.name = name; Registry registry = Spectator.globalRegistry(); PolledMeter.using(registry) .withId(registry.createId("zuul.ErrorStatsData", "ID", name)) .monitorValue(this, NamedCountingMonitor::getCount); } /** * registers this objects */ public NamedCountingMonitor register() { MonitorRegistry.getInstance().registerObject(this); return this; } /** * increments the counter */ public long increment() { return this.count.incrementAndGet(); } @Override public String getName() { return name; } /** * @return the current count */ @Override public long getCount() { return this.count.get(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/RequestMetricsPublisher.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.netflix.zuul.context.SessionContext; /** * User: michaels@netflix.com * Date: 3/9/15 * Time: 5:56 PM */ public interface RequestMetricsPublisher { void collectAndPublish(SessionContext context); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/RouteStatusCodeMonitor.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.google.common.annotations.VisibleForTesting; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.api.patterns.PolledMeter; import com.netflix.zuul.stats.monitoring.NamedCount; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; /** * counter for per route/status code counting * @author Mikey Cohen * Date: 2/3/12 * Time: 3:04 PM */ public class RouteStatusCodeMonitor implements NamedCount { private final String routeCode; @VisibleForTesting final String route; private final int statusCode; private final AtomicLong count = new AtomicLong(); public RouteStatusCodeMonitor(@Nullable String route, int statusCode) { if (route == null) { route = ""; } this.route = route; this.statusCode = statusCode; this.routeCode = route + "_" + statusCode; Registry registry = Spectator.globalRegistry(); PolledMeter.using(registry) .withId(registry.createId("zuul.RouteStatusCodeMonitor", "ID", routeCode)) .monitorValue(this, RouteStatusCodeMonitor::getCount); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || !(o instanceof RouteStatusCodeMonitor)) { return false; } RouteStatusCodeMonitor statsData = (RouteStatusCodeMonitor) o; if (statusCode != statsData.statusCode) { return false; } if (!Objects.equals(route, statsData.route)) { return false; } return true; } @Override public int hashCode() { int result = route != null ? route.hashCode() : 0; result = 31 * result + statusCode; return result; } @Override public String getName() { return routeCode; } @Override public long getCount() { return count.get(); } /** * increment the count */ public void update() { count.incrementAndGet(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/StatsManager.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import com.google.common.annotations.VisibleForTesting; import com.netflix.zuul.message.http.HttpRequestInfo; import com.netflix.zuul.stats.monitoring.MonitorRegistry; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * High level statistics counter manager to count stats on various aspects of requests * * @author Mikey Cohen * Date: 2/3/12 * Time: 3:25 PM */ public class StatsManager { private static final Logger LOG = LoggerFactory.getLogger(StatsManager.class); protected static final Pattern HEX_PATTERN = Pattern.compile("[0-9a-fA-F]+"); // should match *.amazonaws.com, *.nflxvideo.net, or raw IP addresses. private static final Pattern HOST_PATTERN = Pattern.compile("(?:(.+)\\.amazonaws\\.com)|((?:\\d{1,3}\\.?){4})|(ip-\\d+-\\d+-\\d+-\\d+)|" + "(?:(.+)\\.nflxvideo\\.net)|(?:(.+)\\.llnwd\\.net)|(?:(.+)\\.nflximg\\.com)"); @VisibleForTesting static final String HOST_HEADER = "host"; private static final String X_FORWARDED_FOR_HEADER = "x-forwarded-for"; @VisibleForTesting static final String X_FORWARDED_PROTO_HEADER = "x-forwarded-proto"; @VisibleForTesting final ConcurrentMap> routeStatusMap = new ConcurrentHashMap>(); private final ConcurrentMap namedStatusMap = new ConcurrentHashMap(); private final ConcurrentMap hostCounterMap = new ConcurrentHashMap(); private final ConcurrentMap protocolCounterMap = new ConcurrentHashMap(); private final ConcurrentMap ipVersionCounterMap = new ConcurrentHashMap(); protected static StatsManager INSTANCE = new StatsManager(); public static StatsManager getManager() { return INSTANCE; } /** * @param route * @param statusCode * @return the RouteStatusCodeMonitor for the given route and status code */ public RouteStatusCodeMonitor getRouteStatusCodeMonitor(String route, int statusCode) { Map map = routeStatusMap.get(route); if (map == null) { return null; } return map.get(statusCode); } @VisibleForTesting NamedCountingMonitor getHostMonitor(String host) { return this.hostCounterMap.get(hostKey(host)); } @VisibleForTesting NamedCountingMonitor getProtocolMonitor(String proto) { return this.protocolCounterMap.get(protocolKey(proto)); } @VisibleForTesting static final String hostKey(String host) { try { Matcher m = HOST_PATTERN.matcher(host); // I know which type of host matched by the number of the group that is non-null // I use a different replacement string per host type to make the Epic stats more clear if (m.matches()) { if (m.group(1) != null) { host = host.replace(m.group(1), "EC2"); } else if (m.group(2) != null) { host = host.replace(m.group(2), "IP"); } else if (m.group(3) != null) { host = host.replace(m.group(3), "IP"); } else if (m.group(4) != null) { host = host.replace(m.group(4), "CDN"); } else if (m.group(5) != null) { host = host.replace(m.group(5), "CDN"); } else if (m.group(6) != null) { host = host.replace(m.group(6), "CDN"); } } } catch (Exception e) { LOG.error(e.getMessage(), e); } return String.format("host_%s", host); } private static final String protocolKey(String proto) { return String.format("protocol_%s", proto); } /** * Collects counts statistics about the request: client ip address from the x-forwarded-for header; * ipv4 or ipv6 and host name from the host header; * * @param req */ public void collectRequestStats(HttpRequestInfo req) { // ipv4/ipv6 tracking String clientIp; String xForwardedFor = req.getHeaders().getFirst(X_FORWARDED_FOR_HEADER); if (xForwardedFor == null) { clientIp = req.getClientIp(); } else { clientIp = extractClientIpFromXForwardedFor(xForwardedFor); } boolean isIPv6 = (clientIp != null) ? isIPv6(clientIp) : false; String ipVersionKey = isIPv6 ? "ipv6" : "ipv4"; incrementNamedCountingMonitor(ipVersionKey, ipVersionCounterMap); // host header String host = req.getHeaders().getFirst(HOST_HEADER); if (host != null) { int colonIdx; if (isIPv6) { // an ipv6 host might be a raw IP with 7+ colons colonIdx = host.lastIndexOf(":"); } else { // strips port from host colonIdx = host.indexOf(":"); } if (colonIdx > -1) { host = host.substring(0, colonIdx); } incrementNamedCountingMonitor(hostKey(host), this.hostCounterMap); } // http vs. https String protocol = req.getHeaders().getFirst(X_FORWARDED_PROTO_HEADER); if (protocol == null) { protocol = req.getScheme(); } incrementNamedCountingMonitor(protocolKey(protocol), this.protocolCounterMap); } @VisibleForTesting static final boolean isIPv6(String ip) { return ip.split(":", -1).length == 8; } @VisibleForTesting static final String extractClientIpFromXForwardedFor(String xForwardedFor) { return xForwardedFor.split(",", -1)[0]; } /** * helper method to create new monitor, place into map, and register with Epic, if necessary */ protected void incrementNamedCountingMonitor(String name, ConcurrentMap map) { NamedCountingMonitor monitor = map.get(name); if (monitor == null) { monitor = new NamedCountingMonitor(name); NamedCountingMonitor conflict = map.putIfAbsent(name, monitor); if (conflict != null) { monitor = conflict; } else { MonitorRegistry.getInstance().registerObject(monitor); } } monitor.increment(); } /** * collects and increments counts of status code, route/status code and statuc_code bucket, eg 2xx 3xx 4xx 5xx * * @param route * @param statusCode */ public void collectRouteStats(String route, int statusCode) { // increments 200, 301, 401, 503, etc. status counters String preciseStatusString = String.format("status_%d", statusCode); NamedCountingMonitor preciseStatus = namedStatusMap.get(preciseStatusString); if (preciseStatus == null) { preciseStatus = new NamedCountingMonitor(preciseStatusString); NamedCountingMonitor found = namedStatusMap.putIfAbsent(preciseStatusString, preciseStatus); if (found != null) { preciseStatus = found; } else { MonitorRegistry.getInstance().registerObject(preciseStatus); } } preciseStatus.increment(); // increments 2xx, 3xx, 4xx, 5xx status counters String summaryStatusString = String.format("status_%dxx", statusCode / 100); NamedCountingMonitor summaryStatus = namedStatusMap.get(summaryStatusString); if (summaryStatus == null) { summaryStatus = new NamedCountingMonitor(summaryStatusString); NamedCountingMonitor found = namedStatusMap.putIfAbsent(summaryStatusString, summaryStatus); if (found != null) { summaryStatus = found; } else { MonitorRegistry.getInstance().registerObject(summaryStatus); } } summaryStatus.increment(); // increments route and status counter if (route == null) { route = "ROUTE_NOT_FOUND"; } route = route.replace("/", "_"); ConcurrentHashMap statsMap = routeStatusMap.get(route); if (statsMap == null) { statsMap = new ConcurrentHashMap(); routeStatusMap.putIfAbsent(route, statsMap); } RouteStatusCodeMonitor sd = statsMap.get(statusCode); if (sd == null) { // don't register only 404 status codes (these are garbage endpoints) if (statusCode == 404) { if (statsMap.size() == 0) { return; } } sd = new RouteStatusCodeMonitor(route, statusCode); RouteStatusCodeMonitor sd1 = statsMap.putIfAbsent(statusCode, sd); if (sd1 != null) { sd = sd1; } else { MonitorRegistry.getInstance().registerObject(sd); } } sd.update(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/monitoring/Monitor.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.monitoring; /** * Interface to register a counter to monitor * @author Mikey Cohen * Date: 3/18/13 * Time: 4:33 PM */ public interface Monitor { /** * Implement this to add this Counter to a Registry * @param monitorObj */ void register(NamedCount monitorObj); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/monitoring/MonitorRegistry.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.monitoring; /** * Registry to register a Counter. a Monitor publisher should be set to get counter information. * If it isn't set, registration will be ignored. * @author Mikey Cohen * Date: 3/18/13 * Time: 4:24 PM */ public class MonitorRegistry { private static final MonitorRegistry instance = new MonitorRegistry(); private Monitor publisher; /** * A Monitor implementation should be set here * @param publisher */ public void setPublisher(Monitor publisher) { this.publisher = publisher; } public static MonitorRegistry getInstance() { return instance; } public void registerObject(NamedCount monitorObj) { if (publisher != null) { publisher.register(monitorObj); } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/monitoring/NamedCount.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.monitoring; /** * Interface for a named counter * @author Mikey Cohen * Date: 3/18/13 * Time: 4:33 PM */ public interface NamedCount { String getName(); long getCount(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/status/StatusCategory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.status; import com.google.errorprone.annotations.Immutable; /** * Status Category * * Author: Arthur Gonigberg * Date: December 20, 2017 */ @Immutable public interface StatusCategory { String getId(); StatusCategoryGroup getGroup(); String getReason(); String name(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/status/StatusCategoryGroup.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.status; import com.google.errorprone.annotations.Immutable; /** * Status Category Group * * Author: Arthur Gonigberg * Date: December 20, 2017 */ @Immutable public interface StatusCategoryGroup { int getId(); String name(); } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/status/StatusCategoryUtils.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.status; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import javax.annotation.Nullable; /** * User: michaels@netflix.com * Date: 6/9/15 * Time: 2:48 PM */ public class StatusCategoryUtils { public static StatusCategory getStatusCategory(ZuulMessage msg) { return getStatusCategory(msg.getContext()); } @Nullable public static StatusCategory getStatusCategory(SessionContext ctx) { return ctx.get(CommonContextKeys.STATUS_CATEGORY); } @Nullable public static String getStatusCategoryReason(SessionContext ctx) { return ctx.get(CommonContextKeys.STATUS_CATEGORY_REASON); } public static void setStatusCategory(SessionContext ctx, StatusCategory statusCategory) { setStatusCategory(ctx, statusCategory, statusCategory.getReason()); } public static void setStatusCategory(SessionContext ctx, StatusCategory statusCategory, String reason) { ctx.put(CommonContextKeys.STATUS_CATEGORY, statusCategory); ctx.put(CommonContextKeys.STATUS_CATEGORY_REASON, reason); } public static void clearStatusCategory(SessionContext ctx) { ctx.remove(CommonContextKeys.STATUS_CATEGORY); ctx.remove(CommonContextKeys.STATUS_CATEGORY_REASON); } @Nullable public static StatusCategory getOriginStatusCategory(SessionContext ctx) { return ctx.get(CommonContextKeys.ORIGIN_STATUS_CATEGORY); } @Nullable public static String getOriginStatusCategoryReason(SessionContext ctx) { return ctx.get(CommonContextKeys.ORIGIN_STATUS_CATEGORY_REASON); } public static void setOriginStatusCategory(SessionContext ctx, StatusCategory statusCategory) { setOriginStatusCategory(ctx, statusCategory, statusCategory.getReason()); } public static void setOriginStatusCategory(SessionContext ctx, StatusCategory statusCategory, String reason) { ctx.put(CommonContextKeys.ORIGIN_STATUS_CATEGORY, statusCategory); ctx.put(CommonContextKeys.ORIGIN_STATUS_CATEGORY_REASON, reason); } public static void clearOriginStatusCategory(SessionContext ctx) { ctx.remove(CommonContextKeys.ORIGIN_STATUS_CATEGORY); ctx.remove(CommonContextKeys.ORIGIN_STATUS_CATEGORY_REASON); } public static boolean isResponseHttpErrorStatus(HttpResponseMessage response) { boolean isHttpError = false; if (response != null) { int status = response.getStatus(); isHttpError = isResponseHttpErrorStatus(status); } return isHttpError; } public static boolean isResponseHttpErrorStatus(int status) { return (status < 100 || status >= 500); } public static void storeStatusCategoryIfNotAlreadyFailure(SessionContext context, StatusCategory statusCategory) { if (statusCategory != null) { StatusCategory nfs = getStatusCategory(context); if (nfs == null || nfs.getGroup().getId() == ZuulStatusCategoryGroup.SUCCESS.getId()) { setStatusCategory(context, statusCategory); } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/status/ZuulStatusCategory.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.status; import com.google.errorprone.annotations.Immutable; /** * Zuul Status Category * * As some of the origin servers won't/can't return correct HTTP status codes in responses, we use set an * StatusCategory attribute to distinguish the main statuses that we care about from Zuul's perspective. * * These status categories are split into 2 groups: * * SUCCESS | FAILURE * * each of which can have a narrower definition, eg: * * FAILURE_THROTTLED * FAILURE_ORIGIN * etc... * * which _should_ also be subdivided with one of: * * ORIGIN * CLIENT * LOCAL */ @Immutable public enum ZuulStatusCategory implements StatusCategory { SUCCESS(ZuulStatusCategoryGroup.SUCCESS, 1, "Successfully proxied"), SUCCESS_NOT_FOUND( ZuulStatusCategoryGroup.SUCCESS, 3, "Successfully proxied, origin responded with no resource found"), // This is set on for all 404 responses SUCCESS_LOCAL_NOTSET( ZuulStatusCategoryGroup.SUCCESS, 4, "Default status"), // This is set on the SessionContext as the default value. SUCCESS_LOCAL_NO_ROUTE(ZuulStatusCategoryGroup.SUCCESS, 5, "Unable to determine an origin to handle request"), FAILURE_LOCAL(ZuulStatusCategoryGroup.FAILURE, 1, "Failed internally"), FAILURE_LOCAL_THROTTLED_ORIGIN_SERVER_MAXCONN( ZuulStatusCategoryGroup.FAILURE, 7, "Throttled due to reaching max number of connections to origin"), FAILURE_LOCAL_THROTTLED_ORIGIN_CONCURRENCY( ZuulStatusCategoryGroup.FAILURE, 8, "Throttled due to reaching concurrency limit to origin"), FAILURE_LOCAL_IDLE_TIMEOUT(ZuulStatusCategoryGroup.FAILURE, 9, "Idle timeout due to channel inactivity"), FAILURE_LOCAL_HEADER_FIELDS_TOO_LARGE(ZuulStatusCategoryGroup.FAILURE, 5, "Header Fields Too Large"), FAILURE_CLIENT_BAD_REQUEST(ZuulStatusCategoryGroup.FAILURE, 12, "Invalid request provided"), FAILURE_CLIENT_CANCELLED(ZuulStatusCategoryGroup.FAILURE, 13, "Client abandoned/closed the connection"), FAILURE_CLIENT_PIPELINE_REJECT(ZuulStatusCategoryGroup.FAILURE, 17, "Client rejected due to HTTP Pipelining"), FAILURE_CLIENT_TIMEOUT(ZuulStatusCategoryGroup.FAILURE, 18, "Timeout reading the client request"), FAILURE_ORIGIN(ZuulStatusCategoryGroup.FAILURE, 2, "Origin returned an error status"), FAILURE_ORIGIN_READ_TIMEOUT(ZuulStatusCategoryGroup.FAILURE, 3, "Timeout reading the response from origin"), FAILURE_ORIGIN_CONNECTIVITY(ZuulStatusCategoryGroup.FAILURE, 4, "Connection to origin failed"), FAILURE_ORIGIN_THROTTLED(ZuulStatusCategoryGroup.FAILURE, 6, "Throttled by origin returning 503 status"), FAILURE_ORIGIN_NO_SERVERS(ZuulStatusCategoryGroup.FAILURE, 14, "No UP origin servers available in Discovery"), FAILURE_ORIGIN_RESET_CONNECTION( ZuulStatusCategoryGroup.FAILURE, 15, "Connection reset on an established origin connection"), FAILURE_ORIGIN_CLOSE_NOTIFY_CONNECTION(ZuulStatusCategoryGroup.FAILURE, 16, "Connection TLS session shutdown"); private final StatusCategoryGroup group; private final String id; private final String reason; ZuulStatusCategory(StatusCategoryGroup group, int index, String reason) { this.group = group; this.id = (group.getId() + "_" + index).intern(); this.reason = reason; } @Override public String getId() { return id; } @Override public StatusCategoryGroup getGroup() { return group; } @Override public String getReason() { return reason; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/stats/status/ZuulStatusCategoryGroup.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.status; import com.google.errorprone.annotations.Immutable; /** * Zuul Status Category Group * * Author: Arthur Gonigberg * Date: December 20, 2017 */ @Immutable public enum ZuulStatusCategoryGroup implements StatusCategoryGroup { SUCCESS(1), FAILURE(2); private final int id; ZuulStatusCategoryGroup(int id) { this.id = id; } @Override public int getId() { return id; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/util/Gzipper.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import com.netflix.zuul.exception.ZuulException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpContent; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.GZIPOutputStream; /** * Refactored this out of our GZipResponseFilter * * User: michaels@netflix.com * Date: 5/10/16 * Time: 12:31 PM */ public class Gzipper { private final ByteArrayOutputStream baos; private final GZIPOutputStream gzos; public Gzipper() throws RuntimeException { try { baos = new ByteArrayOutputStream(256); gzos = new GZIPOutputStream(baos, true); } catch (IOException e) { throw new RuntimeException("Error finalizing the GzipOutputstream", e); } } private void write(ByteBuf bb) throws IOException { byte[] bytes; int offset; int length = bb.readableBytes(); if (bb.hasArray()) { /* avoid memory copy if possible */ bytes = bb.array(); offset = bb.arrayOffset(); } else { bytes = new byte[length]; bb.getBytes(bb.readerIndex(), bytes); offset = 0; } gzos.write(bytes, offset, length); } public void write(HttpContent chunk) { try { write(chunk.content()); gzos.flush(); } catch (IOException ioEx) { throw new ZuulException(ioEx, "Error Gzipping response content chunk", true); } finally { chunk.release(); } } public void finish() throws RuntimeException { try { gzos.finish(); gzos.flush(); gzos.close(); } catch (IOException ioEx) { throw new ZuulException(ioEx, "Error finalizing the GzipOutputStream", true); } } public ByteBuf getByteBuf() { ByteBuf copy = Unpooled.copiedBuffer(baos.toByteArray()); baos.reset(); return copy; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/util/HttpUtils.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpHeaderNames; import com.netflix.zuul.message.http.HttpRequestInfo; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http2.Http2StreamChannel; import java.util.Locale; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * User: Mike Smith * Date: 4/28/15 * Time: 11:05 PM */ public class HttpUtils { private static final Logger LOG = LoggerFactory.getLogger(HttpUtils.class); private static final char[] MALICIOUS_HEADER_CHARS = {'\r', '\n'}; /** * Get the IP address of client making the request. * * Uses the "x-forwarded-for" HTTP header if available, otherwise uses the remote * IP of requester. * * @param request HttpRequestMessage * @return String IP address */ public static String getClientIP(HttpRequestInfo request) { String xForwardedFor = request.getHeaders().getFirst(HttpHeaderNames.X_FORWARDED_FOR); String clientIP; if (xForwardedFor == null) { clientIP = request.getClientIp(); } else { clientIP = extractClientIpFromXForwardedFor(xForwardedFor); } return clientIP; } /** * Extract the client IP address from an x-forwarded-for header. Returns null if there is no x-forwarded-for header * * @param xForwardedFor a String value * @return a String value */ public static String extractClientIpFromXForwardedFor(String xForwardedFor) { if (xForwardedFor == null) { return null; } xForwardedFor = xForwardedFor.trim(); String tokenized[] = xForwardedFor.split(",", -1); if (tokenized.length == 0) { return null; } else { return tokenized[0].trim(); } } @VisibleForTesting static boolean isCompressed(String contentEncoding) { return contentEncoding.contains(HttpHeaderValues.GZIP.toString()) || contentEncoding.contains(HttpHeaderValues.DEFLATE.toString()) || contentEncoding.contains(HttpHeaderValues.BR.toString()) || contentEncoding.contains(HttpHeaderValues.COMPRESS.toString()); } public static boolean isCompressed(Headers headers) { String ce = headers.getFirst(HttpHeaderNames.CONTENT_ENCODING); return ce != null && isCompressed(ce); } public static boolean acceptsGzip(Headers headers) { String ae = headers.getFirst(HttpHeaderNames.ACCEPT_ENCODING); return ae != null && ae.contains(HttpHeaderValues.GZIP.toString()); } /** * Ensure decoded new lines are not propagated in headers, in order to prevent XSS * * @param input - decoded header string * @return - clean header string */ public static String stripMaliciousHeaderChars(@Nullable String input) { if (input == null) { return null; } // TODO(carl-mastrangelo): implement this more efficiently. for (char c : MALICIOUS_HEADER_CHARS) { if (input.indexOf(c) != -1) { input = input.replace(Character.toString(c), ""); } } return input; } public static boolean hasNonZeroContentLengthHeader(ZuulMessage msg) { Integer contentLengthVal = getContentLengthIfPresent(msg); return (contentLengthVal != null) && (contentLengthVal > 0); } public static Integer getContentLengthIfPresent(ZuulMessage msg) { String contentLengthValue = msg.getHeaders().getFirst(com.netflix.zuul.message.http.HttpHeaderNames.CONTENT_LENGTH); if (!Strings.isNullOrEmpty(contentLengthValue)) { try { return Integer.valueOf(contentLengthValue); } catch (NumberFormatException e) { LOG.info("Invalid Content-Length header value on request. " + "value = {}", contentLengthValue, e); } } return null; } public static Integer getBodySizeIfKnown(ZuulMessage msg) { Integer bodySize = getContentLengthIfPresent(msg); if (bodySize != null) { return bodySize; } if (msg.hasCompleteBody()) { return msg.getBodyLength(); } return null; } public static boolean hasChunkedTransferEncodingHeader(ZuulMessage msg) { boolean isChunked = false; String teValue = msg.getHeaders().getFirst(com.netflix.zuul.message.http.HttpHeaderNames.TRANSFER_ENCODING); if (!Strings.isNullOrEmpty(teValue)) { isChunked = teValue.toLowerCase(Locale.ROOT).equals("chunked"); } return isChunked; } /** * If http/1 then will always want to just use ChannelHandlerContext.channel(), but for http/2 * will want the parent channel (as the child channel is different for each h2 stream). */ public static Channel getMainChannel(ChannelHandlerContext ctx) { return getMainChannel(ctx.channel()); } public static Channel getMainChannel(Channel channel) { if (channel instanceof Http2StreamChannel) { return channel.parent(); } return channel; } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/util/JsonUtility.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import java.util.Arrays; import java.util.Collection; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility for generating JSON from Maps/Lists */ public class JsonUtility { private static final Logger logger = LoggerFactory.getLogger(JsonUtility.class); /** * Pass in a Map and this method will return a JSON string. * *

The map can contain Objects, int[], Object[] and Collections and they will be converted * into string representations. * *

Nested maps can be included as values and the JSON will have nested object notation. * *

Arrays/Collections can have Maps in them as well. * *

See the unit tests for examples. * * @param jsonData */ public static String jsonFromMap(Map jsonData) { try { JsonDocument json = new JsonDocument(); json.startGroup(); for (String key : jsonData.keySet()) { Object data = jsonData.get(key); if (data instanceof Map) { /* it's a nested map, so we'll recursively add the JSON of this map to the current JSON */ json.addValue(key, jsonFromMap((Map) data)); } else if (data instanceof Object[]) { /* it's an object array, so we'll iterate the elements and put them all in here */ json.addValue(key, "[" + stringArrayFromObjectArray((Object[]) data) + "]"); } else if (data instanceof Collection) { /* it's a collection, so we'll iterate the elements and put them all in here */ json.addValue(key, "[" + stringArrayFromObjectArray(((Collection) data).toArray()) + "]"); } else if (data instanceof int[]) { /* it's an int array, so we'll get the string representation */ String intArray = Arrays.toString((int[]) data); /* remove whitespace */ intArray = intArray.replaceAll(" ", ""); json.addValue(key, intArray); } else if (data instanceof JsonCapableObject) { json.addValue(key, jsonFromMap(((JsonCapableObject) data).jsonMap())); } else { /* all other objects we assume we are to just put the string value in */ json.addValue(key, String.valueOf(data)); } } json.endGroup(); logger.debug("created json from map => {}", json); return json.toString(); } catch (Exception e) { logger.error("Could not create JSON from Map. ", e); return "{}"; } } /* * return a string like: "one","two","three" */ private static String stringArrayFromObjectArray(Object data[]) { StringBuilder arrayAsString = new StringBuilder(); for (Object o : data) { if (arrayAsString.length() > 0) { arrayAsString.append(","); } if (o instanceof Map) { arrayAsString.append(jsonFromMap((Map) o)); } else if (o instanceof JsonCapableObject) { arrayAsString.append(jsonFromMap(((JsonCapableObject) o).jsonMap())); } else { arrayAsString.append("\"").append(String.valueOf(o)).append("\""); } } return arrayAsString.toString(); } private static class JsonDocument { final StringBuilder json = new StringBuilder(); private boolean newGroup = false; public JsonDocument startGroup() { newGroup = true; json.append("{"); return this; } public JsonDocument endGroup() { json.append("}"); return this; } public JsonDocument addValue(String key, String value) { if (!newGroup) { // if this is not the first value in a group, put a comma json.append(","); } /* once we're here, the group is no longer "new" */ newGroup = false; /* append the key/value */ json.append("\"").append(key).append("\""); json.append(":"); if (value.trim().startsWith("{") || value.trim().startsWith("[")) { // the value is either JSON or an array, so we won't aggregate with quotes json.append(value); } else { json.append("\"").append(value).append("\""); } return this; } @Override public String toString() { return json.toString(); } } public static interface JsonCapableObject { public Map jsonMap(); } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/util/ProxyUtils.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import com.netflix.config.CachedDynamicBooleanProperty; import com.netflix.zuul.message.HeaderName; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpHeaderNames; import com.netflix.zuul.message.http.HttpRequestMessage; import java.util.HashSet; import java.util.Set; /** * User: michaels@netflix.com * Date: 6/8/15 * Time: 11:50 AM */ public class ProxyUtils { private static final CachedDynamicBooleanProperty OVERWRITE_XF_HEADERS = new CachedDynamicBooleanProperty("zuul.headers.xforwarded.overwrite", false); private static final Set RESP_HEADERS_TO_STRIP = new HashSet<>(); static { RESP_HEADERS_TO_STRIP.add(HttpHeaderNames.CONNECTION); RESP_HEADERS_TO_STRIP.add(HttpHeaderNames.TRANSFER_ENCODING); RESP_HEADERS_TO_STRIP.add(HttpHeaderNames.KEEP_ALIVE); } private static final Set REQ_HEADERS_TO_STRIP = new HashSet<>(); static { REQ_HEADERS_TO_STRIP.add( HttpHeaderNames .CONTENT_LENGTH); // Because the httpclient library sets this itself, and doesn't like it if set REQ_HEADERS_TO_STRIP.add(HttpHeaderNames.CONNECTION); REQ_HEADERS_TO_STRIP.add(HttpHeaderNames.TRANSFER_ENCODING); REQ_HEADERS_TO_STRIP.add(HttpHeaderNames.KEEP_ALIVE); } public static boolean isValidRequestHeader(HeaderName headerName) { return !REQ_HEADERS_TO_STRIP.contains(headerName); } public static boolean isValidResponseHeader(HeaderName headerName) { return !RESP_HEADERS_TO_STRIP.contains(headerName); } public static void addXForwardedHeaders(HttpRequestMessage request) { // Add standard Proxy request headers. Headers headers = request.getHeaders(); addXForwardedHeader(headers, HttpHeaderNames.X_FORWARDED_HOST, request.getOriginalHost()); addXForwardedHeader(headers, HttpHeaderNames.X_FORWARDED_PORT, Integer.toString(request.getPort())); addXForwardedHeader(headers, HttpHeaderNames.X_FORWARDED_PROTO, request.getScheme()); addXForwardedHeader(headers, HttpHeaderNames.X_FORWARDED_FOR, request.getClientIp()); } public static void addXForwardedHeader(Headers headers, HeaderName name, String latestValue) { if (OVERWRITE_XF_HEADERS.get()) { headers.set(name, latestValue); } else { // If this proxy header already exists (possibly due to an upstream ELB or reverse proxy // setting it) then keep that value. String existingValue = headers.getFirst(name); if (existingValue == null) { // Otherwise set new value. if (latestValue != null) { headers.set(name, latestValue); } } } } } ================================================ FILE: zuul-core/src/main/java/com/netflix/zuul/util/VipUtils.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; public final class VipUtils { public static String getVIPPrefix(String vipAddress) { for (int i = 0; i < vipAddress.length(); i++) { char c = vipAddress.charAt(i); if (c == '.' || c == ':') { return vipAddress.substring(0, i); } } return vipAddress; } /** * Use {@link #extractUntrustedAppNameFromVIP} instead. */ @Deprecated public static String extractAppNameFromVIP(String vipAddress) { String vipPrefix = getVIPPrefix(vipAddress); return vipPrefix.split("-", -1)[0]; } /** * Attempts to derive an app name from the VIP. Because the VIP is an arbitrary collection of characters, the * value is just a best guess and not suitable for security purposes. */ public static String extractUntrustedAppNameFromVIP(String vipAddress) { for (int i = 0; i < vipAddress.length(); i++) { char c = vipAddress.charAt(i); if (c == '-' || c == '.' || c == ':') { return vipAddress.substring(0, i); } } return vipAddress; } private VipUtils() {} } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/CloseOnIdleStateHandlerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.zuul.netty.server.http2.DummyChannelHandler; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.timeout.IdleStateEvent; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class CloseOnIdleStateHandlerTest { private final Registry registry = new DefaultRegistry(); private Id counterId; private final String listener = "test-idle-state"; @BeforeEach void setup() { counterId = registry.createId("server.connections.idle.timeout").withTags("id", listener); } @Test void incrementCounterOnIdleStateEvent() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addLast(new DummyChannelHandler()); channel.pipeline().addLast(new CloseOnIdleStateHandler(registry, listener)); channel.pipeline() .context(DummyChannelHandler.class) .fireUserEventTriggered(IdleStateEvent.ALL_IDLE_STATE_EVENT); Counter idleTimeouts = (Counter) registry.get(counterId); assertThat(idleTimeouts.count()).isEqualTo(1); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/HttpServerLifecycleChannelHandlerTest.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.netty.common.HttpLifecycleChannelHandler.State; import com.netflix.netty.common.HttpServerLifecycleChannelHandler.HttpServerLifecycleInboundChannelHandler; import com.netflix.netty.common.HttpServerLifecycleChannelHandler.HttpServerLifecycleOutboundChannelHandler; import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.ReferenceCountUtil; import org.junit.jupiter.api.Test; class HttpServerLifecycleChannelHandlerTest { final class AssertReasonHandler extends ChannelInboundHandlerAdapter { CompleteEvent completeEvent; @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { assertThat(evt).isInstanceOf(CompleteEvent.class); this.completeEvent = (CompleteEvent) evt; } public CompleteEvent getCompleteEvent() { return completeEvent; } } @Test void completionEventReasonIsUpdatedOnPipelineReject() { EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleOutboundChannelHandler()); AssertReasonHandler reasonHandler = new AssertReasonHandler(); channel.pipeline().addLast(reasonHandler); channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED); // emulate pipeline rejection channel.attr(HttpLifecycleChannelHandler.ATTR_HTTP_PIPELINE_REJECT).set(Boolean.TRUE); // Fire close channel.pipeline().close(); assertThat(reasonHandler.getCompleteEvent().getReason()).isEqualTo(CompleteReason.PIPELINE_REJECT); } @Test void completionEventReasonIsCloseByDefault() { EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleOutboundChannelHandler()); AssertReasonHandler reasonHandler = new AssertReasonHandler(); channel.pipeline().addLast(reasonHandler); channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED); // Fire close channel.pipeline().close(); assertThat(reasonHandler.getCompleteEvent().getReason()).isEqualTo(CompleteReason.CLOSE); } @Test void pipelineRejectReleasesIfNeeded() { EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleInboundChannelHandler()); ByteBuf buffer = UnpooledByteBufAllocator.DEFAULT.buffer(); try { assertThat(buffer.refCnt()).isEqualTo(1); FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/whatever", buffer); channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED); channel.writeInbound(httpRequest); assertThat(channel.attr(HttpLifecycleChannelHandler.ATTR_HTTP_PIPELINE_REJECT) .get()) .isEqualTo(Boolean.TRUE); assertThat(buffer.refCnt()).isEqualTo(0); } finally { if (buffer.refCnt() != 0) { ReferenceCountUtil.release(buffer); } } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/SourceAddressChannelHandlerTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.netty.common; import static org.assertj.core.api.Assertions.assertThat; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.opentest4j.TestAbortedException; /** * Unit tests for {@link SourceAddressChannelHandler}. */ class SourceAddressChannelHandlerTest { @Test void ipv6AddressScopeIdRemoved() throws Exception { Inet6Address address = Inet6Address.getByAddress("localhost", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 2); assertThat(address.getScopeId()).isEqualTo(2); String addressString = SourceAddressChannelHandler.getHostAddress(new InetSocketAddress(address, 8080)); assertThat(addressString).isEqualTo("0:0:0:0:0:0:0:1"); } @Test void ipv4AddressString() throws Exception { InetAddress address = Inet4Address.getByAddress("localhost", new byte[] {127, 0, 0, 1}); String addressString = SourceAddressChannelHandler.getHostAddress(new InetSocketAddress(address, 8080)); assertThat(addressString).isEqualTo("127.0.0.1"); } @Test void failsOnUnresolved() { InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 8080); String addressString = SourceAddressChannelHandler.getHostAddress(address); assertThat(addressString).isNull(); } @Test void mapsIpv4AddressFromIpv6Address() throws Exception { // Can't think of a reason why this would ever come up, but testing it just in case. // ::ffff:127.0.0.1 Inet6Address address = Inet6Address.getByAddress( "localhost", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xFF, (byte) 0xFF, 127, 0, 0, 1}, -1); assertThat(address.getScopeId()).isEqualTo(0); String addressString = SourceAddressChannelHandler.getHostAddress(new InetSocketAddress(address, 8080)); assertThat(addressString).isEqualTo("127.0.0.1"); } @Test void ipv6AddressScopeNameRemoved() throws Exception { List nics = Collections.list(NetworkInterface.getNetworkInterfaces()); Assumptions.assumeTrue(!nics.isEmpty(), "No network interfaces"); List failures = new ArrayList<>(); for (NetworkInterface nic : nics) { Inet6Address address; try { address = Inet6Address.getByAddress( "localhost", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, nic); } catch (UnknownHostException e) { // skip, the nic doesn't match failures.add(e); continue; } assertThat(address.toString().contains("%")).as(address.toString()).isTrue(); String addressString = SourceAddressChannelHandler.getHostAddress(new InetSocketAddress(address, 8080)); assertThat(addressString).isEqualTo("0:0:0:0:0:0:0:1"); return; } TestAbortedException failure = new TestAbortedException("No Compatible Nics were found"); failures.forEach(failure::addSuppressed); throw failure; } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/metrics/InstrumentedResourceLeakDetectorTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class InstrumentedResourceLeakDetectorTest { InstrumentedResourceLeakDetector leakDetector; @BeforeEach void setup() { leakDetector = new InstrumentedResourceLeakDetector<>(ByteBuf.class, 1); } @Test void test() { leakDetector.reportTracedLeak("test", "test"); assertThat(leakDetector.leakCounter.get()).isEqualTo(1); leakDetector.reportTracedLeak("test", "test"); assertThat(leakDetector.leakCounter.get()).isEqualTo(2); leakDetector.reportTracedLeak("test", "test"); assertThat(leakDetector.leakCounter.get()).isEqualTo(3); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/proxyprotocol/ElbProxyProtocolChannelHandlerTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.netty.common.proxyprotocol; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.net.InetAddresses; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.haproxy.HAProxyMessage; import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class ElbProxyProtocolChannelHandlerTest { private Registry registry; @BeforeEach void setup() { registry = new DefaultRegistry(); } @Test void noProxy() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, false)); ByteBuf buf = Unpooled.wrappedBuffer( "PROXY TCP4 192.168.0.1 124.123.111.111 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object dropped = channel.readInbound(); assertThat(buf).isEqualTo(dropped); buf.release(); assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNull(); assertThat(channel.pipeline().context("HAProxyMessageChannelHandler")).isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION) .get()) .isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE) .get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS).get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).get()) .isNull(); } @Test void extraDataForwarded() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); ByteBuf buf = Unpooled.wrappedBuffer( "PROXY TCP4 192.168.0.1 124.123.111.111 10008 443\r\nPOTATO".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object msg = channel.readInbound(); assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNull(); ByteBuf readBuf = (ByteBuf) msg; assertThat(new String(ByteBufUtil.getBytes(readBuf), StandardCharsets.US_ASCII)) .isEqualTo("POTATO"); readBuf.release(); } @Test void passThrough_ProxyProtocolEnabled_nonProxyBytes() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); // Note that the bytes aren't prefixed by PROXY, as required by the spec ByteBuf buf = Unpooled.wrappedBuffer( "TCP4 192.168.0.1 124.123.111.111 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object dropped = channel.readInbound(); assertThat(buf).isEqualTo(dropped); buf.release(); assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNull(); assertThat(channel.pipeline().context("HAProxyMessageChannelHandler")).isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION) .get()) .isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE) .get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS).get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get()) .isNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).get()) .isNull(); } @Test void incrementCounterWhenPPEnabledButNonHAPMMessage() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); int port = 7007; channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(port); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); // Note that the bytes aren't prefixed by PROXY, as required by the spec ByteBuf buf = Unpooled.wrappedBuffer( "TCP4 192.168.0.1 124.123.111.111 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object dropped = channel.readInbound(); assertThat(buf).isEqualTo(dropped); buf.release(); Counter counter = registry.counter( "zuul.hapm.decode", "success", "false", "port", String.valueOf(port), "needs_more_data", "false"); assertThat(counter.count()).isEqualTo(1); } @Disabled @Test void detectsSplitPpv1Message() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); ByteBuf buf1 = Unpooled.wrappedBuffer("PROXY TCP4".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf1); ByteBuf buf2 = Unpooled.wrappedBuffer("192.168.0.1 124.123.111.111 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf2); Object msg = channel.readInbound(); assertThat(msg instanceof HAProxyMessage).isTrue(); buf1.release(); buf2.release(); ((HAProxyMessage) msg).release(); // The handler should remove itself. assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.class)) .isNull(); } @Test void tracksSplitMessage() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); int port = 7007; channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(port); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); ByteBuf buf1 = Unpooled.wrappedBuffer("PROXY TCP4".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf1); Object msg = channel.readInbound(); assertThat(msg).isEqualTo(buf1); buf1.release(); // The handler should remove itself. assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.class)) .isNull(); Counter counter = registry.counter( "zuul.hapm.decode", "success", "false", "port", String.valueOf(port), "needs_more_data", "true"); assertThat(counter.count()).isEqualTo(1); } @Test void negotiateProxy_ppv1_ipv4() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); ByteBuf buf = Unpooled.wrappedBuffer( "PROXY TCP4 192.168.0.1 124.123.111.111 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object dropped = channel.readInbound(); assertThat(dropped).isNull(); // The handler should remove itself. assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNull(); assertThat(channel.pipeline().context(HAProxyMessageChannelHandler.class)) .isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION) .get()) .isEqualTo(HAProxyProtocolVersion.V1); // TODO(carl-mastrangelo): this check is in place, but it should be removed. The message is not properly GC'd // in later versions of netty. assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE) .get()) .isNotNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS).get()) .isEqualTo("124.123.111.111"); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).get()) .isEqualTo(new InetSocketAddress(InetAddresses.forString("124.123.111.111"), 443)); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get()) .isEqualTo("192.168.0.1"); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).get()) .isEqualTo(new InetSocketAddress(InetAddresses.forString("192.168.0.1"), 10008)); } @Test void negotiateProxy_ppv1_ipv6() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); ByteBuf buf = Unpooled.wrappedBuffer("PROXY TCP6 ::1 ::2 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object dropped = channel.readInbound(); assertThat(dropped).isNull(); // The handler should remove itself. assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION) .get()) .isEqualTo(HAProxyProtocolVersion.V1); // TODO(carl-mastrangelo): this check is in place, but it should be removed. The message is not properly GC'd // in later versions of netty. assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE) .get()) .isNotNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS).get()) .isEqualTo("::2"); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).get()) .isEqualTo(new InetSocketAddress(InetAddresses.forString("::2"), 443)); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get()) .isEqualTo("::1"); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).get()) .isEqualTo(new InetSocketAddress(InetAddresses.forString("::1"), 10008)); } @Test void negotiateProxy_ppv2_ipv4() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(7007); channel.pipeline() .addLast(ElbProxyProtocolChannelHandler.NAME, new ElbProxyProtocolChannelHandler(registry, true)); ByteBuf buf = Unpooled.wrappedBuffer(new byte[] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, 0x21, 0x11, 0x00, 0x0C, (byte) 0xC0, (byte) 0xA8, 0x00, 0x01, 0x7C, 0x7B, 0x6F, 0x6F, 0x27, 0x18, 0x01, (byte) 0xbb }); channel.writeInbound(buf); Object dropped = channel.readInbound(); assertThat(dropped).isNull(); // The handler should remove itself. assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNull(); assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_VERSION) .get()) .isEqualTo(HAProxyProtocolVersion.V2); // TODO(carl-mastrangelo): this check is in place, but it should be removed. The message is not properly GC'd // in later versions of netty. assertThat(channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE) .get()) .isNotNull(); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDRESS).get()) .isEqualTo("124.123.111.111"); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).get()) .isEqualTo(new InetSocketAddress(InetAddresses.forString("124.123.111.111"), 443)); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get()) .isEqualTo("192.168.0.1"); assertThat(channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).get()) .isEqualTo(new InetSocketAddress(InetAddresses.forString("192.168.0.1"), 10008)); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/proxyprotocol/HAProxyMessageChannelHandlerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.proxyprotocol; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.haproxy.HAProxyMessage; import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; import io.netty.handler.codec.haproxy.HAProxyTLV; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; class HAProxyMessageChannelHandlerTest { @Test void setClientDestPortForHAPM() { EmbeddedChannel channel = new EmbeddedChannel(); // This is normally done by Server. channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); // This is to emulate `ElbProxyProtocolChannelHandler` channel.pipeline() .addLast(HAProxyMessageDecoder.class.getSimpleName(), new HAProxyMessageDecoder()) .addLast(HAProxyMessageChannelHandler.class.getSimpleName(), new HAProxyMessageChannelHandler()); ByteBuf buf = Unpooled.wrappedBuffer( "PROXY TCP4 192.168.0.1 124.123.111.111 10008 443\r\n".getBytes(StandardCharsets.US_ASCII)); channel.writeInbound(buf); Object result = channel.readInbound(); assertThat(result).isNull(); InetSocketAddress destAddress = channel.attr( SourceAddressChannelHandler.ATTR_PROXY_PROTOCOL_DESTINATION_ADDRESS) .get(); InetSocketAddress srcAddress = (InetSocketAddress) channel.attr(SourceAddressChannelHandler.ATTR_REMOTE_ADDR).get(); assertThat(destAddress.getHostString()).isEqualTo("124.123.111.111"); assertThat(destAddress.getPort()).isEqualTo(443); assertThat(srcAddress.getHostString()).isEqualTo("192.168.0.1"); assertThat(srcAddress.getPort()).isEqualTo(10008); Attrs attrs = channel.attr(Server.CONN_DIMENSIONS).get(); Integer port = HAProxyMessageChannelHandler.HAPM_DEST_PORT.get(attrs); assertThat(port.intValue()).isEqualTo(443); String sourceIpVersion = HAProxyMessageChannelHandler.HAPM_SRC_IP_VERSION.get(attrs); assertThat(sourceIpVersion).isEqualTo("v4"); String destIpVersion = HAProxyMessageChannelHandler.HAPM_DEST_IP_VERSION.get(attrs); assertThat(destIpVersion).isEqualTo("v4"); } @Test void v2parseCustomTLVs() { byte[] header = new byte[46]; // Build \r\n\r\n\0\r\nQUIT\n header[0] = 0x0D; // header[1] = 0x0A; // ----- header[2] = 0x0D; // ----- header[3] = 0x0A; // ----- header[4] = 0x00; // ----- header[5] = 0x0D; // ----- header[6] = 0x0A; // ----- header[7] = 0x51; // ----- header[8] = 0x55; // ----- header[9] = 0x49; // ----- header[10] = 0x54; // ----- header[11] = 0x0A; // ----- header[12] = 0x21; // v2 PROXY header[13] = 0x11; // TCP over IPv4 header[14] = 0x00; // Addl. bytes header[15] = (byte) 0x1E; // ----- header[16] = (byte) 0xc0; // Src Addr 192.168.0.1 header[17] = (byte) 0xa8; // ----- header[18] = 0x00; // ----- header[19] = 0x01; // ----- header[20] = (byte) 0x7c; // Dst Addr 124.123.111.111 header[21] = (byte) 0x7b; // ----- header[22] = (byte) 0x6f; // ----- header[23] = (byte) 0x6f; // ----- header[24] = (byte) 0x27; // Source Port 10006 header[25] = 0x16; // ----- header[26] = 0x01; // Destination Port 443 header[27] = (byte) 0xbb; // ----- header[28] = (byte) (byte) 0xe2; // custom TLV type per spec header[29] = (byte) 0x00; // Remaining bytes header[30] = (byte) 0x0F; // ----- header[31] = (byte) 0x6e; // n header[32] = (byte) 0x66; // f header[33] = (byte) 0x6c; // l header[34] = (byte) 0x78; // x header[35] = (byte) 0x2e; // . header[36] = (byte) 0x63; // c header[37] = (byte) 0x75; // u header[38] = (byte) 0x73; // s header[39] = (byte) 0x74; // t header[40] = (byte) 0x6f; // o header[41] = (byte) 0x6d; // m header[42] = (byte) 0x2e; // . header[43] = (byte) 0x74; // t header[44] = (byte) 0x6c; // l header[45] = (byte) 0x76; // v EmbeddedChannel channel = new EmbeddedChannel(); channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.pipeline() .addLast(HAProxyMessageDecoder.class.getSimpleName(), new HAProxyMessageDecoder()) .addLast(HAProxyMessageChannelHandler.class.getSimpleName(), new HAProxyMessageChannelHandler()); channel.writeInbound(Unpooled.wrappedBuffer(header)); Object result = channel.readInbound(); assertThat(result).isNull(); HAProxyMessage hapm = channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE).get(); assertThat(hapm.sourceAddress()).isEqualTo("192.168.0.1"); assertThat(hapm.destinationAddress()).isEqualTo("124.123.111.111"); assertThat(hapm.sourcePort()).isEqualTo(10006); assertThat(hapm.destinationPort()).isEqualTo(443); List nflxTLV = channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_CUSTOM_TLVS) .get(); assertThat(nflxTLV.size()).isEqualTo(1); String payload = nflxTLV.get(0).content().toString(StandardCharsets.UTF_8); assertThat(payload).isEqualTo("nflx.custom.tlv"); } @Test void validatev2TCPV4NoTLVs() { byte[] header = new byte[28]; // Build \r\n\r\n\0\r\nQUIT\n header[0] = 0x0D; // header[1] = 0x0A; // ----- header[2] = 0x0D; // ----- header[3] = 0x0A; // ----- header[4] = 0x00; // ----- header[5] = 0x0D; // ----- header[6] = 0x0A; // ----- header[7] = 0x51; // ----- header[8] = 0x55; // ----- header[9] = 0x49; // ----- header[10] = 0x54; // ----- header[11] = 0x0A; // ----- header[12] = 0x21; // v2 PROXY header[13] = 0x11; // TCP over IPv4 header[14] = 0x00; // Addl. bytes header[15] = (byte) 0x0C; // ----- header[16] = (byte) 0xc0; // Src Addr 192.168.0.1 header[17] = (byte) 0xa8; // ----- header[18] = 0x00; // ----- header[19] = 0x01; // ----- header[20] = (byte) 0x7c; // Dst Addr 124.123.111.111 header[21] = (byte) 0x7b; // ----- header[22] = (byte) 0x6f; // ----- header[23] = (byte) 0x6f; // ----- header[24] = (byte) 0x27; // Source Port 10006 header[25] = 0x16; // ----- header[26] = 0x01; // Destination Port 443 header[27] = (byte) 0xbb; // ----- EmbeddedChannel channel = new EmbeddedChannel(); channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.pipeline() .addLast(HAProxyMessageDecoder.class.getSimpleName(), new HAProxyMessageDecoder()) .addLast(HAProxyMessageChannelHandler.class.getSimpleName(), new HAProxyMessageChannelHandler()); channel.writeInbound(Unpooled.wrappedBuffer(header)); Object result = channel.readInbound(); assertThat(result).isNull(); HAProxyMessage hapm = channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE).get(); assertThat(hapm.sourceAddress()).isEqualTo("192.168.0.1"); assertThat(hapm.destinationAddress()).isEqualTo("124.123.111.111"); assertThat(hapm.sourcePort()).isEqualTo(10006); assertThat(hapm.destinationPort()).isEqualTo(443); List customTLV = channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_CUSTOM_TLVS) .get(); assertThat(customTLV.isEmpty()).isEqualTo(true); } @Test void validateV2TCPV6NoTLVS() { byte[] header = new byte[52]; // Build \r\n\r\n\0\r\nQUIT\n header[0] = 0x0D; // header[1] = 0x0A; // ----- header[2] = 0x0D; // ----- header[3] = 0x0A; // ----- header[4] = 0x00; // ----- header[5] = 0x0D; // ----- header[6] = 0x0A; // ----- header[7] = 0x51; // ----- header[8] = 0x55; // ----- header[9] = 0x49; // ----- header[10] = 0x54; // ----- header[11] = 0x0A; // ----- header[12] = 0x21; // v2 PROXY header[13] = 0x21; // TCP over IPv6 header[14] = 0x00; // Addl. bytes header[15] = 0x24; // ----- header[16] = 0x20; // Source Address header[17] = 0x01; // ----- header[18] = 0x0d; // ----- header[19] = (byte) 0xb8; // ----- header[20] = (byte) 0x85; // ----- header[21] = (byte) 0xa3; // ----- header[22] = 0x00; // ----- header[23] = 0x00; // ----- header[24] = 0x00; // ----- header[25] = 0x00; // ----- header[26] = (byte) 0x8a; // ----- header[27] = 0x2e; // ----- header[28] = 0x03; // ----- header[29] = 0x70; // ----- header[30] = 0x73; // ----- header[31] = 0x34; // ----- header[32] = 0x10; // Destination Address header[33] = 0x50; // ----- header[34] = 0x00; // ----- header[35] = 0x00; // ----- header[36] = 0x00; // ----- header[37] = 0x00; // ----- header[38] = 0x00; // ----- header[39] = 0x00; // ----- header[40] = 0x00; // ----- header[41] = 0x05; // ----- header[42] = 0x06; // ----- header[43] = 0x00; // ----- header[44] = 0x30; // ----- header[45] = 0x0c; // ----- header[46] = 0x32; // ----- header[47] = 0x6b; // ----- header[48] = (byte) 0x27; // Source Port 10006 header[49] = 0x16; // ----- header[50] = 0x01; // Destination Port 443 header[51] = (byte) 0xbb; // ----- EmbeddedChannel channel = new EmbeddedChannel(); channel.attr(Server.CONN_DIMENSIONS).set(Attrs.newInstance()); channel.pipeline() .addLast(HAProxyMessageDecoder.class.getSimpleName(), new HAProxyMessageDecoder()) .addLast(HAProxyMessageChannelHandler.class.getSimpleName(), new HAProxyMessageChannelHandler()); channel.writeInbound(Unpooled.wrappedBuffer(header)); Object result = channel.readInbound(); assertThat(result).isNull(); HAProxyMessage hapm = channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_MESSAGE).get(); assertThat(hapm.sourceAddress()).isEqualTo("2001:db8:85a3:0:0:8a2e:370:7334"); assertThat(hapm.destinationAddress()).isEqualTo("1050:0:0:0:5:600:300c:326b"); assertThat(hapm.sourcePort()).isEqualTo(10006); assertThat(hapm.destinationPort()).isEqualTo(443); List customTLV = channel.attr(HAProxyMessageChannelHandler.ATTR_HAPROXY_CUSTOM_TLVS) .get(); assertThat(customTLV.isEmpty()).isEqualTo(true); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/proxyprotocol/StripUntrustedProxyHeadersHandlerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.proxyprotocol; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler.AllowWhen; import com.netflix.netty.common.ssl.SslHandshakeInfo; import com.netflix.zuul.netty.server.ssl.SslHandshakeInfoHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.ssl.ClientAuth; import io.netty.util.AttributeKey; import io.netty.util.DefaultAttributeMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; /** * Strip Untrusted Proxy Headers Handler Test * * @author Arthur Gonigberg * @since May 27, 2020 */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class StripUntrustedProxyHeadersHandlerTest { @Mock private ChannelHandlerContext channelHandlerContext; @Mock private HttpRequest msg; private HttpHeaders headers; @Mock private Channel channel; @Mock private SslHandshakeInfo sslHandshakeInfo; @BeforeEach void before() { when(channelHandlerContext.channel()).thenReturn(channel); DefaultAttributeMap attributeMap = new DefaultAttributeMap(); attributeMap.attr(SslHandshakeInfoHandler.ATTR_SSL_INFO).set(sslHandshakeInfo); when(channel.attr(any())).thenAnswer(arg -> attributeMap.attr((AttributeKey) arg.getArguments()[0])); headers = new DefaultHttpHeaders(); when(msg.headers()).thenReturn(headers); headers.add(HttpHeaderNames.HOST, "netflix.com"); } @Test void allow_never() throws Exception { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.NEVER); stripHandler.channelRead(channelHandlerContext, msg); verify(stripHandler).stripXFFHeaders(any()); } @Test void allow_always() throws Exception { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.ALWAYS); stripHandler.channelRead(channelHandlerContext, msg); verify(stripHandler, never()).stripXFFHeaders(any()); verify(stripHandler).checkBlacklist(any(), any()); } @Test void allow_mtls_noCert() throws Exception { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.MUTUAL_SSL_AUTH); stripHandler.channelRead(channelHandlerContext, msg); verify(stripHandler).stripXFFHeaders(any()); } @Test void allow_mtls_cert() throws Exception { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.MUTUAL_SSL_AUTH); when(sslHandshakeInfo.getClientAuthRequirement()).thenReturn(ClientAuth.REQUIRE); stripHandler.channelRead(channelHandlerContext, msg); verify(stripHandler, never()).stripXFFHeaders(any()); verify(stripHandler).checkBlacklist(any(), any()); } @Test void blacklist_noMatch() { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.MUTUAL_SSL_AUTH); stripHandler.checkBlacklist(msg, ImmutableList.of("netflix.net")); verify(stripHandler, never()).stripXFFHeaders(any()); } @Test void blacklist_match() { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.MUTUAL_SSL_AUTH); stripHandler.checkBlacklist(msg, ImmutableList.of("netflix.com")); verify(stripHandler).stripXFFHeaders(any()); } @Test void blacklist_match_casing() { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.MUTUAL_SSL_AUTH); stripHandler.checkBlacklist(msg, ImmutableList.of("NeTfLiX.cOm")); verify(stripHandler).stripXFFHeaders(any()); } @Test void strip_match() { StripUntrustedProxyHeadersHandler stripHandler = getHandler(AllowWhen.MUTUAL_SSL_AUTH); headers.add("x-forwarded-for", "abcd"); stripHandler.stripXFFHeaders(msg); assertThat(headers.contains("x-forwarded-for")).isFalse(); } private StripUntrustedProxyHeadersHandler getHandler(AllowWhen allowWhen) { return spy(new StripUntrustedProxyHeadersHandler(allowWhen)); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/ssl/ServerSslConfigTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.netty.common.ssl; import static org.assertj.core.api.Assertions.assertThat; import io.netty.handler.ssl.ClientAuth; import java.io.File; import java.util.List; import org.junit.jupiter.api.Test; class ServerSslConfigTest { @Test void builderSetsDefaults() { ServerSslConfig config = ServerSslConfig.builder().build(); assertThat(config.getClientAuth()).isEqualTo(ClientAuth.NONE); assertThat(config.getSessionTimeout()).isGreaterThan(0); assertThat(config.isSessionTicketsEnabled()).isFalse(); assertThat(config.getProtocols()).isNull(); assertThat(config.getCiphers()).isNull(); assertThat(config.getCertChainFile()).isNull(); assertThat(config.getKeyFile()).isNull(); assertThat(config.getClientAuthTrustStoreFile()).isNull(); assertThat(config.getClientAuthTrustStorePassword()).isNull(); assertThat(config.getClientAuthTrustStorePasswordFile()).isNull(); } @Test void builderSetsAllFields() { File certFile = new File("cert.pem"); File keyFile = new File("key.pem"); File trustStoreFile = new File("truststore.jks"); List ciphers = List.of("TLS_AES_128_GCM_SHA256"); ServerSslConfig config = ServerSslConfig.builder() .protocols(new String[] {"TLSv1.3", "TLSv1.2"}) .ciphers(ciphers) .certChainFile(certFile) .keyFile(keyFile) .clientAuth(ClientAuth.REQUIRE) .clientAuthTrustStoreFile(trustStoreFile) .clientAuthTrustStorePassword("secret") .sessionTimeout(3600) .sessionTicketsEnabled(true) .build(); assertThat(config.getProtocols()).containsExactly("TLSv1.3", "TLSv1.2"); assertThat(config.getCiphers()).containsExactly("TLS_AES_128_GCM_SHA256"); assertThat(config.getCertChainFile()).isEqualTo(certFile); assertThat(config.getKeyFile()).isEqualTo(keyFile); assertThat(config.getClientAuth()).isEqualTo(ClientAuth.REQUIRE); assertThat(config.getClientAuthTrustStoreFile()).isEqualTo(trustStoreFile); assertThat(config.getClientAuthTrustStorePassword()).isEqualTo("secret"); assertThat(config.getSessionTimeout()).isEqualTo(3600); assertThat(config.isSessionTicketsEnabled()).isTrue(); } @Test void getDefaultCiphersReturnsNonEmptyList() { assertThat(ServerSslConfig.getDefaultCiphers()).isNotEmpty(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/netty/common/throttle/MaxInboundConnectionsHandlerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.netty.common.throttle; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.zuul.netty.server.http2.DummyChannelHandler; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class MaxInboundConnectionsHandlerTest { private final Registry registry = new DefaultRegistry(); private final String listener = "test-throttled"; private Id counterId; @BeforeEach void setup() { counterId = registry.createId("server.connections.throttled").withTags("id", listener); } @Test void verifyPassportStateAndAttrs() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addLast(new DummyChannelHandler()); channel.pipeline().addLast(new MaxInboundConnectionsHandler(registry, listener, 1)); // Fire twice to increment current conns. count channel.pipeline().context(DummyChannelHandler.class).fireChannelActive(); channel.pipeline().context(DummyChannelHandler.class).fireChannelActive(); Counter throttledCount = (Counter) registry.get(counterId); assertThat(throttledCount.count()).isEqualTo(1); assertThat(CurrentPassport.fromChannel(channel).getState()).isEqualTo(PassportState.SERVER_CH_THROTTLING); assertThat(channel.attr(MaxInboundConnectionsHandler.ATTR_CH_THROTTLED).get()) .isTrue(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/AttrsTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; class AttrsTest { @Test void keysAreUnique() { Attrs attrs = Attrs.newInstance(); Attrs.Key key1 = Attrs.newKey("foo"); key1.put(attrs, "bar"); Attrs.Key key2 = Attrs.newKey("foo"); key2.put(attrs, "baz"); assertThat(attrs.keySet()).containsExactlyInAnyOrder(key1, key2); } @Test void newKeyFailsOnNull() { assertThatThrownBy(() -> Attrs.newKey(null)).isInstanceOf(NullPointerException.class); } @Test void attrsPutFailsOnNull() { Attrs attrs = Attrs.newInstance(); Attrs.Key key = Attrs.newKey("foo"); assertThatThrownBy(() -> key.put(attrs, null)).isInstanceOf(NullPointerException.class); } @Test void attrsPutReplacesOld() { Attrs attrs = Attrs.newInstance(); Attrs.Key key = Attrs.newKey("foo"); key.put(attrs, "bar"); key.put(attrs, "baz"); assertThat(key.get(attrs)).isEqualTo("baz"); assertThat(attrs.keySet()).containsExactly(key); } @Test void getReturnsNull() { Attrs attrs = Attrs.newInstance(); Attrs.Key key = Attrs.newKey("foo"); assertThat(key.get(attrs)).isNull(); } @Test void getOrDefault_picksDefault() { Attrs attrs = Attrs.newInstance(); Attrs.Key key = Attrs.newKey("foo"); assertThat(key.getOrDefault(attrs, "bar")).isEqualTo("bar"); } @Test void getOrDefault_failsOnNullDefault() { Attrs attrs = Attrs.newInstance(); Attrs.Key key = Attrs.newKey("foo"); key.put(attrs, "bar"); assertThatThrownBy(() -> key.getOrDefault(attrs, null)).isInstanceOf(NullPointerException.class); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/DynamicFilterLoaderTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.filters.BaseSyncFilter; import com.netflix.zuul.filters.FilterRegistry; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.MutableFilterRegistry; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.ZuulMessage; import java.util.Collection; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; class DynamicFilterLoaderTest { private final FilterRegistry registry = new MutableFilterRegistry(); private final FilterFactory filterFactory = new DefaultFilterFactory(); private DynamicFilterLoader loader; @BeforeEach void before() throws Exception { MockitoAnnotations.initMocks(this); loader = new DynamicFilterLoader(registry, filterFactory); } @Test void testPutFiltersForClasses() throws Exception { loader.putFiltersForClasses(new String[] {TestZuulFilter.class.getName()}); Collection> filters = registry.getAllFilters(); assertThat(filters.size()).isEqualTo(1); } @Test void testPutFiltersForClassesException() throws Exception { Exception caught = null; try { loader.putFiltersForClasses(new String[] {"asdf"}); } catch (ClassNotFoundException e) { caught = e; } assertThat(caught != null).isTrue(); Collection> filters = registry.getAllFilters(); assertThat(filters.size()).isEqualTo(0); } @Test void testGetFiltersByType() throws Exception { loader.putFiltersForClasses(new String[] {TestZuulFilter.class.getName()}); Collection> filters = registry.getAllFilters(); assertThat(filters.size()).isEqualTo(1); Collection> list = loader.getFiltersByType(FilterType.INBOUND); assertThat(list != null).isTrue(); assertThat(list.size()).isEqualTo(1); ZuulFilter filter = list.iterator().next(); assertThat(filter != null).isTrue(); assertThat(filter.filterType()).isEqualTo(FilterType.INBOUND); } private static final class TestZuulFilter extends BaseSyncFilter { TestZuulFilter() { super(); } @Override public FilterType filterType() { return FilterType.INBOUND; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter(ZuulMessage msg) { return false; } @Override public ZuulMessage apply(ZuulMessage msg) { return null; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/StaticFilterLoaderTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableSet; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.filters.http.HttpInboundSyncFilter; import com.netflix.zuul.message.http.HttpRequestMessage; import java.util.ArrayList; import java.util.List; import java.util.SortedSet; import org.junit.jupiter.api.Test; class StaticFilterLoaderTest { private static final FilterFactory factory = new DefaultFilterFactory(); @Test void getFiltersByType() { StaticFilterLoader filterLoader = new StaticFilterLoader( factory, ImmutableSet.of(DummyFilter2.class, DummyFilter1.class, DummyFilter22.class)); SortedSet> filters = filterLoader.getFiltersByType(FilterType.INBOUND); assertThat(filters).hasSize(3); List> filterList = new ArrayList<>(filters); assertThat(filterList.get(0)).isInstanceOf(DummyFilter1.class); assertThat(filterList.get(1)).isInstanceOf(DummyFilter2.class); assertThat(filterList.get(2)).isInstanceOf(DummyFilter22.class); } @Test void getFilterByNameAndType() { StaticFilterLoader filterLoader = new StaticFilterLoader(factory, ImmutableSet.of(DummyFilter2.class, DummyFilter1.class)); ZuulFilter filter = filterLoader.getFilterByNameAndType("Robin", FilterType.INBOUND); assertThat(filter).isInstanceOf(DummyFilter2.class); } @Filter(order = 0, type = FilterType.INBOUND) static class DummyFilter1 extends HttpInboundSyncFilter { @Override public String filterName() { return "Batman"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter(HttpRequestMessage msg) { return true; } @Override public HttpRequestMessage apply(HttpRequestMessage input) { return input; } } @Filter(order = 1, type = FilterType.INBOUND) static class DummyFilter2 extends HttpInboundSyncFilter { @Override public String filterName() { return "Robin"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter(HttpRequestMessage msg) { return true; } @Override public HttpRequestMessage apply(HttpRequestMessage input) { return input; } } @Filter(order = 1, type = FilterType.INBOUND) static class DummyFilter22 extends HttpInboundSyncFilter { @Override public String filterName() { return "Williams"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter(HttpRequestMessage msg) { return true; } @Override public HttpRequestMessage apply(HttpRequestMessage input) { return input; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/com/netflix/zuul/netty/server/push/PushConnectionTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.com.netflix.zuul.netty.server.push; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.netty.server.push.PushConnection; import com.netflix.zuul.netty.server.push.PushProtocol; import org.junit.jupiter.api.Test; /** * Author: Susheel Aroskar * Date: 10/18/2018 */ class PushConnectionTest { @Test void testOneMessagePerSecond() throws InterruptedException { PushConnection conn = new PushConnection(PushProtocol.WEBSOCKET, null); for (int i = 0; i < 5; i++) { assertThat(conn.isRateLimited()).isFalse(); Thread.sleep(1000); } } @Test void testThreeMessagesInSuccession() { PushConnection conn = new PushConnection(PushProtocol.WEBSOCKET, null); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); } @Test void testFourMessagesInSuccession() { PushConnection conn = new PushConnection(PushProtocol.WEBSOCKET, null); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isTrue(); } @Test void testFirstThreeMessagesSuccess() { PushConnection conn = new PushConnection(PushProtocol.WEBSOCKET, null); for (int i = 0; i < 10; i++) { if (i < 3) { assertThat(conn.isRateLimited()).isFalse(); } else { assertThat(conn.isRateLimited()).isTrue(); } } } @Test void testMessagesInBatches() throws InterruptedException { PushConnection conn = new PushConnection(PushProtocol.WEBSOCKET, null); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isTrue(); Thread.sleep(2000); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isFalse(); assertThat(conn.isRateLimited()).isTrue(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/context/DebugTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import static com.netflix.zuul.context.Debug.addRequestDebug; import static com.netflix.zuul.context.Debug.addRoutingDebug; import static com.netflix.zuul.context.Debug.debugRequest; import static com.netflix.zuul.context.Debug.debugRouting; import static com.netflix.zuul.context.Debug.getRequestDebug; import static com.netflix.zuul.context.Debug.getRoutingDebug; import static com.netflix.zuul.context.Debug.setDebugRequest; import static com.netflix.zuul.context.Debug.setDebugRouting; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import com.netflix.zuul.message.util.HttpRequestBuilder; import io.netty.handler.codec.http.HttpMethod; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class DebugTest { private SessionContext ctx; private Headers headers; private HttpQueryParams params; private HttpRequestMessage request; private HttpResponseMessage response; @BeforeEach void setup() { ctx = new SessionContext(); headers = new Headers(); headers.add("lah", "deda"); params = new HttpQueryParams(); params.add("k1", "v1"); request = new HttpRequestBuilder(ctx) .withMethod(HttpMethod.POST) .withUri("/some/where") .withHeaders(headers) .withQueryParams(params) .build(); request.setBodyAsText("some text"); request.storeInboundRequest(); response = new HttpResponseMessageImpl(ctx, headers, request, 200); response.setBodyAsText("response text"); } @Test void testRequestDebug() { assertThat(debugRouting(ctx)).isFalse(); assertThat(debugRequest(ctx)).isFalse(); setDebugRouting(ctx, true); setDebugRequest(ctx, true); assertThat(debugRouting(ctx)).isTrue(); assertThat(debugRequest(ctx)).isTrue(); addRoutingDebug(ctx, "test1"); assertThat(getRoutingDebug(ctx).contains("test1")).isTrue(); addRequestDebug(ctx, "test2"); assertThat(getRequestDebug(ctx).contains("test2")).isTrue(); } @Test void testWriteInboundRequestDebug() { ctx.setDebugRequest(true); ctx.setDebugRequestHeadersOnly(true); Debug.writeDebugRequest(ctx, request, true).toBlocking().single(); List debugLines = getRequestDebug(ctx); assertThat(debugLines) .containsExactlyInAnyOrder( "REQUEST_INBOUND:: > LINE: POST /some/where?k1=v1 HTTP/1.1", "REQUEST_INBOUND:: > HDR: Content-Length:13", "REQUEST_INBOUND:: > HDR: lah:deda"); } @Test void testWriteOutboundRequestDebug() { ctx.setDebugRequest(true); ctx.setDebugRequestHeadersOnly(true); Debug.writeDebugRequest(ctx, request, false).toBlocking().single(); List debugLines = getRequestDebug(ctx); assertThat(debugLines) .containsExactlyInAnyOrder( "REQUEST_OUTBOUND:: > LINE: POST /some/where?k1=v1 HTTP/1.1", "REQUEST_OUTBOUND:: > HDR: Content-Length:13", "REQUEST_OUTBOUND:: > HDR: lah:deda"); } @Test void testWriteRequestDebug_WithBody() { ctx.setDebugRequest(true); ctx.setDebugRequestHeadersOnly(false); Debug.writeDebugRequest(ctx, request, true).toBlocking().single(); List debugLines = getRequestDebug(ctx); assertThat(debugLines) .containsExactlyInAnyOrder( "REQUEST_INBOUND:: > LINE: POST /some/where?k1=v1 HTTP/1.1", "REQUEST_INBOUND:: > HDR: Content-Length:13", "REQUEST_INBOUND:: > HDR: lah:deda", "REQUEST_INBOUND:: > BODY: some text"); } @Test void testWriteInboundResponseDebug() { ctx.setDebugRequest(true); ctx.setDebugRequestHeadersOnly(true); Debug.writeDebugResponse(ctx, response, true).toBlocking().single(); List debugLines = getRequestDebug(ctx); assertThat(debugLines) .containsExactlyInAnyOrder( "RESPONSE_INBOUND:: < STATUS: 200", "RESPONSE_INBOUND:: < HDR: Content-Length:13", "RESPONSE_INBOUND:: < HDR: lah:deda"); } @Test void testWriteOutboundResponseDebug() { ctx.setDebugRequest(true); ctx.setDebugRequestHeadersOnly(true); Debug.writeDebugResponse(ctx, response, false).toBlocking().single(); List debugLines = getRequestDebug(ctx); assertThat(debugLines) .containsExactlyInAnyOrder( "RESPONSE_OUTBOUND:: < STATUS: 200", "RESPONSE_OUTBOUND:: < HDR: Content-Length:13", "RESPONSE_OUTBOUND:: < HDR: lah:deda"); } @Test void testWriteResponseDebug_WithBody() { ctx.setDebugRequest(true); ctx.setDebugRequestHeadersOnly(false); Debug.writeDebugResponse(ctx, response, true).toBlocking().single(); List debugLines = getRequestDebug(ctx); assertThat(debugLines) .containsExactlyInAnyOrder( "RESPONSE_INBOUND:: < STATUS: 200", "RESPONSE_INBOUND:: < HDR: Content-Length:13", "RESPONSE_INBOUND:: < HDR: lah:deda", "RESPONSE_INBOUND:: < BODY: response text"); } @Test void testNoCMEWhenComparingContexts() { SessionContext context = new SessionContext(); SessionContext copy = new SessionContext(); context.set("foo", "bar"); Debug.compareContextState("testfilter", context, copy); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/context/SessionContextTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.context; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class SessionContextTest { @Test void testBoolean() { SessionContext context = new SessionContext(); assertThat(context.getBoolean("boolean_test")).isEqualTo(Boolean.FALSE); assertThat(context.getBoolean("boolean_test", true)).isEqualTo(true); } @Test void keysAreUnique() { SessionContext context = new SessionContext(); SessionContext.Key key1 = SessionContext.newKey("foo"); context.put(key1, "bar"); SessionContext.Key key2 = SessionContext.newKey("foo"); context.put(key2, "baz"); assertThat(context.keys()).containsExactlyInAnyOrder(key1, key2); } @Test void newKeyFailsOnNull() { assertThatThrownBy(() -> SessionContext.newKey(null)).isInstanceOf(NullPointerException.class); } @Test void putFailsOnNull() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); assertThatThrownBy(() -> context.put(key, null)).isInstanceOf(NullPointerException.class); } @Test void putReplacesOld() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); context.put(key, "bar"); context.put(key, "baz"); assertThat(context.get(key)).isEqualTo("baz"); assertThat(context.keys()).containsExactly(key); } @Test void getReturnsNull() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); assertThat(context.get(key)).isNull(); } @Test void getOrDefault_picksDefault() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); assertThat(context.getOrDefault(key, "bar")).isEqualTo("bar"); } @Test void getOrDefault_failsOnNullDefault() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); context.put(key, "bar"); assertThatThrownBy(() -> context.getOrDefault(key, null)).isInstanceOf(NullPointerException.class); } @Test void getUsesDefaultValueSupplier() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo", () -> "bar"); assertThat(context.get(key)).isEqualTo("bar"); } @Test void getOrDefaultUsesDefaultValueSupplier() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo", () -> "bar"); assertThat(context.getOrDefault(key)).isEqualTo("bar"); } @Test void getOrDefaultUsesDefaultValueSupplierFailsWithout() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); assertThatThrownBy(() -> context.getOrDefault(key)).isInstanceOf(NullPointerException.class); } @Test void remove() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); context.put(key, "bar"); assertThat(context.get(key)).isEqualTo("bar"); String val = context.remove(key); assertThat(context.get(key)).isNull(); assertThat(val).isEqualTo("bar"); } @Test void containsKey() { SessionContext context = new SessionContext(); SessionContext.Key key = SessionContext.newKey("foo"); context.put(key, "bar"); assertThat(context.containsKey(key)).isTrue(); String val = context.remove(key); assertThat(val).isEqualTo("bar"); assertThat(context.containsKey(key)).isFalse(); } @Test void setInBrownoutModeWithReason() { SessionContext context = new SessionContext(); assertThat(context.getBrownoutReason()).isNull(); context.setInBrownoutMode("High CPU usage"); assertThat(context.isInBrownoutMode()).isTrue(); assertThat(context.getBrownoutReason()).isEqualTo("High CPU usage"); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/filters/BaseFilterTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.config.ConfigurationManager; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.ZuulMessageImpl; import org.apache.commons.configuration.AbstractConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import rx.Observable; /** * Tests for {@link BaseFilter} */ @ExtendWith(MockitoExtension.class) class BaseFilterTest { private final AbstractConfiguration config = ConfigurationManager.getConfigInstance(); @BeforeEach public void setUpTest() { config.clear(); } @Test void validateDefaultConcurrencyLimit() throws InterruptedException { int[] limit = {0}; class ConcInboundFilter extends BaseFilter { @Override public Observable applyAsync(ZuulMessage input) { limit[0] = calculateConcurency(); return Observable.just(new ZuulMessageImpl(new SessionContext())); } @Override public FilterType filterType() { return FilterType.INBOUND; } @Override public boolean shouldFilter(ZuulMessage msg) { return true; } } new ConcInboundFilter() .applyAsync(new ZuulMessageImpl(new SessionContext(), new Headers())) .toBlocking() .single(); assertThat(limit[0]).isEqualTo(4000); } @Test void validateFilterGlobalConcurrencyLimitOverride() throws InterruptedException { config.setProperty("zuul.filter.concurrency.limit.default", 7000); config.setProperty("zuul.ConcInboundFilter.in.concurrency.limit", 4000); int[] limit = {0}; class ConcInboundFilter extends BaseFilter { @Override public Observable applyAsync(ZuulMessage input) { limit[0] = calculateConcurency(); return Observable.just(new ZuulMessageImpl(new SessionContext())); } @Override public FilterType filterType() { return FilterType.INBOUND; } @Override public boolean shouldFilter(ZuulMessage msg) { return true; } } new ConcInboundFilter() .applyAsync(new ZuulMessageImpl(new SessionContext(), new Headers())) .toBlocking() .single(); assertThat(limit[0]).isEqualTo(7000); } @Test void validateFilterSpecificConcurrencyLimitOverride() throws InterruptedException { config.setProperty("zuul.filter.concurrency.limit.default", 7000); config.setProperty("zuul.ConcInboundFilter.in.concurrency.limit", 4300); int[] limit = {0}; class ConcInboundFilter extends BaseFilter { @Override public Observable applyAsync(ZuulMessage input) { limit[0] = calculateConcurency(); return Observable.just(new ZuulMessageImpl(new SessionContext())); } @Override public FilterType filterType() { return FilterType.INBOUND; } @Override public boolean shouldFilter(ZuulMessage msg) { return true; } } new ConcInboundFilter() .applyAsync(new ZuulMessageImpl(new SessionContext(), new Headers())) .toBlocking() .single(); assertThat(limit[0]).isEqualTo(4300); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/filters/common/GZipResponseFilterTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.common; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpHeaderNames; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.zip.GZIPInputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class GZipResponseFilterTest { private final SessionContext context = new SessionContext(); private final Headers originalRequestHeaders = new Headers(); @Mock private HttpRequestMessage request; @Mock private HttpRequestMessage originalRequest; GZipResponseFilter filter; HttpResponseMessage response; @BeforeEach void setup() { // when(request.getContext()).thenReturn(context); when(originalRequest.getHeaders()).thenReturn(originalRequestHeaders); filter = Mockito.spy(new GZipResponseFilter()); response = new HttpResponseMessageImpl(context, request, 99); response.getHeaders().set(HttpHeaderNames.CONTENT_TYPE, "text/html"); when(response.getInboundRequest()).thenReturn(originalRequest); } @Test void prepareResponseBody_NeedsGZipping() throws Exception { originalRequestHeaders.set("Accept-Encoding", "gzip"); byte[] originBody = "blah".getBytes(UTF_8); response.getHeaders().set("Content-Length", Integer.toString(originBody.length)); Mockito.when(filter.isRightSizeForGzip(response)).thenReturn(true); // Force GZip for small response response.setHasBody(true); assertThat(filter.shouldFilter(response)).isTrue(); HttpResponseMessage result = filter.apply(response); HttpContent hc1 = filter.processContentChunk( response, new DefaultHttpContent(Unpooled.wrappedBuffer(originBody)).retain()); HttpContent hc2 = filter.processContentChunk(response, new DefaultLastHttpContent()); byte[] body = new byte[hc1.content().readableBytes() + hc2.content().readableBytes()]; int hc1Len = hc1.content().readableBytes(); int hc2Len = hc2.content().readableBytes(); hc1.content().readBytes(body, 0, hc1Len); hc2.content().readBytes(body, hc1Len, hc2Len); String bodyStr; // Check body is a gzipped version of the origin body. try (ByteArrayInputStream bais = new ByteArrayInputStream(body); GZIPInputStream gzis = new GZIPInputStream(bais); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int b; while ((b = gzis.read()) != -1) { baos.write(b); } bodyStr = baos.toString("UTF-8"); } assertThat(bodyStr).isEqualTo("blah"); assertThat(result.getHeaders().getFirst("Content-Encoding")).isEqualTo("gzip"); // Check Content-Length header has been removed assertThat(result.getHeaders().getAll("Content-Length").size()).isEqualTo(0); } @Test void prepareResponseBody_NeedsGZipping_gzipDeflate() throws Exception { originalRequestHeaders.set("Accept-Encoding", "gzip,deflate"); byte[] originBody = "blah".getBytes(UTF_8); response.getHeaders().set("Content-Length", Integer.toString(originBody.length)); Mockito.when(filter.isRightSizeForGzip(response)).thenReturn(true); // Force GZip for small response response.setHasBody(true); assertThat(filter.shouldFilter(response)).isTrue(); HttpResponseMessage result = filter.apply(response); HttpContent hc1 = filter.processContentChunk( response, new DefaultHttpContent(Unpooled.wrappedBuffer(originBody)).retain()); HttpContent hc2 = filter.processContentChunk(response, new DefaultLastHttpContent()); byte[] body = new byte[hc1.content().readableBytes() + hc2.content().readableBytes()]; int hc1Len = hc1.content().readableBytes(); int hc2Len = hc2.content().readableBytes(); hc1.content().readBytes(body, 0, hc1Len); hc2.content().readBytes(body, hc1Len, hc2Len); String bodyStr; // Check body is a gzipped version of the origin body. try (ByteArrayInputStream bais = new ByteArrayInputStream(body); GZIPInputStream gzis = new GZIPInputStream(bais); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int b; while ((b = gzis.read()) != -1) { baos.write(b); } bodyStr = baos.toString("UTF-8"); } assertThat(bodyStr).isEqualTo("blah"); assertThat(result.getHeaders().getFirst("Content-Encoding")).isEqualTo("gzip"); // Check Content-Length header has been removed assertThat(result.getHeaders().getAll("Content-Length").size()).isEqualTo(0); } @Test void prepareResponseBody_alreadyZipped() throws Exception { originalRequestHeaders.set("Accept-Encoding", "gzip,deflate"); byte[] originBody = "blah".getBytes(UTF_8); response.getHeaders().set("Content-Length", Integer.toString(originBody.length)); response.getHeaders().set("Content-Type", "application/json"); response.getHeaders().set("Content-Encoding", "gzip"); response.setHasBody(true); assertThat(filter.shouldFilter(response)).isFalse(); } @Test void prepareResponseBody_alreadyDeflated() throws Exception { originalRequestHeaders.set("Accept-Encoding", "gzip,deflate"); byte[] originBody = "blah".getBytes(UTF_8); response.getHeaders().set("Content-Length", Integer.toString(originBody.length)); response.getHeaders().set("Content-Type", "application/json"); response.getHeaders().set("Content-Encoding", "deflate"); response.setHasBody(true); assertThat(filter.shouldFilter(response)).isFalse(); } @Test void prepareResponseBody_NeedsGZipping_butTooSmall() throws Exception { originalRequestHeaders.set("Accept-Encoding", "gzip"); byte[] originBody = "blah".getBytes(UTF_8); response.getHeaders().set("Content-Length", Integer.toString(originBody.length)); response.setHasBody(true); assertThat(filter.shouldFilter(response)).isFalse(); } @Test void prepareChunkedEncodedResponseBody_NeedsGZipping() throws Exception { originalRequestHeaders.set("Accept-Encoding", "gzip"); response.getHeaders().set("Transfer-Encoding", "chunked"); response.setHasBody(true); assertThat(filter.shouldFilter(response)).isTrue(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/filters/endpoint/ProxyEndpointTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.endpoint; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.netflix.appinfo.InstanceInfo; import com.netflix.spectator.api.Spectator; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.netty.NettyRequestAttemptFactory; import com.netflix.zuul.netty.connectionpool.DefaultOriginChannelInitializer; import com.netflix.zuul.netty.connectionpool.PooledConnection; import com.netflix.zuul.netty.server.MethodBinding; import com.netflix.zuul.netty.timeouts.OriginTimeoutManager; import com.netflix.zuul.niws.RequestAttempt; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.origins.BasicNettyOriginManager; import com.netflix.zuul.origins.NettyOrigin; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportItem; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.local.LocalAddress; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Promise; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class ProxyEndpointTest { @Mock private ChannelHandlerContext chc; @Mock private NettyOrigin nettyOrigin; @Mock private OriginTimeoutManager timeoutManager; @Mock private NettyRequestAttemptFactory attemptFactory; private ProxyEndpoint proxyEndpoint; private SessionContext context; private HttpRequestMessage request; private HttpResponse response; private CurrentPassport passport; private EmbeddedChannel channel; @BeforeEach void setup() { channel = new EmbeddedChannel(); doReturn(channel).when(chc).channel(); context = new SessionContext(); request = createRequest(context, "POST", "/some/where"); request.storeInboundRequest(); request.setBody("Hello There".getBytes(UTF_8)); BasicNettyOriginManager originManager = new BasicNettyOriginManager(Spectator.globalRegistry()); context.set(CommonContextKeys.ORIGIN_MANAGER, originManager); context.setRouteVIP("some-vip"); passport = CurrentPassport.create(); context.put(CommonContextKeys.PASSPORT, passport); context.put(CommonContextKeys.REQUEST_ATTEMPTS, new RequestAttempts()); Promise promise = channel.eventLoop().newPromise(); doReturn(promise).when(nettyOrigin).connectToOrigin(any(), any(), anyInt(), any(), any(), any()); proxyEndpoint = spy(new ProxyEndpoint(request, chc, null, MethodBinding.NO_OP_BINDING, attemptFactory) { @Override public NettyOrigin getOrigin(HttpRequestMessage request) { return nettyOrigin; } @Override protected OriginTimeoutManager getTimeoutManager(NettyOrigin origin) { return timeoutManager; } }); doNothing().when(proxyEndpoint).operationComplete(any()); doNothing().when(proxyEndpoint).invokeNext((HttpResponseMessage) any()); } @Test void testRecordProxyRequestEndIsCalledOnce() { proxyEndpoint.apply(request); proxyEndpoint.finish(false); verify(nettyOrigin, times(1)).recordProxyRequestEnd(); } @Test void testRetryWillResetBodyReader() { assertThat(new String(request.getBody(), UTF_8)).isEqualTo("Hello There"); // move the body readerIndex to the end to mimic nettys behavior after writing to the origin channel request.getBodyContents() .forEach((b) -> b.content().readerIndex(b.content().capacity())); createResponse(HttpResponseStatus.SERVICE_UNAVAILABLE); DiscoveryResult discoveryResult = createDiscoveryResult(); // when retrying a response, the request body reader should have it's indexes reset proxyEndpoint.handleOriginNonSuccessResponse(response, discoveryResult); assertThat(new String(request.getBody(), UTF_8)).isEqualTo("Hello There"); } @Test void retryWhenNoAdjustment() { createResponse(HttpResponseStatus.SERVICE_UNAVAILABLE); proxyEndpoint.handleOriginNonSuccessResponse(response, createDiscoveryResult()); verify(nettyOrigin).adjustRetryPolicyIfNeeded(eq(request)); verify(nettyOrigin).originRetryPolicyAdjustmentIfNeeded(request, response); verify(nettyOrigin).connectToOrigin(any(), any(), anyInt(), any(), any(), any()); } @Test void testRetryAdjustsLimit() { createResponse(HttpResponseStatus.SERVICE_UNAVAILABLE); disableRetriesOnAdjustment(); proxyEndpoint.handleOriginNonSuccessResponse(response, createDiscoveryResult()); validateNoRetry(); } @Test void noRetryAdjustmentOnNonRetriableStatusCode() { createResponse(HttpResponseStatus.BAD_REQUEST); proxyEndpoint.handleOriginNonSuccessResponse(response, createDiscoveryResult()); verify(nettyOrigin, never()).adjustRetryPolicyIfNeeded(request); verify(nettyOrigin, never()).originRetryPolicyAdjustmentIfNeeded(request, response); validateNoRetry(); } @Test public void onErrorFromOriginNoRetryAdjustment() { doReturn(OutboundErrorType.RESET_CONNECTION).when(attemptFactory).mapNettyToOutboundErrorType(any()); proxyEndpoint.errorFromOrigin(new RuntimeException()); verify(nettyOrigin).adjustRetryPolicyIfNeeded(request); verify(nettyOrigin).connectToOrigin(any(), any(), anyInt(), any(), any(), any()); } @Test void onErrorFromOriginWithRetryAdjustment() { doReturn(OutboundErrorType.RESET_CONNECTION).when(attemptFactory).mapNettyToOutboundErrorType(any()); disableRetriesOnAdjustment(); proxyEndpoint.errorFromOrigin(new RuntimeException()); validateNoRetry(); } @Test public void onErrorFromOriginNoRetryOnNonRetriableError() { doReturn(OutboundErrorType.OTHER).when(attemptFactory).mapNettyToOutboundErrorType(any()); disableRetriesOnAdjustment(); proxyEndpoint.errorFromOrigin(new RuntimeException()); verify(nettyOrigin, never()).adjustRetryPolicyIfNeeded(request); verify(nettyOrigin, never()).originRetryPolicyAdjustmentIfNeeded(request, response); validateNoRetry(); } @Test public void lastContentAfterProxyStartedIsConsideredReplayable() { Promise promise = channel.eventLoop().newPromise(); PooledConnection pooledConnection = Mockito.mock(PooledConnection.class); promise.setSuccess(pooledConnection); doReturn(channel).when(pooledConnection).getChannel(); doReturn(promise).when(nettyOrigin).connectToOrigin(any(), any(), anyInt(), any(), any(), any()); doReturn(Mockito.mock(RequestAttempt.class)).when(nettyOrigin).newRequestAttempt(any(), any(), any(), anyInt()); request = createRequest(context, "POST", "/some/where"); request.storeInboundRequest(); proxyEndpoint = spy(new ProxyEndpoint(request, chc, null, MethodBinding.NO_OP_BINDING, attemptFactory) { @Override public NettyOrigin getOrigin(HttpRequestMessage request) { return nettyOrigin; } @Override protected OriginTimeoutManager getTimeoutManager(NettyOrigin origin) { return timeoutManager; } }); channel.pipeline() .addLast(DefaultOriginChannelInitializer.CONNECTION_POOL_HANDLER, new ChannelInboundHandlerAdapter()); proxyEndpoint.apply(request); LastHttpContent lastContent = new DefaultLastHttpContent(); assertThat(proxyEndpoint.isRequestReplayable()).isFalse(); proxyEndpoint.processContentChunk(request, lastContent); assertThat(proxyEndpoint.isRequestReplayable()).isTrue(); channel.releaseOutbound(); assertThat(lastContent.refCnt()) .as("ref count should be 1 in case a retry is needed") .isEqualTo(1); ReferenceCountUtil.safeRelease(lastContent); } @Test void testMassageRequestURIWithEncodedAmpersand() { // Test that encoded ampersands in query parameter values are handled correctly // and do not create additional parameters SessionContext context = new SessionContext(); context.set("overrideURI", "/path?param=123%26hidden%3Dvalue"); HttpRequestMessage request = createRequest(context, "GET", "/original"); HttpRequestMessage result = ProxyEndpoint.massageRequestURI(request); assertThat(result.getPath()).isEqualTo("/path"); HttpQueryParams params = result.getQueryParams(); assertThat(params.getFirst("param")).isEqualTo("123&hidden=value"); assertThat(params.contains("hidden")).isFalse(); } @Test void testMassageRequestURIWithMultipleEncodedParams() { SessionContext context = new SessionContext(); context.set("overrideURI", "/path?foo=bar¶m=a%26b&another=test%3Dvalue"); HttpRequestMessage request = createRequest(context, "GET", "/original"); HttpRequestMessage result = ProxyEndpoint.massageRequestURI(request); assertThat(result.getPath()).isEqualTo("/path"); HttpQueryParams params = result.getQueryParams(); assertThat(params.getFirst("foo")).isEqualTo("bar"); assertThat(params.getFirst("param")).isEqualTo("a&b"); assertThat(params.getFirst("another")).isEqualTo("test=value"); } @Test void testMassageRequestURIWithNoQueryString() { SessionContext context = new SessionContext(); context.set("overrideURI", "/path/to/resource"); HttpRequestMessage request = createRequest(context, "GET", "/original"); HttpRequestMessage result = ProxyEndpoint.massageRequestURI(request); assertThat(result.getPath()).isEqualTo("/path/to/resource"); assertThat(result.getQueryParams().entries()).isEmpty(); } @Test void testMassageRequestURIWithRequestURIContext() { SessionContext context = new SessionContext(); context.set("requestURI", "/contextpath?key=value%20with%20spaces"); HttpRequestMessage request = createRequest(context, "GET", "/original"); HttpRequestMessage result = ProxyEndpoint.massageRequestURI(request); assertThat(result.getPath()).isEqualTo("/contextpath"); HttpQueryParams params = result.getQueryParams(); assertThat(params.getFirst("key")).isEqualTo("value with spaces"); } @Test void testMassageRequestURIOverrideURITakesPrecedence() { // Test that overrideURI takes precedence over requestURI SessionContext context = new SessionContext(); context.set("requestURI", "/first?key=first"); context.set("overrideURI", "/second?key=second"); HttpRequestMessage request = createRequest(context, "GET", "/original"); HttpRequestMessage result = ProxyEndpoint.massageRequestURI(request); assertThat(result.getPath()).isEqualTo("/second"); HttpQueryParams params = result.getQueryParams(); assertThat(params.getFirst("key")).isEqualTo("second"); } @Test void testMassageRequestURIWithNoContextOverride() { // Test that when neither requestURI nor overrideURI are set, the request is returned unchanged SessionContext context = new SessionContext(); HttpRequestMessage request = createRequest(context, "GET", "/original"); HttpRequestMessage result = ProxyEndpoint.massageRequestURI(request); // Path and query params should remain as they were in the original request assertThat(result).isSameAs(request); } private void validateNoRetry() { verify(nettyOrigin, never()).connectToOrigin(any(), any(), anyInt(), any(), any(), any()); passport.getHistory().stream() .map(PassportItem::getState) .filter(s -> s == PassportState.ORIGIN_RETRY_START) .findAny() .ifPresent(s -> org.junit.jupiter.api.Assertions.fail()); } private void disableRetriesOnAdjustment() { doAnswer(invocation -> { doReturn(-1).when(nettyOrigin).getMaxRetriesForRequest(context); return null; }) .when(nettyOrigin) .adjustRetryPolicyIfNeeded(request); } private static DiscoveryResult createDiscoveryResult() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("app") .setHostName("localhost") .setPort(443) .build(); return DiscoveryResult.from(instanceInfo, true); } private void createResponse(HttpResponseStatus status) { response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); } private HttpRequestMessage createRequest(SessionContext context, String method, String path) { return new HttpRequestMessageImpl( context, "HTTP/1.1", method, path, null, null, "192.168.0.2", "https", 7002, "localhost", new LocalAddress("777"), false); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/message/HeadersTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import com.netflix.zuul.exception.ZuulException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; /** * Tests for {@link Headers}. */ class HeadersTest { @Test void copyOf() { Headers headers = new Headers(); headers.set("Content-Length", "5"); Headers headers2 = Headers.copyOf(headers); headers2.add("Via", "duct"); assertThat(headers.getAll("Via")).isEmpty(); assertThat(headers2.size()).isEqualTo(2); assertThat(headers2.getAll("Content-Length")).containsExactly("5"); } @Test void getFirst_normalizesName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getFirst("cOOkIE")).isEqualTo("this=that"); } @Test void getFirst_headerName_normalizesName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getFirst(new HeaderName("cOOkIE"))).isEqualTo("this=that"); } @Test void getFirst_returnsNull() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getFirst("Date")).isNull(); } @Test void getFirst_headerName_returnsNull() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getFirst(new HeaderName("Date"))).isNull(); } @Test void getFirst_returnsDefault() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getFirst("Date", "tuesday")).isEqualTo("tuesday"); } @Test void getFirst_headerName_returnsDefault() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getFirst(new HeaderName("Date"), "tuesday")).isEqualTo("tuesday"); } @Test void forEachNormalised() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=Frazzle"); Map> result = new LinkedHashMap<>(); headers.forEachNormalised((k, v) -> result.computeIfAbsent(k, discard -> new ArrayList<>()).add(v)); assertThat(result) .containsExactly( entry("via", Collections.singletonList("duct")), entry("cookie", Arrays.asList("this=that", "frizzle=Frazzle"))); } @Test void getAll() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getAll("CookiE")).containsExactly("this=that", "frizzle=frazzle"); } @Test void getAll_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThat(headers.getAll(new HeaderName("CookiE"))).containsExactly("this=that", "frizzle=frazzle"); } @Test void setClearsExisting() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.set("cookIe", "dilly=dally"); assertThat(headers.getAll("CookiE")).containsExactly("dilly=dally"); assertThat(headers.size()).isEqualTo(2); } @Test void setClearsExisting_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.set(new HeaderName("cookIe"), "dilly=dally"); assertThat(headers.getAll("CookiE")).containsExactly("dilly=dally"); assertThat(headers.size()).isEqualTo(2); } @Test void setNullIsEmtpy() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.set("cookIe", null); assertThat(headers.getAll("CookiE")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void setNullIsEmtpy_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.set(new HeaderName("cookIe"), null); assertThat(headers.getAll("CookiE")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void setIfValidNullIsEmtpy() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfValid("cookIe", null); assertThat(headers.getAll("CookiE")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void setIfValidNullIsEmtpy_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfValid(new HeaderName("cookIe"), null); assertThat(headers.getAll("CookiE")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void setIfValidIgnoresInvalidValues() { Headers headers = new Headers(); headers.add("X-Valid-K1", "abc-xyz"); headers.add("X-Valid-K2", "def-xyz"); headers.add("X-Valid-K3", "xyz-xyz"); headers.setIfValid("X-Valid-K1", "abc\r\n-xy\r\nz"); headers.setIfValid("X-Valid-K2", "abc\r-xy\rz"); headers.setIfValid("X-Valid-K3", "abc\n-xy\nz"); assertThat(headers.getAll("X-Valid-K1")).containsExactly("abc-xyz"); assertThat(headers.getAll("X-Valid-K2")).containsExactly("def-xyz"); assertThat(headers.getAll("X-Valid-K3")).containsExactly("xyz-xyz"); assertThat(headers.size()).isEqualTo(3); } @Test void setIfValidIgnoresInvalidValues_headerName() { Headers headers = new Headers(); headers.add("X-Valid-K1", "abc-xyz"); headers.add("X-Valid-K2", "def-xyz"); headers.add("X-Valid-K3", "xyz-xyz"); headers.setIfValid(new HeaderName("X-Valid-K1"), "abc\r\n-xy\r\nz"); headers.setIfValid(new HeaderName("X-Valid-K2"), "abc\r-xy\rz"); headers.setIfValid(new HeaderName("X-Valid-K3"), "abc\n-xy\nz"); assertThat(headers.getAll("X-Valid-K1")).containsExactly("abc-xyz"); assertThat(headers.getAll("X-Valid-K2")).containsExactly("def-xyz"); assertThat(headers.getAll("X-Valid-K3")).containsExactly("xyz-xyz"); assertThat(headers.size()).isEqualTo(3); } @Test void setIfValidIgnoresInvalidKey() { Headers headers = new Headers(); headers.add("X-Valid-K1", "abc-xyz"); headers.setIfValid("X-K\r\ney-1", "abc-def"); headers.setIfValid("X-K\ney-2", "def-xyz"); headers.setIfValid("X-K\rey-3", "xyz-xyz"); assertThat(headers.getAll("X-Valid-K1")).containsExactly("abc-xyz"); assertThat(headers.getAll("X-K\r\ney-1")).isEmpty(); assertThat(headers.getAll("X-K\ney-2")).isEmpty(); assertThat(headers.getAll("X-K\rey-3")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void setIfValidIgnoresInvalidKey_headerName() { Headers headers = new Headers(); headers.add("X-Valid-K1", "abc-xyz"); headers.setIfValid(new HeaderName("X-K\r\ney-1"), "abc-def"); headers.setIfValid(new HeaderName("X-K\ney-2"), "def-xyz"); headers.setIfValid(new HeaderName("X-K\rey-3"), "xyz-xyz"); assertThat(headers.getAll("X-Valid-K1")).containsExactly("abc-xyz"); assertThat(headers.getAll("X-K\r\ney-1")).isEmpty(); assertThat(headers.getAll("X-K\ney-2")).isEmpty(); assertThat(headers.getAll("X-K\rey-3")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void setIfAbsentKeepsExisting() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfAbsent("cookIe", "dilly=dally"); assertThat(headers.getAll("CookiE")).containsExactly("this=that", "frizzle=frazzle"); } @Test void setIfAbsentKeepsExisting_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfAbsent(new HeaderName("cookIe"), "dilly=dally"); assertThat(headers.getAll("CookiE")).containsExactly("this=that", "frizzle=frazzle"); } @Test void setIfAbsentFailsOnNull() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThatThrownBy(() -> headers.setIfAbsent("cookIe", null)).isInstanceOf(NullPointerException.class); } @Test void setIfAbsentFailsOnNull_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); assertThatThrownBy(() -> headers.setIfAbsent(new HeaderName("cookIe"), null)) .isInstanceOf(NullPointerException.class); } @Test void setIfAbsent() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfAbsent("X-Netflix-Awesome", "true"); assertThat(headers.getAll("X-netflix-Awesome")).containsExactly("true"); } @Test void setIfAbsent_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfAbsent(new HeaderName("X-Netflix-Awesome"), "true"); assertThat(headers.getAll("X-netflix-Awesome")).containsExactly("true"); } @Test void setIfAbsentAndValid() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.setIfAbsentAndValid("X-Netflix-Awesome", "true"); headers.setIfAbsentAndValid("X-Netflix-Awesome", "True"); assertThat(headers.getAll("X-netflix-Awesome")).containsExactly("true"); assertThat(headers.size()).isEqualTo(4); } @Test void setIfAbsentAndValidIgnoresInvalidValues() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.setIfAbsentAndValid("X-Invalid-K1", "abc\r\nxy\r\nz"); headers.setIfAbsentAndValid("X-Invalid-K2", "abc\rxy\rz"); headers.setIfAbsentAndValid("X-Invalid-K3", "abc\nxy\nz"); assertThat(headers.getAll("Via")).containsExactly("duct"); assertThat(headers.getAll("X-Invalid-K1")).isEmpty(); assertThat(headers.getAll("X-Invalid-K2")).isEmpty(); assertThat(headers.getAll("X-Invalid-K3")).isEmpty(); assertThat(headers.size()).isEqualTo(1); } @Test void add() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.add("via", "con Dios"); assertThat(headers.getAll("Via")).containsExactly("duct", "con Dios"); } @Test void add_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.add(new HeaderName("via"), "con Dios"); assertThat(headers.getAll("Via")).containsExactly("duct", "con Dios"); } @Test void addIfValid() { Headers headers = new Headers(); headers.addIfValid("Via", "duct"); headers.addIfValid("Cookie", "abc=def"); headers.addIfValid("cookie", "uvw=xyz"); assertThat(headers.getAll("Via")).containsExactly("duct"); assertThat(headers.getAll("Cookie")).containsExactly("abc=def", "uvw=xyz"); assertThat(headers.size()).isEqualTo(3); } @Test void addIfValid_headerName() { Headers headers = new Headers(); headers.addIfValid("Via", "duct"); headers.addIfValid("Cookie", "abc=def"); headers.addIfValid(new HeaderName("cookie"), "uvw=xyz"); assertThat(headers.getAll("Via")).containsExactly("duct"); assertThat(headers.getAll("Cookie")).containsExactly("abc=def", "uvw=xyz"); assertThat(headers.size()).isEqualTo(3); } @Test void addIfValidIgnoresInvalidValues() { Headers headers = new Headers(); headers.addIfValid("Via", "duct"); headers.addIfValid("Cookie", "abc=def"); headers.addIfValid("X-Invalid-K1", "abc\r\nxy\r\nz"); headers.addIfValid("X-Invalid-K2", "abc\rxy\rz"); headers.addIfValid("X-Invalid-K3", "abc\nxy\nz"); assertThat(headers.getAll("Via")).containsExactly("duct"); assertThat(headers.getAll("Cookie")).containsExactly("abc=def"); assertThat(headers.getAll("X-Invalid-K1")).isEmpty(); assertThat(headers.getAll("X-Invalid-K2")).isEmpty(); assertThat(headers.getAll("X-Invalid-K3")).isEmpty(); assertThat(headers.size()).isEqualTo(2); } @Test void putAll() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); Headers other = new Headers(); other.add("cookie", "a=b"); other.add("via", "com"); headers.putAll(other); // Only check the order per field, not for the entire set. assertThat(headers.getAll("Via")).containsExactly("duct", "com"); assertThat(headers.getAll("coOkiE")).containsExactly("this=that", "frizzle=frazzle", "a=b"); assertThat(headers.size()).isEqualTo(5); } @Test void remove() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.add("Soup", "salad"); List removed = headers.remove("Cookie"); assertThat(headers.getAll("Cookie")).isEmpty(); assertThat(headers.getAll("Soup")).containsExactly("salad"); assertThat(headers.size()).isEqualTo(2); assertThat(removed).containsExactly("this=that", "frizzle=frazzle"); } @Test void remove_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.add("Soup", "salad"); List removed = headers.remove(new HeaderName("Cookie")); assertThat(headers.getAll("Cookie")).isEmpty(); assertThat(headers.getAll("Soup")).containsExactly("salad"); assertThat(headers.size()).isEqualTo(2); assertThat(removed).containsExactly("this=that", "frizzle=frazzle"); } @Test void removeEmpty() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.add("Soup", "salad"); List removed = headers.remove("Monkey"); assertThat(headers.getAll("Cookie")).isNotEmpty(); assertThat(headers.getAll("Soup")).containsExactly("salad"); assertThat(headers.size()).isEqualTo(4); assertThat(removed).isEmpty(); } @Test void removeEmpty_headerName() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("Cookie", "frizzle=frazzle"); headers.add("Soup", "salad"); List removed = headers.remove(new HeaderName("Monkey")); assertThat(headers.getAll("Cookie")).isNotEmpty(); assertThat(headers.getAll("Soup")).containsExactly("salad"); assertThat(headers.size()).isEqualTo(4); assertThat(removed).isEmpty(); } @Test void removeIf() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("Cookie", "this=that"); headers.add("COOkie", "frizzle=frazzle"); headers.add("Soup", "salad"); boolean removed = headers.removeIf(entry -> entry.getKey().getName().equals("Cookie")); assertThat(removed).isTrue(); assertThat(headers.getAll("cOoKie")).containsExactly("frizzle=frazzle"); assertThat(headers.size()).isEqualTo(3); } @Test void keySet() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("COOKie", "this=that"); headers.add("cookIE", "frizzle=frazzle"); headers.add("Soup", "salad"); Set keySet = headers.keySet(); assertThat(keySet) .containsExactlyInAnyOrder(new HeaderName("COOKie"), new HeaderName("Soup"), new HeaderName("Via")); for (HeaderName headerName : keySet) { if (headerName.getName().equals("COOKie")) { return; } } throw new AssertionError("didn't find right cookie in keys: " + keySet); } @Test void contains() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("COOKie", "this=that"); headers.add("cookIE", "frizzle=frazzle"); headers.add("Soup", "salad"); assertThat(headers.contains("CoOkIe")).isTrue(); assertThat(headers.contains(new HeaderName("CoOkIe"))).isTrue(); assertThat(headers.contains("cookie")).isTrue(); assertThat(headers.contains(new HeaderName("cookie"))).isTrue(); assertThat(headers.contains("Cookie")).isTrue(); assertThat(headers.contains(new HeaderName("Cookie"))).isTrue(); assertThat(headers.contains("COOKIE")).isTrue(); assertThat(headers.contains(new HeaderName("COOKIE"))).isTrue(); assertThat(headers.contains("Monkey")).isFalse(); assertThat(headers.contains(new HeaderName("Monkey"))).isFalse(); headers.remove("cookie"); assertThat(headers.contains("cookie")).isFalse(); assertThat(headers.contains(new HeaderName("cookie"))).isFalse(); } @Test void containsValue() { Headers headers = new Headers(); headers.add("Via", "duct"); headers.add("COOKie", "this=that"); headers.add("cookIE", "frizzle=frazzle"); headers.add("Soup", "salad"); // note the swapping of the two cookie casings. assertThat(headers.contains("CoOkIe", "frizzle=frazzle")).isTrue(); assertThat(headers.contains(new HeaderName("CoOkIe"), "frizzle=frazzle")) .isTrue(); assertThat(headers.contains("Via", "lin")).isFalse(); assertThat(headers.contains(new HeaderName("Soup"), "of the day")).isFalse(); } @Test void testCaseInsensitiveKeys_Set() { Headers headers = new Headers(); headers.set("Content-Length", "5"); headers.set("content-length", "10"); assertThat(headers.getFirst("Content-Length")).isEqualTo("10"); assertThat(headers.getFirst("content-length")).isEqualTo("10"); assertThat(headers.getAll("content-length").size()).isEqualTo(1); } @Test void testCaseInsensitiveKeys_Add() { Headers headers = new Headers(); headers.add("Content-Length", "5"); headers.add("content-length", "10"); List values = headers.getAll("content-length"); assertThat(values.contains("10")).isTrue(); assertThat(values.contains("5")).isTrue(); assertThat(values.size()).isEqualTo(2); } @Test void testCaseInsensitiveKeys_SetIfAbsent() { Headers headers = new Headers(); headers.set("Content-Length", "5"); headers.setIfAbsent("content-length", "10"); List values = headers.getAll("content-length"); assertThat(values.size()).isEqualTo(1); assertThat(values.get(0)).isEqualTo("5"); } @Test void testCaseInsensitiveKeys_PutAll() { Headers headers = new Headers(); headers.add("Content-Length", "5"); headers.add("content-length", "10"); Headers headers2 = new Headers(); headers2.putAll(headers); List values = headers2.getAll("content-length"); assertThat(values.contains("10")).isTrue(); assertThat(values.contains("5")).isTrue(); assertThat(values.size()).isEqualTo(2); } @Test void testSanitizeValues_CRLF() { Headers headers = new Headers(); assertThatThrownBy(() -> headers.addAndValidate("x-test-break1", "a\r\nb\r\nc")) .isInstanceOf(ZuulException.class); assertThatThrownBy(() -> headers.setAndValidate("x-test-break1", "a\r\nb\r\nc")) .isInstanceOf(ZuulException.class); } @Test void testSanitizeValues_LF() { Headers headers = new Headers(); assertThatThrownBy(() -> headers.addAndValidate("x-test-break1", "a\nb\nc")) .isInstanceOf(ZuulException.class); assertThatThrownBy(() -> headers.setAndValidate("x-test-break1", "a\nb\nc")) .isInstanceOf(ZuulException.class); } @Test void testSanitizeValues_ISO88591Value() { Headers headers = new Headers(); headers.addAndValidate("x-test-ISO-8859-1", "P Venkmän"); assertThat(headers.getAll("x-test-ISO-8859-1")).containsExactly("P Venkmän"); assertThat(headers.size()).isEqualTo(1); headers.setAndValidate("x-test-ISO-8859-1", "Venkmän"); assertThat(headers.getAll("x-test-ISO-8859-1")).containsExactly("Venkmän"); assertThat(headers.size()).isEqualTo(1); } @Test void testSanitizeValues_UTF8Value() { // Ideally Unicode characters should not appear in the Header values. Headers headers = new Headers(); String rawHeaderValue = "\u017d" + "\u0172" + "\u016e" + "\u013F"; // ŽŲŮĽ byte[] bytes = rawHeaderValue.getBytes(StandardCharsets.UTF_8); String utf8HeaderValue = new String(bytes, StandardCharsets.UTF_8); headers.addAndValidate("x-test-UTF8", utf8HeaderValue); assertThat(headers.getAll("x-test-UTF8")).containsExactly(utf8HeaderValue); assertThat(headers.size()).isEqualTo(1); rawHeaderValue = "\u017d" + "\u0172" + "uuu" + "\u016e" + "\u013F"; // ŽŲuuuŮĽ bytes = rawHeaderValue.getBytes(StandardCharsets.UTF_8); utf8HeaderValue = new String(bytes, StandardCharsets.UTF_8); headers.setAndValidate("x-test-UTF8", utf8HeaderValue); assertThat(headers.getAll("x-test-UTF8")).containsExactly(utf8HeaderValue); assertThat(headers.size()).isEqualTo(1); } @Test void testSanitizeValues_addSetHeaderName() { Headers headers = new Headers(); assertThatThrownBy(() -> headers.setAndValidate(new HeaderName("x-test-break1"), "a\nb\nc")) .isInstanceOf(ZuulException.class); assertThatThrownBy(() -> headers.addAndValidate(new HeaderName("x-test-break2"), "a\r\nb\r\nc")) .isInstanceOf(ZuulException.class); } @Test void testSanitizeValues_nameCRLF() { Headers headers = new Headers(); assertThatThrownBy(() -> headers.addAndValidate("x-test-br\r\neak1", "a\r\nb\r\nc")) .isInstanceOf(ZuulException.class); assertThatThrownBy(() -> headers.setAndValidate("x-test-br\r\neak2", "a\r\nb\r\nc")) .isInstanceOf(ZuulException.class); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/message/ZuulMessageImplTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.message; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.context.SessionContext; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class ZuulMessageImplTest { private static final String TEXT1 = "Hello World!"; private static final String TEXT2 = "Goodbye World!"; @Test void testClone() { SessionContext ctx1 = new SessionContext(); ctx1.set("k1", "v1"); Headers headers1 = new Headers(); headers1.set("k1", "v1"); ZuulMessage msg1 = new ZuulMessageImpl(ctx1, headers1); ZuulMessage msg2 = msg1.clone(); assertThat(msg2.getBodyAsText()).isEqualTo(msg1.getBodyAsText()); assertThat(msg2.getHeaders()).isEqualTo(msg1.getHeaders()); assertThat(msg2.getContext()).isEqualTo(msg1.getContext()); // Verify that values of the 2 messages are decoupled. msg1.getHeaders().set("k1", "v_new"); msg1.getContext().set("k1", "v_new"); assertThat(msg2.getHeaders().getFirst("k1")).isEqualTo("v1"); assertThat(msg2.getContext().get("k1")).isEqualTo("v1"); } @Test void testBufferBody2GetBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("Hello ".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultLastHttpContent(Unpooled.copiedBuffer("World!".getBytes(UTF_8)))); String body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(0); } @Test void testBufferBody3GetBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("Hello ".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("World!".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultLastHttpContent()); String body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(0); } @Test void testBufferBody3GetBodyAsText() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("Hello ".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("World!".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultLastHttpContent()); String body = msg.getBodyAsText(); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(0); } @Test void testSetBodyGetBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.setBody(TEXT1.getBytes(UTF_8)); String body = new String(msg.getBody(), UTF_8); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT1.length())); } @Test void testSetBodyAsTextGetBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.setBodyAsText(TEXT1); String body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT1.length())); } @Test void testSetBodyAsTextGetBodyAsText() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.setBodyAsText(TEXT1); String body = msg.getBodyAsText(); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT1.length())); } @Test void testMultiSetBodyAsTextGetBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.setBodyAsText(TEXT1); String body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT1.length())); msg.setBodyAsText(TEXT2); body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT2); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT2.length())); } @Test void testMultiSetBodyGetBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.setBody(TEXT1.getBytes(UTF_8)); String body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT1); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT1.length())); msg.setBody(TEXT2.getBytes(UTF_8)); body = new String(msg.getBody(), UTF_8); assertThat(msg.hasBody()).isTrue(); assertThat(msg.hasCompleteBody()).isTrue(); assertThat(body).isEqualTo(TEXT2); assertThat(msg.getHeaders().getAll("Content-Length").size()).isEqualTo(1); assertThat(msg.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT2.length())); } @Test void testResettingBodyReaderIndex() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("Hello ".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultLastHttpContent(Unpooled.copiedBuffer("World!".getBytes(UTF_8)))); // replicate what happens in nettys tls channel writer which moves the reader index on the contained ByteBuf for (HttpContent c : msg.getBodyContents()) { c.content().readerIndex(c.content().capacity()); } for (HttpContent c : msg.getBodyContents()) { assertThat(c.content().isReadable()).isFalse(); assertThat(c.content().readableBytes()).isEqualTo(0); } msg.resetBodyReader(); for (HttpContent c : msg.getBodyContents()) { assertThat(c.content().isReadable()).isTrue(); assertThat(c.content().readableBytes() > 0).isTrue(); } } @Test void testFetchingBodyReturnsEntireBuffer() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("Hello ".getBytes(UTF_8)))); msg.bufferBodyContents(new DefaultLastHttpContent(Unpooled.copiedBuffer("World!".getBytes(UTF_8)))); // move the reader indexes to the end of the content buffers for (HttpContent c : msg.getBodyContents()) { c.content().readerIndex(c.content().capacity()); } // ensure body returns entire chunk content irregardless of reader index movement above assertThat(msg.getBodyLength()).isEqualTo(12); assertThat(new String(msg.getBody(), StandardCharsets.UTF_8)).isEqualTo(TEXT1); // buffer more content and ensure body returns entire chunk content msg.bufferBodyContents(new DefaultLastHttpContent(Unpooled.copiedBuffer(" Bye".getBytes(UTF_8)))); assertThat(msg.getBodyLength()).isEqualTo(16); assertThat(new String(msg.getBody(), StandardCharsets.UTF_8)).isEqualTo("Hello World! Bye"); } @Test void testFetchingEmptyBody() { ZuulMessage msg = new ZuulMessageImpl(new SessionContext(), new Headers()); assertThat(msg.getBodyLength()).isEqualTo(0); assertThat(msg.getBody()).isNull(); msg.bufferBodyContents(new DefaultHttpContent(Unpooled.copiedBuffer("".getBytes(UTF_8)))); assertThat(msg.getBodyLength()).isEqualTo(0); assertThat(msg.getBody().length).isEqualTo(0); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/message/http/CookiesTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; public class CookiesTest { @Test public void getNamesReturnsEmptySetWhenNoCookies() { Cookies cookies = new Cookies(); Set names = cookies.getNames(); assertNotNull(names); assertTrue(names.isEmpty()); } @Test public void getNamesReturnsSingleCookieName() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("sessionId", "abc123")); Set names = cookies.getNames(); assertEquals(1, names.size()); assertTrue(names.contains("sessionId")); } @Test public void getNamesReturnsMultipleCookieNames() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("NetflixId", "value1")); cookies.add(new DefaultCookie("SecureNetflixId", "value2")); cookies.add(new DefaultCookie("vdid", "value3")); Set names = cookies.getNames(); assertEquals(3, names.size()); assertTrue(names.contains("NetflixId")); assertTrue(names.contains("SecureNetflixId")); assertTrue(names.contains("vdid")); } @Test public void getNamesReturnsAllUniqueNames() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("first", "1")); cookies.add(new DefaultCookie("second", "2")); cookies.add(new DefaultCookie("third", "3")); Set names = cookies.getNames(); assertEquals(3, names.size()); assertTrue(names.contains("first")); assertTrue(names.contains("second")); assertTrue(names.contains("third")); } @Test public void getNamesReturnsUniqueNames() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("duplicate", "value1")); cookies.add(new DefaultCookie("duplicate", "value2")); cookies.add(new DefaultCookie("other", "value3")); Set names = cookies.getNames(); assertEquals(2, names.size()); assertTrue(names.contains("duplicate")); assertTrue(names.contains("other")); } @Test public void getAllReturnsAllCookies() { Cookies cookies = new Cookies(); Cookie c1 = new DefaultCookie("a", "1"); Cookie c2 = new DefaultCookie("b", "2"); cookies.add(c1); cookies.add(c2); List all = cookies.getAll(); assertEquals(2, all.size()); assertEquals(c1, all.get(0)); assertEquals(c2, all.get(1)); } @Test public void getReturnsMatchingCookies() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("session", "value1")); cookies.add(new DefaultCookie("session", "value2")); cookies.add(new DefaultCookie("other", "value3")); List sessionCookies = cookies.get("session"); assertNotNull(sessionCookies); assertEquals(2, sessionCookies.size()); assertEquals("session", sessionCookies.get(0).name()); assertEquals("value1", sessionCookies.get(0).value()); assertEquals("session", sessionCookies.get(1).name()); assertEquals("value2", sessionCookies.get(1).value()); } @Test public void getReturnsNullForNonExistentCookie() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("exists", "value")); List result = cookies.get("doesNotExist"); assertNull(result); } @Test public void getFirstReturnsFirstMatchingCookie() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("session", "first")); cookies.add(new DefaultCookie("session", "second")); Cookie first = cookies.getFirst("session"); assertNotNull(first); assertEquals("session", first.name()); assertEquals("first", first.value()); } @Test public void getFirstReturnsNullForNonExistentCookie() { Cookies cookies = new Cookies(); Cookie result = cookies.getFirst("doesNotExist"); assertNull(result); } @Test public void getFirstValueReturnsValueOfFirstMatchingCookie() { Cookies cookies = new Cookies(); cookies.add(new DefaultCookie("token", "abc123")); cookies.add(new DefaultCookie("token", "def456")); String value = cookies.getFirstValue("token"); assertEquals("abc123", value); } @Test public void getFirstValueReturnsNullForNonExistentCookie() { Cookies cookies = new Cookies(); String value = cookies.getFirstValue("doesNotExist"); assertNull(value); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/message/http/HttpQueryParamsTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class HttpQueryParamsTest { @Test void testMultiples() { HttpQueryParams qp = new HttpQueryParams(); qp.add("k1", "v1"); qp.add("k1", "v2"); qp.add("k2", "v3"); assertThat(qp.toEncodedString()).isEqualTo("k1=v1&k1=v2&k2=v3"); } @Test void testDuplicateKv() { HttpQueryParams qp = new HttpQueryParams(); qp.add("k1", "v1"); qp.add("k1", "v1"); qp.add("k1", "v1"); assertThat(qp.toEncodedString()).isEqualTo("k1=v1&k1=v1&k1=v1"); assertThat(qp.get("k1")).isEqualTo(List.of("v1", "v1", "v1")); } @Test void testToEncodedString() { HttpQueryParams qp = new HttpQueryParams(); qp.add("k'1", "v1&"); assertThat(qp.toEncodedString()).isEqualTo("k%271=v1%26"); qp = new HttpQueryParams(); qp.add("k+", "\n"); assertThat(qp.toEncodedString()).isEqualTo("k%2B=%0A"); } @Test void testToString() { HttpQueryParams qp = new HttpQueryParams(); qp.add("k'1", "v1&"); assertThat(qp.toString()).isEqualTo("k'1=v1&"); qp = new HttpQueryParams(); qp.add("k+", "\n"); assertThat(qp.toString()).isEqualTo("k+=\n"); } @Test void testEquals() { HttpQueryParams qp1 = new HttpQueryParams(); qp1.add("k1", "v1"); qp1.add("k2", "v2"); HttpQueryParams qp2 = new HttpQueryParams(); qp2.add("k1", "v1"); qp2.add("k2", "v2"); assertThat(qp2).isEqualTo(qp1); } @Test void parseKeysWithoutValues() { HttpQueryParams expected = new HttpQueryParams(); expected.add("k1", ""); expected.add("k2", "v2"); expected.add("k3", ""); HttpQueryParams actual = HttpQueryParams.parse("k1=&k2=v2&k3="); assertThat(actual).isEqualTo(expected); assertThat(actual.toEncodedString()).isEqualTo("k1=&k2=v2&k3="); } @Test void parseKeyWithoutValueEquals() { HttpQueryParams expected = new HttpQueryParams(); expected.add("k1", ""); HttpQueryParams actual = HttpQueryParams.parse("k1="); assertThat(actual).isEqualTo(expected); assertThat(actual.toEncodedString()).isEqualTo("k1="); } @Test void parseKeyWithoutValue() { HttpQueryParams expected = new HttpQueryParams(); expected.add("k1", ""); HttpQueryParams actual = HttpQueryParams.parse("k1"); assertThat(actual).isEqualTo(expected); assertThat(actual.toEncodedString()).isEqualTo("k1"); } @Test void parseKeyWithoutValueShort() { HttpQueryParams expected = new HttpQueryParams(); expected.add("=", ""); HttpQueryParams actual = HttpQueryParams.parse("="); assertThat(actual).isEqualTo(expected); assertThat(actual.toEncodedString()).isEqualTo("%3D"); } @Test void parseKeysWithoutValuesMixedTrailers() { HttpQueryParams expected = new HttpQueryParams(); expected.add("k1", ""); expected.add("k2", "v2"); expected.add("k3", ""); expected.add("k4", "v4"); HttpQueryParams actual = HttpQueryParams.parse("k1=&k2=v2&k3&k4=v4"); assertThat(actual).isEqualTo(expected); assertThat(actual.toEncodedString()).isEqualTo("k1=&k2=v2&k3&k4=v4"); } @Test void parseKeysIgnoreCase() { String camelCaseKey = "keyName"; HttpQueryParams queryParams = new HttpQueryParams(); queryParams.add("foo", "bar"); queryParams.add(camelCaseKey.toLowerCase(Locale.ROOT), "value"); assertThat(queryParams.containsIgnoreCase(camelCaseKey)).isTrue(); } @Test void maintainsOrderOnToString() { String queryString = IntStream.range(0, 100).mapToObj(i -> "k%d=v%d".formatted(i, i)).collect(Collectors.joining("&")); HttpQueryParams queryParams = HttpQueryParams.parse(queryString); assertThat(queryParams.toEncodedString()).isEqualTo(queryString); assertThat(queryParams.toString()).isEqualTo(queryString); assertThat(queryParams.immutableCopy().toString()).isEqualTo(queryString); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/message/http/HttpRequestMessageImplTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.net.InetAddresses; import com.netflix.config.ConfigurationManager; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import io.netty.channel.local.LocalAddress; import io.netty.handler.codec.http.cookie.Cookie; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URISyntaxException; import java.util.List; import java.util.Optional; import org.apache.commons.configuration.AbstractConfiguration; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @SuppressWarnings("AddressSelection") @ExtendWith(MockitoExtension.class) class HttpRequestMessageImplTest { HttpRequestMessageImpl request; private final AbstractConfiguration config = ConfigurationManager.getConfigInstance(); @AfterEach void resetConfig() { config.clearProperty("zuul.HttpRequestMessage.host.header.strict.validation"); } @Test void testOriginalRequestInfo() { HttpQueryParams queryParams = new HttpQueryParams(); queryParams.add("flag", "5"); Headers headers = new Headers(); headers.add("Host", "blah.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost", new LocalAddress("777"), false); request.storeInboundRequest(); HttpRequestInfo originalRequest = request.getInboundRequest(); assertThat(originalRequest.getPort()).isEqualTo(request.getPort()); assertThat(originalRequest.getPath()).isEqualTo(request.getPath()); assertThat(originalRequest.getQueryParams().getFirst("flag")) .isEqualTo(request.getQueryParams().getFirst("flag")); assertThat(originalRequest.getHeaders().getFirst("Host")) .isEqualTo(request.getHeaders().getFirst("Host")); request.setPort(8080); request.setPath("/another/place"); request.getQueryParams().set("flag", "20"); request.getHeaders().set("Host", "wah.netflix.com"); assertThat(originalRequest.getPort()).isEqualTo(7002); assertThat(originalRequest.getPath()).isEqualTo("/some/where"); assertThat(originalRequest.getQueryParams().getFirst("flag")).isEqualTo("5"); assertThat(originalRequest.getHeaders().getFirst("Host")).isEqualTo("blah.netflix.com"); } @Test void testReconstructURI() { HttpQueryParams queryParams = new HttpQueryParams(); queryParams.add("flag", "5"); Headers headers = new Headers(); headers.add("Host", "blah.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.reconstructURI()).isEqualTo("https://blah.netflix.com:7002/some/where?flag=5"); queryParams = new HttpQueryParams(); headers = new Headers(); headers.add("X-Forwarded-Host", "place.netflix.com"); headers.add("X-Forwarded-Port", "80"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "http", 7002, "localhost"); assertThat(request.reconstructURI()).isEqualTo("http://place.netflix.com/some/where"); queryParams = new HttpQueryParams(); headers = new Headers(); headers.add("X-Forwarded-Host", "place.netflix.com"); headers.add("X-Forwarded-Proto", "https"); headers.add("X-Forwarded-Port", "443"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "http", 7002, "localhost"); assertThat(request.reconstructURI()).isEqualTo("https://place.netflix.com/some/where"); queryParams = new HttpQueryParams(); headers = new Headers(); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "http", 7002, "localhost"); assertThat(request.reconstructURI()).isEqualTo("http://localhost:7002/some/where"); queryParams = new HttpQueryParams(); queryParams.add("flag", "5"); queryParams.add("flag B", "9"); headers = new Headers(); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some%20where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.reconstructURI()).isEqualTo("https://localhost:7002/some%20where?flag=5&flag+B=9"); } @Test void testReconstructURI_immutable() { HttpQueryParams queryParams = new HttpQueryParams(); queryParams.add("flag", "5"); Headers headers = new Headers(); headers.add("Host", "blah.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost", new SocketAddress() {}, true); // Check it's the same value 2nd time. assertThat(request.reconstructURI()).isEqualTo("https://blah.netflix.com:7002/some/where?flag=5"); assertThat(request.reconstructURI()).isEqualTo("https://blah.netflix.com:7002/some/where?flag=5"); // Check that cached on 1st usage. request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost", new SocketAddress() {}, true); request = spy(request); when(request._reconstructURI()).thenReturn("http://testhost/blah"); verify(request, times(1))._reconstructURI(); assertThat(request.reconstructURI()).isEqualTo("http://testhost/blah"); assertThat(request.reconstructURI()).isEqualTo("http://testhost/blah"); // Check that throws exception if we try to mutate it. try { request.setPath("/new-path"); fail(); } catch (IllegalStateException e) { assertThat(true).isTrue(); } } @Test void testPathAndQuery() { HttpQueryParams queryParams = new HttpQueryParams(); queryParams.add("flag", "5"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, new Headers(), "192.168.0.2", "https", 7002, "localhost"); // Check that value changes. assertThat(request.getPathAndQuery()).isEqualTo("/some/where?flag=5"); request.getQueryParams().add("k", "v"); assertThat(request.getPathAndQuery()).isEqualTo("/some/where?flag=5&k=v"); request.setPath("/other"); assertThat(request.getPathAndQuery()).isEqualTo("/other?flag=5&k=v"); } @Test void testPathAndQuery_immutable() { HttpQueryParams queryParams = new HttpQueryParams(); queryParams.add("flag", "5"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, new Headers(), "192.168.0.2", "https", 7002, "localhost", new SocketAddress() {}, true); // Check it's the same value 2nd time. assertThat(request.getPathAndQuery()).isEqualTo("/some/where?flag=5"); assertThat(request.getPathAndQuery()).isEqualTo("/some/where?flag=5"); // Check that cached on 1st usage. request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, new Headers(), "192.168.0.2", "https", 7002, "localhost", new SocketAddress() {}, true); request = spy(request); when(request.generatePathAndQuery()).thenReturn("/blah"); verify(request, times(1)).generatePathAndQuery(); assertThat(request.getPathAndQuery()).isEqualTo("/blah"); assertThat(request.getPathAndQuery()).isEqualTo("/blah"); } @Test void testGetOriginalHost_immutable() { HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); headers.add("Host", "blah.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost", new SocketAddress() {}, true); // Check it's the same value 2nd time. assertThat(request.getOriginalHost()).isEqualTo("blah.netflix.com"); assertThat(request.getOriginalHost()).isEqualTo("blah.netflix.com"); // Update the Host header value and ensure the result didn't change. headers.set("Host", "testOriginalHost2"); assertThat(request.getOriginalHost()).isEqualTo("blah.netflix.com"); } @Test void testGetOriginalHost() { HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); headers.add("Host", "blah.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("blah.netflix.com"); queryParams = new HttpQueryParams(); headers = new Headers(); headers.add("Host", "0.0.0.1"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("0.0.0.1"); queryParams = new HttpQueryParams(); headers = new Headers(); headers.add("Host", "0.0.0.1:2"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("0.0.0.1"); queryParams = new HttpQueryParams(); headers = new Headers(); headers.add("Host", "[::2]"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("[::2]"); queryParams = new HttpQueryParams(); headers = new Headers(); headers.add("Host", "[::2]:3"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("[::2]"); headers = new Headers(); headers.add("Host", "blah.netflix.com"); headers.add("X-Forwarded-Host", "foo.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("foo.netflix.com"); headers = new Headers(); headers.add("X-Forwarded-Host", "foo.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("foo.netflix.com"); headers = new Headers(); headers.add("Host", "blah.netflix.com:8080"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("blah.netflix.com"); } @Test void testGetOriginalHost_handlesNonRFC2396Hostnames() { config.setProperty("zuul.HttpRequestMessage.host.header.strict.validation", false); HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); headers.add("Host", "my_underscore_endpoint.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("my_underscore_endpoint.netflix.com"); headers = new Headers(); headers.add("Host", "my_underscore_endpoint.netflix.com:8080"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("my_underscore_endpoint.netflix.com"); headers = new Headers(); headers.add("Host", "my_underscore_endpoint^including~more-chars.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("my_underscore_endpoint^including~more-chars.netflix.com"); headers = new Headers(); headers.add("Host", "hostname%5Ewith-url-encoded.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalHost()).isEqualTo("hostname%5Ewith-url-encoded.netflix.com"); } @Test void getOriginalHost_failsOnUnbracketedIpv6Address() { HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); headers.add("Host", "ba::dd"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThatThrownBy(() -> HttpRequestMessageImpl.getOriginalHost(headers, "server")) .isInstanceOf(URISyntaxException.class); } @Test void getOriginalHost_fallsBackOnUnbracketedIpv6Address_WithNonStrictValidation() { config.setProperty("zuul.HttpRequestMessage.host.header.strict.validation", false); HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); headers.add("Host", "ba::dd"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "server"); assertThat(request.getOriginalHost()).isEqualTo("server"); } @Test void testGetOriginalPort() { HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(7002); headers = new Headers(); headers.add("Host", "blah.netflix.com"); headers.add("X-Forwarded-Port", "443"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(443); headers = new Headers(); headers.add("Host", "blah.netflix.com:443"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(443); headers = new Headers(); headers.add("Host", "127.0.0.2:443"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(443); headers = new Headers(); headers.add("Host", "127.0.0.2"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(7002); headers = new Headers(); headers.add("Host", "[::2]:443"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(443); headers = new Headers(); headers.add("Host", "[::2]"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(7002); headers = new Headers(); headers.add("Host", "blah.netflix.com:443"); headers.add("X-Forwarded-Port", "7005"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(7005); headers = new Headers(); headers.add("Host", "host_with_underscores.netflix.com:8080"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()) .as("should fallback to server port") .isEqualTo(7002); } @Test void testGetOriginalPort_NonStrictValidation() { config.setProperty("zuul.HttpRequestMessage.host.header.strict.validation", false); HttpQueryParams queryParams = new HttpQueryParams(); Headers headers = new Headers(); headers.add("Host", "host_with_underscores.netflix.com:8080"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(8080); headers = new Headers(); headers.add("Host", "host-with-carrots^1.0.0.netflix.com:8080"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(8080); headers = new Headers(); headers.add("Host", "host-with-carrots-no-port^1.0.0.netflix.com"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", queryParams, headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getOriginalPort()).isEqualTo(7002); } @Test void getOriginalPort_fallsBackOnUnbracketedIpv6Address() throws URISyntaxException { Headers headers = new Headers(); headers.add("Host", "ba::33"); assertThat(HttpRequestMessageImpl.getOriginalPort(new SessionContext(), headers, 9999)) .isEqualTo(9999); } @Test void getOriginalPort_EmptyXFFPort() throws URISyntaxException { Headers headers = new Headers(); headers.add(HttpHeaderNames.X_FORWARDED_PORT, ""); // Default to using server port assertThat(HttpRequestMessageImpl.getOriginalPort(new SessionContext(), headers, 9999)) .isEqualTo(9999); } @Test void getOriginalPort_respectsProxyProtocol() throws URISyntaxException { SessionContext context = new SessionContext(); context.set( CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS, new InetSocketAddress(InetAddresses.forString("1.1.1.1"), 443)); Headers headers = new Headers(); headers.add("X-Forwarded-Port", "6000"); assertThat(HttpRequestMessageImpl.getOriginalPort(context, headers, 9999)) .isEqualTo(443); } @Test void testCleanCookieHeaders() { assertThat(HttpRequestMessageImpl.cleanCookieHeader("BlahId=12345; Secure, something=67890;")) .isEqualTo("BlahId=12345; something=67890;"); assertThat(HttpRequestMessageImpl.cleanCookieHeader("BlahId=12345; something=67890;")) .isEqualTo("BlahId=12345; something=67890;"); assertThat(HttpRequestMessageImpl.cleanCookieHeader(" Secure, BlahId=12345; Secure, something=67890;")) .isEqualTo(" BlahId=12345; something=67890;"); assertThat(HttpRequestMessageImpl.cleanCookieHeader("")).isEqualTo(""); } @Test void shouldPreferClientDestPortWhenInitialized() { HttpRequestMessageImpl message = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", new HttpQueryParams(), new Headers(), "192.168.0.2", "https", 7002, "localhost", new InetSocketAddress("api.netflix.com", 443), true); assertThat(Optional.of(443)).isEqualTo(message.getClientDestinationPort()); } @Test public void duplicateCookieNames() { Headers headers = new Headers(); headers.add("cookie", "k=v1;k=v2"); HttpRequestMessageImpl message = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/some/where", new HttpQueryParams(), headers, "192.168.0.2", "https", 7002, "localhost", new InetSocketAddress("api.netflix.com", 443), true); Cookies cookies = message.parseCookies(); assertThat(cookies.getAll().size()).isEqualTo(2); List kCookies = cookies.get("k"); assertThat(kCookies.size()).isEqualTo(2); assertThat(kCookies.get(0).value()).isEqualTo("v1"); assertThat(kCookies.get(1).value()).isEqualTo("v2"); } @Test public void testGetDecodedPath() { Headers headers = new Headers(); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/%C3%B1", new HttpQueryParams(), headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getPath()).isEqualTo("/%C3%B1"); assertThat(request.getDecodedPath()).isEqualTo("/ñ"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/ñ", new HttpQueryParams(), headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getPath()).isEqualTo("/ñ"); assertThat(request.getDecodedPath()).isEqualTo("/ñ"); request = new HttpRequestMessageImpl( new SessionContext(), "HTTP/1.1", "POST", "/path", new HttpQueryParams(), headers, "192.168.0.2", "https", 7002, "localhost"); assertThat(request.getPath()).isEqualTo("/path"); assertThat(request.getDecodedPath()).isEqualTo("/path"); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/message/http/HttpResponseMessageImplTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.message.http; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for {@link HttpResponseMessageImpl}. */ @ExtendWith(MockitoExtension.class) class HttpResponseMessageImplTest { private static final String TEXT1 = "Hello World!"; private static final String TEXT2 = "Goodbye World!"; @Mock private HttpRequestMessage request; private HttpResponseMessageImpl response; @BeforeEach void setup() { response = new HttpResponseMessageImpl(new SessionContext(), new Headers(), request, 200); } @Test void testHasSetCookieWithName() { response.getHeaders() .add( "Set-Cookie", "c1=1234; Max-Age=-1; Expires=Tue, 01 Sep 2015 22:49:57 GMT; Path=/; Domain=.netflix.com"); response.getHeaders() .add( "Set-Cookie", "c2=4567; Max-Age=-1; Expires=Tue, 01 Sep 2015 22:49:57 GMT; Path=/; Domain=.netflix.com"); assertThat(response.hasSetCookieWithName("c1")).isTrue(); assertThat(response.hasSetCookieWithName("c2")).isTrue(); assertThat(response.hasSetCookieWithName("XX")).isFalse(); } @Test void testRemoveExistingSetCookie() { response.getHeaders() .add( "Set-Cookie", "c1=1234; Max-Age=-1; Expires=Tue, 01 Sep 2015 22:49:57 GMT; Path=/; Domain=.netflix.com"); response.getHeaders() .add( "Set-Cookie", "c2=4567; Max-Age=-1; Expires=Tue, 01 Sep 2015 22:49:57 GMT; Path=/; Domain=.netflix.com"); response.removeExistingSetCookie("c1"); assertThat(response.getHeaders().size()).isEqualTo(1); assertThat(response.hasSetCookieWithName("c1")).isFalse(); assertThat(response.hasSetCookieWithName("c2")).isTrue(); } @Test void testContentLengthHeaderHasCorrectValue() { assertThat(response.getHeaders().getAll("Content-Length").size()).isEqualTo(0); response.setBodyAsText(TEXT1); assertThat(response.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT1.length())); response.setBody(TEXT2.getBytes(StandardCharsets.UTF_8)); assertThat(response.getHeaders().getFirst("Content-Length")).isEqualTo(String.valueOf(TEXT2.length())); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/monitoring/ConnCounterTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Gauge; import com.netflix.spectator.api.Registry; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.jupiter.api.Test; class ConnCounterTest { @Test void record() { EmbeddedChannel chan = new EmbeddedChannel(); Attrs attrs = Attrs.newInstance(); chan.attr(Server.CONN_DIMENSIONS).set(attrs); Registry registry = new DefaultRegistry(); ConnCounter counter = ConnCounter.install(chan, registry, registry.createId("foo")); counter.increment("start"); counter.increment("middle"); Attrs.newKey("bar").put(attrs, "baz"); counter.increment("end"); Gauge meter1 = registry.gauge(registry.createId("foo.start", "from", "nascent")); assertThat(meter1).isNotNull(); assertThat(meter1.value()).isCloseTo(1.0, org.assertj.core.data.Offset.offset(0.0)); Gauge meter2 = registry.gauge(registry.createId("foo.middle", "from", "start")); assertThat(meter2).isNotNull(); assertThat(meter2.value()).isCloseTo(1.0, org.assertj.core.data.Offset.offset(0.0)); Gauge meter3 = registry.gauge(registry.createId("foo.end", "from", "middle", "bar", "baz")); assertThat(meter3).isNotNull(); assertThat(meter3.value()).isCloseTo(1.0, org.assertj.core.data.Offset.offset(0.0)); } @Test void activeConnsCount() { EmbeddedChannel channel = new EmbeddedChannel(); Attrs attrs = Attrs.newInstance(); channel.attr(Server.CONN_DIMENSIONS).set(attrs); Registry registry = new DefaultRegistry(); ConnCounter.install(channel, registry, registry.createId("foo")); // Dedup increments ConnCounter.from(channel).increment("active"); ConnCounter.from(channel).increment("active"); assertThat(ConnCounter.from(channel).getCurrentActiveConns()) .isCloseTo(1.0, org.assertj.core.data.Offset.offset(0.0)); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/monitoring/ConnTimerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.monitoring; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.histogram.PercentileTimer; import com.netflix.zuul.Attrs; import com.netflix.zuul.netty.server.Server; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.jupiter.api.Test; class ConnTimerTest { @Test void record() { EmbeddedChannel chan = new EmbeddedChannel(); Attrs attrs = Attrs.newInstance(); chan.attr(Server.CONN_DIMENSIONS).set(attrs); Registry registry = new DefaultRegistry(); ConnTimer timer = ConnTimer.install(chan, registry, registry.createId("foo")); timer.record(1000L, "start"); timer.record(2000L, "middle"); Attrs.newKey("bar").put(attrs, "baz"); timer.record(4000L, "end"); PercentileTimer meter1 = PercentileTimer.get(registry, registry.createId("foo.start-middle")); assertThat(meter1).isNotNull(); assertThat(meter1.totalTime()).isEqualTo(1000L); PercentileTimer meter2 = PercentileTimer.get(registry, registry.createId("foo.middle-end", "bar", "baz")); assertThat(meter2).isNotNull(); assertThat(meter2.totalTime()).isEqualTo(2000L); PercentileTimer meter3 = PercentileTimer.get(registry, registry.createId("foo.start-end", "bar", "baz")); assertThat(meter3).isNotNull(); assertThat(meter3.totalTime()).isEqualTo(3000L); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/NettyRequestAttemptFactoryTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.exception.OutboundException; import com.netflix.zuul.netty.connectionpool.OriginConnectException; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.origins.OriginConcurrencyExceededException; import com.netflix.zuul.origins.OriginName; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.timeout.ReadTimeoutException; import java.nio.channels.ClosedChannelException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class NettyRequestAttemptFactoryTest { private NettyRequestAttemptFactory factory; @BeforeEach public void setup() { factory = new NettyRequestAttemptFactory(); } @Test public void mapNettyToOutboundExceptionMapsToOutboundException() { Exception e = new OutboundException(OutboundErrorType.OTHER, new RequestAttempts()); OutboundException mapException = factory.mapNettyToOutboundException(e, new SessionContext()); assertThat(mapException).isEqualTo(e); // check that the type is OutboundException assertThat(mapException).isNotNull(); } @Test public void mapNettyToOutboundExceptionMapsToReadTimeoutException() { OutboundException mapException = factory.mapNettyToOutboundException(new ReadTimeoutException(), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.READ_TIMEOUT); } @Test public void mapNettyToOutboundExceptionMapsToOriginConcurrencyExceededException() { OutboundException mapException = factory.mapNettyToOutboundException( new OriginConcurrencyExceededException(OriginName.fromVipAndApp("vip", "app")), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.ORIGIN_CONCURRENCY_EXCEEDED); } @Test public void mapNettyToOutboundExceptionMapsToOriginConnectException() { OutboundException mapException = factory.mapNettyToOutboundException( new OriginConnectException("error", OutboundErrorType.OTHER), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.OTHER); } @Test public void mapNettyToOutboundExceptionMapsToClosedChannelException() { OutboundException mapException = factory.mapNettyToOutboundException(new ClosedChannelException(), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.RESET_CONNECTION); } @Test public void mapNettyToOutboundExceptionMapsToHeaderListSizeException() { OutboundException mapException = factory.mapNettyToOutboundException( Http2Exception.headerListSizeError(1, Http2Error.CONNECT_ERROR, false, ""), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.HEADER_FIELDS_TOO_LARGE); } @Test public void mapNettyToOutboundExceptionMapsToIllegalStateException() { OutboundException mapException = factory.mapNettyToOutboundException( new Exception(new IllegalStateException(new Throwable("No available server"))), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.NO_AVAILABLE_SERVERS); } @Test public void mapNettyToOutboundExceptionMapsToOtherExceptionType() { OutboundException mapException = factory.mapNettyToOutboundException(new Exception(), new SessionContext()); // check that the type is OutboundException assertThat(mapException).isNotNull(); assertThat(mapException.getOutboundErrorType()).isEqualTo(OutboundErrorType.OTHER); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/connectionpool/ClientTimeoutHandlerTest.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.util.ReferenceCountUtil; import java.time.Duration; import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * @author Justin Guerra * @since 7/30/24 */ @ExtendWith(MockitoExtension.class) class ClientTimeoutHandlerTest { @Mock private PooledConnection pooledConnection; private EmbeddedChannel channel; private WriteVerifyingHandler verifier; @BeforeEach public void setup() { channel = new EmbeddedChannel(); channel.attr(PooledConnection.CHANNEL_ATTR).set(pooledConnection); verifier = new WriteVerifyingHandler(); channel.pipeline().addLast(verifier); channel.pipeline().addLast(new ClientTimeoutHandler.OutboundHandler()); } @AfterEach public void cleanup() { channel.finishAndReleaseAll(); } @Test public void dontStartReadTimeoutHandlerIfNotLastContent() { addTimeoutToChannel(); channel.writeOutbound(new DefaultHttpContent(Unpooled.wrappedBuffer("yo".getBytes(UTF_8)))); verify(pooledConnection, never()).startReadTimeoutHandler(any()); verifyWrite(); } @Test public void dontStartReadTimeoutHandlerIfNoTimeout() { channel.writeOutbound(new DefaultLastHttpContent()); verify(pooledConnection, never()).startReadTimeoutHandler(any()); verifyWrite(); } @Test public void dontStartReadTimeoutHandlerOnFailedPromise() { addTimeoutToChannel(); channel.pipeline().addFirst(new ChannelDuplexHandler() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { ReferenceCountUtil.safeRelease(msg); promise.setFailure(new RuntimeException()); } }); try { channel.writeOutbound(new DefaultLastHttpContent()); } catch (RuntimeException e) { // expected } verify(pooledConnection, never()).startReadTimeoutHandler(any()); verifyWrite(); } @Test public void startReadTimeoutHandlerOnSuccessfulPromise() { Duration timeout = addTimeoutToChannel(); channel.writeOutbound(new DefaultLastHttpContent()); verify(pooledConnection).startReadTimeoutHandler(timeout); verifyWrite(); } private Duration addTimeoutToChannel() { Duration timeout = Duration.of(5, ChronoUnit.SECONDS); channel.attr(ClientTimeoutHandler.ORIGIN_RESPONSE_READ_TIMEOUT).set(timeout); return timeout; } private void verifyWrite() { assertThat(verifier.seenWrite).isTrue(); } private static class WriteVerifyingHandler extends ChannelDuplexHandler { boolean seenWrite; @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { seenWrite = true; super.write(ctx, msg, promise); } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/connectionpool/ConnectionPoolConfigImplTest.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static com.netflix.zuul.netty.connectionpool.ConnectionPoolConfigImpl.MAX_REQUESTS_PER_CONNECTION; import static com.netflix.zuul.netty.connectionpool.ConnectionPoolConfigImpl.PER_SERVER_WATERLINE; import static com.netflix.zuul.netty.connectionpool.ConnectionPoolConfigImpl.TCP_KEEP_ALIVE; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfigKey; import com.netflix.zuul.origins.OriginName; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * @author Justin Guerra * @since 6/20/24 */ class ConnectionPoolConfigImplTest { private ConnectionPoolConfig connectionPoolConfig; private DefaultClientConfigImpl clientConfig; @BeforeEach void setup() { OriginName originName = OriginName.fromVip("whatever"); clientConfig = DefaultClientConfigImpl.getEmptyConfig(); connectionPoolConfig = new ConnectionPoolConfigImpl(originName, clientConfig); } @Test void testGetConnectTimeout() { assertThat(connectionPoolConfig.getConnectTimeout()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_CONNECT_TIMEOUT); } @Test void testGetConnectTimeoutOverride() { clientConfig.set(IClientConfigKey.Keys.ConnectTimeout, 1000); assertThat(connectionPoolConfig.getConnectTimeout()).isEqualTo(1000); } @Test void testGetMaxRequestsPerConnection() { assertThat(connectionPoolConfig.getMaxRequestsPerConnection()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_MAX_REQUESTS_PER_CONNECTION); } @Test void testGetMaxRequestsPerConnectionOverride() { clientConfig.set(MAX_REQUESTS_PER_CONNECTION, 2000); assertThat(connectionPoolConfig.getMaxRequestsPerConnection()).isEqualTo(2000); } @Test void testMaxConnectionsPerHost() { assertThat(connectionPoolConfig.maxConnectionsPerHost()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_MAX_CONNS_PER_HOST); } @Test void testMaxConnectionsPerHostOverride() { clientConfig.set(IClientConfigKey.Keys.MaxConnectionsPerHost, 60); assertThat(connectionPoolConfig.maxConnectionsPerHost()).isEqualTo(60); } @Test void testPerServerWaterline() { assertThat(connectionPoolConfig.perServerWaterline()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_PER_SERVER_WATERLINE); } @Test void testPerServerWaterlineOverride() { clientConfig.set(PER_SERVER_WATERLINE, 5); assertThat(connectionPoolConfig.perServerWaterline()).isEqualTo(5); } @Test void testGetIdleTimeout() { assertThat(connectionPoolConfig.getIdleTimeout()).isEqualTo(ConnectionPoolConfigImpl.DEFAULT_IDLE_TIMEOUT); } @Test void testGetIdleTimeoutOverride() { clientConfig.set(IClientConfigKey.Keys.ConnIdleEvictTimeMilliSeconds, 70000); assertThat(connectionPoolConfig.getIdleTimeout()).isEqualTo(70000); } @Test void testGetTcpKeepAlive() { assertThat(connectionPoolConfig.getTcpKeepAlive()).isFalse(); } @Test void testGetTcpKeepAliveOverride() { clientConfig.set(TCP_KEEP_ALIVE, true); assertThat(connectionPoolConfig.getTcpKeepAlive()).isTrue(); } @Test void testGetTcpNoDelay() { assertThat(connectionPoolConfig.getTcpNoDelay()).isTrue(); } @Test void testGetTcpNoDelayOverride() { clientConfig.set(ConnectionPoolConfigImpl.TCP_NO_DELAY, true); assertThat(connectionPoolConfig.getTcpNoDelay()).isTrue(); } @Test void testGetTcpReceiveBufferSize() { assertThat(connectionPoolConfig.getTcpReceiveBufferSize()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_BUFFER_SIZE); } @Test void testGetTcpReceiveBufferSizeOverride() { clientConfig.set(IClientConfigKey.Keys.ReceiveBufferSize, 40000); assertThat(connectionPoolConfig.getTcpReceiveBufferSize()).isEqualTo(40000); } @Test void testGetTcpSendBufferSize() { assertThat(connectionPoolConfig.getTcpSendBufferSize()).isEqualTo(ConnectionPoolConfigImpl.DEFAULT_BUFFER_SIZE); } @Test void testGetTcpSendBufferSizeOverride() { clientConfig.set(IClientConfigKey.Keys.SendBufferSize, 40000); assertThat(connectionPoolConfig.getTcpSendBufferSize()).isEqualTo(40000); } @Test void testGetNettyWriteBufferHighWaterMark() { assertThat(connectionPoolConfig.getNettyWriteBufferHighWaterMark()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_WRITE_BUFFER_HIGH_WATER_MARK); } @Test void testGetNettyWriteBufferHighWaterMarkOverride() { clientConfig.set(ConnectionPoolConfigImpl.WRITE_BUFFER_HIGH_WATER_MARK, 40000); assertThat(connectionPoolConfig.getNettyWriteBufferHighWaterMark()).isEqualTo(40000); } @Test void testGetNettyWriteBufferLowWaterMark() { assertThat(connectionPoolConfig.getNettyWriteBufferLowWaterMark()) .isEqualTo(ConnectionPoolConfigImpl.DEFAULT_WRITE_BUFFER_LOW_WATER_MARK); } @Test void testGetNettyWriteBufferLowWaterMarkOverride() { clientConfig.set(ConnectionPoolConfigImpl.WRITE_BUFFER_LOW_WATER_MARK, 10000); assertThat(connectionPoolConfig.getNettyWriteBufferLowWaterMark()).isEqualTo(10000); } @Test void testGetNettyAutoRead() { assertThat(connectionPoolConfig.getNettyAutoRead()).isFalse(); } @Test void testGetNettyAutoReadOverride() { clientConfig.set(ConnectionPoolConfigImpl.AUTO_READ, true); assertThat(connectionPoolConfig.getNettyAutoRead()).isTrue(); } @Test void testIsSecure() { assertThat(connectionPoolConfig.isSecure()).isFalse(); } @Test void testIsSecureOverride() { clientConfig.set(IClientConfigKey.Keys.IsSecure, true); assertThat(connectionPoolConfig.isSecure()).isTrue(); } @Test void testUseIPAddrForServer() { assertThat(connectionPoolConfig.useIPAddrForServer()).isTrue(); } @Test void testUseIPAddrForServerOverride() { clientConfig.set(IClientConfigKey.Keys.UseIPAddrForServer, false); assertThat(connectionPoolConfig.useIPAddrForServer()).isFalse(); } @Test void testIsCloseOnCircuitBreakerEnabled() { assertThat(connectionPoolConfig.isCloseOnCircuitBreakerEnabled()).isTrue(); } @Test void testIsCloseOnCircuitBreakerEnabledOverride() { clientConfig.set(ConnectionPoolConfigImpl.CLOSE_ON_CIRCUIT_BREAKER, false); assertThat(connectionPoolConfig.isCloseOnCircuitBreakerEnabled()).isFalse(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/connectionpool/ConnectionPoolMetricsTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.Lists; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Tag; import com.netflix.zuul.origins.OriginName; import java.util.Map; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; /** * @author Justin Guerra * @since 2/28/25 */ class ConnectionPoolMetricsTest { @Test public void validateMetricNames() { DefaultRegistry registry = new DefaultRegistry(); OriginName originName = OriginName.fromVipAndApp("whatever", "whatever"); ConnectionPoolMetrics metrics = ConnectionPoolMetrics.create(originName, registry); validateCounter("connectionpool_create", metrics.createNewConnCounter()); validateCounter("connectionpool_create_success", metrics.createConnSucceededCounter()); validateCounter("connectionpool_create_fail", metrics.createConnFailedCounter()); validateCounter("connectionpool_close", metrics.closeConnCounter()); validateCounter("connectionpool_closeAbovePoolHighWaterMark", metrics.closeAbovePoolHighWaterMarkCounter()); validateCounter("connectionpool_closeExpiredConnLifetime", metrics.closeExpiredConnLifetimeCounter()); validateCounter("connectionpool_request", metrics.requestConnCounter()); validateCounter("connectionpool_reuse", metrics.reuseConnCounter()); validateCounter("connectionpool_release", metrics.releaseConnCounter()); validateCounter("connectionpool_alreadyClosed", metrics.alreadyClosedCounter()); validateCounter("connectionpool_fromPoolIsClosed", metrics.connTakenFromPoolIsNotOpen()); validateCounter("connectionpool_maxConnsPerHostExceeded", metrics.maxConnsPerHostExceededCounter()); validateCounter("connectionpool_closeWrtBusyConnCounter", metrics.closeWrtBusyConnCounter()); validateCounter("connectionpool_closeCircuitBreaker", metrics.circuitBreakerClose()); validateCounter("connectionpool_idle", metrics.idleCounter()); validateCounter("connectionpool_inactive", metrics.inactiveCounter()); validateCounter("connectionpool_error", metrics.errorCounter()); validateCounter("connectionpool_headerClose", metrics.headerCloseCounter()); validateCounter("connectionpool_sslClose", metrics.sslCloseCompletionCounter()); } private void validateCounter(String name, Counter counter) { assertThat(counter.id().name()).isEqualTo(name); Map tags = Lists.newArrayList(counter.id().tags().iterator()).stream() .collect(Collectors.toMap(Tag::key, Tag::value)); assertThat(tags.get("id")).isEqualTo("whatever"); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/connectionpool/DefaultClientChannelManagerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.net.InetAddresses; import com.netflix.appinfo.InstanceInfo; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.discovery.DynamicServerResolver; import com.netflix.zuul.discovery.NonDiscoveryServer; import com.netflix.zuul.netty.server.Server; import com.netflix.zuul.origins.OriginName; import com.netflix.zuul.passport.CurrentPassport; import io.netty.channel.DefaultEventLoop; import io.netty.channel.EventLoop; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.concurrent.Promise; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.SocketAddress; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.mockito.Mockito; /** * Tests for {@link DefaultClientChannelManager}. These tests don't use IPv6 addresses because {@link InstanceInfo} is * not capable of expressing them. */ class DefaultClientChannelManagerTest { @Test void pickAddressInternal_discovery() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("app") .setHostName("192.168.0.1") .setPort(443) .build(); DiscoveryResult s = DiscoveryResult.from(instanceInfo, true); SocketAddress addr = DefaultClientChannelManager.pickAddressInternal(s, OriginName.fromVip("vip")); assertThat(addr).isInstanceOf(InetSocketAddress.class); InetSocketAddress socketAddress = (InetSocketAddress) addr; assertThat(socketAddress.getAddress()).isEqualTo(InetAddresses.forString("192.168.0.1")); assertThat(socketAddress.getPort()).isEqualTo(443); } @Test void pickAddressInternal_discovery_unresolved() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("app") .setHostName("localhost") .setPort(443) .build(); DiscoveryResult s = DiscoveryResult.from(instanceInfo, true); SocketAddress addr = DefaultClientChannelManager.pickAddressInternal(s, OriginName.fromVip("vip")); assertThat(addr).isInstanceOf(InetSocketAddress.class); InetSocketAddress socketAddress = (InetSocketAddress) addr; assertThat(socketAddress.getAddress().isLoopbackAddress()) .as(socketAddress.toString()) .isTrue(); assertThat(socketAddress.getPort()).isEqualTo(443); } @Test void pickAddressInternal_nonDiscovery() { NonDiscoveryServer s = new NonDiscoveryServer("192.168.0.1", 443); SocketAddress addr = DefaultClientChannelManager.pickAddressInternal(s, OriginName.fromVip("vip")); assertThat(addr).isInstanceOf(InetSocketAddress.class); InetSocketAddress socketAddress = (InetSocketAddress) addr; assertThat(socketAddress.getAddress()).isEqualTo(InetAddresses.forString("192.168.0.1")); assertThat(socketAddress.getPort()).isEqualTo(443); } @Test void pickAddressInternal_nonDiscovery_unresolved() { NonDiscoveryServer s = new NonDiscoveryServer("localhost", 443); SocketAddress addr = DefaultClientChannelManager.pickAddressInternal(s, OriginName.fromVip("vip")); assertThat(addr).isInstanceOf(InetSocketAddress.class); InetSocketAddress socketAddress = (InetSocketAddress) addr; assertThat(socketAddress.getAddress().isLoopbackAddress()) .as(socketAddress.toString()) .isTrue(); assertThat(socketAddress.getPort()).isEqualTo(443); } @Test void updateServerRefOnEmptyDiscoveryResult() { OriginName originName = OriginName.fromVip("vip", "test"); DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); DynamicServerResolver resolver = mock(DynamicServerResolver.class); when(resolver.resolve(any())).thenReturn(DiscoveryResult.EMPTY); DefaultClientChannelManager clientChannelManager = new DefaultClientChannelManager(originName, clientConfig, resolver, new DefaultRegistry()); AtomicReference serverRef = new AtomicReference<>(); Promise promise = clientChannelManager.acquire( new DefaultEventLoop(), null, CurrentPassport.create(), serverRef, new AtomicReference<>()); assertThat(promise.isSuccess()).isFalse(); assertThat(serverRef.get()).isSameAs(DiscoveryResult.EMPTY); } @Test void updateServerRefOnValidDiscoveryResult() throws InterruptedException { OriginName originName = OriginName.fromVip("vip", "test"); DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); DynamicServerResolver resolver = mock(DynamicServerResolver.class); InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("server-equality") .setHostName("server-equality") .setPort(7777) .build(); DiscoveryResult discoveryResult = DiscoveryResult.from(instanceInfo, false); when(resolver.resolve(any())).thenReturn(discoveryResult); DefaultClientChannelManager clientChannelManager = new DefaultClientChannelManager(originName, clientConfig, resolver, new DefaultRegistry()); AtomicReference serverRef = new AtomicReference<>(); // TODO(argha-c) capture and assert on the promise once we have a dummy with ServerStats initialized var unusedFuture = clientChannelManager.acquire( new DefaultEventLoop(), null, CurrentPassport.create(), serverRef, new AtomicReference<>()); assertThat(serverRef.get()).isSameAs(discoveryResult); } @Test void initializeAndShutdown() throws Exception { String appName = "app-" + UUID.randomUUID(); ServerSocket serverSocket = new ServerSocket(0); InetSocketAddress serverSocketAddress = (InetSocketAddress) serverSocket.getLocalSocketAddress(); String serverHostname = serverSocketAddress.getHostName(); int serverPort = serverSocketAddress.getPort(); OriginName originName = OriginName.fromVipAndApp("vip", appName); DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); Server.defaultOutboundChannelType.set(NioSocketChannel.class); InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName(appName) .setHostName(serverHostname) .setPort(serverPort) .build(); DiscoveryResult discoveryResult = DiscoveryResult.from(instanceInfo, true); DynamicServerResolver resolver = mock(DynamicServerResolver.class); when(resolver.resolve(any())).thenReturn(discoveryResult); when(resolver.hasServers()).thenReturn(true); Registry registry = new DefaultRegistry(); DefaultClientChannelManager clientChannelManager = new DefaultClientChannelManager(originName, clientConfig, resolver, registry); NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(10); EventLoop eventLoop = eventLoopGroup.next(); clientChannelManager.init(); assertThat(clientChannelManager.getConnsInUse()).isEqualTo(0); Promise promiseConn = clientChannelManager.acquire(eventLoop); promiseConn.await(200, TimeUnit.MILLISECONDS); assertThat(promiseConn.isDone()).isTrue(); assertThat(promiseConn.isSuccess()).isTrue(); PooledConnection connection = promiseConn.get(); assertThat(connection.isActive()).isTrue(); assertThat(connection.isInPool()).isFalse(); assertThat(clientChannelManager.getConnsInUse()).isEqualTo(1); boolean releaseResult = clientChannelManager.release(connection); assertThat(releaseResult).isTrue(); assertThat(connection.isInPool()).isTrue(); assertThat(clientChannelManager.getConnsInUse()).isEqualTo(0); clientChannelManager.shutdown(); serverSocket.close(); } @Test void closeOnCircuitBreaker() { OriginName originName = OriginName.fromVipAndApp("whatever", "whatever"); DefaultClientChannelManager manager = new DefaultClientChannelManager( originName, new DefaultClientConfigImpl(), Mockito.mock(DynamicServerResolver.class), new NoopRegistry()) { @Override protected void updateServerStatsOnRelease(PooledConnection conn) {} }; PooledConnection connection = mock(PooledConnection.class); DiscoveryResult discoveryResult = mock(DiscoveryResult.class); doReturn(discoveryResult).when(connection).getServer(); doReturn(true).when(discoveryResult).isCircuitBreakerTripped(); doReturn(new EmbeddedChannel()).when(connection).getChannel(); assertThat(manager.release(connection)).isFalse(); verify(connection).setInPool(false); verify(connection).close(); } @Test void skipCloseOnCircuitBreaker() { OriginName originName = OriginName.fromVipAndApp("whatever", "whatever"); DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); DefaultClientChannelManager manager = new DefaultClientChannelManager( originName, clientConfig, Mockito.mock(DynamicServerResolver.class), new NoopRegistry()) { @Override protected void updateServerStatsOnRelease(PooledConnection conn) {} @Override protected void releaseHandlers(PooledConnection conn) {} }; PooledConnection connection = mock(PooledConnection.class); DiscoveryResult discoveryResult = mock(DiscoveryResult.class); doReturn(discoveryResult).when(connection).getServer(); doReturn(true).when(discoveryResult).isCircuitBreakerTripped(); doReturn(true).when(connection).isActive(); doReturn(new EmbeddedChannel()).when(connection).getChannel(); IConnectionPool connectionPool = mock(IConnectionPool.class); doReturn(true).when(connectionPool).release(connection); manager.getPerServerPools().put(discoveryResult, connectionPool); clientConfig.set(ConnectionPoolConfigImpl.CLOSE_ON_CIRCUIT_BREAKER, false); assertThat(manager.release(connection)).isTrue(); verify(connection, never()).setInPool(anyBoolean()); verify(connection, never()).close(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/connectionpool/PerServerConnectionPoolTest.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.spy; import com.netflix.appinfo.InstanceInfo; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfigKey.Keys; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Timer; import com.netflix.zuul.discovery.DiscoveryResult; import com.netflix.zuul.netty.connectionpool.PooledConnection.ConnectionState; import com.netflix.zuul.netty.server.Server; import com.netflix.zuul.origins.OriginName; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoop; import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; import io.netty.channel.local.LocalIoHandler; import io.netty.channel.local.LocalServerChannel; import io.netty.handler.codec.DecoderException; import io.netty.util.concurrent.Promise; import java.util.Deque; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLHandshakeException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * @author Justin Guerra * @since 2/24/23 */ class PerServerConnectionPoolTest { private static LocalAddress LOCAL_ADDRESS; private static MultithreadEventLoopGroup ORIGIN_EVENT_LOOP_GROUP; private static MultithreadEventLoopGroup CLIENT_EVENT_LOOP_GROUP; private static EventLoop CLIENT_EVENT_LOOP; private static Class PREVIOUS_CHANNEL_TYPE; @Mock private ClientChannelManager channelManager; private Registry registry; private DiscoveryResult discoveryResult; private DefaultClientConfigImpl clientConfig; private ConnectionPoolConfig connectionPoolConfig; private PerServerConnectionPool pool; private Counter createNewConnCounter; private Counter createConnSucceededCounter; private Counter createConnFailedCounter; private Counter requestConnCounter; private Counter reuseConnCounter; private Counter connTakenFromPoolIsNotOpen; private Counter closeAboveHighWaterMarkCounter; private Counter maxConnsPerHostExceededCounter; private Timer connEstablishTimer; private AtomicInteger connsInPool; private AtomicInteger connsInUse; @BeforeAll @SuppressWarnings("deprecation") static void staticSetup() throws InterruptedException { LOCAL_ADDRESS = new LocalAddress(UUID.randomUUID().toString()); CLIENT_EVENT_LOOP_GROUP = new MultiThreadIoEventLoopGroup(1, LocalIoHandler.newFactory()); CLIENT_EVENT_LOOP = CLIENT_EVENT_LOOP_GROUP.next(); ORIGIN_EVENT_LOOP_GROUP = new MultiThreadIoEventLoopGroup(1, LocalIoHandler.newFactory()); ServerBootstrap bootstrap = new ServerBootstrap() .group(ORIGIN_EVENT_LOOP_GROUP) .localAddress(LOCAL_ADDRESS) .channel(LocalServerChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(LocalChannel ch) {} }); bootstrap.bind().sync(); PREVIOUS_CHANNEL_TYPE = Server.defaultOutboundChannelType.getAndSet(LocalChannel.class); } @AfterAll @SuppressWarnings("deprecation") static void staticCleanup() { ORIGIN_EVENT_LOOP_GROUP.shutdownGracefully(); CLIENT_EVENT_LOOP_GROUP.shutdownGracefully(); if (PREVIOUS_CHANNEL_TYPE != null) { Server.defaultOutboundChannelType.set(PREVIOUS_CHANNEL_TYPE); } } @BeforeEach void setup() { MockitoAnnotations.openMocks(this); registry = new DefaultRegistry(); int index = 0; createNewConnCounter = registry.counter("fake_counter" + index++); createConnSucceededCounter = registry.counter("fake_counter" + index++); createConnFailedCounter = registry.counter("fake_counter" + index++); requestConnCounter = registry.counter("fake_counter" + index++); reuseConnCounter = registry.counter("fake_counter" + index++); connTakenFromPoolIsNotOpen = registry.counter("fake_counter" + index++); closeAboveHighWaterMarkCounter = registry.counter("fake_counter" + index++); maxConnsPerHostExceededCounter = registry.counter("fake_counter" + index++); connEstablishTimer = registry.timer("fake_timer"); connsInPool = new AtomicInteger(); connsInUse = new AtomicInteger(); OriginName originName = OriginName.fromVipAndApp("whatever", "whatever-secure"); InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setIPAddr("175.45.176.0") .setPort(7001) .setAppName("whatever") .build(); discoveryResult = DiscoveryResult.from(instanceInfo, true); clientConfig = new DefaultClientConfigImpl(); connectionPoolConfig = spy(new ConnectionPoolConfigImpl(originName, clientConfig)); NettyClientConnectionFactory nettyConnectionFactory = new NettyClientConnectionFactory(connectionPoolConfig, new ChannelInitializer() { @Override protected void initChannel(Channel ch) {} }); PooledConnectionFactory pooledConnectionFactory = this::newPooledConnection; pool = new PerServerConnectionPool( discoveryResult, LOCAL_ADDRESS, nettyConnectionFactory, pooledConnectionFactory, connectionPoolConfig, clientConfig, createNewConnCounter, createConnSucceededCounter, createConnFailedCounter, requestConnCounter, reuseConnCounter, connTakenFromPoolIsNotOpen, closeAboveHighWaterMarkCounter, maxConnsPerHostExceededCounter, connEstablishTimer, connsInPool, connsInUse); } @Test void acquireNewConnectionHitsMaxConnections() { CurrentPassport currentPassport = CurrentPassport.create(); clientConfig.set(Keys.MaxConnectionsPerHost, 1); discoveryResult.incrementOpenConnectionsCount(); Promise promise = pool.acquire(CLIENT_EVENT_LOOP, currentPassport, new AtomicReference<>()); assertThat(promise.isSuccess()).isFalse(); assertThat(promise.cause() instanceof OriginConnectException).isTrue(); assertThat(maxConnsPerHostExceededCounter.count()).isEqualTo(1); } @Test void acquireNewConnection() throws InterruptedException, ExecutionException { CurrentPassport currentPassport = CurrentPassport.create(); Promise promise = pool.acquire(CLIENT_EVENT_LOOP, currentPassport, new AtomicReference<>()); PooledConnection connection = promise.sync().get(); assertThat(requestConnCounter.count()).isEqualTo(1); assertThat(createNewConnCounter.count()).isEqualTo(1); assertThat(currentPassport.findState(PassportState.ORIGIN_CH_CONNECTING)) .isNotNull(); assertThat(currentPassport.findState(PassportState.ORIGIN_CH_CONNECTED)).isNotNull(); assertThat(createConnSucceededCounter.count()).isEqualTo(1); assertThat(connsInUse.get()).isEqualTo(1); // check state on PooledConnection - not all thread safe CLIENT_EVENT_LOOP .submit(() -> { checkChannelState(connection, currentPassport, 1); }) .sync(); } @Test void acquireConnectionFromPoolAndRelease() throws InterruptedException, ExecutionException { CurrentPassport currentPassport = CurrentPassport.create(); Promise promise = pool.acquire(CLIENT_EVENT_LOOP, currentPassport, new AtomicReference<>()); PooledConnection connection = promise.sync().get(); CLIENT_EVENT_LOOP .submit(() -> { pool.release(connection); }) .sync(); assertThat(connsInPool.get()).isEqualTo(1); CurrentPassport newPassport = CurrentPassport.create(); Promise secondPromise = pool.acquire(CLIENT_EVENT_LOOP, newPassport, new AtomicReference<>()); PooledConnection connection2 = secondPromise.sync().get(); assertThat(connection2).isEqualTo(connection); assertThat(requestConnCounter.count()).isEqualTo(2); assertThat(connsInPool.get()).isEqualTo(0); CLIENT_EVENT_LOOP .submit(() -> { checkChannelState(connection, newPassport, 2); }) .sync(); } @Test void releaseFromPoolButAlreadyClosed() throws InterruptedException, ExecutionException { CurrentPassport currentPassport = CurrentPassport.create(); Promise promise = pool.acquire(CLIENT_EVENT_LOOP, currentPassport, new AtomicReference<>()); PooledConnection connection = promise.sync().get(); CLIENT_EVENT_LOOP .submit(() -> { pool.release(connection); }) .sync(); // make the connection invalid connection.getChannel().deregister().sync(); CurrentPassport newPassport = CurrentPassport.create(); Promise secondPromise = pool.acquire(CLIENT_EVENT_LOOP, newPassport, new AtomicReference<>()); PooledConnection connection2 = secondPromise.sync().get(); assertThat(connection).isNotEqualTo(connection2); assertThat(connTakenFromPoolIsNotOpen.count()).isEqualTo(1); assertThat(connsInPool.get()).isEqualTo(0); assertThat(connection.getChannel().closeFuture().await(5, TimeUnit.SECONDS)) .as("Channel should have been closed by pool") .isTrue(); } @Test void releaseFromPoolAboveHighWaterMark() throws InterruptedException, ExecutionException { CurrentPassport currentPassport = CurrentPassport.create(); clientConfig.set(ConnectionPoolConfigImpl.PER_SERVER_WATERLINE, 0); Promise promise = pool.acquire(CLIENT_EVENT_LOOP, currentPassport, new AtomicReference<>()); PooledConnection connection = promise.sync().get(); CLIENT_EVENT_LOOP .submit(() -> { assertThat(pool.release(connection)).isFalse(); assertThat(closeAboveHighWaterMarkCounter.count()).isEqualTo(1); assertThat(connection.isInPool()).isFalse(); }) .sync(); assertThat(connection.getChannel().closeFuture().await(5, TimeUnit.SECONDS)) .as("connection should have been closed") .isTrue(); } @Test void releaseFromPoolWhileDraining() throws InterruptedException, ExecutionException { Promise promise = pool.acquire(CLIENT_EVENT_LOOP, CurrentPassport.create(), new AtomicReference<>()); PooledConnection connection = promise.sync().get(); pool.drain(); CLIENT_EVENT_LOOP .submit(() -> { assertThat(connection.isInPool()).isFalse(); assertThat(connection.getChannel().isActive()) .as("connection was incorrectly closed during the drain") .isTrue(); pool.release(connection); }) .sync(); assertThat(connection.getChannel().closeFuture().await(5, TimeUnit.SECONDS)) .as("connection should have been closed after release") .isTrue(); } @Test void acquireWhileDraining() { pool.drain(); assertThat(pool.isAvailable()).isFalse(); assertThatThrownBy(() -> pool.acquire(CLIENT_EVENT_LOOP, CurrentPassport.create(), new AtomicReference<>())) .isInstanceOf(IllegalStateException.class); } @Test void gracefulDrain() { EmbeddedChannel channel1 = new EmbeddedChannel(); EmbeddedChannel channel2 = new EmbeddedChannel(); PooledConnection connection1 = newPooledConnection(channel1); PooledConnection connection2 = newPooledConnection(channel2); Deque connections = pool.getPoolForEventLoop(channel1.eventLoop()); connections.add(connection1); connections.add(connection2); connsInPool.set(2); assertThat(connsInPool.get()).isEqualTo(2); pool.drainIdleConnectionsOnEventLoop(channel1.eventLoop()); channel1.runPendingTasks(); assertThat(connsInPool.get()).isEqualTo(0); assertThat(connection1.getChannel().closeFuture().isSuccess()).isTrue(); assertThat(connection2.getChannel().closeFuture().isSuccess()).isTrue(); } @Test void handleConnectCompletionWithException() { EmbeddedChannel channel = new EmbeddedChannel(); Promise promise = CLIENT_EVENT_LOOP.newPromise(); pool.handleConnectCompletion( channel.newFailedFuture(new RuntimeException("runtime failure")), promise, CurrentPassport.create()); assertThat(promise.isSuccess()).isFalse(); assertThat(promise.cause()).isNotNull(); assertThat(promise.cause()).isInstanceOf(OriginConnectException.class); assertThat(promise.cause().getCause()).as("expect cause remains").isInstanceOf(RuntimeException.class); } @Test void handleConnectCompletionWithDecoderExceptionIsUnwrapped() { EmbeddedChannel channel = new EmbeddedChannel(); Promise promise = CLIENT_EVENT_LOOP.newPromise(); pool.handleConnectCompletion( channel.newFailedFuture(new DecoderException(new SSLHandshakeException("Invalid tls cert"))), promise, CurrentPassport.create()); assertThat(promise.isSuccess()).isFalse(); assertThat(promise.cause()).isNotNull(); assertThat(promise.cause()).isInstanceOf(OriginConnectException.class); assertThat(promise.cause().getCause()) .as("expect decoder exception is unwrapped") .isInstanceOf(SSLHandshakeException.class); } private void checkChannelState(PooledConnection connection, CurrentPassport passport, int expectedUsage) { Channel channel = connection.getChannel(); assertThat(connection.getUsageCount()).isEqualTo(expectedUsage); assertThat(CurrentPassport.fromChannelOrNull(channel)).isEqualTo(passport); assertThat(connection.isReleased()).isFalse(); assertThat(connection.getConnectionState()).isEqualTo(ConnectionState.WRITE_BUSY); assertThat(channel.pipeline().get(DefaultClientChannelManager.IDLE_STATE_HANDLER_NAME)) .isNull(); } private PooledConnection newPooledConnection(Channel ch) { return new PooledConnection( ch, discoveryResult, channelManager, registry.counter("fake_close_counter"), registry.counter("fake_close_wrt_counter")); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/connectionpool/PooledConnectionTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.connectionpool; import static com.netflix.zuul.netty.connectionpool.PooledConnection.READ_TIMEOUT_HANDLER_NAME; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.NoopRegistry; import com.netflix.zuul.discovery.DiscoveryResult; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import java.time.Duration; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * @author Justin Guerra * @since 10/3/25 */ @ExtendWith(MockitoExtension.class) class PooledConnectionTest { @Mock private ClientChannelManager manager; private EmbeddedChannel channel; private PooledConnection connection; @BeforeEach public void setup() { channel = new EmbeddedChannel(); NoopRegistry noopRegistry = new NoopRegistry(); connection = new PooledConnection( channel, DiscoveryResult.EMPTY, manager, noopRegistry.counter("close"), noopRegistry.counter("closeBusy")); } @Test void startReadTimeoutHandler() { channel.pipeline() .addLast(DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER, new ChannelInboundHandlerAdapter()); connection.startReadTimeoutHandler(Duration.ofSeconds(1)); List names = channel.pipeline().names(); assertThat(names.get(0)).isEqualTo(READ_TIMEOUT_HANDLER_NAME); assertThat(names.get(1)).isEqualTo(DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER); } @Test void startReadTimeoutHandlerInactive() { channel.close(); connection.startReadTimeoutHandler(Duration.ofSeconds(1)); assertThat(channel.pipeline().get(READ_TIMEOUT_HANDLER_NAME)).isNull(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/filter/BaseZuulFilterRunnerTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.Filter; import com.netflix.zuul.FilterConstraint; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.BaseFilter; import com.netflix.zuul.filters.FilterSyncType; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.util.HttpRequestBuilder; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.local.LocalChannel; import io.netty.channel.local.LocalIoHandler; import io.netty.handler.codec.http.HttpContent; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import lombok.NonNull; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import rx.Observable; /** * @author Justin Guerra * @since 5/20/25 */ @ExtendWith(MockitoExtension.class) public class BaseZuulFilterRunnerTest { @Mock private FilterUsageNotifier notifier; @Mock private FilterRunner nextStage; private ZuulMessage message; private TestBaseZuulFilterRunner runner; private MultithreadEventLoopGroup group; private TestResumer resumer; private ErrorCapturingHandler errorCapturingHandler; private TestConstraint constraint; @BeforeEach public void setup() { constraint = new TestConstraint(); runner = new TestBaseZuulFilterRunner(FilterType.INBOUND, notifier, nextStage, new NoopRegistry()); SessionContext sessionContext = new SessionContext(); message = new HttpRequestBuilder(sessionContext).build(); group = new MultiThreadIoEventLoopGroup(1, LocalIoHandler.newFactory()); LocalChannel localChannel = new LocalChannel(); errorCapturingHandler = new ErrorCapturingHandler(); localChannel.pipeline().addLast(new ChannelInboundHandlerAdapter()); localChannel.pipeline().addLast(errorCapturingHandler); group.register(localChannel); ChannelHandlerContext context = localChannel.pipeline().context(ChannelInboundHandlerAdapter.class); sessionContext.set(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT, context); resumer = new TestResumer(Function.identity()); } @AfterEach public void teardown() { group.shutdownGracefully(); } @Test public void onCompleteCalledBeforeResume() throws Exception { AsyncFilter asyncFilter = new AsyncFilter(); asyncFilter.output.set(message.clone()); resumer.validator = m -> { assertThat(asyncFilter.getConcurrency()) .as("concurrency should have been decremented before the filter chain was resumed") .isEqualTo(0); return m; }; runner.executeFilter(asyncFilter, message); ZuulMessage filteredMessage = resumer.future.get(5, TimeUnit.SECONDS); // sanity check to verify the message was transformed by AsyncFilter assertThat(filteredMessage).isNotSameAs(message); } @Test public void onCompleteCalledWithNullMessage() throws Exception { AsyncFilter asyncFilter = new AsyncFilter(); asyncFilter.output.set(null); resumer.validator = m -> { assertThat(asyncFilter.getConcurrency()) .as("concurrency should have been decremented before the filter chain was resumed") .isEqualTo(0); assertThat(m).isNotNull(); return m; }; runner.executeFilter(asyncFilter, message); ZuulMessage filteredMessage = resumer.future.get(5, TimeUnit.SECONDS); // sanity check to verify the message was transformed by AsyncFilter assertThat(filteredMessage).isSameAs(message); } @Test public void onCompleteThrows() { doThrow(new RuntimeException()).when(notifier).notify(any(), any()); AsyncFilter asyncFilter = new AsyncFilter(); asyncFilter.output.set(message); runner.executeFilter(asyncFilter, message); Awaitility.await("request should have failed with an exception") .atMost(5, TimeUnit.SECONDS) .until(() -> errorCapturingHandler.error.get() != null); } @Test public void endpointFilterNeverSkipped() { EndpointFilter filter = new EndpointFilter(false); message.getContext().stopFilterProcessing(); message.getContext().cancel(); assertThat(runner.shouldSkipFilter(message, filter)).isFalse(); } @Test public void stopFilterProcessingSkipsFilter() { InboundFilter filter = new InboundFilter(true); message.getContext().stopFilterProcessing(); assertThat(runner.shouldSkipFilter(message, filter)).isTrue(); } @Test public void stopFilterProcessingDoesNotSkipWhenOverridden() { OverrideStopFilter filter = new OverrideStopFilter(true); message.getContext().stopFilterProcessing(); assertThat(runner.shouldSkipFilter(message, filter)).isFalse(); } @Test public void cancelledRequestSkipsFilter() { InboundFilter filter = new InboundFilter(true); message.getContext().cancel(); assertThat(runner.shouldSkipFilter(message, filter)).isTrue(); } @Test public void constrainedFilterSkipped() { InboundFilter filter = new InboundFilter(true); constraint.constrained = true; assertThat(runner.shouldSkipFilter(message, filter)).isTrue(); } @Test public void shouldFilterFalseSkipsFilter() { InboundFilter filter = new InboundFilter(false); assertThat(runner.shouldSkipFilter(message, filter)).isTrue(); } @Test public void shouldFilterTrueDoesNotSkip() { InboundFilter filter = new InboundFilter(true); assertThat(runner.shouldSkipFilter(message, filter)).isFalse(); } @Filter(type = FilterType.INBOUND, sync = FilterSyncType.ASYNC, order = 1) private static class AsyncFilter extends BaseFilter { private AtomicReference output = new AtomicReference<>(); @Override public Observable applyAsync(ZuulMessage input) { return Observable.just(output.get()); } @Override public boolean shouldFilter(ZuulMessage msg) { return true; } } private static class TestResumer { private final CompletableFuture future; private volatile Function validator; public TestResumer(Function validator) { this.future = new CompletableFuture<>(); this.validator = validator; } public void resume(ZuulMessage zuulMesg) { try { ZuulMessage zuulMessage = validator.apply(zuulMesg); future.complete(zuulMessage); } catch (Throwable e) { future.completeExceptionally(e); } } } private class TestBaseZuulFilterRunner extends BaseZuulFilterRunner { protected TestBaseZuulFilterRunner( FilterType filterType, FilterUsageNotifier usageNotifier, FilterRunner nextStage, Registry registry) { super(filterType, usageNotifier, nextStage, new FilterConstraints(List.of(constraint)), registry); } @Override protected void resume(ZuulMessage zuulMesg) { resumer.resume(zuulMesg); } @Override public void filter(ZuulMessage zuulMesg) {} @Override public void filter(ZuulMessage zuulMesg, HttpContent chunk) {} } private static class ErrorCapturingHandler extends ChannelInboundHandlerAdapter { private final AtomicReference error = new AtomicReference<>(); @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { error.set(cause); } } @Filter( type = FilterType.INBOUND, order = 1, constraints = {TestConstraint.class}) private static class InboundFilter extends BaseFilter { private final boolean shouldFilter; InboundFilter(boolean shouldFilter) { this.shouldFilter = shouldFilter; } @Override public Observable applyAsync(ZuulMessage input) { return Observable.just(input); } @Override public boolean shouldFilter(ZuulMessage msg) { return shouldFilter; } } @Filter(type = FilterType.ENDPOINT, order = 1) private static class EndpointFilter extends BaseFilter { private final boolean shouldFilter; EndpointFilter(boolean shouldFilter) { this.shouldFilter = shouldFilter; } @Override public Observable applyAsync(ZuulMessage input) { return Observable.just(input); } @Override public boolean shouldFilter(ZuulMessage msg) { return shouldFilter; } } @Filter(type = FilterType.INBOUND, order = 1) private static class OverrideStopFilter extends BaseFilter { private final boolean shouldFilter; OverrideStopFilter(boolean shouldFilter) { this.shouldFilter = shouldFilter; } @Override public boolean overrideStopFilterProcessing() { return true; } @Override public Observable applyAsync(ZuulMessage input) { return Observable.just(input); } @Override public boolean shouldFilter(ZuulMessage msg) { return shouldFilter; } } private static class TestConstraint implements FilterConstraint { boolean constrained; @Override public boolean isConstrained(@NonNull ZuulMessage msg) { return constrained; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/filter/EventExecutorSchedulerTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.fail; import com.google.common.util.concurrent.Uninterruptibles; import io.netty.channel.EventLoop; import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.nio.NioIoHandler; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import rx.Scheduler.Worker; import rx.Subscription; import rx.functions.Action0; /** * @author Justin Guerra * @since 5/20/25 */ class EventExecutorSchedulerTest { private MultithreadEventLoopGroup group; private EventLoop eventLoop; private EventExecutorScheduler scheduler; @BeforeEach void setUp() { group = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); eventLoop = group.next(); scheduler = new EventExecutorScheduler(eventLoop); } @AfterEach void tearDown() { group.shutdownGracefully(); } @Test void nullExecutor() { assertThatThrownBy(() -> new EventExecutorScheduler(null)).isInstanceOf(NullPointerException.class); } @Test void alreadyOnEventLoopImmediatelyRuns() { Worker worker = scheduler.createWorker(); CountDownLatch latch = new CountDownLatch(1); eventLoop.execute(() -> { AtomicBoolean executed = new AtomicBoolean(false); Action0 action = () -> { executed.set(true); }; Subscription schedule = worker.schedule(action); assertThat(executed.get()).isTrue(); assertThat(schedule.isUnsubscribed()).isTrue(); latch.countDown(); }); if (!Uninterruptibles.awaitUninterruptibly(latch, 5, TimeUnit.SECONDS)) { fail("schedule did not complete in a reasonable amount of time"); } } @Test void nonEventLoopThreadExecutedOnEventLoop() throws Exception { AtomicBoolean inEventLoop = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); Action0 action = () -> { inEventLoop.set(eventLoop.inEventLoop()); if (!Uninterruptibles.awaitUninterruptibly(latch, 5, TimeUnit.SECONDS)) { fail("action not completed in a reasonable amount of time"); } }; Worker worker = scheduler.createWorker(); Subscription schedule = worker.schedule(action); assertThat(schedule.isUnsubscribed()).isFalse(); latch.countDown(); // ensure the original action finished eventLoop.submit(() -> {}).get(5, TimeUnit.SECONDS); assertThat(inEventLoop.get()).isTrue(); assertThat(schedule.isUnsubscribed()).isTrue(); } @Test void workerUnsubscribe() { Worker worker = scheduler.createWorker(); assertThat(worker.isUnsubscribed()).isFalse(); worker.unsubscribe(); assertThat(worker.isUnsubscribed()).isTrue(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/filter/FilterConstraintsTest.java ================================================ /* * Copyright 2026 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.netflix.zuul.FilterConstraint; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.util.HttpRequestBuilder; import java.util.List; import lombok.NonNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * @author Justin Guerra * @since 2/13/26 */ @SuppressWarnings("unchecked") class FilterConstraintsTest { private HttpRequestMessage request; private FilterConstraints filterConstraints; private boolean constraintAResult; private boolean constraintBResult; @BeforeEach void setUp() { request = new HttpRequestBuilder(new SessionContext()).build(); constraintAResult = false; constraintBResult = false; filterConstraints = new FilterConstraints(List.of(new ConstraintA(), new ConstraintB())); } @Test void nullConstraintsFromFilter() { ZuulFilter filter = mockFilter(null); assertThat(filterConstraints.isConstrained(request, filter)).isFalse(); } @Test void emptyConstraintsFromFilter() { ZuulFilter filter = mockFilter(new Class[0]); assertThat(filterConstraints.isConstrained(request, filter)).isFalse(); } @Test void singleConstraintMatches() { constraintAResult = true; ZuulFilter filter = mockFilter(new Class[] {ConstraintA.class}); assertThat(filterConstraints.isConstrained(request, filter)).isTrue(); } @Test void singleConstraintDoesNotMatch() { ZuulFilter filter = mockFilter(new Class[] {ConstraintA.class}); assertThat(filterConstraints.isConstrained(request, filter)).isFalse(); } @Test void multipleConstraintsFirstMatches() { constraintAResult = true; ZuulFilter filter = mockFilter(new Class[] {ConstraintA.class, ConstraintB.class}); assertThat(filterConstraints.isConstrained(request, filter)).isTrue(); } @Test void multipleConstraintsSecondMatches() { constraintBResult = true; ZuulFilter filter = mockFilter(new Class[] {ConstraintA.class, ConstraintB.class}); assertThat(filterConstraints.isConstrained(request, filter)).isTrue(); } @Test void multipleConstraintsNoneMatch() { ZuulFilter filter = mockFilter(new Class[] {ConstraintA.class, ConstraintB.class}); assertThat(filterConstraints.isConstrained(request, filter)).isFalse(); } @Test void constraintNotInLookup() { FilterConstraints limited = new FilterConstraints(List.of(new ConstraintA())); constraintBResult = true; ZuulFilter filter = mockFilter(new Class[] {ConstraintB.class}); assertThat(limited.isConstrained(request, filter)).isFalse(); } @Test public void constraintsCached() { FilterConstraints limited = new FilterConstraints(List.of(new ConstraintA(), new ConstraintB())); constraintAResult = false; constraintBResult = true; ZuulFilter filter = mockFilter(new Class[] {ConstraintA.class}); assertThat(limited.isConstrained(request, filter)).isFalse(); // this can't happen with a real annotation, but test the caching logic by changing the constraints. Because // the initial constraints are cached the new one should be ignored when(filter.constraints()).thenReturn(new Class[] {ConstraintA.class, ConstraintB.class}); assertThat(limited.isConstrained(request, filter)).isFalse(); } private ZuulFilter mockFilter(Class[] constraints) { ZuulFilter filter = mock(ZuulFilter.class); when(filter.constraints()).thenReturn(constraints); return filter; } private class ConstraintA implements FilterConstraint { @Override public boolean isConstrained(@NonNull ZuulMessage msg) { return constraintAResult; } } private class ConstraintB implements FilterConstraint { @Override public boolean isConstrained(@NonNull ZuulMessage msg) { return constraintBResult; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/filter/ZuulEndPointRunnerTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.Filter; import com.netflix.zuul.FilterCategory; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.Endpoint; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import io.netty.channel.ChannelHandlerContext; import io.netty.util.concurrent.ImmediateEventExecutor; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import rx.Observable; class ZuulEndPointRunnerTest { private static final String BASIC_ENDPOINT = "basicEndpoint"; private ZuulEndPointRunner endpointRunner; private FilterUsageNotifier usageNotifier; private FilterLoader filterLoader; private FilterRunner filterRunner; private Registry registry; private HttpRequestMessageImpl request; @BeforeEach void beforeEachTest() { usageNotifier = mock(FilterUsageNotifier.class); filterLoader = mock(FilterLoader.class); when(filterLoader.getFilterByNameAndType(ZuulEndPointRunner.DEFAULT_ERROR_ENDPOINT.get(), FilterType.ENDPOINT)) .thenReturn(new ErrorEndpoint()); when(filterLoader.getFilterByNameAndType(BASIC_ENDPOINT, FilterType.ENDPOINT)) .thenReturn(new BasicEndpoint()); filterRunner = mock(FilterRunner.class); registry = new NoopRegistry(); endpointRunner = new ZuulEndPointRunner( usageNotifier, filterLoader, filterRunner, new FilterConstraints(List.of()), registry); SessionContext context = new SessionContext(); Headers headers = new Headers(); ChannelHandlerContext chc = mock(ChannelHandlerContext.class); when(chc.executor()).thenReturn(ImmediateEventExecutor.INSTANCE); context.put(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT, chc); request = new HttpRequestMessageImpl( context, "http", "GET", "/foo/bar", new HttpQueryParams(), headers, "127.0.0.1", "http", 8080, "server123"); request.storeInboundRequest(); } @Test void nonErrorEndpoint() { request.getContext().setShouldSendErrorResponse(false); request.getContext().setEndpoint(BASIC_ENDPOINT); assertThat(request.getContext().get(CommonContextKeys.ZUUL_ENDPOINT)).isNull(); endpointRunner.filter(request); ZuulFilter filter = request.getContext().get(CommonContextKeys.ZUUL_ENDPOINT); assertThat(filter instanceof BasicEndpoint).isTrue(); ArgumentCaptor captor = ArgumentCaptor.forClass(HttpResponseMessage.class); verify(filterRunner, times(1)).filter(captor.capture()); HttpResponseMessage capturedResponseMessage = captor.getValue(); assertThat(request.getInboundRequest()).isEqualTo(capturedResponseMessage.getInboundRequest()); assertThat(capturedResponseMessage.getContext().getEndpoint()).isEqualTo("basicEndpoint"); assertThat(capturedResponseMessage.getContext().errorResponseSent()).isFalse(); } @Test void errorEndpoint() { request.getContext().setShouldSendErrorResponse(true); assertThat(request.getContext().get(CommonContextKeys.ZUUL_ENDPOINT)).isNull(); endpointRunner.filter(request); ZuulFilter filter = request.getContext().get(CommonContextKeys.ZUUL_ENDPOINT); assertThat(filter instanceof ErrorEndpoint).isTrue(); ArgumentCaptor captor = ArgumentCaptor.forClass(HttpResponseMessage.class); verify(filterRunner, times(1)).filter(captor.capture()); HttpResponseMessage capturedResponseMessage = captor.getValue(); assertThat(request.getInboundRequest()).isEqualTo(capturedResponseMessage.getInboundRequest()); assertThat(capturedResponseMessage.getContext().getEndpoint()).isNull(); assertThat(capturedResponseMessage.getContext().errorResponseSent()).isTrue(); } @Filter(order = 10, type = FilterType.ENDPOINT) static class ErrorEndpoint extends Endpoint { @Override public FilterCategory category() { return super.category(); } @Override public Observable applyAsync(ZuulMessage input) { return Observable.just(buildHttpResponseMessage(input)); } } @Filter(order = 20, type = FilterType.ENDPOINT) static class BasicEndpoint extends Endpoint { @Override public FilterCategory category() { return super.category(); } @Override public Observable applyAsync(ZuulMessage input) { return Observable.just(buildHttpResponseMessage(input)); } } private static HttpResponseMessage buildHttpResponseMessage(ZuulMessage request) { return new HttpResponseMessageImpl(request.getContext(), (HttpRequestMessage) request, 200); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/filter/ZuulFilterChainRunnerTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.filter; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import com.netflix.spectator.api.Registry; import com.netflix.zuul.ExecutionStatus; import com.netflix.zuul.Filter; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.filters.http.HttpInboundFilter; import com.netflix.zuul.filters.http.HttpOutboundFilter; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import rx.Observable; class ZuulFilterChainRunnerTest { private HttpRequestMessage request; private HttpResponseMessage response; @BeforeEach void before() { SessionContext context = new SessionContext(); Headers headers = new Headers(); EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter()); ChannelHandlerContext ctx = channel.pipeline().context(ChannelInboundHandlerAdapter.class); context.put(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT, ctx); request = new HttpRequestMessageImpl( context, "http", "GET", "/foo/bar", new HttpQueryParams(), headers, "127.0.0.1", "http", 8080, "server123"); request.storeInboundRequest(); response = new HttpResponseMessageImpl(context, request, 200); } @Test void testInboundFilterChain() { SimpleInboundFilter inbound1 = spy(new SimpleInboundFilter(true)); SimpleInboundFilter inbound2 = spy(new SimpleInboundFilter(false)); ZuulFilter[] filters = new ZuulFilter[] {inbound1, inbound2}; FilterUsageNotifier notifier = mock(FilterUsageNotifier.class); Registry registry = mock(Registry.class); ZuulFilterChainRunner runner = new ZuulFilterChainRunner(filters, notifier, new FilterConstraints(List.of()), registry); runner.filter(request); verify(inbound1, times(1)).applyAsync(eq(request)); verify(inbound2, never()).applyAsync(eq(request)); verify(notifier).notify(eq(inbound1), eq(ExecutionStatus.SUCCESS)); verify(notifier).notify(eq(inbound2), eq(ExecutionStatus.SKIPPED)); verifyNoMoreInteractions(notifier); } @Test void testOutboundFilterChain() { SimpleOutboundFilter outbound1 = spy(new SimpleOutboundFilter(true)); SimpleOutboundFilter outbound2 = spy(new SimpleOutboundFilter(false)); ZuulFilter[] filters = new ZuulFilter[] {outbound1, outbound2}; FilterUsageNotifier notifier = mock(FilterUsageNotifier.class); Registry registry = mock(Registry.class); ZuulFilterChainRunner runner = new ZuulFilterChainRunner(filters, notifier, new FilterConstraints(List.of()), registry); runner.filter(response); verify(outbound1, times(1)).applyAsync(any()); verify(outbound2, never()).applyAsync(any()); verify(notifier).notify(eq(outbound1), eq(ExecutionStatus.SUCCESS)); verify(notifier).notify(eq(outbound2), eq(ExecutionStatus.SKIPPED)); verifyNoMoreInteractions(notifier); } @Filter(order = 1) class SimpleInboundFilter extends HttpInboundFilter { private final boolean shouldFilter; public SimpleInboundFilter(boolean shouldFilter) { this.shouldFilter = shouldFilter; } @Override public int filterOrder() { return 0; } @Override public FilterType filterType() { return FilterType.INBOUND; } @Override public Observable applyAsync(HttpRequestMessage input) { return Observable.just(input); } @Override public boolean shouldFilter(HttpRequestMessage msg) { return this.shouldFilter; } } @Filter(order = 1) class SimpleOutboundFilter extends HttpOutboundFilter { private final boolean shouldFilter; public SimpleOutboundFilter(boolean shouldFilter) { this.shouldFilter = shouldFilter; } @Override public int filterOrder() { return 0; } @Override public FilterType filterType() { return FilterType.OUTBOUND; } @Override public Observable applyAsync(HttpResponseMessage input) { return Observable.just(input); } @Override public boolean shouldFilter(HttpResponseMessage msg) { return this.shouldFilter; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/insights/ServerStateHandlerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.insights; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.zuul.netty.insights.ServerStateHandler.InboundHandler; import com.netflix.zuul.netty.server.http2.DummyChannelHandler; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class ServerStateHandlerTest { private Registry registry; private Id connectsId; private Id errorsId; private Id closesId; final String listener = "test-conn-throttled"; @BeforeEach void init() { registry = new DefaultRegistry(); connectsId = registry.createId("server.connections.connect").withTags("id", listener); closesId = registry.createId("server.connections.close").withTags("id", listener); errorsId = registry.createId("server.connections.errors").withTags("id", listener); } @Test void verifyConnMetrics() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addLast(new DummyChannelHandler()); channel.pipeline().addLast(new InboundHandler(registry, listener)); Counter connects = (Counter) registry.get(connectsId); Counter closes = (Counter) registry.get(closesId); Counter errors = (Counter) registry.get(errorsId); // Connects X 3 channel.pipeline().context(DummyChannelHandler.class).fireChannelActive(); channel.pipeline().context(DummyChannelHandler.class).fireChannelActive(); channel.pipeline().context(DummyChannelHandler.class).fireChannelActive(); assertThat(connects.count()).isEqualTo(3); // Closes X 1 channel.pipeline().context(DummyChannelHandler.class).fireChannelInactive(); assertThat(connects.count()).isEqualTo(3); assertThat(closes.count()).isEqualTo(1); assertThat(errors.count()).isEqualTo(0); } @Test void setPassportStateOnConnect() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addLast(new DummyChannelHandler()); channel.pipeline().addLast(new InboundHandler(registry, listener)); channel.pipeline().context(DummyChannelHandler.class).fireChannelActive(); assertThat(CurrentPassport.fromChannel(channel).getState()).isEqualTo(PassportState.SERVER_CH_ACTIVE); } @Test void setPassportStateOnDisconnect() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addLast(new DummyChannelHandler()); channel.pipeline().addLast(new InboundHandler(registry, listener)); channel.pipeline().context(DummyChannelHandler.class).fireChannelInactive(); assertThat(CurrentPassport.fromChannel(channel).getState()).isEqualTo(PassportState.SERVER_CH_INACTIVE); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/BaseZuulChannelInitializerTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.PerEventLoopMetricsChannelHandler; import com.netflix.netty.common.proxyprotocol.ElbProxyProtocolChannelHandler; import com.netflix.netty.common.throttle.MaxInboundConnectionsHandler; import com.netflix.spectator.api.NoopRegistry; import com.netflix.zuul.netty.insights.ServerStateHandler; import com.netflix.zuul.netty.ratelimiting.NullChannelHandlerProvider; import io.netty.channel.Channel; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.util.concurrent.GlobalEventExecutor; import org.junit.jupiter.api.Test; /** * Unit tests for {@link BaseZuulChannelInitializer}. */ class BaseZuulChannelInitializerTest { @Test void tcpHandlersAdded() { ChannelConfig channelConfig = new ChannelConfig(); ChannelConfig channelDependencies = new ChannelConfig(); channelDependencies.set(ZuulDependencyKeys.registry, new NoopRegistry()); channelDependencies.set( ZuulDependencyKeys.rateLimitingChannelHandlerProvider, new NullChannelHandlerProvider()); channelDependencies.set( ZuulDependencyKeys.sslClientCertCheckChannelHandlerProvider, new NullChannelHandlerProvider()); ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); BaseZuulChannelInitializer init = new BaseZuulChannelInitializer("1234", channelConfig, channelDependencies, channelGroup) { @Override protected void initChannel(Channel ch) {} }; EmbeddedChannel channel = new EmbeddedChannel(); init.addTcpRelatedHandlers(channel.pipeline()); assertThat(channel.pipeline().context(SourceAddressChannelHandler.class)) .isNotNull(); assertThat(channel.pipeline().context(PerEventLoopMetricsChannelHandler.Connections.class)) .isNotNull(); assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNotNull(); assertThat(channel.pipeline().context(MaxInboundConnectionsHandler.class)) .isNotNull(); } @Test void tcpHandlersAdded_withProxyProtocol() { ChannelConfig channelConfig = new ChannelConfig(); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); ChannelConfig channelDependencies = new ChannelConfig(); channelDependencies.set(ZuulDependencyKeys.registry, new NoopRegistry()); channelDependencies.set( ZuulDependencyKeys.rateLimitingChannelHandlerProvider, new NullChannelHandlerProvider()); channelDependencies.set( ZuulDependencyKeys.sslClientCertCheckChannelHandlerProvider, new NullChannelHandlerProvider()); ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); BaseZuulChannelInitializer init = new BaseZuulChannelInitializer("1234", channelConfig, channelDependencies, channelGroup) { @Override protected void initChannel(Channel ch) {} }; EmbeddedChannel channel = new EmbeddedChannel(); init.addTcpRelatedHandlers(channel.pipeline()); assertThat(channel.pipeline().context(SourceAddressChannelHandler.class)) .isNotNull(); assertThat(channel.pipeline().context(PerEventLoopMetricsChannelHandler.Connections.class)) .isNotNull(); assertThat(channel.pipeline().context(ElbProxyProtocolChannelHandler.NAME)) .isNotNull(); assertThat(channel.pipeline().context(MaxInboundConnectionsHandler.class)) .isNotNull(); } @Test void serverStateHandlerAdded() { ChannelConfig channelConfig = new ChannelConfig(); ChannelConfig channelDependencies = new ChannelConfig(); channelDependencies.set(ZuulDependencyKeys.registry, new NoopRegistry()); channelDependencies.set( ZuulDependencyKeys.rateLimitingChannelHandlerProvider, new NullChannelHandlerProvider()); channelDependencies.set( ZuulDependencyKeys.sslClientCertCheckChannelHandlerProvider, new NullChannelHandlerProvider()); ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); BaseZuulChannelInitializer init = new BaseZuulChannelInitializer("1234", channelConfig, channelDependencies, channelGroup) { @Override protected void initChannel(Channel ch) {} }; EmbeddedChannel channel = new EmbeddedChannel(); init.addPassportHandler(channel.pipeline()); assertThat(channel.pipeline().context(ServerStateHandler.InboundHandler.class)) .isNotNull(); assertThat(channel.pipeline().context(ServerStateHandler.OutboundHandler.class)) .isNotNull(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/ClientConnectionsShutdownTest.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import com.netflix.appinfo.InstanceInfo.InstanceStatus; import com.netflix.config.ConfigurationManager; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaEventListener; import com.netflix.discovery.StatusChangeEvent; import com.netflix.zuul.netty.server.ClientConnectionsShutdown.ShutdownType; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultEventLoop; import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.MultithreadEventLoopGroup; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; import io.netty.channel.local.LocalIoHandler; import io.netty.channel.local.LocalServerChannel; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.commons.configuration.AbstractConfiguration; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; /** * @author Justin Guerra * @since 2/28/23 */ class ClientConnectionsShutdownTest { // using LocalChannels instead of EmbeddedChannels to re-create threading behavior in an actual deployment private static LocalAddress LOCAL_ADDRESS; private static MultithreadEventLoopGroup SERVER_EVENT_LOOP; private static MultithreadEventLoopGroup CLIENT_EVENT_LOOP; private static DefaultEventLoop EVENT_LOOP; @BeforeAll static void staticSetup() throws InterruptedException { LOCAL_ADDRESS = new LocalAddress(UUID.randomUUID().toString()); CLIENT_EVENT_LOOP = new MultiThreadIoEventLoopGroup(4, LocalIoHandler.newFactory()); SERVER_EVENT_LOOP = new MultiThreadIoEventLoopGroup(4, LocalIoHandler.newFactory()); ServerBootstrap serverBootstrap = new ServerBootstrap() .group(SERVER_EVENT_LOOP) .localAddress(LOCAL_ADDRESS) .channel(LocalServerChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(LocalChannel ch) {} }); serverBootstrap.bind().sync(); EVENT_LOOP = new DefaultEventLoop(Executors.newSingleThreadExecutor()); } @AfterAll static void staticCleanup() { CLIENT_EVENT_LOOP.shutdownGracefully(); SERVER_EVENT_LOOP.shutdownGracefully(); EVENT_LOOP.shutdownGracefully(); } private ChannelGroup channels; private ClientConnectionsShutdown shutdown; @BeforeEach void setup() { channels = new DefaultChannelGroup(EVENT_LOOP); shutdown = new ClientConnectionsShutdown(channels, EVENT_LOOP, null); } @Test @SuppressWarnings("unchecked") void discoveryShutdown() { String configName = "server.outofservice.connections.shutdown"; AbstractConfiguration configuration = ConfigurationManager.getConfigInstance(); try { configuration.setProperty(configName, "true"); EurekaClient eureka = Mockito.mock(EurekaClient.class); EventExecutor executor = Mockito.mock(EventExecutor.class); ArgumentCaptor captor = ArgumentCaptor.forClass(EurekaEventListener.class); shutdown = spy(new ClientConnectionsShutdown(channels, executor, eureka)); verify(eureka).registerEventListener(captor.capture()); doReturn(Mockito.mock(Promise.class)).when(shutdown).gracefullyShutdownClientChannels(); EurekaEventListener listener = captor.getValue(); listener.onEvent(new StatusChangeEvent(InstanceStatus.UP, InstanceStatus.DOWN)); verify(executor).schedule(ArgumentMatchers.isA(Callable.class), anyLong(), eq(TimeUnit.MILLISECONDS)); Mockito.reset(executor); listener.onEvent(new StatusChangeEvent(InstanceStatus.UP, InstanceStatus.OUT_OF_SERVICE)); verify(executor).schedule(ArgumentMatchers.isA(Callable.class), anyLong(), eq(TimeUnit.MILLISECONDS)); Mockito.reset(executor); listener.onEvent(new StatusChangeEvent(InstanceStatus.STARTING, InstanceStatus.OUT_OF_SERVICE)); verify(executor, never()) .schedule(ArgumentMatchers.isA(Callable.class), anyLong(), eq(TimeUnit.MILLISECONDS)); } finally { configuration.setProperty(configName, "false"); } } @Test void allConnectionsGracefullyClosed() throws Exception { createChannels(100); Promise promise = shutdown.gracefullyShutdownClientChannels(); Promise testPromise = EVENT_LOOP.newPromise(); promise.addListener(future -> { if (future.isSuccess()) { testPromise.setSuccess(null); } else { testPromise.setFailure(future.cause()); } }); channels.forEach(Channel::close); testPromise.await(10, TimeUnit.SECONDS); assertThat(channels.isEmpty()).isTrue(); } @Test void connectionNeedsToBeForceClosed() throws Exception { String configName = "server.outofservice.close.timeout"; AbstractConfiguration configuration = ConfigurationManager.getConfigInstance(); try { configuration.setProperty(configName, "0"); createChannels(10); shutdown.gracefullyShutdownClientChannels().await(10, TimeUnit.SECONDS); assertThat(channels.isEmpty()) .as("All channels in group should have been force closed after the timeout was triggered") .isTrue(); } finally { configuration.setProperty(configName, "30"); } } @Test void connectionNeedsToBeForceClosedAndOneChannelThrowsAnException() throws Exception { String configName = "server.outofservice.close.timeout"; AbstractConfiguration configuration = ConfigurationManager.getConfigInstance(); try { configuration.setProperty(configName, "0"); createChannels(5); ChannelFuture connect = new Bootstrap() .group(CLIENT_EVENT_LOOP) .channel(LocalChannel.class) .handler(new ChannelInitializer<>() { @Override protected void initChannel(Channel ch) { ch.pipeline().addLast(new ChannelOutboundHandlerAdapter() { @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { throw new Exception(); } }); } }) .remoteAddress(LOCAL_ADDRESS) .connect() .sync(); channels.add(connect.channel()); boolean await = shutdown.gracefullyShutdownClientChannels().await(10, TimeUnit.SECONDS); assertThat(await) .as("the promise should finish even if a channel failed to close") .isTrue(); assertThat(channels.size()) .as("all other channels should have been closed") .isEqualTo(1); } finally { configuration.setProperty(configName, "30"); } } @Test void connectionsNotForceClosed() throws Exception { String configName = "server.outofservice.close.timeout"; AbstractConfiguration configuration = ConfigurationManager.getConfigInstance(); DefaultEventLoop eventLoop = spy(EVENT_LOOP); shutdown = new ClientConnectionsShutdown(channels, eventLoop, null); try { configuration.setProperty(configName, "0"); createChannels(10); Promise promise = shutdown.gracefullyShutdownClientChannels(ShutdownType.OUT_OF_SERVICE); verify(eventLoop, never()).schedule(isA(Runnable.class), anyLong(), isA(TimeUnit.class)); channels.forEach(Channel::close); promise.await(10, TimeUnit.SECONDS); assertThat(channels.isEmpty()) .as("All channels in group should have been closed") .isTrue(); } finally { configuration.setProperty(configName, "30"); } } @Test public void shutdownTypeForwardedToFlag() throws InterruptedException { shutdown = spy(shutdown); doNothing().when(shutdown).flagChannelForClose(any(), any()); createChannels(1); Channel channel = channels.iterator().next(); for (ShutdownType type : ShutdownType.values()) { shutdown.gracefullyShutdownClientChannels(type); verify(shutdown).flagChannelForClose(channel, type); } channels.close().await(5, TimeUnit.SECONDS); } private void createChannels(int numChannels) throws InterruptedException { ChannelInitializer initializer = new ChannelInitializer<>() { @Override protected void initChannel(LocalChannel ch) {} }; for (int i = 0; i < numChannels; ++i) { ChannelFuture connect = new Bootstrap() .group(CLIENT_EVENT_LOOP) .channel(LocalChannel.class) .handler(initializer) .remoteAddress(LOCAL_ADDRESS) .connect() .sync(); channels.add(connect.channel()); } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/ClientRequestReceiverTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.net.InetAddresses; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent; import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import com.netflix.zuul.netty.insights.PassportLoggingHandler; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; 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.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpRequestEncoder; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for {@link ClientRequestReceiver}. */ @ExtendWith(MockitoExtension.class) class ClientRequestReceiverTest { @Test void proxyProtocol_portSetInSessionContextAndInHttpRequestMessageImpl() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); InetSocketAddress hapmDestinationAddress = new InetSocketAddress(InetAddresses.forString("2.2.2.2"), 444); channel.attr(SourceAddressChannelHandler.ATTR_PROXY_PROTOCOL_DESTINATION_ADDRESS) .set(hapmDestinationAddress); channel.attr(SourceAddressChannelHandler.ATTR_LOCAL_ADDR).set(hapmDestinationAddress); HttpRequestMessageImpl result; { channel.writeInbound( new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(hapmDestinationAddress.getPort()) .isEqualTo((int) result.getClientDestinationPort().get()); int destinationPort = ((InetSocketAddress) result.getContext().get(CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS)) .getPort(); assertThat(destinationPort).isEqualTo(444); assertThat(result.getOriginalPort()).isEqualTo(444); channel.close(); } @Test void parseUriFromNetty_relative() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.POST, "/foo/bar/somePath/%5E1.0.0?param1=foo¶m2=bar¶m3=baz", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/foo/bar/somePath/%5E1.0.0"); channel.close(); } @Test void parseUriFromNetty_absolute() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.POST, "https://www.netflix.com/foo/bar/somePath/%5E1.0.0?param1=foo¶m2=bar¶m3=baz", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/foo/bar/somePath/%5E1.0.0"); channel.close(); } @Test void parseUriFromNetty_unknown() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound( new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "asdf", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("asdf"); channel.close(); } @Test void parseQueryParamsWithEncodedCharsInURI() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.POST, "/foo/bar/somePath/%5E1.0.0?param1=foo¶m2=bar¶m3=baz", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getQueryParams().getFirst("param1")).isEqualTo("foo"); assertThat(result.getQueryParams().getFirst("param2")).isEqualTo("bar"); assertThat(result.getQueryParams().getFirst("param3")).isEqualTo("baz"); channel.close(); } @Test void largeResponse_atLimit() { ClientRequestReceiver receiver = new ClientRequestReceiver(null); EmbeddedChannel channel = new EmbeddedChannel(receiver); // Required for messages channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); int maxSize; // Figure out the max size, since it isn't public. { ByteBuf buf = Unpooled.buffer(1).writeByte('a'); channel.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", buf)); HttpRequestMessageImpl res = channel.readInbound(); maxSize = res.getMaxBodySize(); res.disposeBufferedBody(); } HttpRequestMessageImpl result; { ByteBuf buf = Unpooled.buffer(maxSize); buf.writerIndex(maxSize); channel.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", buf)); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getContext().getError()).isNull(); assertThat(result.getContext().shouldSendErrorResponse()).isFalse(); channel.close(); } @Test void largeResponse_aboveLimit() { ClientRequestReceiver receiver = new ClientRequestReceiver(null); EmbeddedChannel channel = new EmbeddedChannel(receiver); // Required for messages channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); int maxSize; // Figure out the max size, since it isn't public. { ByteBuf buf = Unpooled.buffer(1).writeByte('a'); channel.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", buf)); HttpRequestMessageImpl res = channel.readInbound(); maxSize = res.getMaxBodySize(); res.disposeBufferedBody(); } HttpRequestMessageImpl result; { ByteBuf buf = Unpooled.buffer(maxSize + 1); buf.writerIndex(maxSize + 1); channel.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", buf)); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getContext().getError()).isNotNull(); assertThat(result.getContext().getError().getMessage().contains("too large")) .isTrue(); assertThat(result.getContext().shouldSendErrorResponse()).isTrue(); assertThat(StatusCategoryUtils.getStatusCategory(result)) .isEqualTo(ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST); assertThat(StatusCategoryUtils.getStatusCategoryReason(result.getContext())) .startsWith("Invalid request provided: Request body size "); channel.close(); } @Test void maxHeaderSizeExceeded_setBadRequestStatus() { int maxInitialLineLength = BaseZuulChannelInitializer.MAX_INITIAL_LINE_LENGTH.get(); int maxHeaderSize = 10; int maxChunkSize = BaseZuulChannelInitializer.MAX_CHUNK_SIZE.get(); ClientRequestReceiver receiver = new ClientRequestReceiver(null); EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestEncoder()); PassportLoggingHandler loggingHandler = new PassportLoggingHandler(new DefaultRegistry()); // Required for messages channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); channel.pipeline().addLast(new HttpServerCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize, false)); channel.pipeline().addLast(receiver); channel.pipeline().addLast(loggingHandler); String str = "test-header-value"; ByteBuf buf = Unpooled.buffer(1); HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", buf); for (int i = 0; i < 100; i++) { httpRequest.headers().add("test-header" + i, str); } channel.writeOutbound(httpRequest); ByteBuf byteBuf = channel.readOutbound(); channel.writeInbound(byteBuf); channel.readInbound(); channel.close(); HttpRequestMessage request = ClientRequestReceiver.getRequestFromChannel(channel); assertThat(StatusCategoryUtils.getStatusCategory(request.getContext())) .isEqualTo(ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST); assertThat(StatusCategoryUtils.getStatusCategoryReason(request.getContext())) .isEqualTo("Invalid request provided: Decode failure"); } @Test void multipleHostHeaders_setBadRequestStatus() { ClientRequestReceiver receiver = new ClientRequestReceiver(null); EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestEncoder()); PassportLoggingHandler loggingHandler = new PassportLoggingHandler(new DefaultRegistry()); // Required for messages channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); channel.pipeline().addLast(new HttpServerCodec()); channel.pipeline().addLast(receiver); channel.pipeline().addLast(loggingHandler); HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post"); httpRequest.headers().add("Host", "foo.bar.com"); httpRequest.headers().add("Host", "bar.foo.com"); channel.writeOutbound(httpRequest); ByteBuf byteBuf = channel.readOutbound(); channel.writeInbound(byteBuf); channel.readInbound(); channel.close(); HttpRequestMessage request = ClientRequestReceiver.getRequestFromChannel(channel); SessionContext context = request.getContext(); assertThat(StatusCategoryUtils.getStatusCategory(context)) .isEqualTo(ZuulStatusCategory.FAILURE_CLIENT_BAD_REQUEST); assertThat(context.getError().getMessage()).isEqualTo("Multiple Host headers"); assertThat(StatusCategoryUtils.getStatusCategoryReason(context)) .isEqualTo("Invalid request provided: Multiple Host headers"); } @Test void setStatusCategoryForHttpPipelining() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); DefaultFullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.POST, "?ELhAWDLM1hwm8bhU0UT4", Unpooled.buffer()); // Write the message and save a copy channel.writeInbound(request); HttpRequestMessage inboundRequest = ClientRequestReceiver.getRequestFromChannel(channel); // Set the attr to emulate pipelining rejection channel.attr(HttpLifecycleChannelHandler.ATTR_HTTP_PIPELINE_REJECT).set(Boolean.TRUE); // Fire completion event channel.pipeline() .fireUserEventTriggered(new CompleteEvent( CompleteReason.PIPELINE_REJECT, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST))); channel.close(); assertThat(StatusCategoryUtils.getStatusCategory(inboundRequest.getContext())) .isEqualTo(ZuulStatusCategory.FAILURE_CLIENT_PIPELINE_REJECT); } @Test void headersAllCopied() { ClientRequestReceiver receiver = new ClientRequestReceiver(null); EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestEncoder()); PassportLoggingHandler loggingHandler = new PassportLoggingHandler(new DefaultRegistry()); // Required for messages channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); channel.pipeline().addLast(new HttpServerCodec()); channel.pipeline().addLast(receiver); channel.pipeline().addLast(loggingHandler); HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post"); httpRequest.headers().add("Header1", "Value1"); httpRequest.headers().add("Header2", "Value2"); httpRequest.headers().add("Duplicate", "Duplicate1"); httpRequest.headers().add("Duplicate", "Duplicate2"); channel.writeOutbound(httpRequest); ByteBuf byteBuf = channel.readOutbound(); channel.writeInbound(byteBuf); channel.readInbound(); channel.close(); HttpRequestMessage request = ClientRequestReceiver.getRequestFromChannel(channel); Headers headers = request.getHeaders(); assertThat(headers.size()).isEqualTo(4); assertThat(headers.getFirst("Header1")).isEqualTo("Value1"); assertThat(headers.getFirst("Header2")).isEqualTo("Value2"); List duplicates = headers.getAll("Duplicate"); assertThat(duplicates).isEqualTo(Arrays.asList("Duplicate1", "Duplicate2")); } @Test void clientIpSet() { String clientIp = "123.456.789.012"; ClientRequestReceiver receiver = new ClientRequestReceiver(null); EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestEncoder()); // Required for messages channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).set(clientIp); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); channel.pipeline().addLast(new HttpServerCodec()); channel.pipeline().addLast(receiver); HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post"); channel.writeOutbound(httpRequest); ByteBuf byteBuf = channel.readOutbound(); channel.writeInbound(byteBuf); channel.readInbound(); channel.close(); HttpRequestMessage request = ClientRequestReceiver.getRequestFromChannel(channel); assertThat(request.getClientIp()).isEqualTo(clientIp); } @Test void handleClientChannelInactiveEventCalledOnInactiveComplete() { TestClientRequestReceiver receiver = new TestClientRequestReceiver(); EmbeddedChannel channel = new EmbeddedChannel(receiver); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/post", Unpooled.buffer()); channel.writeInbound(request); channel.readInbound(); channel.pipeline() .fireUserEventTriggered(new CompleteEvent( CompleteReason.INACTIVE, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK))); channel.close(); assertThat(receiver.handleClientCancelledEventCalled).isTrue(); } private static class TestClientRequestReceiver extends ClientRequestReceiver { boolean handleClientCancelledEventCalled = false; TestClientRequestReceiver() { super(null); } @Override protected void handleClientChannelInactiveEvent(@NonNull HttpRequestMessage zuulRequest) { handleClientCancelledEventCalled = true; super.handleClientChannelInactiveEvent(zuulRequest); } } @Test void pathTraversal_basicDotDot() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "/public/../admin/", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/admin/"); channel.close(); } @Test void pathTraversal_multipleDotDots() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "/a/b/c/../../d/", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/a/d/"); channel.close(); } @Test void pathTraversal_dotSegments() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "/./foo/./bar/.", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/foo/bar/"); channel.close(); } @Test void pathTraversal_multipleSlashes() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "//foo///bar////baz", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/bar/baz"); channel.close(); } @Test void pathTraversal_escapeRoot() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "/../../../etc/passwd", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/etc/passwd"); channel.close(); } @Test void pathTraversal_complexMix() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "/foo/./bar/../baz//qux/../", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/foo/baz/"); channel.close(); } @Test void pathTraversal_withQueryString() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "/public/../admin/?param=value", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/admin/"); assertThat(result.getQueryParams().getFirst("param")).isEqualTo("value"); channel.close(); } @Test void pathTraversal_withOpaqueURI() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound(new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, "foo.netflix.net:443", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("foo.netflix.net:443"); channel.close(); } @Test void pathNormalization_emptyPath() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound( new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo(""); channel.close(); } @Test void pathNormalization_rootOnly() { EmbeddedChannel channel = new EmbeddedChannel(new ClientRequestReceiver(null)); channel.attr(SourceAddressChannelHandler.ATTR_SERVER_LOCAL_PORT).set(1234); HttpRequestMessageImpl result; { channel.writeInbound( new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.buffer())); result = channel.readInbound(); result.disposeBufferedBody(); } assertThat(result.getPath()).isEqualTo("/"); channel.close(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/ClientResponseWriterTest.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.netty.common.HttpLifecycleChannelHandler; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.BasicRequestCompleteHandler; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import com.netflix.zuul.message.util.HttpRequestBuilder; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.ReferenceCountUtil; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class ClientResponseWriterTest { @Test void exemptClientTimeoutResponseBeforeRequestRead() { ClientResponseWriter responseWriter = new ClientResponseWriter(new BasicRequestCompleteHandler()); EmbeddedChannel channel = new EmbeddedChannel(); SessionContext context = new SessionContext(); StatusCategoryUtils.setStatusCategory(context, ZuulStatusCategory.FAILURE_CLIENT_TIMEOUT); HttpRequestMessage request = new HttpRequestBuilder(context).withDefaults(); channel.attr(ClientRequestReceiver.ATTR_ZUUL_REQ).set(request); assertThat(responseWriter.shouldAllowPreemptiveResponse(channel)).isTrue(); } @Test void flagResponseBeforeRequestRead() { ClientResponseWriter responseWriter = new ClientResponseWriter(new BasicRequestCompleteHandler()); EmbeddedChannel channel = new EmbeddedChannel(); SessionContext context = new SessionContext(); StatusCategoryUtils.setStatusCategory(context, ZuulStatusCategory.FAILURE_LOCAL); HttpRequestMessage request = new HttpRequestBuilder(context).withDefaults(); channel.attr(ClientRequestReceiver.ATTR_ZUUL_REQ).set(request); assertThat(responseWriter.shouldAllowPreemptiveResponse(channel)).isFalse(); } @Test void allowExtensionForPremptingResponse() { ZuulStatusCategory customStatus = ZuulStatusCategory.SUCCESS_LOCAL_NO_ROUTE; ClientResponseWriter responseWriter = new ClientResponseWriter(new BasicRequestCompleteHandler()) { @Override protected boolean shouldAllowPreemptiveResponse(Channel channel) { StatusCategory status = StatusCategoryUtils.getStatusCategory(ClientRequestReceiver.getRequestFromChannel(channel)); return status == customStatus; } }; EmbeddedChannel channel = new EmbeddedChannel(); SessionContext context = new SessionContext(); StatusCategoryUtils.setStatusCategory(context, customStatus); HttpRequestMessage request = new HttpRequestBuilder(context).withDefaults(); channel.attr(ClientRequestReceiver.ATTR_ZUUL_REQ).set(request); assertThat(responseWriter.shouldAllowPreemptiveResponse(channel)).isTrue(); } @Test public void clearReferenceOnComplete() { ClientResponseWriter responseWriter = new ClientResponseWriter(new BasicRequestCompleteHandler()); EmbeddedChannel channel = new EmbeddedChannel(responseWriter); AtomicReference nettyResp = new AtomicReference<>(); channel.pipeline().addFirst(new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof HttpResponse response) { nettyResp.set(response); } ReferenceCountUtil.safeRelease(msg); } }); SessionContext ctx = new SessionContext(); HttpRequestMessage request = new HttpRequestBuilder(ctx).build(); request.storeInboundRequest(); HttpResponseMessageImpl response = new HttpResponseMessageImpl(ctx, request, 200); response.setHeaders(new Headers()); channel.attr(ClientRequestReceiver.ATTR_ZUUL_REQ).set(request); DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); ctx.set(CommonContextKeys.NETTY_HTTP_REQUEST, nettyRequest); channel.pipeline().fireUserEventTriggered(new HttpLifecycleChannelHandler.StartEvent(nettyRequest)); channel.writeInbound(response); HttpResponseMessage zuulResponse = responseWriter.getZuulResponse(); assertThat(zuulResponse).isNotNull(); assertThat(nettyResp.get()).isNotNull(); channel.pipeline() .fireUserEventTriggered(new HttpLifecycleChannelHandler.CompleteEvent( HttpLifecycleChannelHandler.CompleteReason.SESSION_COMPLETE, null, nettyResp.get())); assertThat(responseWriter.getZuulResponse()).isNull(); } @ParameterizedTest @ValueSource(booleans = {false, true}) void warningOnlyForRequestsWithBody(boolean hasBodyChunk) { Registry registry = new DefaultRegistry(); Counter warningCounter = registry.counter("server.http.requests.responseBeforeReceivedLastContent"); ClientResponseWriter responseWriter = new ClientResponseWriter(new BasicRequestCompleteHandler(), registry); EmbeddedChannel channel = new EmbeddedChannel(responseWriter); SessionContext ctx = new SessionContext(); HttpRequestMessage request = new HttpRequestBuilder(ctx).build(); if (hasBodyChunk) { request.bufferBodyContents(new io.netty.handler.codec.http.DefaultHttpContent( Unpooled.copiedBuffer("body", java.nio.charset.StandardCharsets.UTF_8))); } request.storeInboundRequest(); HttpResponseMessageImpl response = new HttpResponseMessageImpl(ctx, request, 200); response.setHeaders(new Headers()); channel.attr(ClientRequestReceiver.ATTR_ZUUL_REQ).set(request); DefaultHttpRequest nettyRequest = new DefaultHttpRequest( HttpVersion.HTTP_1_1, hasBodyChunk ? HttpMethod.POST : HttpMethod.GET, hasBodyChunk ? "/api" : "/favicon.ico"); ctx.set(CommonContextKeys.NETTY_HTTP_REQUEST, nettyRequest); channel.pipeline().fireUserEventTriggered(new HttpLifecycleChannelHandler.StartEvent(nettyRequest)); channel.writeInbound(response); assertThat(responseWriter.getZuulResponse()).isNotNull(); assertThat(request.hasBody()).isEqualTo(hasBodyChunk); if (hasBodyChunk) { assertThat(warningCounter.count()) .as("should warn as is body expected") .isEqualTo(1); } else { assertThat(warningCounter.count()) .as("should not warn as no body expected") .isEqualTo(0); } request.disposeBufferedBody(); channel.close(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/IoUringTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import com.netflix.config.ConfigurationManager; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Spectator; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.uring.IoUring; import io.netty.channel.uring.IoUringServerSocketChannel; import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.internal.PlatformDependent; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.configuration.AbstractConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /* Goals of this test: 1) verify that the server starts 2) verify that the server is listening on 2 ports 3) verify that the correct number of IOUringSocketChannel's are initialized 4) verify that the server stops */ @SuppressWarnings("AddressSelection") @Disabled class IoUringTest { private static final Logger LOGGER = LoggerFactory.getLogger(IoUringTest.class); private static final boolean IS_OS_LINUX = PlatformDependent.normalizedOs().equals("linux"); @BeforeEach void beforeTest() { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("zuul.server.netty.socket.force_io_uring", "true"); config.setProperty("zuul.server.netty.socket.force_nio", "false"); } @Test void testIoUringServer() throws Exception { LOGGER.info("IOUring.isAvailable: {}", IoUring.isAvailable()); LOGGER.info("IS_OS_LINUX: {}", IS_OS_LINUX); if (IS_OS_LINUX) { exerciseIoUringServer(); } } private void exerciseIoUringServer() throws Exception { IoUring.ensureAvailability(); ServerStatusManager ssm = mock(ServerStatusManager.class); Map> initializers = new HashMap<>(); List ioUringChannels = Collections.synchronizedList(new ArrayList()); ChannelInitializer init = new ChannelInitializer() { @Override protected void initChannel(Channel ch) { LOGGER.info("Channel: {}, isActive={}, isOpen={}", ch.getClass().getName(), ch.isActive(), ch.isOpen()); if (ch instanceof IoUringServerSocketChannel) { ioUringChannels.add((IoUringServerSocketChannel) ch); } } }; initializers.put(new NamedSocketAddress("test", new InetSocketAddress(0)), init); // The port to channel map keys on the port, post bind. This should be unique even if InetAddress is same initializers.put(new NamedSocketAddress("test2", new InetSocketAddress(0)), init); ClientConnectionsShutdown ccs = new ClientConnectionsShutdown( new DefaultChannelGroup(GlobalEventExecutor.INSTANCE), GlobalEventExecutor.INSTANCE, /* discoveryClient= */ null); EventLoopGroupMetrics elgm = new EventLoopGroupMetrics(Spectator.globalRegistry()); EventLoopConfig elc = new EventLoopConfig() { @Override public int eventLoopCount() { return 1; } @Override public int acceptorCount() { return 1; } }; Server s = new Server(new NoopRegistry(), ssm, initializers, ccs, elgm, elc); s.start(); List addresses = s.getListeningAddresses(); assertThat(addresses.size()).isEqualTo(2); addresses.forEach(address -> { assertThat(address.unwrap() instanceof InetSocketAddress).isTrue(); InetSocketAddress inetAddress = ((InetSocketAddress) address.unwrap()); assertThat(inetAddress.getPort()).isNotEqualTo(0); checkConnection(inetAddress.getPort()); }); await().atMost(1, TimeUnit.SECONDS).until(() -> ioUringChannels.size() == 2); s.stop(); assertThat(ioUringChannels.size()).isEqualTo(2); for (IoUringServerSocketChannel ch : ioUringChannels) { assertThat(ch.eventLoop().isShutdown()).as("isShutdown").isTrue(); } } @SuppressWarnings("EmptyCatch") private static void checkConnection(int port) { LOGGER.info("checkConnection port {}", port); Socket sock = null; try { InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", port); sock = new Socket(); sock.setSoTimeout(100); sock.connect(socketAddress, 100); OutputStream out = sock.getOutputStream(); out.write("Hello".getBytes(StandardCharsets.UTF_8)); out.flush(); out.close(); } catch (Exception exception) { fail("checkConnection failed. port=" + port + " " + exception); } finally { try { sock.close(); } catch (Exception ignored) { } } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/OriginResponseReceiverTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.netflix.zuul.filters.endpoint.ProxyEndpoint; 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.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.ReferenceCountUtil; import java.lang.reflect.Field; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class OriginResponseReceiverTest { @Mock private ProxyEndpoint proxyEndpoint; private OriginResponseReceiver receiver; private ChannelHandlerContext ctx; private EmbeddedChannel channel; private HttpContent chunk; @BeforeEach void setup() { channel = new EmbeddedChannel(); channel.pipeline().addLast(new SimpleChannelInboundHandler() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) {} }); ctx = channel.pipeline().firstContext(); receiver = new OriginResponseReceiver(proxyEndpoint); // allocate a chunk for testing chunk = new DefaultHttpContent(Unpooled.wrappedBuffer("yo".getBytes(UTF_8))); } @Test void channelReadWithManualReadTriggersRead() throws Exception { // when triggerRead=true (default), should manually trigger read after processing receiver.channelReadInternal(ctx, chunk, true); verify(proxyEndpoint, times(1)).invokeNext(any(HttpContent.class)); assertThat(chunk.refCnt()).isEqualTo(1); } @Test void channelReadWithoutManualReadDoesNotTriggerRead() throws Exception { HttpContent chunk2 = new DefaultHttpContent(Unpooled.wrappedBuffer("test data".getBytes(UTF_8))); // when triggerRead=false, should NOT trigger read receiver.channelReadInternal(ctx, chunk2, false); verify(proxyEndpoint, times(1)).invokeNext(any(HttpContent.class)); assertThat(chunk2.refCnt()).isEqualTo(1); chunk2.release(); } @Test void unlinkFromClientRequestNullsEdgeProxyField() throws Exception { verifyProxyLink(receiver, false); // link is still there, so pipeline should be called & chunk retained receiver.channelReadInternal(ctx, chunk, true); verify(proxyEndpoint, times(1)).invokeNext(any(HttpContent.class)); assertThat(chunk.refCnt()).isEqualTo(1); // when the client request is unlinked, field should be nulled receiver.unlinkFromClientRequest(); verifyProxyLink(receiver, true); // data frames should be ignored now that the link is gone receiver.channelReadInternal(ctx, chunk, true); // pipeline should not be called again, as link is gone, so still 1 time verify(proxyEndpoint, times(1)).invokeNext(any(HttpContent.class)); // verify chunk was released assertThat(chunk.refCnt()).isEqualTo(0); } @Test void unlinkPreventsStaleChunksFromBeingProcessed() throws Exception { // simulate retry scenario where connection is unlinked receiver.unlinkFromClientRequest(); verifyProxyLink(receiver, true); // stale chunk arrives after unlink receiver.channelReadInternal(ctx, chunk, true); // should NOT call invokeNext - chunk should be released instead verify(proxyEndpoint, never()).invokeNext(any(HttpContent.class)); // verify chunk was released (no memory leak) assertThat(chunk.refCnt()).isEqualTo(0); } @Test void httpResponseProcessedCorrectly() throws Exception { HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); receiver.channelReadInternal(ctx, response, true); verify(proxyEndpoint, times(1)).responseFromOrigin(any(HttpResponse.class)); } @Test void httpResponseReleasedWhenUnlinked() throws Exception { // use FullHttpResponse which has a refCnt HttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer("body".getBytes(UTF_8))); assertThat(ReferenceCountUtil.refCnt(response)).isEqualTo(1); // unlink first receiver.unlinkFromClientRequest(); // process response - should be released since unlinked receiver.channelReadInternal(ctx, response, true); verify(proxyEndpoint, never()).responseFromOrigin(any(HttpResponse.class)); assertThat(ReferenceCountUtil.refCnt(response)).isEqualTo(0); } @Test void chunksReleasedWhenUnlinked() throws Exception { receiver.unlinkFromClientRequest(); receiver.channelReadInternal(ctx, chunk, true); verify(proxyEndpoint, never()).invokeNext(any(HttpContent.class)); assertThat(chunk.refCnt()).isEqualTo(0); } private void verifyProxyLink(OriginResponseReceiver receiver, boolean expectedNull) throws Exception { Field proxyField = OriginResponseReceiver.class.getDeclaredField("edgeProxy"); proxyField.setAccessible(true); if (expectedNull) { assertThat(proxyField.get(receiver)).isNull(); } else { assertThat(proxyField.get(receiver)).isNotNull(); } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/ServerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import com.netflix.config.ConfigurationManager; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.NoopRegistry; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Spectator; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.concurrent.GlobalEventExecutor; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.configuration.AbstractConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Tests for {@link Server}. */ @SuppressWarnings("AddressSelection") class ServerTest { private static final Logger LOGGER = LoggerFactory.getLogger(ServerTest.class); @BeforeEach void beforeTest() { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("zuul.server.netty.socket.force_nio", "true"); config.setProperty("zuul.server.netty.socket.force_io_uring", "false"); } @Test void getListeningSockets() throws Exception { ServerStatusManager ssm = mock(ServerStatusManager.class); Map> initializers = new HashMap<>(); List nioChannels = Collections.synchronizedList(new ArrayList()); ChannelInitializer init = new ChannelInitializer() { @Override protected void initChannel(Channel ch) { LOGGER.info("Channel: {}, isActive={}, isOpen={}", ch.getClass().getName(), ch.isActive(), ch.isOpen()); if (ch instanceof NioSocketChannel) { nioChannels.add((NioSocketChannel) ch); } } }; initializers.put(new NamedSocketAddress("test", new InetSocketAddress(0)), init); // The port to channel map keys on the port, post bind. This should be unique even if InetAddress is same initializers.put(new NamedSocketAddress("test2", new InetSocketAddress(0)), init); ClientConnectionsShutdown ccs = new ClientConnectionsShutdown( new DefaultChannelGroup(GlobalEventExecutor.INSTANCE), GlobalEventExecutor.INSTANCE, /* discoveryClient= */ null); EventLoopGroupMetrics elgm = new EventLoopGroupMetrics(Spectator.globalRegistry()); EventLoopConfig elc = new EventLoopConfig() { @Override public int eventLoopCount() { return 1; } @Override public int acceptorCount() { return 1; } @Override public int getBacklogSize() { return 1024; } }; Server s = new Server(new NoopRegistry(), ssm, initializers, ccs, elgm, elc); s.start(); List addrs = s.getListeningAddresses(); assertThat(addrs.size()).isEqualTo(2); for (NamedSocketAddress address : addrs) { assertThat(address.unwrap() instanceof InetSocketAddress).isTrue(); int port = ((InetSocketAddress) address.unwrap()).getPort(); assertThat(port).isNotEqualTo(0); checkConnection(port); } await().atMost(1, TimeUnit.SECONDS).until(() -> nioChannels.size() == 2); nioChannels.stream() .map(NioSocketChannel::parent) .map(ServerSocketChannel::config) .forEach(config -> assertThat(config.getBacklog()).isEqualTo(elc.getBacklogSize())); s.stop(); assertThat(nioChannels.size()).isEqualTo(2); for (NioSocketChannel ch : nioChannels) { assertThat(ch.isShutdown()).as("isShutdown").isTrue(); } } @Test void acceptorMetricsAreRegistered() throws Exception { Registry registry = new DefaultRegistry(); ServerStatusManager ssm = mock(ServerStatusManager.class); Map> initializers = new HashMap<>(); ChannelInitializer init = new ChannelInitializer<>() { @Override protected void initChannel(Channel ch) {} }; initializers.put(new NamedSocketAddress("test", new InetSocketAddress(0)), init); ClientConnectionsShutdown ccs = new ClientConnectionsShutdown( new DefaultChannelGroup(GlobalEventExecutor.INSTANCE), GlobalEventExecutor.INSTANCE, null); EventLoopGroupMetrics elgm = new EventLoopGroupMetrics(Spectator.globalRegistry()); EventLoopConfig elc = new EventLoopConfig() { @Override public int eventLoopCount() { return 1; } @Override public int acceptorCount() { return 1; } @Override public int getBacklogSize() { return 1024; } }; Server s = new Server(registry, ssm, initializers, ccs, elgm, elc); s.start(); List addrs = s.getListeningAddresses(); int port = ((InetSocketAddress) addrs.getFirst().unwrap()).getPort(); checkConnection(port); checkConnection(port); await().atMost(1, TimeUnit.SECONDS).until(() -> { Counter counter = registry.counter("zuul.conn.acceptor.accepts", "port", String.valueOf(port)); return counter.count() >= 2; }); s.stop(); } @SuppressWarnings("EmptyCatch") private static void checkConnection(int port) { Socket sock = null; try { InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", port); sock = new Socket(); sock.setSoTimeout(100); sock.connect(socketAddress, 100); OutputStream out = sock.getOutputStream(); out.write("Hello".getBytes(StandardCharsets.UTF_8)); out.flush(); out.close(); } catch (Exception exception) { fail("checkConnection failed. port=" + port + " " + exception); } finally { try { sock.close(); } catch (Exception ignored) { } } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/SocketAddressPropertyTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.netflix.zuul.netty.server.SocketAddressProperty.BindType; import io.netty.channel.unix.DomainSocketAddress; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Arrays; import org.junit.jupiter.api.Test; class SocketAddressPropertyTest { @Test void defaultValueWorks() { SocketAddressProperty prop = new SocketAddressProperty("com.netflix.zuul.netty.server.testprop", "=7001"); SocketAddress address = prop.getValue(); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); } @Test void bindTypeWorks_any() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("ANY=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); } @Test void bindTypeWorks_blank() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); } @Test void bindTypeWorks_ipv4Any() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("IPV4_ANY=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); assertThat(inetSocketAddress.getAddress() instanceof Inet4Address).isTrue(); assertThat(inetSocketAddress.getAddress().isAnyLocalAddress()).isTrue(); } @Test void bindTypeWorks_ipv6Any() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("IPV6_ANY=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); assertThat(inetSocketAddress.getAddress() instanceof Inet6Address).isTrue(); assertThat(inetSocketAddress.getAddress().isAnyLocalAddress()).isTrue(); } @Test void bindTypeWorks_anyLocal() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("ANY_LOCAL=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); assertThat(inetSocketAddress.getAddress().isLoopbackAddress()).isTrue(); } @Test void bindTypeWorks_ipv4Local() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("IPV4_LOCAL=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); assertThat(inetSocketAddress.getAddress() instanceof Inet4Address).isTrue(); assertThat(inetSocketAddress.getAddress().isLoopbackAddress()).isTrue(); } @Test void bindTypeWorks_ipv6Local() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("IPV6_LOCAL=7001"); assertThat(address.getClass()).isEqualTo(InetSocketAddress.class); InetSocketAddress inetSocketAddress = (InetSocketAddress) address; assertThat(inetSocketAddress.getPort()).isEqualTo(7001); assertThat(inetSocketAddress.isUnresolved()).isFalse(); assertThat(inetSocketAddress.getAddress() instanceof Inet6Address).isTrue(); assertThat(inetSocketAddress.getAddress().isLoopbackAddress()).isTrue(); } @Test void bindTypeWorks_uds() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("UDS=/var/run/zuul.sock"); assertThat(address.getClass()).isEqualTo(DomainSocketAddress.class); DomainSocketAddress domainSocketAddress = (DomainSocketAddress) address; assertThat(domainSocketAddress.path()).isEqualTo("/var/run/zuul.sock"); } @Test void bindTypeWorks_udsWithEquals() { SocketAddress address = SocketAddressProperty.Decoder.INSTANCE.apply("UDS=/var/run/zuul=.sock"); assertThat(address.getClass()).isEqualTo(DomainSocketAddress.class); DomainSocketAddress domainSocketAddress = (DomainSocketAddress) address; assertThat(domainSocketAddress.path()).isEqualTo("/var/run/zuul=.sock"); } @Test void failsOnMissingEqual() { assertThatThrownBy(() -> { SocketAddressProperty.Decoder.INSTANCE.apply("ANY"); }) .isInstanceOf(IllegalArgumentException.class); } @Test void failsOnBadPort() { for (BindType type : Arrays.asList( BindType.ANY, BindType.IPV4_ANY, BindType.IPV6_ANY, BindType.ANY_LOCAL, BindType.IPV4_LOCAL, BindType.IPV6_LOCAL)) { assertThatThrownBy(() -> { SocketAddressProperty.Decoder.INSTANCE.apply(type.name() + "=bogus"); }) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Port"); } } @Test public void failsOnBadAddress() throws Exception { assertThatThrownBy(() -> { SocketAddressProperty.Decoder.INSTANCE.apply(""); }) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid address"); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/http2/Http2ConnectionErrorHandlerTest.java ================================================ /** * Copyright 2023 Netflix, Inc. *

* 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. */ package com.netflix.zuul.netty.server.http2; import static org.assertj.core.api.Assertions.assertThat; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * @author Justin Guerra * @since 11/15/23 */ class Http2ConnectionErrorHandlerTest { private EmbeddedChannel channel; private ExceptionCapturingHandler exceptionCapturingHandler; @BeforeEach void setup() { exceptionCapturingHandler = new ExceptionCapturingHandler(); channel = new EmbeddedChannel(new Http2ConnectionErrorHandler(), exceptionCapturingHandler); } @Test public void nonHttp2ExceptionsPassedUpPipeline() { RuntimeException exception = new RuntimeException(); channel.pipeline().fireExceptionCaught(exception); assertThat(exceptionCapturingHandler.caught).isEqualTo(exception); } private static class ExceptionCapturingHandler extends ChannelInboundHandlerAdapter { private Throwable caught; @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { this.caught = cause; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/http2/Http2ContentLengthEnforcingHandlerTest.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.FullHttpRequest; 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.Http2ResetFrame; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class Http2ContentLengthEnforcingHandlerTest { private EmbeddedChannel channel; @BeforeEach void setup() { channel = new EmbeddedChannel(new Http2ContentLengthEnforcingHandler()); } @Test void validRequestPassesThrough() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/"); req.headers().set(HttpHeaderNames.CONTENT_LENGTH, 3); channel.writeInbound(req); assertThat(channel.readOutbound()).isNull(); assertThat(channel.readInbound()).isSameAs(req); } @Test void requestWithNoContentLengthPassesThrough() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); channel.writeInbound(req); assertThat(channel.readOutbound()).isNull(); assertThat(channel.readInbound()).isSameAs(req); } @Test void rejectsMultipleContentLengthHeaders() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 1); req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 2); channel.writeInbound(req); assertThat((Object) channel.readOutbound()).isInstanceOf(Http2ResetFrame.class); } @Test void failsOnNonNumericContentLength() { EmbeddedChannel chan = new EmbeddedChannel(); chan.pipeline().addLast(new Http2ContentLengthEnforcingHandler()); ByteBuf content = Unpooled.buffer(8); FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/", content); req.headers().set(HttpHeaderNames.CONTENT_LENGTH, "not_a_number"); chan.writeInbound(req); Object out = chan.readOutbound(); assertThat(out).isInstanceOf(Http2ResetFrame.class); assertThat(content.refCnt()).isEqualTo(0); } @Test void rejectsNegativeContentLength() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/"); req.headers().set(HttpHeaderNames.CONTENT_LENGTH, -5); channel.writeInbound(req); assertThat((Object) channel.readOutbound()).isInstanceOf(Http2ResetFrame.class); } @Test void rejectsMixedContentLengthAndChunked() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 1); req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, "identity, chunked"); req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, "fzip"); channel.writeInbound(req); assertThat((Object) channel.readOutbound()).isInstanceOf(Http2ResetFrame.class); } @Test void rejectsContentExceedingDeclaredLength() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 1); channel.writeInbound(req); assertThat(channel.readOutbound()).isNull(); DefaultHttpContent content = new DefaultHttpContent(ByteBufUtil.writeUtf8(UnpooledByteBufAllocator.DEFAULT, "a")); channel.writeInbound(content); assertThat(channel.readOutbound()).isNull(); DefaultHttpContent content2 = new DefaultHttpContent(ByteBufUtil.writeUtf8(UnpooledByteBufAllocator.DEFAULT, "a")); channel.writeInbound(content2); assertThat((Object) channel.readOutbound()).isInstanceOf(Http2ResetFrame.class); } @Test void rejectsContentShorterThanDeclaredLength() { DefaultHttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); req.headers().add(HttpHeaderNames.CONTENT_LENGTH, 2); channel.writeInbound(req); assertThat(channel.readOutbound()).isNull(); DefaultHttpContent content = new DefaultHttpContent(ByteBufUtil.writeUtf8(UnpooledByteBufAllocator.DEFAULT, "a")); channel.writeInbound(content); assertThat(channel.readOutbound()).isNull(); channel.writeInbound(new DefaultLastHttpContent()); assertThat((Object) channel.readOutbound()).isInstanceOf(Http2ResetFrame.class); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/http2/Http2OrHttpHandlerTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.http2; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.netty.common.Http2ConnectionCloseHandler; import com.netflix.netty.common.Http2ConnectionExpiryHandler; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.ChannelConfigValue; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.Http2MetricsChannelHandlers; import com.netflix.spectator.api.NoopRegistry; import com.netflix.zuul.netty.server.BaseZuulChannelInitializer; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.ssl.ApplicationProtocolNames; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * @author Argha C * @since November 18, 2020 */ class Http2OrHttpHandlerTest { private EmbeddedChannel channel; private ChannelConfig channelConfig; @BeforeEach void setUp() { channel = new EmbeddedChannel(); channelConfig = new ChannelConfig(); } @AfterEach void tearDown() { channel.finishAndReleaseAll(); } @Test void swapInHttp2HandlerBasedOnALPN() throws Exception { NoopRegistry registry = new NoopRegistry(); channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.maxHttp2HeaderListSize, 32768)); Http2ConnectionCloseHandler connectionCloseHandler = new Http2ConnectionCloseHandler(registry); Http2ConnectionExpiryHandler connectionExpiryHandler = new Http2ConnectionExpiryHandler(100, 100, 20 * 60 * 1000); Http2MetricsChannelHandlers http2MetricsChannelHandlers = new Http2MetricsChannelHandlers(registry, "server", "http2-443"); Http2OrHttpHandler http2OrHttpHandler = new Http2OrHttpHandler( new Http2StreamInitializer( channel, (x) -> {}, http2MetricsChannelHandlers, connectionCloseHandler, connectionExpiryHandler), channelConfig, cp -> {}); channel.pipeline().addLast("codec_placeholder", new DummyChannelHandler()); channel.pipeline().addLast(Http2OrHttpHandler.class.getSimpleName(), http2OrHttpHandler); http2OrHttpHandler.configurePipeline(channel.pipeline().lastContext(), ApplicationProtocolNames.HTTP_2); assertThat(channel.pipeline().get(Http2FrameCodec.class)).isInstanceOf(Http2FrameCodec.class); assertThat(channel.pipeline().get(BaseZuulChannelInitializer.HTTP_CODEC_HANDLER_NAME)) .isInstanceOf(Http2MultiplexHandler.class); assertThat(channel.attr(Http2OrHttpHandler.PROTOCOL_NAME).get()).isEqualTo("HTTP/2"); } @Test void protocolCloseHandlerAddedByDefault() throws Exception { channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.maxHttp2HeaderListSize, 32768)); Http2OrHttpHandler http2OrHttpHandler = new Http2OrHttpHandler(new ChannelInboundHandlerAdapter(), channelConfig, cp -> {}); channel.pipeline().addLast("codec_placeholder", new DummyChannelHandler()); channel.pipeline().addLast(Http2OrHttpHandler.class.getSimpleName(), http2OrHttpHandler); http2OrHttpHandler.configurePipeline(channel.pipeline().lastContext(), ApplicationProtocolNames.HTTP_2); assertThat(channel.pipeline().context(Http2ConnectionErrorHandler.class)) .isNotNull(); } @Test void skipProtocolCloseHandler() throws Exception { channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.http2CatchConnectionErrors, false)); channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.maxHttp2HeaderListSize, 32768)); Http2OrHttpHandler http2OrHttpHandler = new Http2OrHttpHandler(new ChannelInboundHandlerAdapter(), channelConfig, cp -> {}); channel.pipeline().addLast("codec_placeholder", new DummyChannelHandler()); channel.pipeline().addLast(Http2OrHttpHandler.class.getSimpleName(), http2OrHttpHandler); http2OrHttpHandler.configurePipeline(channel.pipeline().lastContext(), ApplicationProtocolNames.HTTP_2); assertThat(channel.pipeline().context(Http2ConnectionErrorHandler.class)) .isNull(); } @Test void validateHttp2Settings() throws Exception { boolean connectProtocolEnabled = !CommonChannelConfigKeys.http2ConnectProtocolEnabled.defaultValue(); int maxConcurrentStreams = CommonChannelConfigKeys.maxConcurrentStreams.defaultValue() + 1; int initialWindowSize = CommonChannelConfigKeys.initialWindowSize.defaultValue() + 1; int maxHeaderTableSize = CommonChannelConfigKeys.maxHttp2HeaderTableSize.defaultValue() + 1; int maxHeaderListSize = 1024; channelConfig.add( new ChannelConfigValue<>(CommonChannelConfigKeys.http2ConnectProtocolEnabled, connectProtocolEnabled)); channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.maxConcurrentStreams, maxConcurrentStreams)); channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.initialWindowSize, initialWindowSize)); channelConfig.add( new ChannelConfigValue<>(CommonChannelConfigKeys.maxHttp2HeaderTableSize, maxHeaderTableSize)); channelConfig.add(new ChannelConfigValue<>(CommonChannelConfigKeys.maxHttp2HeaderListSize, maxHeaderListSize)); Http2OrHttpHandler http2OrHttpHandler = new Http2OrHttpHandler(new ChannelInboundHandlerAdapter(), channelConfig, cp -> {}); channel.pipeline().addLast("codec_placeholder", new DummyChannelHandler()); channel.pipeline().addLast(Http2OrHttpHandler.class.getSimpleName(), http2OrHttpHandler); http2OrHttpHandler.configurePipeline(channel.pipeline().lastContext(), ApplicationProtocolNames.HTTP_2); // triggers settings to be written channel.pipeline().fireChannelActive(); Http2FrameCodec http2FrameCodec = channel.pipeline().get(Http2FrameCodec.class); Http2Settings http2Settings = http2FrameCodec.encoder().pollSentSettings(); assertThat(http2Settings.connectProtocolEnabled()).isEqualTo(connectProtocolEnabled); assertThat(http2Settings.maxConcurrentStreams()).isEqualTo(maxConcurrentStreams); assertThat(http2Settings.initialWindowSize()).isEqualTo(initialWindowSize); assertThat(http2Settings.headerTableSize()).isEqualTo(maxHeaderTableSize); assertThat(http2Settings.maxHeaderListSize()).isEqualTo(maxHeaderListSize); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/push/PushAuthHandlerTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; class PushAuthHandlerTest { @Test void testIsInvalidOrigin() { ZuulPushAuthHandlerTest authHandler = new ZuulPushAuthHandlerTest(); DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/ws", Unpooled.buffer()); // Invalid input assertThat(authHandler.isInvalidOrigin(request)).isTrue(); request.headers().add(HttpHeaderNames.ORIGIN, "zuul-push.foo.com"); assertThat(authHandler.isInvalidOrigin(request)).isTrue(); // Valid input request.headers().remove(HttpHeaderNames.ORIGIN); request.headers().add(HttpHeaderNames.ORIGIN, "zuul-push.netflix.com"); assertThat(authHandler.isInvalidOrigin(request)).isFalse(); } class ZuulPushAuthHandlerTest extends PushAuthHandler { public ZuulPushAuthHandlerTest() { super("/ws", ".netflix.com"); } @Override protected boolean isDelayedAuth(FullHttpRequest req, ChannelHandlerContext ctx) { return false; } @Override protected PushUserAuth doAuth(FullHttpRequest req, ChannelHandlerContext ctx) { return null; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/push/PushConnectionRegistryTest.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class PushConnectionRegistryTest { private PushConnectionRegistry pushConnectionRegistry; private PushConnection pushConnection; @BeforeEach void setUp() { pushConnectionRegistry = new PushConnectionRegistry(); pushConnection = mock(PushConnection.class); } @Test void testPutAndGet() { assertThat(pushConnectionRegistry.get("clientId1")).isNull(); pushConnectionRegistry.put("clientId1", pushConnection); assertThat(pushConnectionRegistry.get("clientId1")).isEqualTo(pushConnection); } @Test void testGetAll() { pushConnectionRegistry.put("clientId1", pushConnection); pushConnectionRegistry.put("clientId2", pushConnection); List connections = pushConnectionRegistry.getAll(); assertThat(connections.size()).isEqualTo(2); } @Test void testMintNewSecureToken() { String token = pushConnectionRegistry.mintNewSecureToken(); assertThat(token).isNotNull(); assertThat(token.length()).isEqualTo(20); // 15 bytes become 20 characters when Base64-encoded } @Test void testPutAssignsTokenToConnection() { pushConnectionRegistry.put("clientId1", pushConnection); verify(pushConnection).setSecureToken(anyString()); } @Test void testRemove() { pushConnectionRegistry.put("clientId1", pushConnection); assertThat(pushConnectionRegistry.remove("clientId1")).isEqualTo(pushConnection); assertThat(pushConnectionRegistry.get("clientId1")).isNull(); } @Test void testSize() { assertThat(pushConnectionRegistry.size()).isEqualTo(0); pushConnectionRegistry.put("clientId1", pushConnection); assertThat(pushConnectionRegistry.size()).isEqualTo(1); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/push/PushMessageSenderInitializerTest.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * Unit tests for {@link PushMessageSenderInitializer}. */ class PushMessageSenderInitializerTest { private PushMessageSenderInitializer initializer; private Channel channel; private ChannelHandler handler; @BeforeEach void setUp() { handler = mock(ChannelHandler.class); // Initialize mock handler initializer = new PushMessageSenderInitializer() { @Override protected void addPushMessageHandlers(ChannelPipeline pipeline) { pipeline.addLast("mockHandler", handler); } }; channel = new EmbeddedChannel(); } @Test void testInitChannel() throws Exception { initializer.initChannel(channel); assertThat(channel.pipeline().context(HttpServerCodec.class)).isNotNull(); assertThat(channel.pipeline().context(HttpObjectAggregator.class)).isNotNull(); assertThat(channel.pipeline().get("mockHandler")).isNotNull(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/push/PushRegistrationHandlerTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.push; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import com.google.common.util.concurrent.MoreExecutors; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.DefaultEventLoop; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.util.concurrent.ScheduledFuture; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** * @author Justin Guerra * @since 8/31/22 */ class PushRegistrationHandlerTest { private static ExecutorService EXECUTOR; @Captor private ArgumentCaptor scheduledCaptor; @Captor private ArgumentCaptor writeCaptor; @Mock private ChannelHandlerContext context; @Mock private ChannelFuture channelFuture; @Mock private ChannelPipeline pipelineMock; @Mock private Channel channel; private PushConnectionRegistry registry; private PushRegistrationHandler handler; private DefaultEventLoop eventLoopSpy; private TestAuth successfulAuth; @BeforeAll static void classSetup() { EXECUTOR = Executors.newSingleThreadExecutor(); } @AfterAll static void classCleanup() { MoreExecutors.shutdownAndAwaitTermination(EXECUTOR, 5, TimeUnit.SECONDS); } @BeforeEach void setup() { MockitoAnnotations.openMocks(this); registry = new PushConnectionRegistry(); handler = new PushRegistrationHandler(registry, PushProtocol.WEBSOCKET); successfulAuth = new TestAuth(true); eventLoopSpy = spy(new DefaultEventLoop(EXECUTOR)); doReturn(eventLoopSpy).when(context).executor(); doReturn(channelFuture).when(context).writeAndFlush(writeCaptor.capture()); doReturn(pipelineMock).when(context).pipeline(); doReturn(channel).when(context).channel(); } @Test void closeIfNotAuthenticated() throws Exception { doHandshakeComplete(); Runnable scheduledTask = scheduledCaptor.getValue(); scheduledTask.run(); validateConnectionClosed(1000, "Server closed connection"); } @Test void authFailed() throws Exception { doHandshakeComplete(); handler.userEventTriggered(context, new TestAuth(false)); validateConnectionClosed(1008, "Auth failed"); } @Test void authSuccess() throws Exception { doHandshakeComplete(); authenticateChannel(); } @Test void requestClientToCloseInactiveConnection() throws Exception { doHandshakeComplete(); Mockito.reset(eventLoopSpy); authenticateChannel(); verify(eventLoopSpy).schedule(scheduledCaptor.capture(), anyLong(), eq(TimeUnit.SECONDS)); Runnable requestClientToClose = scheduledCaptor.getValue(); requestClientToClose.run(); validateConnectionClosed(1000, "Server closed connection"); } @Test void requestClientToClose() throws Exception { doHandshakeComplete(); Mockito.reset(eventLoopSpy); authenticateChannel(); verify(eventLoopSpy).schedule(scheduledCaptor.capture(), anyLong(), eq(TimeUnit.SECONDS)); Runnable requestClientToClose = scheduledCaptor.getValue(); int taskListSize = handler.getScheduledFutures().size(); doReturn(true).when(channel).isActive(); requestClientToClose.run(); assertThat(handler.getScheduledFutures().size()).isEqualTo(taskListSize + 1); Object capture = writeCaptor.getValue(); assertThat(capture instanceof TextWebSocketFrame).isTrue(); TextWebSocketFrame frame = (TextWebSocketFrame) capture; assertThat(frame.text()).isEqualTo("_CLOSE_"); } @Test void channelInactiveCancelsTasks() throws Exception { doHandshakeComplete(); TestAuth testAuth = new TestAuth(true); authenticateChannel(); List> copyOfFutures = new ArrayList<>(handler.getScheduledFutures()); handler.channelInactive(context); assertThat(registry.get(testAuth.getClientIdentity())).isNull(); assertThat(handler.getScheduledFutures().isEmpty()).isTrue(); copyOfFutures.forEach(f -> assertThat(f.isCancelled()).isTrue()); verify(context).close(); } private void doHandshakeComplete() throws Exception { handler.userEventTriggered(context, PushProtocol.WEBSOCKET.getHandshakeCompleteEvent()); assertThat(handler.getPushConnection()).isNotNull(); verify(eventLoopSpy).schedule(scheduledCaptor.capture(), anyLong(), eq(TimeUnit.SECONDS)); } private void authenticateChannel() throws Exception { handler.userEventTriggered(context, successfulAuth); assertThat(registry.get(successfulAuth.getClientIdentity())).isNotNull(); assertThat(handler.getScheduledFutures().size()).isEqualTo(2); verify(pipelineMock).remove(PushAuthHandler.NAME); } private void validateConnectionClosed(int expected, String messaged) { Object capture = writeCaptor.getValue(); assertThat(capture instanceof CloseWebSocketFrame).isTrue(); CloseWebSocketFrame closeFrame = (CloseWebSocketFrame) capture; assertThat(closeFrame.statusCode()).isEqualTo(expected); assertThat(closeFrame.reasonText()).isEqualTo(messaged); verify(channelFuture).addListener(ChannelFutureListener.CLOSE); } private static class TestAuth implements PushUserAuth { private final boolean success; public TestAuth(boolean success) { this.success = success; } @Override public boolean isSuccess() { return success; } @Override public int statusCode() { return 0; } @Override public String getClientIdentity() { return "whatever"; } } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/server/ssl/SslHandshakeInfoHandlerTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.server.ssl; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.netflix.config.DynamicBooleanProperty; import com.netflix.netty.common.SourceAddressChannelHandler; import com.netflix.netty.common.ssl.SslHandshakeInfo; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.passport.PassportState; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.ssl.util.SelfSignedCertificate; import io.netty.util.ReferenceCountUtil; import java.nio.channels.ClosedChannelException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; import java.util.Objects; import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; /** * Unit tests for {@link SslHandshakeInfoHandler}. */ public class SslHandshakeInfoHandlerTest { @BeforeEach public void setup() { SslHandshakeInfoHandler.SNI_LOGGING_ENABLED = new DynamicBooleanProperty("zuul.ssl.handshake.snilogging.enabled", true); } @Test public void sslEarlyHandshakeFailure() throws Exception { EmbeddedChannel clientChannel = new EmbeddedChannel(); SSLEngine clientEngine = SslContextBuilder.forClient().build().newEngine(clientChannel.alloc()); clientChannel.pipeline().addLast(new SslHandler(clientEngine)); EmbeddedChannel serverChannel = new EmbeddedChannel(); SelfSignedCertificate cert = new SelfSignedCertificate("localhorse"); SSLEngine serverEngine = SslContextBuilder.forServer(cert.key(), cert.cert()).build().newEngine(serverChannel.alloc()); serverChannel.pipeline().addLast(new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { // Simulate an early closure form the client. ReferenceCountUtil.safeRelease(msg); promise.setFailure(new ClosedChannelException()); } }); serverChannel.pipeline().addLast(new SslHandler(serverEngine)); serverChannel.pipeline().addLast(new SslHandshakeInfoHandler()); Object clientHello = clientChannel.readOutbound(); assertThat(clientHello).isNotNull(); ReferenceCountUtil.retain(clientHello); serverChannel.writeInbound(clientHello); // Assert that the handler removes itself from the pipeline, since it was torn down. assertThat(serverChannel.pipeline().context(SslHandshakeInfoHandler.class)) .isNull(); } @Test public void getFailureCauses() { SslHandshakeInfoHandler handler = new SslHandshakeInfoHandler(); RuntimeException noMessage = new RuntimeException(); assertThat(handler.getFailureCause(noMessage)).isEqualTo(noMessage.toString()); RuntimeException withMessage = new RuntimeException("some unexpected message"); assertThat(handler.getFailureCause(withMessage)).isEqualTo("some unexpected message"); RuntimeException openSslMessage = new RuntimeException("javax.net.ssl.SSLHandshakeException: error:1000008e:SSL" + " routines:OPENSSL_internal:DIGEST_CHECK_FAILED"); assertThat(handler.getFailureCause(openSslMessage)).isEqualTo("DIGEST_CHECK_FAILED"); } @Test public void handshakeFailureWithSSLException() throws Exception { Registry registry = new DefaultRegistry(); // Mock SSL engine and session SSLEngine sslEngine = mock(SSLEngine.class); ExtendedSSLSession sslSession = mock(ExtendedSSLSession.class); X509Certificate serverCert = mock(X509Certificate.class); // Setup SSL session when(sslEngine.getSession()).thenReturn(sslSession); when(sslEngine.getNeedClientAuth()).thenReturn(false); when(sslEngine.getWantClientAuth()).thenReturn(false); when(sslSession.getProtocol()).thenReturn("TLSv1.3"); when(sslSession.getCipherSuite()).thenReturn("TLS_AES_256_GCM_SHA384"); when(sslSession.getLocalCertificates()).thenReturn(new Certificate[] {serverCert}); when(sslSession.getPeerCertificates()).thenReturn(new Certificate[0]); // Setup SNI with www.netflix.com List sniNames = Arrays.asList(new SNIHostName("www.netflix.com")); when(sslSession.getRequestedServerNames()).thenReturn(sniNames); // Create channel and context EmbeddedChannel channel = new EmbeddedChannel(); CurrentPassport.fromChannel(channel); channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).set("192.168.1.1"); SslHandshakeInfoHandler handler = new SslHandshakeInfoHandler(registry, false); SslHandler sslHandler = mock(SslHandler.class); when(sslHandler.engine()).thenReturn(sslEngine); channel.pipeline().addLast("ssl", sslHandler); channel.pipeline().addLast(handler); ChannelHandlerContext ctx = channel.pipeline().context(handler); // Create a failed handshake event SSLException sslException = new SSLException("Received fatal alert: certificate_unknown"); SslHandshakeCompletionEvent failedEvent = new SslHandshakeCompletionEvent(sslException); // Trigger the event handler.userEventTriggered(ctx, failedEvent); // Verify success counter was incremented assertThat(registry.counter( "server.ssl.handshake", "success", "false", "sni", "www.netflix.com", "failure_cause", "Received fatal alert: certificate_unknown") .count()) .isEqualTo(1); // Verify handler was removed from pipeline assertThat(channel.pipeline().context(SslHandshakeInfoHandler.class)).isNull(); } @Test public void handshakeFailureWithClosedChannelException() throws Exception { // Setup mocks Registry registry = mock(Registry.class); Counter counter = mock(Counter.class); when(registry.counter(anyString())).thenReturn(counter); // Create handler with mocked registry SslHandshakeInfoHandler handler = new SslHandshakeInfoHandler(registry, false); // Create channel and context EmbeddedChannel channel = new EmbeddedChannel(); CurrentPassport.fromChannel(channel); channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).set("192.168.1.1"); channel.pipeline().addLast(handler); ChannelHandlerContext ctx = channel.pipeline().context(handler); // Create a failed handshake event with ClosedChannelException ClosedChannelException closedException = new ClosedChannelException(); SslHandshakeCompletionEvent failedEvent = new SslHandshakeCompletionEvent(closedException); // Trigger the event handler.userEventTriggered(ctx, failedEvent); // Verify handler was removed from pipeline assertThat(channel.pipeline().context(SslHandshakeInfoHandler.class)).isNull(); } @Test public void handshakeFailureWithHandshakeTimeout() throws Exception { // Setup mocks Registry registry = mock(Registry.class); Counter counter = mock(Counter.class); when(registry.counter(anyString())).thenReturn(counter); // Create handler with mocked registry SslHandshakeInfoHandler handler = new SslHandshakeInfoHandler(registry, false); // Create channel and context EmbeddedChannel channel = new EmbeddedChannel(); CurrentPassport.fromChannel(channel); channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).set("192.168.1.1"); channel.pipeline().addLast(handler); ChannelHandlerContext ctx = channel.pipeline().context(handler); // Create a failed handshake event with timeout SSLException timeoutException = new SSLException("handshake timed out"); SslHandshakeCompletionEvent failedEvent = new SslHandshakeCompletionEvent(timeoutException); // Trigger the event handler.userEventTriggered(ctx, failedEvent); // Verify handler was removed from pipeline assertThat(channel.pipeline().context(SslHandshakeInfoHandler.class)).isNull(); // Note: Counters should NOT be incremented for timeout as it's handled separately } @ParameterizedTest @ValueSource(strings = {"www.netflix.com", ""}) public void handshakeSuccessWithSNI(String sni) throws Exception { Registry registry = new DefaultRegistry(); // Create handler SslHandshakeInfoHandler handler = new SslHandshakeInfoHandler(registry, false); // Create channel EmbeddedChannel channel = new EmbeddedChannel(); CurrentPassport.fromChannel(channel); // Mock SSL engine and session SSLEngine sslEngine = mock(SSLEngine.class); ExtendedSSLSession sslSession = mock(ExtendedSSLSession.class); X509Certificate serverCert = mock(X509Certificate.class); // Setup SSL session when(sslEngine.getSession()).thenReturn(sslSession); when(sslEngine.getNeedClientAuth()).thenReturn(false); when(sslEngine.getWantClientAuth()).thenReturn(false); when(sslSession.getProtocol()).thenReturn("TLSv1.3"); when(sslSession.getCipherSuite()).thenReturn("TLS_AES_256_GCM_SHA384"); when(sslSession.getLocalCertificates()).thenReturn(new Certificate[] {serverCert}); when(sslSession.getPeerCertificates()).thenReturn(new Certificate[0]); if (!Objects.equals(sni, "")) { List sniNames = Arrays.asList(new SNIHostName("www.netflix.com")); when(sslSession.getRequestedServerNames()).thenReturn(sniNames); } // Add SSL handler and info handler to pipeline SslHandler sslHandler = mock(SslHandler.class); when(sslHandler.engine()).thenReturn(sslEngine); channel.pipeline().addLast("ssl", sslHandler); channel.pipeline().addLast(handler); ChannelHandlerContext ctx = channel.pipeline().context(handler); // Create successful handshake event SslHandshakeCompletionEvent successEvent = SslHandshakeCompletionEvent.SUCCESS; // Trigger the event handler.userEventTriggered(ctx, successEvent); String verifySni = Objects.equals(sni, "") ? "none" : sni; assertThat(registry.counter( "server.ssl.handshake", "success", "true", "sni", verifySni, "protocol", "TLSv1.3", "ciphersuite", "TLS_AES_256_GCM_SHA384", "clientauth", "NONE", "namedgroup", "unknown") .count()) .isEqualTo(1); SslHandshakeInfo info = channel.attr(SslHandshakeInfoHandler.ATTR_SSL_INFO).get(); assertThat(info).isNotNull(); assertThat(info.getRequestedSni()).isEqualTo(verifySni); assertThat(info.getProtocol()).isEqualTo("TLSv1.3"); assertThat(info.getCipherSuite()).isEqualTo("TLS_AES_256_GCM_SHA384"); assertThat(info.getNamedGroup()).isNull(); assertThat(info.getClientAuthRequirement()).isEqualTo(ClientAuth.NONE); assertThat(info.getClientCertificate()).isNull(); assertThat(CurrentPassport.fromChannel(channel).getState()) .isEqualTo(PassportState.SERVER_CH_SSL_HANDSHAKE_COMPLETE); assertThat(channel.pipeline().context(SslHandshakeInfoHandler.class)).isNull(); } @Test public void handshakeSuccessWithNamedGroup() throws Exception { Registry registry = new DefaultRegistry(); SslHandshakeInfoHandler handler = new SslHandshakeInfoHandler(registry, false); EmbeddedChannel channel = new EmbeddedChannel(); CurrentPassport.fromChannel(channel); channel.attr(SslHandshakeInfoHandler.ATTR_SSL_NAMED_GROUP).set("x25519"); SSLEngine sslEngine = mock(SSLEngine.class); ExtendedSSLSession sslSession = mock(ExtendedSSLSession.class); X509Certificate serverCert = mock(X509Certificate.class); when(sslEngine.getSession()).thenReturn(sslSession); when(sslEngine.getNeedClientAuth()).thenReturn(false); when(sslEngine.getWantClientAuth()).thenReturn(false); when(sslSession.getProtocol()).thenReturn("TLSv1.3"); when(sslSession.getCipherSuite()).thenReturn("TLS_AES_128_GCM_SHA256"); when(sslSession.getLocalCertificates()).thenReturn(new Certificate[] {serverCert}); when(sslSession.getPeerCertificates()).thenReturn(new Certificate[0]); when(sslSession.getRequestedServerNames()).thenReturn(List.of(new SNIHostName("www.netflix.com"))); SslHandler sslHandler = mock(SslHandler.class); when(sslHandler.engine()).thenReturn(sslEngine); channel.pipeline().addLast("ssl", sslHandler); channel.pipeline().addLast(handler); ChannelHandlerContext ctx = channel.pipeline().context(handler); handler.userEventTriggered(ctx, SslHandshakeCompletionEvent.SUCCESS); assertThat(registry.counter( "server.ssl.handshake", "success", "true", "sni", "www.netflix.com", "protocol", "TLSv1.3", "ciphersuite", "TLS_AES_128_GCM_SHA256", "clientauth", "NONE", "namedgroup", "x25519") .count()) .isEqualTo(1); SslHandshakeInfo info = channel.attr(SslHandshakeInfoHandler.ATTR_SSL_INFO).get(); assertThat(info).isNotNull(); assertThat(info.getNamedGroup()).isEqualTo("x25519"); assertThat(info.getProtocol()).isEqualTo("TLSv1.3"); assertThat(info.getCipherSuite()).isEqualTo("TLS_AES_128_GCM_SHA256"); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/ssl/BaseSslContextFactoryTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ssl; import static org.assertj.core.api.Assertions.assertThat; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslProvider; import java.lang.reflect.Field; import org.junit.jupiter.api.Test; /** * Tests for {@link BaseSslContextFactory}. */ class BaseSslContextFactoryTest { @Test void testDefaultSslProviderIsOpenSsl() { assertThat(BaseSslContextFactory.chooseSslProvider()).isEqualTo(SslProvider.OPENSSL); } @Test void defaultNamedGroupsMatchNettyDefaults() throws Exception { Field nettyDefaultsField = OpenSsl.class.getDeclaredField("DEFAULT_NAMED_GROUPS"); nettyDefaultsField.setAccessible(true); String[] nettyDefaultNamedGroups = (String[]) nettyDefaultsField.get(null); Field zuulField = BaseSslContextFactory.class.getDeclaredField("DEFAULT_NAMED_GROUPS"); zuulField.setAccessible(true); String[] zuulGroups = (String[]) zuulField.get(null); assertThat(zuulGroups).as("should match netty defaults").containsExactly(nettyDefaultNamedGroups); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/ssl/ClientSslContextFactoryTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ssl; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.spectator.api.DefaultRegistry; import io.netty.handler.ssl.OpenSslClientContext; import io.netty.handler.ssl.SslContext; import java.util.Arrays; import java.util.List; import javax.net.ssl.SSLSessionContext; import org.junit.jupiter.api.Test; /** * Tests for {@link ClientSslContextFactory}. */ class ClientSslContextFactoryTest { @Test void enableTls13() { String[] protos = ClientSslContextFactory.maybeAddTls13(true, "TLSv1.2"); assertThat(Arrays.asList(protos)).isEqualTo(Arrays.asList("TLSv1.3", "TLSv1.2")); } @Test void disableTls13() { String[] protos = ClientSslContextFactory.maybeAddTls13(false, "TLSv1.2"); assertThat(Arrays.asList(protos)).isEqualTo(Arrays.asList("TLSv1.2")); } @Test void testGetSslContext() { ClientSslContextFactory factory = new ClientSslContextFactory(new DefaultRegistry()); SslContext sslContext = factory.getClientSslContext(); assertThat(sslContext).isInstanceOf(OpenSslClientContext.class); assertThat(sslContext.isClient()).isTrue(); assertThat(sslContext.isServer()).isFalse(); SSLSessionContext sessionContext = sslContext.sessionContext(); assertThat(sessionContext.getSessionCacheSize()).isEqualTo(20480); assertThat(sessionContext.getSessionTimeout()).isEqualTo(300); } @Test void testGetProtocols() { ClientSslContextFactory factory = new ClientSslContextFactory(new DefaultRegistry()); assertThat(factory.getProtocols()).isEqualTo(new String[] {"TLSv1.2"}); } @Test void testGetCiphers() throws Exception { ClientSslContextFactory factory = new ClientSslContextFactory(new DefaultRegistry()); List ciphers = factory.getCiphers(); assertThat(ciphers).isNotEmpty(); assertThat(ciphers).doesNotHaveDuplicates(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/ssl/OpenSslTest.java ================================================ /* * Copyright 2023 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.ssl; import static org.assertj.core.api.Assertions.assertThat; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class OpenSslTest { @BeforeEach void beforeEach() { OpenSsl.ensureAvailability(); assertThat(OpenSsl.isAvailable()).isTrue(); } @Test void testBoringSsl() { assertThat(OpenSsl.versionString()).isEqualTo("BoringSSL"); assertThat(SslProvider.isAlpnSupported(SslProvider.OPENSSL)).isTrue(); assertThat(SslProvider.isTlsv13Supported(SslProvider.OPENSSL)).isTrue(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/timeouts/HttpHeadersTimeoutHandlerTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.timeouts; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import com.netflix.spectator.api.Counter; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import java.nio.channels.ClosedChannelException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class HttpHeadersTimeoutHandlerTest { @Mock private Counter timeoutCounter; @Test public void testTimeout() { EmbeddedChannel ch = new EmbeddedChannel( new HttpServerCodec(), new HttpHeadersTimeoutHandler.InboundHandler(() -> true, () -> 0, timeoutCounter, null)); DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); assertThrows(ClosedChannelException.class, () -> ch.writeInbound(request)); verify(timeoutCounter).increment(); assertNull(ch.attr(HttpHeadersTimeoutHandler.HTTP_HEADERS_READ_TIMEOUT_FUTURE) .get()); assertNull( ch.attr(HttpHeadersTimeoutHandler.HTTP_HEADERS_READ_START_TIME).get()); } @Test public void testNoTimeout() { EmbeddedChannel ch = new EmbeddedChannel( new HttpServerCodec(), new HttpHeadersTimeoutHandler.InboundHandler(() -> true, () -> 10000, timeoutCounter, null)); DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); ch.writeInbound(request); verify(timeoutCounter, never()).increment(); assertNull(ch.attr(HttpHeadersTimeoutHandler.HTTP_HEADERS_READ_TIMEOUT_FUTURE) .get()); assertNull( ch.attr(HttpHeadersTimeoutHandler.HTTP_HEADERS_READ_START_TIME).get()); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/netty/timeouts/OriginTimeoutManagerTest.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.netty.timeouts; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfig; import com.netflix.zuul.context.CommonContextKeys; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.origins.NettyOrigin; import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * Origin Timeout Manager Test * * @author Arthur Gonigberg * @since March 23, 2021 */ @ExtendWith(MockitoExtension.class) class OriginTimeoutManagerTest { @Mock private NettyOrigin origin; @Mock private HttpRequestMessage request; private SessionContext context; private IClientConfig requestConfig; private IClientConfig originConfig; private OriginTimeoutManager originTimeoutManager; @BeforeEach void before() { originTimeoutManager = new OriginTimeoutManager(origin); context = new SessionContext(); when(request.getContext()).thenReturn(context); requestConfig = new DefaultClientConfigImpl(); originConfig = new DefaultClientConfigImpl(); context.put(CommonContextKeys.REST_CLIENT_CONFIG, requestConfig); when(origin.getClientConfig()).thenReturn(originConfig); } @Test void computeReadTimeout_default() { Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(OriginTimeoutManager.MAX_OUTBOUND_READ_TIMEOUT_MS.get()); } @Test void computeReadTimeout_requestOnly() { requestConfig.set(CommonClientConfigKey.ReadTimeout, 1000); Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(1000); } @Test void computeReadTimeout_originOnly() { originConfig.set(CommonClientConfigKey.ReadTimeout, 1000); Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(1000); } @Test void computeReadTimeout_bolth_equal() { requestConfig.set(CommonClientConfigKey.ReadTimeout, 1000); originConfig.set(CommonClientConfigKey.ReadTimeout, 1000); Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(1000); } @Test void computeReadTimeout_bolth_originLower() { requestConfig.set(CommonClientConfigKey.ReadTimeout, 1000); originConfig.set(CommonClientConfigKey.ReadTimeout, 100); Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(100); } @Test void computeReadTimeout_bolth_requestLower() { requestConfig.set(CommonClientConfigKey.ReadTimeout, 100); originConfig.set(CommonClientConfigKey.ReadTimeout, 1000); Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(100); } @Test void computeReadTimeout_bolth_enforceMax() { requestConfig.set( CommonClientConfigKey.ReadTimeout, (int) OriginTimeoutManager.MAX_OUTBOUND_READ_TIMEOUT_MS.get() + 1000); originConfig.set( CommonClientConfigKey.ReadTimeout, (int) OriginTimeoutManager.MAX_OUTBOUND_READ_TIMEOUT_MS.get() + 10000); Duration timeout = originTimeoutManager.computeReadTimeout(request, 1); assertThat(timeout.toMillis()).isEqualTo(OriginTimeoutManager.MAX_OUTBOUND_READ_TIMEOUT_MS.get()); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/niws/RequestAttemptTest.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.niws; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import com.netflix.zuul.exception.OutboundErrorType; import com.netflix.zuul.netty.connectionpool.OriginConnectException; import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.Http2Error; import io.netty.handler.codec.http2.Http2Exception; import java.io.IOException; import java.security.cert.CertificateException; import javax.net.ssl.SSLHandshakeException; import org.junit.jupiter.api.Test; public class RequestAttemptTest { @Test void exceptionHandled() { RequestAttempt attempt = new RequestAttempt(1, null, null, "target", "chosen", 200, null, null, 0, 0, 0); attempt.setException(new RuntimeException("runtime failure")); assertThat(attempt.getError()).isEqualTo("runtime failure"); } @Test void originConnectExceptionUnwrapped() { RequestAttempt attempt = new RequestAttempt(1, null, null, "target", "chosen", 200, null, null, 0, 0, 0); attempt.setException(new OriginConnectException( "origin connect failure", new SSLHandshakeException("Invalid tls cert"), OutboundErrorType.CONNECT_ERROR)); assertThat(attempt.getError()).isEqualTo("ORIGIN_CONNECT_ERROR"); assertThat(attempt.getCause()).isEqualTo("Invalid tls cert"); } @Test void originConnectExceptionWithSSLHandshakeCauseUnwrapped() { SSLHandshakeException handshakeException = mock(SSLHandshakeException.class); when(handshakeException.getCause()).thenReturn(new CertificateException("Cert doesn't match expected")); RequestAttempt attempt = new RequestAttempt(1, null, null, "target", "chosen", 200, null, null, 0, 0, 0); attempt.setException(new OriginConnectException( "origin connect failure", handshakeException, OutboundErrorType.CONNECT_ERROR)); assertThat(attempt.getError()).isEqualTo("ORIGIN_CONNECT_ERROR"); assertThat(attempt.getCause()).isEqualTo("Cert doesn't match expected"); } @Test void originConnectExceptionWithCauseNotUnwrapped() { RequestAttempt attempt = new RequestAttempt(1, null, null, "target", "chosen", 200, null, null, 0, 0, 0); attempt.setException(new OriginConnectException( "origin connect failure", new IOException(new RuntimeException("socket failure")), OutboundErrorType.CONNECT_ERROR)); assertThat(attempt.getError()).isEqualTo("ORIGIN_CONNECT_ERROR"); assertThat(attempt.getCause()).isEqualTo("java.lang.RuntimeException: socket failure"); } @Test void h2ExceptionCauseHandled() { // mock out a real-ish h2 stream exception Exception h2Exception = spy(Http2Exception.streamError( 100, Http2Error.REFUSED_STREAM, "Cannot create stream 100 greater than Last-Stream-ID 99 from GOAWAY.", new Object[] {100, 99})); // mock a stacktrace to ensure we don't actually capture it completely when(h2Exception.getStackTrace()).thenReturn(new StackTraceElement[] { new StackTraceElement( DefaultHttp2Connection.class.getCanonicalName(), "createStream", "DefaultHttp2Connection.java", 772), new StackTraceElement( DefaultHttp2Connection.class.getCanonicalName(), "checkNewStreamAllowed", "DefaultHttp2Connection.java", 902) }); RequestAttempt attempt = new RequestAttempt(1, null, null, "target", "chosen", 200, null, null, 0, 0, 0); attempt.setException(h2Exception); assertThat(attempt.getError()) .isEqualTo("Cannot create stream 100 greater than Last-Stream-ID 99 from GOAWAY."); assertThat(attempt.getExceptionType()).isEqualTo("StreamException"); assertThat(attempt.getCause()) .isEqualTo( "io.netty.handler.codec.http2.DefaultHttp2Connection.createStream(DefaultHttp2Connection.java:772)"); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/origins/OriginNameTest.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.origins; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; class OriginNameTest { @Test void getAuthority() { OriginName trusted = OriginName.fromVipAndApp("woodly-doodly", "westerndigital"); assertThat(trusted.getAuthority()).isEqualTo("westerndigital"); } @Test void getMetrics() { OriginName trusted = OriginName.fromVipAndApp("WOODLY-doodly", "westerndigital"); assertThat(trusted.getMetricId()).isEqualTo("woodly-doodly"); assertThat(trusted.getNiwsClientName()).isEqualTo("WOODLY-doodly"); } @Test void equals() { OriginName name1 = OriginName.fromVipAndApp("woodly-doodly", "westerndigital"); OriginName name2 = OriginName.fromVipAndApp("woodly-doodly", "westerndigital", "woodly-doodly"); assertThat(name2).isEqualTo(name1); assertThat(name2.hashCode()).isEqualTo(name1.hashCode()); } @Test @SuppressWarnings("deprecation") void equals_legacy_niws() { OriginName name1 = OriginName.fromVip("woodly-doodly", "westerndigital"); OriginName name2 = OriginName.fromVipAndApp("woodly-doodly", "woodly", "westerndigital"); assertThat(name2).isEqualTo(name1); assertThat(name2.hashCode()).isEqualTo(name1.hashCode()); } @Test void equals_legacy() { OriginName name1 = OriginName.fromVip("woodly-doodly"); OriginName name2 = OriginName.fromVipAndApp("woodly-doodly", "woodly", "woodly-doodly"); assertThat(name2).isEqualTo(name1); assertThat(name2.hashCode()).isEqualTo(name1.hashCode()); } @Test void noNull() { assertThatThrownBy(() -> OriginName.fromVipAndApp(null, "app")).isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> OriginName.fromVipAndApp("vip", null)).isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> OriginName.fromVipAndApp(null, "app", "niws")) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> OriginName.fromVipAndApp("vip", null, "niws")) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> OriginName.fromVipAndApp("vip", "app", null)).isInstanceOf(NullPointerException.class); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/passport/CurrentPassportTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.passport; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import org.junit.jupiter.api.Test; class CurrentPassportTest { @Test void test_findEachPairOf_1pair() { CurrentPassport passport = CurrentPassport.parseFromToString( "CurrentPassport {start_ms=0, [+0=IN_REQ_HEADERS_RECEIVED, +5=FILTERS_INBOUND_START," + " +50=IN_REQ_LAST_CONTENT_RECEIVED, +200=MISC_IO_START, +250=MISC_IO_STOP," + " +350=FILTERS_INBOUND_END, +1117794707=NOW]}"); List pairs = passport.findEachPairOf( PassportState.IN_REQ_HEADERS_RECEIVED, PassportState.IN_REQ_LAST_CONTENT_RECEIVED); assertThat(pairs.size()).isEqualTo(1); assertThat(pairs.get(0).startTime).isEqualTo(0); assertThat(pairs.get(0).endTime).isEqualTo(50); } @Test void test_findEachPairOf_2pairs() { CurrentPassport passport = CurrentPassport.parseFromToString( "CurrentPassport {start_ms=0, [+0=IN_REQ_HEADERS_RECEIVED, +5=FILTERS_INBOUND_START," + " +50=IN_REQ_LAST_CONTENT_RECEIVED, +200=MISC_IO_START, +250=MISC_IO_STOP, +300=MISC_IO_START," + " +350=FILTERS_INBOUND_END, +400=MISC_IO_STOP, +1117794707=NOW]}"); List pairs = passport.findEachPairOf(PassportState.MISC_IO_START, PassportState.MISC_IO_STOP); assertThat(pairs.size()).isEqualTo(2); assertThat(pairs.get(0).startTime).isEqualTo(200); assertThat(pairs.get(0).endTime).isEqualTo(250); assertThat(pairs.get(1).startTime).isEqualTo(300); assertThat(pairs.get(1).endTime).isEqualTo(400); } @Test void test_findEachPairOf_noneFound() { CurrentPassport passport = CurrentPassport.parseFromToString( "CurrentPassport {start_ms=0, [+0=FILTERS_INBOUND_START, +200=MISC_IO_START, +1117794707=NOW]}"); List pairs = passport.findEachPairOf( PassportState.IN_REQ_HEADERS_RECEIVED, PassportState.IN_REQ_LAST_CONTENT_RECEIVED); assertThat(pairs.size()).isEqualTo(0); } @Test void test_findEachPairOf_endButNoStart() { CurrentPassport passport = CurrentPassport.parseFromToString( "CurrentPassport {start_ms=0, [+0=FILTERS_INBOUND_START, +50=IN_REQ_LAST_CONTENT_RECEIVED," + " +200=MISC_IO_START, +1117794707=NOW]}"); List pairs = passport.findEachPairOf( PassportState.IN_REQ_HEADERS_RECEIVED, PassportState.IN_REQ_LAST_CONTENT_RECEIVED); assertThat(pairs.size()).isEqualTo(0); } @Test void test_findEachPairOf_wrongOrder() { CurrentPassport passport = CurrentPassport.parseFromToString( "CurrentPassport {start_ms=0, [+0=FILTERS_INBOUND_START, +50=IN_REQ_LAST_CONTENT_RECEIVED," + " +200=MISC_IO_START, +250=IN_REQ_HEADERS_RECEIVED, +1117794707=NOW]}"); List pairs = passport.findEachPairOf( PassportState.IN_REQ_HEADERS_RECEIVED, PassportState.IN_REQ_LAST_CONTENT_RECEIVED); assertThat(pairs.size()).isEqualTo(0); } @Test void testFindBackwards() { CurrentPassport passport = CurrentPassport.parseFromToString( "CurrentPassport {start_ms=0, [+0=FILTERS_INBOUND_START, +50=IN_REQ_LAST_CONTENT_RECEIVED," + " +200=MISC_IO_START, +250=IN_REQ_HEADERS_RECEIVED, +1117794707=NOW]}"); assertThat(passport.findStateBackwards(PassportState.MISC_IO_START).getTime()) .isEqualTo(200); } @Test void testGetStateWithNoHistory() { assertThat(CurrentPassport.create().getState()).isNull(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/stats/ErrorStatsDataTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for {@link ErrorStatsData}. */ @ExtendWith(MockitoExtension.class) class ErrorStatsDataTest { @Test void testUpdateStats() { ErrorStatsData sd = new ErrorStatsData("route", "test"); sd.update(); assertThat(sd.getCount()).isEqualTo(1); sd.update(); assertThat(sd.getCount()).isEqualTo(2); } @Test void testEquals() { ErrorStatsData sd = new ErrorStatsData("route", "test"); ErrorStatsData sd1 = new ErrorStatsData("route", "test"); ErrorStatsData sd2 = new ErrorStatsData("route", "test1"); ErrorStatsData sd3 = new ErrorStatsData("route", "test"); assertThat(sd1).isEqualTo(sd); assertThat(sd).isEqualTo(sd1); assertThat(sd).isNotEqualTo(sd2); assertThat(sd2).isNotEqualTo(sd3); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/stats/ErrorStatsManagerTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for {@link ErrorStatsManager}. */ @ExtendWith(MockitoExtension.class) class ErrorStatsManagerTest { @Test void testPutStats() { ErrorStatsManager sm = new ErrorStatsManager(); assertThat(sm).isNotNull(); sm.putStats("test", "cause"); assertThat(sm.routeMap.get("test")).isNotNull(); ConcurrentHashMap map = sm.routeMap.get("test"); ErrorStatsData sd = map.get("cause"); assertThat(sd.getCount()).isEqualTo(1); sm.putStats("test", "cause"); assertThat(sd.getCount()).isEqualTo(2); } @Test void testGetStats() { ErrorStatsManager sm = new ErrorStatsManager(); assertThat(sm).isNotNull(); sm.putStats("test", "cause"); assertThat(sm.getStats("test", "cause")).isNotNull(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/stats/RouteStatusCodeMonitorTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; /** * Unit tests for {@link RouteStatusCodeMonitor}. */ class RouteStatusCodeMonitorTest { @Test void testUpdateStats() { RouteStatusCodeMonitor sd = new RouteStatusCodeMonitor("test", 200); assertThat(sd.route).isEqualTo("test"); sd.update(); assertThat(sd.getCount()).isEqualTo(1); sd.update(); assertThat(sd.getCount()).isEqualTo(2); } @Test void testEquals() { RouteStatusCodeMonitor sd = new RouteStatusCodeMonitor("test", 200); RouteStatusCodeMonitor sd1 = new RouteStatusCodeMonitor("test", 200); RouteStatusCodeMonitor sd2 = new RouteStatusCodeMonitor("test1", 200); RouteStatusCodeMonitor sd3 = new RouteStatusCodeMonitor("test", 201); assertThat(sd1).isEqualTo(sd); assertThat(sd).isEqualTo(sd1); assertThat(sd).isNotEqualTo(sd2); assertThat(sd).isNotEqualTo(sd3); assertThat(sd2).isNotEqualTo(sd3); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/stats/StatsManagerTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpRequestInfo; import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for {@link StatsManager}. */ @ExtendWith(MockitoExtension.class) class StatsManagerTest { @Test void testCollectRouteStats() { String route = "test"; int status = 500; StatsManager sm = StatsManager.getManager(); assertThat(sm).isNotNull(); // 1st request sm.collectRouteStats(route, status); ConcurrentHashMap routeStatusMap = sm.routeStatusMap.get("test"); assertThat(routeStatusMap).isNotNull(); // 2nd request sm.collectRouteStats(route, status); } @Test void testGetRouteStatusCodeMonitor() { StatsManager sm = StatsManager.getManager(); assertThat(sm).isNotNull(); sm.collectRouteStats("test", 500); assertThat(sm.getRouteStatusCodeMonitor("test", 500)).isNotNull(); } @Test void testCollectRequestStats() { String host = "api.netflix.com"; String proto = "https"; HttpRequestInfo req = Mockito.mock(HttpRequestInfo.class); Headers headers = new Headers(); when(req.getHeaders()).thenReturn(headers); headers.set(StatsManager.HOST_HEADER, host); headers.set(StatsManager.X_FORWARDED_PROTO_HEADER, proto); when(req.getClientIp()).thenReturn("127.0.0.1"); StatsManager sm = StatsManager.getManager(); sm.collectRequestStats(req); NamedCountingMonitor hostMonitor = sm.getHostMonitor(host); assertThat(hostMonitor).as("hostMonitor should not be null").isNotNull(); NamedCountingMonitor protoMonitor = sm.getProtocolMonitor(proto); assertThat(protoMonitor).as("protoMonitor should not be null").isNotNull(); assertThat(hostMonitor.getCount()).isEqualTo(1); assertThat(protoMonitor.getCount()).isEqualTo(1); } @Test void createsNormalizedHostKey() { assertThat(StatsManager.hostKey("ec2-174-129-179-89.compute-1.amazonaws.com")) .isEqualTo("host_EC2.amazonaws.com"); assertThat(StatsManager.hostKey("12.345.6.789")).isEqualTo("host_IP"); assertThat(StatsManager.hostKey("ip-10-86-83-168")).isEqualTo("host_IP"); assertThat(StatsManager.hostKey("002.ie.llnw.nflxvideo.net")).isEqualTo("host_CDN.nflxvideo.net"); assertThat(StatsManager.hostKey("netflix-635.vo.llnwd.net")).isEqualTo("host_CDN.llnwd.net"); assertThat(StatsManager.hostKey("cdn-0.nflximg.com")).isEqualTo("host_CDN.nflximg.com"); } @Test void extractsClientIpFromXForwardedFor() { String ip1 = "hi"; String ip2 = "hey"; assertThat(StatsManager.extractClientIpFromXForwardedFor(ip1)).isEqualTo(ip1); assertThat(StatsManager.extractClientIpFromXForwardedFor(String.format("%s,%s", ip1, ip2))) .isEqualTo(ip1); assertThat(StatsManager.extractClientIpFromXForwardedFor(String.format("%s, %s", ip1, ip2))) .isEqualTo(ip1); } @Test void isIPv6() { assertThat(StatsManager.isIPv6("0:0:0:0:0:0:0:1")).isTrue(); assertThat(StatsManager.isIPv6("2607:fb10:2:232:72f3:95ff:fe03:a6e7")).isTrue(); assertThat(StatsManager.isIPv6("127.0.0.1")).isFalse(); assertThat(StatsManager.isIPv6("10.2.233.134")).isFalse(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/stats/status/ZuulStatusCategoryTest.java ================================================ /* * Copyright 2024 Netflix, Inc. * * 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. */ package com.netflix.zuul.stats.status; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; /** * @author Justin Guerra * @since 10/29/24 */ public class ZuulStatusCategoryTest { @Test public void categoriesUseUniqueIds() { ZuulStatusCategory[] values = ZuulStatusCategory.values(); Set ids = Arrays.stream(values).map(ZuulStatusCategory::getId).collect(Collectors.toSet()); assertThat(ids.size()).isEqualTo(values.length); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/util/HttpUtilsTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.ZuulMessage; import com.netflix.zuul.message.ZuulMessageImpl; import com.netflix.zuul.message.http.HttpQueryParams; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpRequestMessageImpl; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import org.junit.jupiter.api.Test; /** * Unit tests for {@link HttpUtils}. */ class HttpUtilsTest { @Test void detectsGzip() { assertThat(HttpUtils.isCompressed("gzip")).isTrue(); } @Test void detectsDeflate() { assertThat(HttpUtils.isCompressed("deflate")).isTrue(); } @Test void detectsCompress() { assertThat(HttpUtils.isCompressed("compress")).isTrue(); } @Test void detectsBR() { assertThat(HttpUtils.isCompressed("br")).isTrue(); } @Test void detectsNonGzip() { assertThat(HttpUtils.isCompressed("identity")).isFalse(); } @Test void detectsGzipAmongOtherEncodings() { assertThat(HttpUtils.isCompressed("gzip, deflate")).isTrue(); } @Test void acceptsGzip() { Headers headers = new Headers(); headers.add("Accept-Encoding", "gzip, deflate"); assertThat(HttpUtils.acceptsGzip(headers)).isTrue(); } @Test void acceptsGzip_only() { Headers headers = new Headers(); headers.add("Accept-Encoding", "deflate"); assertThat(HttpUtils.acceptsGzip(headers)).isFalse(); } @Test void stripMaliciousHeaderChars() { assertThat(HttpUtils.stripMaliciousHeaderChars("some\r\nthing")).isEqualTo("something"); assertThat(HttpUtils.stripMaliciousHeaderChars("some thing")).isEqualTo("some thing"); assertThat(HttpUtils.stripMaliciousHeaderChars("\nsome\r\nthing\r")).isEqualTo("something"); assertThat(HttpUtils.stripMaliciousHeaderChars("\r")).isEqualTo(""); assertThat(HttpUtils.stripMaliciousHeaderChars("")).isEqualTo(""); assertThat(HttpUtils.stripMaliciousHeaderChars(null)).isNull(); } @Test void getBodySizeIfKnown_returnsContentLengthValue() { SessionContext context = new SessionContext(); Headers headers = new Headers(); headers.add(com.netflix.zuul.message.http.HttpHeaderNames.CONTENT_LENGTH, "23450"); ZuulMessage msg = new ZuulMessageImpl(context, headers); assertThat(HttpUtils.getBodySizeIfKnown(msg)).isEqualTo(Integer.valueOf(23450)); } @Test void getBodySizeIfKnown_returnsResponseBodySize() { SessionContext context = new SessionContext(); Headers headers = new Headers(); HttpQueryParams queryParams = new HttpQueryParams(); HttpRequestMessage request = new HttpRequestMessageImpl( context, "http", "GET", "/path", queryParams, headers, "127.0.0.1", "scheme", 6666, "server-name"); request.storeInboundRequest(); HttpResponseMessage response = new HttpResponseMessageImpl(context, request, 200); response.setBodyAsText("Hello world"); assertThat(HttpUtils.getBodySizeIfKnown(response)).isEqualTo(Integer.valueOf(11)); } @Test void getBodySizeIfKnown_returnsNull() { SessionContext context = new SessionContext(); Headers headers = new Headers(); ZuulMessage msg = new ZuulMessageImpl(context, headers); assertThat(HttpUtils.getBodySizeIfKnown(msg)).isNull(); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/util/JsonUtilityTest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; /** * Unit tests for {@link JsonUtility}. */ class JsonUtilityTest { // I'm using LinkedHashMap in the testing so I get consistent ordering for the expected results @Test void testSimpleOne() { Map jsonData = new LinkedHashMap(); jsonData.put("myKey", "myValue"); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":\"myValue\"}"; assertThat(json).isEqualTo(expected); } @Test void testSimpleTwo() { Map jsonData = new LinkedHashMap(); jsonData.put("myKey", "myValue"); jsonData.put("myKey2", "myValue2"); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":\"myValue\",\"myKey2\":\"myValue2\"}"; assertThat(json).isEqualTo(expected); } @Test void testNestedMapOne() { Map jsonData = new LinkedHashMap(); jsonData.put("myKey", "myValue"); Map jsonData2 = new LinkedHashMap(); jsonData2.put("myNestedKey", "myNestedValue"); jsonData.put("myNestedData", jsonData2); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":\"myValue\",\"myNestedData\":{\"myNestedKey\":\"myNestedValue\"}}"; assertThat(json).isEqualTo(expected); } @Test void testNestedMapTwo() { Map jsonData = new LinkedHashMap(); jsonData.put("myKey", "myValue"); Map jsonData2 = new LinkedHashMap(); jsonData2.put("myNestedKey", "myNestedValue"); jsonData2.put("myNestedKey2", "myNestedValue2"); jsonData.put("myNestedData", jsonData2); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":\"myValue\",\"myNestedData\":{\"myNestedKey\":\"myNestedValue\",\"myNestedKey2\":\"myNestedValue2\"}}"; assertThat(json).isEqualTo(expected); } @Test void testArrayOne() { Map jsonData = new LinkedHashMap(); int[] numbers = {1, 2, 3, 4}; jsonData.put("myKey", numbers); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":[1,2,3,4]}"; assertThat(json).isEqualTo(expected); } @Test void testArrayTwo() { Map jsonData = new LinkedHashMap(); String[] values = {"one", "two", "three", "four"}; jsonData.put("myKey", values); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":[\"one\",\"two\",\"three\",\"four\"]}"; assertThat(json).isEqualTo(expected); } @Test void testCollectionOne() { Map jsonData = new LinkedHashMap(); ArrayList values = new ArrayList(); values.add("one"); values.add("two"); values.add("three"); values.add("four"); jsonData.put("myKey", values); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":[\"one\",\"two\",\"three\",\"four\"]}"; assertThat(json).isEqualTo(expected); } @Test void testMapAndList() { Map jsonData = new LinkedHashMap(); jsonData.put("myKey", "myValue"); int[] numbers = {1, 2, 3, 4}; jsonData.put("myNumbers", numbers); Map jsonData2 = new LinkedHashMap(); jsonData2.put("myNestedKey", "myNestedValue"); jsonData2.put("myNestedKey2", "myNestedValue2"); String[] values = {"one", "two", "three", "four"}; jsonData2.put("myStringNumbers", values); jsonData.put("myNestedData", jsonData2); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"myKey\":\"myValue\",\"myNumbers\":[1,2,3,4],\"myNestedData\":{\"myNestedKey\":\"myNestedValue\",\"myNestedKey2\":\"myNestedValue2\",\"myStringNumbers\":[\"one\",\"two\",\"three\",\"four\"]}}"; assertThat(json).isEqualTo(expected); } @Test void testArrayOfMaps() { Map jsonData = new LinkedHashMap(); ArrayList> messages = new ArrayList>(); Map message1 = new LinkedHashMap(); message1.put("a", "valueA1"); message1.put("b", "valueB1"); messages.add(message1); Map message2 = new LinkedHashMap(); message2.put("a", "valueA2"); message2.put("b", "valueB2"); messages.add(message2); jsonData.put("messages", messages); String json = JsonUtility.jsonFromMap(jsonData); String expected = "{\"messages\":[{\"a\":\"valueA1\",\"b\":\"valueB1\"},{\"a\":\"valueA2\",\"b\":\"valueB2\"}]}"; assertThat(json).isEqualTo(expected); } } ================================================ FILE: zuul-core/src/test/java/com/netflix/zuul/util/VipUtilsTest.java ================================================ /* * Copyright 2019 Netflix, Inc. * * 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. */ package com.netflix.zuul.util; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; /** * Unit tests for {@link VipUtils}. */ class VipUtilsTest { @Test void testGetVIPPrefix() { assertThatThrownBy(() -> { assertThat(VipUtils.getVIPPrefix("api-test.netflix.net:7001")) .isEqualTo("api-test"); assertThat(VipUtils.getVIPPrefix("api-test.netflix.net")).isEqualTo("api-test"); assertThat(VipUtils.getVIPPrefix("api-test:7001")).isEqualTo("api-test"); assertThat(VipUtils.getVIPPrefix("api-test")).isEqualTo("api-test"); assertThat(VipUtils.getVIPPrefix("")).isEqualTo(""); VipUtils.getVIPPrefix(null); }) .isInstanceOf(NullPointerException.class); } @Test void testExtractAppNameFromVIP() { assertThatThrownBy(() -> { assertThat(VipUtils.extractUntrustedAppNameFromVIP("api-test.netflix.net:7001")) .isEqualTo("api"); assertThat(VipUtils.extractUntrustedAppNameFromVIP("api-test-blah.netflix.net:7001")) .isEqualTo("api"); assertThat(VipUtils.extractUntrustedAppNameFromVIP("api")).isEqualTo("api"); assertThat(VipUtils.extractUntrustedAppNameFromVIP("")).isEqualTo(""); VipUtils.extractUntrustedAppNameFromVIP(null); }) .isInstanceOf(NullPointerException.class); } } ================================================ FILE: zuul-discovery/build.gradle ================================================ apply plugin: "java-library" dependencies { implementation libraries.guava implementation libraries.slf4j api "com.netflix.ribbon:ribbon-loadbalancer:${versions_ribbon}" implementation "com.netflix.ribbon:ribbon-core:${versions_ribbon}" implementation "com.netflix.ribbon:ribbon-eureka:${versions_ribbon}" implementation "com.netflix.ribbon:ribbon-archaius:${versions_ribbon}" // Eureka implementation "com.netflix.eureka:eureka-client:2.0.4" // unfortunately, servo is still a transitive dependency of eureka-client that stopped getting picked up // after switching to version 2 api "com.netflix.servo:servo-core:0.13.2" testImplementation libraries.jupiterApi, libraries.jupiterParams, libraries.jupiterEngine, libraries.junitPlatformLauncher, libraries.mockito, libraries.assertj } test { testLogging { showStandardStreams = false } } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/discovery/DiscoveryResult.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; import com.google.common.annotations.VisibleForTesting; import com.netflix.appinfo.AmazonInfo; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.PortType; import com.netflix.loadbalancer.LoadBalancerStats; import com.netflix.loadbalancer.ServerStats; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import java.util.Locale; import java.util.Objects; import java.util.Optional; import javax.annotation.Nullable; /** * @author Argha C * @since 2/25/21 *

* Wraps a single instance of discovery enabled server, and stats related to it. */ public final class DiscoveryResult implements ResolverResult { private final DiscoveryEnabledServer server; private final ServerStats serverStats; /** * This exists to allow for a semblance of type safety, and encourages avoiding null checks on the underlying Server, * thus representing a sentinel value for an empty resolution result. */ public static final DiscoveryResult EMPTY = DiscoveryResult.from( InstanceInfo.Builder.newBuilder() .setAppName("undefined") .setHostName("undefined") .setPort(-1) .build(), false); public DiscoveryResult(DiscoveryEnabledServer server, LoadBalancerStats lbStats) { this.server = server; Objects.requireNonNull(lbStats, "Loadbalancer stats must be a valid instance"); this.serverStats = lbStats.getSingleServerStat(server); } /** * This solely exists to create a result object from incomplete InstanceInfo. * Usage of this for production code is strongly discouraged, since the underlying instances are prone to memory leaks */ public DiscoveryResult(DiscoveryEnabledServer server) { this.server = server; this.serverStats = new ServerStats() { @Override public String toString() { return "no stats configured for server"; } }; } /** * This convenience method exists for usage in tests. For production usage, please use the constructor linked: * * @see DiscoveryResult#DiscoveryResult(DiscoveryEnabledServer, LoadBalancerStats) */ @VisibleForTesting public static DiscoveryResult from(InstanceInfo instanceInfo, boolean useSecurePort) { DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, useSecurePort); return new DiscoveryResult(server); } public Optional getIPAddr() { if (this.equals(DiscoveryResult.EMPTY)) { return Optional.empty(); } if (server.getInstanceInfo() != null) { String ip = server.getInstanceInfo().getIPAddr(); if (ip != null && !ip.isEmpty()) { return Optional.of(ip); } return Optional.empty(); } return Optional.empty(); } @Override public String getHost() { return server == null ? "undefined" : server.getHost(); } @Override public boolean isDiscoveryEnabled() { return server != null; } @Override public int getPort() { return server == null ? -1 : server.getPort(); } public int getSecurePort() { return server.getInstanceInfo().getSecurePort(); } public boolean isSecurePortEnabled() { return server.getInstanceInfo().isPortEnabled(PortType.SECURE); } public String getTarget() { InstanceInfo instanceInfo = server.getInstanceInfo(); if (server.getPort() == instanceInfo.getSecurePort()) { return instanceInfo.getSecureVipAddress(); } else { return instanceInfo.getVIPAddress(); } } public SimpleMetaInfo getMetaInfo() { return new SimpleMetaInfo(server.getMetaInfo()); } @Nullable public String getAvailabilityZone() { InstanceInfo instanceInfo = server.getInstanceInfo(); if (instanceInfo.getDataCenterInfo() instanceof AmazonInfo) { return ((AmazonInfo) instanceInfo.getDataCenterInfo()).getMetadata().get("availability-zone"); } return null; } public String getZone() { return server.getZone(); } public String getServerId() { return server.getInstanceInfo().getId(); } public DiscoveryEnabledServer getServer() { return server; } @VisibleForTesting ServerStats getServerStats() { return this.serverStats; } public String getASGName() { return server.getInstanceInfo().getASGName(); } public String getAppName() { return server.getInstanceInfo().getAppName().toLowerCase(Locale.ROOT); } public void noteResponseTime(double msecs) { serverStats.noteResponseTime(msecs); } public boolean isCircuitBreakerTripped() { return serverStats.isCircuitBreakerTripped(); } public void incrementActiveRequestsCount() { serverStats.incrementActiveRequestsCount(); } public void incrementOpenConnectionsCount() { serverStats.incrementOpenConnectionsCount(); } public void incrementSuccessiveConnectionFailureCount() { serverStats.incrementSuccessiveConnectionFailureCount(); } public void incrementNumRequests() { serverStats.incrementNumRequests(); } public int getOpenConnectionsCount() { return serverStats.getOpenConnectionsCount(); } public long getTotalRequestsCount() { return serverStats.getTotalRequestsCount(); } public int getActiveRequestsCount() { return serverStats.getActiveRequestsCount(); } public void decrementOpenConnectionsCount() { serverStats.decrementOpenConnectionsCount(); } public void decrementActiveRequestsCount() { serverStats.decrementActiveRequestsCount(); } public void clearSuccessiveConnectionFailureCount() { serverStats.clearSuccessiveConnectionFailureCount(); } public void addToFailureCount() { serverStats.addToFailureCount(); } public void stopPublishingStats() { serverStats.close(); } @Override public int hashCode() { return Objects.hashCode(server); } /** * Two instances are deemed identical if they wrap the same underlying discovery server instance. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof DiscoveryResult)) { return false; } DiscoveryResult other = (DiscoveryResult) obj; return server.equals(other.server); } } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/discovery/DynamicServerResolver.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import com.google.common.collect.Sets; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.DynamicServerListLoadBalancer; import com.netflix.loadbalancer.Server; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import com.netflix.zuul.resolver.Resolver; import com.netflix.zuul.resolver.ResolverListener; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Argha C * @since 2/25/21 *

* Implements a resolver, wrapping a ribbon load-balancer. */ public class DynamicServerResolver implements Resolver { private static final Logger LOG = LoggerFactory.getLogger(DynamicServerResolver.class); private final DynamicServerListLoadBalancer loadBalancer; private ResolverListener listener; @Deprecated public DynamicServerResolver(IClientConfig clientConfig, ResolverListener listener) { this.loadBalancer = createLoadBalancer(clientConfig); this.loadBalancer.addServerListChangeListener(this::onUpdate); this.listener = listener; } public DynamicServerResolver(IClientConfig clientConfig) { this(createLoadBalancer(clientConfig)); } public DynamicServerResolver(DynamicServerListLoadBalancer loadBalancer) { this.loadBalancer = Objects.requireNonNull(loadBalancer); } @Override public void setListener(ResolverListener listener) { if (this.listener != null) { LOG.warn("Ignoring call to setListener, because a listener was already set"); return; } this.listener = Objects.requireNonNull(listener); this.loadBalancer.addServerListChangeListener(this::onUpdate); } @Override public DiscoveryResult resolve(@Nullable Object key) { Server server = loadBalancer.chooseServer(key); return server != null ? new DiscoveryResult((DiscoveryEnabledServer) server, loadBalancer.getLoadBalancerStats()) : DiscoveryResult.EMPTY; } @Override public boolean hasServers() { return !loadBalancer.getReachableServers().isEmpty(); } @Override public void shutdown() { loadBalancer.shutdown(); } private static DynamicServerListLoadBalancer createLoadBalancer(IClientConfig clientConfig) { // TODO(argha-c): Revisit this style of LB initialization post modularization. Ideally the LB should be // pluggable. // Use a hard coded string for the LB default name to avoid a dependency on Ribbon classes. String loadBalancerClassName = clientConfig.get( CommonClientConfigKey.NFLoadBalancerClassName, "com.netflix.loadbalancer.ZoneAwareLoadBalancer"); DynamicServerListLoadBalancer lb; try { Class clazz = Class.forName(loadBalancerClassName); lb = clazz.asSubclass(DynamicServerListLoadBalancer.class) .getConstructor() .newInstance(); lb.initWithNiwsConfig(clientConfig); } catch (Exception e) { Throwables.throwIfUnchecked(e); throw new IllegalStateException("Could not instantiate LoadBalancer " + loadBalancerClassName, e); } return lb; } @VisibleForTesting void onUpdate(List oldList, List newList) { Set oldSet = new HashSet<>(oldList); Set newSet = new HashSet<>(newList); List discoveryResults = Sets.difference(oldSet, newSet).stream() .map(server -> new DiscoveryResult((DiscoveryEnabledServer) server, loadBalancer.getLoadBalancerStats())) .collect(Collectors.toList()); listener.onChange(discoveryResults); } } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/discovery/NonDiscoveryServer.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; import com.netflix.loadbalancer.Server; import java.util.Objects; /** * @author Argha C * @since 3/1/21 *

* This exists merely to wrap a resolver lookup result, that is not discovery enabled. */ public final class NonDiscoveryServer implements ResolverResult { private final Server server; public NonDiscoveryServer(String host, int port) { Objects.requireNonNull(host, "host name"); this.server = new Server(host, validatePort(port)); } @Override public String getHost() { return server.getHost(); } @Override public int getPort() { return server.getPort(); } @Override public boolean isDiscoveryEnabled() { return false; } private int validatePort(int port) { if (port < 0 || port > 0xFFFF) { throw new IllegalArgumentException("port out of range:" + port); } return port; } } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/discovery/ResolverResult.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; /** * @author Argha C * @since 2/25/21 * * Wraps the result of a resolution attempt. * At this time, it doesn't encapsulate a collection of instances, but ideally should. */ public interface ResolverResult { // TODO(argha-c): This should ideally model returning a collection of host/port pairs. public String getHost(); public int getPort(); public boolean isDiscoveryEnabled(); } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/discovery/SimpleMetaInfo.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; import com.netflix.loadbalancer.Server.MetaInfo; /** * @author Argha C * @since 2/25/21 * * placeholder to mimic metainfo for a non-Eureka enabled server. * This exists to preserve compatibility with some current logic, but should be revisited. */ public final class SimpleMetaInfo { private final MetaInfo metaInfo; public SimpleMetaInfo(MetaInfo metaInfo) { this.metaInfo = metaInfo; } public String getServerGroup() { return metaInfo.getServerGroup(); } public String getServiceIdForDiscovery() { return metaInfo.getServiceIdForDiscovery(); } public String getInstanceId() { return metaInfo.getInstanceId(); } } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/resolver/Resolver.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.resolver; /** * @author Argha C * @since 2/25/21 * * Resolves a key to a discovery result type. */ public interface Resolver { /** * * @param key unique identifier that may be used by certain resolvers as part of lookup. Implementations * can narrow this down to be nullable. * @return the result of a resolver lookup */ // TODO(argha-c) Param needs to be typed, once the ribbon LB lookup API is figured out. T resolve(Object key); /** * @return true if the resolver has available servers, false otherwise */ boolean hasServers(); /** * hook to perform activities on shutdown */ void shutdown(); default void setListener(ResolverListener listener) {} } ================================================ FILE: zuul-discovery/src/main/java/com/netflix/zuul/resolver/ResolverListener.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.resolver; import java.util.List; /** * @author Argha C * @since 2/25/21 * * Listener for resolver updates. */ public interface ResolverListener { /** * Hook to respond to resolver updates * @param removedSet the servers removed from the latest resolver update, but included in the previous update. */ void onChange(List removedSet); } ================================================ FILE: zuul-discovery/src/test/java/com/netflix/zuul/discovery/DiscoveryResultTest.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.PortType; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.loadbalancer.DynamicServerListLoadBalancer; import com.netflix.loadbalancer.Server; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import java.util.Optional; import org.junit.jupiter.api.Test; class DiscoveryResultTest { @Test void hashCodeForNull() { DiscoveryResult discoveryResult = new DiscoveryResult(null); assertThat(discoveryResult.hashCode()).isEqualTo(0); } @Test void serverStatsForEmptySentinel() { assertThat(DiscoveryResult.EMPTY.getServerStats().toString()).isEqualTo("no stats configured for server"); } @Test void hostAndPortForNullServer() { DiscoveryResult discoveryResult = new DiscoveryResult(null); assertThat(discoveryResult.getHost()).isEqualTo("undefined"); assertThat(discoveryResult.getPort()).isEqualTo(-1); } @Test void serverStatsCacheForSameServer() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("serverstats-cache") .setHostName("serverstats-cache") .setPort(7777) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, false); DiscoveryEnabledServer serverSecure = new DiscoveryEnabledServer(instanceInfo, true); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); DiscoveryResult result1 = new DiscoveryResult(serverSecure, lb.getLoadBalancerStats()); assertThat(result.getServerStats()).isSameAs(result1.getServerStats()); } @Test void serverStatsDifferForDifferentServer() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("serverstats-cache") .setHostName("serverstats-cache") .setPort(7777) .build(); InstanceInfo otherInstance = InstanceInfo.Builder.newBuilder() .setAppName("serverstats-cache-2") .setHostName("serverstats-cache-2") .setPort(7777) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, false); DiscoveryEnabledServer serverSecure = new DiscoveryEnabledServer(otherInstance, false); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); DiscoveryResult result1 = new DiscoveryResult(serverSecure, lb.getLoadBalancerStats()); assertThat(result.getServerStats()).isNotSameAs(result1.getServerStats()); } @Test void ipAddrV4FromInstanceInfo() { String ipAddr = "100.1.0.1"; InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("ipAddrv4") .setHostName("ipAddrv4") .setIPAddr(ipAddr) .setPort(7777) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, false); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); assertThat(result.getIPAddr()).isEqualTo(Optional.of(ipAddr)); } @Test void ipAddrEmptyForIncompleteInstanceInfo() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("ipAddrMissing") .setHostName("ipAddrMissing") .setPort(7777) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, false); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); assertThat(result.getIPAddr()).isEqualTo(Optional.empty()); } @Test void sameUnderlyingInstanceInfoEqualsSameResult() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("server-equality") .setHostName("server-equality") .setPort(7777) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, false); DiscoveryEnabledServer otherServer = new DiscoveryEnabledServer(instanceInfo, false); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); DiscoveryResult otherResult = new DiscoveryResult(otherServer, lb.getLoadBalancerStats()); assertThat(result).isEqualTo(otherResult); } @Test void serverInstancesExposingDiffPortsAreNotEqual() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("server-equality") .setHostName("server-equality") .setPort(7777) .build(); InstanceInfo otherPort = InstanceInfo.Builder.newBuilder() .setAppName("server-equality") .setHostName("server-equality") .setPort(9999) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, false); DiscoveryEnabledServer otherServer = new DiscoveryEnabledServer(otherPort, false); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); DiscoveryResult otherResult = new DiscoveryResult(otherServer, lb.getLoadBalancerStats()); assertThat(result).isNotEqualTo(otherResult); } @Test void securePortMustCheckInstanceInfo() { InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName("secure-port") .setHostName("secure-port") .setPort(7777) .enablePort(PortType.SECURE, false) .build(); InstanceInfo secureEnabled = InstanceInfo.Builder.newBuilder() .setAppName("secure-port") .setHostName("secure-port") .setPort(7777) .enablePort(PortType.SECURE, true) .build(); DiscoveryEnabledServer server = new DiscoveryEnabledServer(instanceInfo, true); DiscoveryEnabledServer secureServer = new DiscoveryEnabledServer(secureEnabled, true); DynamicServerListLoadBalancer lb = new DynamicServerListLoadBalancer<>(new DefaultClientConfigImpl()); DiscoveryResult result = new DiscoveryResult(server, lb.getLoadBalancerStats()); DiscoveryResult secure = new DiscoveryResult(secureServer, lb.getLoadBalancerStats()); assertThat(result.isSecurePortEnabled()).isFalse(); assertThat(secure.isSecurePortEnabled()).isTrue(); } } ================================================ FILE: zuul-discovery/src/test/java/com/netflix/zuul/discovery/DynamicServerResolverTest.java ================================================ /* * Copyright 2021 Netflix, Inc. * * 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. */ package com.netflix.zuul.discovery; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.netflix.appinfo.InstanceInfo; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import com.netflix.zuul.resolver.ResolverListener; import java.util.List; import org.junit.jupiter.api.Test; class DynamicServerResolverTest { @Test void verifyListenerUpdates() { class CustomListener implements ResolverListener { private List resultSet = Lists.newArrayList(); @Override public void onChange(List changedSet) { resultSet = changedSet; } public List updatedList() { return resultSet; } } CustomListener listener = new CustomListener(); DynamicServerResolver resolver = new DynamicServerResolver(new DefaultClientConfigImpl()); resolver.setListener(listener); InstanceInfo first = InstanceInfo.Builder.newBuilder() .setAppName("zuul-discovery-1") .setHostName("zuul-discovery-1") .setIPAddr("100.10.10.1") .setPort(443) .build(); InstanceInfo second = InstanceInfo.Builder.newBuilder() .setAppName("zuul-discovery-2") .setHostName("zuul-discovery-2") .setIPAddr("100.10.10.2") .setPort(443) .build(); DiscoveryEnabledServer server1 = new DiscoveryEnabledServer(first, true); DiscoveryEnabledServer server2 = new DiscoveryEnabledServer(second, true); resolver.onUpdate(ImmutableList.of(server1, server2), ImmutableList.of()); assertThat(listener.updatedList()).containsExactly(new DiscoveryResult(server1), new DiscoveryResult(server2)); } @Test void properSentinelValueWhenServersUnavailable() { DynamicServerResolver resolver = new DynamicServerResolver(new DefaultClientConfigImpl()); DiscoveryResult nonExistentServer = resolver.resolve(null); assertThat(nonExistentServer).isSameAs(DiscoveryResult.EMPTY); assertThat(nonExistentServer.getHost()).isEqualTo("undefined"); assertThat(nonExistentServer.getPort()).isEqualTo(-1); } } ================================================ FILE: zuul-integration-test/build.gradle ================================================ apply plugin: "java" apply plugin: 'application' dependencies { implementation project(":zuul-core"), project(":zuul-discovery") implementation "io.netty:netty-transport-classes-epoll" implementation "com.netflix.eureka:eureka-client:2.0.4" implementation "com.netflix.ribbon:ribbon-eureka:$versions_ribbon" implementation "com.netflix.ribbon:ribbon-loadbalancer:$versions_ribbon" implementation 'commons-configuration:commons-configuration:1.10' annotationProcessor project(":zuul-processor") testImplementation 'org.wiremock:wiremock:3.10.0' testImplementation 'javax.servlet:javax.servlet-api:4.0.1' testImplementation libraries.assertj, libraries.jupiterApi, libraries.jupiterParams, libraries.jupiterEngine, libraries.junitPlatformLauncher, libraries.jupiterMockito, libraries.okhttp testImplementation "io.netty:netty-transport-native-io_uring" // testImplementation "io.netty.incubator:netty-transport-native-io_uring::linux-x86_64" testImplementation "org.slf4j:slf4j-api:2.0.16" testImplementation 'org.apache.commons:commons-lang3:3.18.0' testImplementation "com.aayushatharva.brotli4j:brotli4j:$versions_brotli4j" testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.25.2' testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl:2.25.2' testRuntimeOnly "com.aayushatharva.brotli4j:native-osx-aarch64:$versions_brotli4j" testRuntimeOnly "com.aayushatharva.brotli4j:native-osx-x86_64:$versions_brotli4j" testRuntimeOnly "com.aayushatharva.brotli4j:native-linux-x86_64:$versions_brotli4j" testRuntimeOnly "com.aayushatharva.brotli4j:native-linux-aarch64:$versions_brotli4j" testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64" } tasks.withType(Test).all { systemProperty("io.netty.leakDetection.level", "paranoid") } tasks.withType(PublishToMavenRepository) { onlyIf { false } } tasks.withType(PublishToMavenLocal) { onlyIf { false } } tasks.withType(PublishToIvyRepository) { onlyIf { false } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/netty/common/metrics/CustomLeakDetector.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.netty.common.metrics; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; public class CustomLeakDetector extends InstrumentedResourceLeakDetector { private static final List GLOBAL_REGISTRY = new CopyOnWriteArrayList<>(); public static void assertZeroLeaks() { List leaks = GLOBAL_REGISTRY.stream() .filter(detector -> detector.leakCounter.get() > 0) .collect(Collectors.toList()); assertThat(leaks.isEmpty()).as("LEAKS DETECTED: " + leaks).isTrue(); } private final String resourceTypeName; public CustomLeakDetector(Class resourceType, int samplingInterval) { super(resourceType, samplingInterval); this.resourceTypeName = resourceType.getSimpleName(); GLOBAL_REGISTRY.add(this); } public CustomLeakDetector(Class resourceType, int samplingInterval, long maxActive) { this(resourceType, samplingInterval); } @Override public String toString() { return "CustomLeakDetector: " + this.resourceTypeName + " leakCount=" + leakCounter.get(); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/BaseIntegrationTest.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import com.aayushatharva.brotli4j.decoder.DecoderJNI; import com.aayushatharva.brotli4j.decoder.DirectDecompress; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.http.Fault; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.stubbing.Scenario; import com.google.common.collect.ImmutableSet; import com.netflix.config.ConfigurationManager; import com.netflix.netty.common.metrics.CustomLeakDetector; import com.netflix.zuul.integration.server.HeaderNames; import com.netflix.zuul.integration.server.TestUtil; import io.netty.channel.epoll.Epoll; import io.netty.handler.codec.compression.Brotli; import io.netty.util.ResourceLeakDetector; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.Socket; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import okhttp3.Call; import okhttp3.Callback; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.BufferedSink; import org.apache.commons.configuration.AbstractConfiguration; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; abstract class BaseIntegrationTest { static { System.setProperty("io.netty.customResourceLeakDetector", CustomLeakDetector.class.getCanonicalName()); } private static final Duration CLIENT_READ_TIMEOUT = Duration.ofSeconds(3); static final Duration ORIGIN_READ_TIMEOUT = Duration.ofSeconds(1); @RegisterExtension static WireMockExtension wireMockExtension = WireMockExtension.newInstance() .configureStaticDsl(true) .options(wireMockConfig() .maxLoggedResponseSize(1000) .dynamicPort() .useChunkedTransferEncoding(Options.ChunkedEncodingPolicy.ALWAYS) .notifier(new Slf4jNotifier(true))) .build(); @BeforeAll static void beforeAll() { assertThat(ResourceLeakDetector.isEnabled()).isTrue(); assertThat(ResourceLeakDetector.getLevel()).isEqualTo(ResourceLeakDetector.Level.PARANOID); int wireMockPort = wireMockExtension.getPort(); AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("api.ribbon.listOfServers", "127.0.0.1:" + wireMockPort); CustomLeakDetector.assertZeroLeaks(); } @AfterAll static void afterAll() { CustomLeakDetector.assertZeroLeaks(); ConfigurationManager.getConfigInstance().clear(); } private final int zuulServerPort; private final String zuulBaseUri; private String pathSegment; private WireMock wireMock; public BaseIntegrationTest(int zuulServerPort) { this.zuulServerPort = zuulServerPort; zuulBaseUri = "http://localhost:" + this.zuulServerPort; } @BeforeEach void beforeEachTest() { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("server.http.request.headers.read.timeout.enabled", false); config.setProperty("server.http.request.headers.read.timeout", 10000); this.pathSegment = randomPathSegment(); this.wireMock = wireMockExtension.getRuntimeInfo().getWireMock(); } private static OkHttpClient setupOkHttpClient(Protocol... protocols) { return new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.MILLISECONDS) .readTimeout(CLIENT_READ_TIMEOUT) .followRedirects(false) .followSslRedirects(false) .retryOnConnectionFailure(false) .protocols(Arrays.asList(protocols)) .build(); } private Request.Builder setupRequestBuilder(boolean requestBodyBuffering, boolean responseBodyBuffering) { HttpUrl url = new HttpUrl.Builder() .scheme("http") .host("localhost") .port(zuulServerPort) .addPathSegment(pathSegment) .addQueryParameter("bufferRequestBody", "" + requestBodyBuffering) .addQueryParameter("bufferResponseBody", "" + responseBodyBuffering) .build(); return new Request.Builder().url(url); } static Stream arguments() { List list = new ArrayList(); for (Protocol protocol : ImmutableSet.of(Protocol.HTTP_1_1)) { for (boolean requestBodyBuffering : ImmutableSet.of(Boolean.TRUE, Boolean.FALSE)) { for (boolean responseBodyBuffering : ImmutableSet.of(Boolean.TRUE, Boolean.FALSE)) { list.add(Arguments.of( protocol.name(), setupOkHttpClient(protocol), requestBodyBuffering, responseBodyBuffering)); } } } return list.stream(); } @ParameterizedTest @MethodSource("arguments") void httpGetHappyPath( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()).willReturn(ok().withBody("hello world"))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("hello world"); verifyResponseHeaders(response); } @ParameterizedTest @MethodSource("arguments") void httpPostHappyPath( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(post(anyUrl()).willReturn(ok().withBody("Thank you next"))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .post(RequestBody.create("Simple POST request body".getBytes(StandardCharsets.UTF_8))) .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("Thank you next"); verifyResponseHeaders(response); } @Test void httpPostWithSeveralChunks() throws Exception { OkHttpClient okHttp = setupOkHttpClient(Protocol.HTTP_1_1); wireMock.register(post(anyUrl()).willReturn(ok().withBody("Thank you next"))); StreamingRequestBody body = new StreamingRequestBody(); Request request = setupRequestBuilder(true, false).post(body).build(); SimpleCallback simpleCallback = new SimpleCallback(); okHttp.newCall(request).enqueue(simpleCallback); body.write("chunk1"); body.write("chunk2"); body.write("chunk3"); body.stop(); Response response = simpleCallback.getResponse(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("Thank you next"); verifyResponseHeaders(response); } @ParameterizedTest @MethodSource("arguments") void httpPostWithInvalidHostHeader( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(post(anyUrl()).willReturn(ok().withBody("Thank you next"))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .addHeader("Host", "_invalid_hostname_") .post(RequestBody.create("Simple POST request body".getBytes(StandardCharsets.UTF_8))) .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(500); verify(0, anyRequestedFor(anyUrl())); } @ParameterizedTest @MethodSource("arguments") void httpGetFailsDueToOriginReadTimeout( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()) .willReturn(ok().withFixedDelay((int) ORIGIN_READ_TIMEOUT.toMillis() + 50) .withBody("Slow poke"))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(504); assertThat(response.body().string()).isEqualTo(""); verifyResponseHeaders(response); } @ParameterizedTest @MethodSource("arguments") void httpGetHappyPathWithHeadersReadTimeout( final String description, final OkHttpClient okHttp, final boolean requestBodyBuffering, final boolean responseBodyBuffering) throws Exception { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("server.http.request.headers.read.timeout.enabled", true); wireMock.register(get(anyUrl()).willReturn(ok().withBody("hello world"))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("hello world"); verifyResponseHeaders(response); } @ParameterizedTest @MethodSource("arguments") void httpPostHappyPathWithHeadersReadTimeout( final String description, final OkHttpClient okHttp, final boolean requestBodyBuffering, final boolean responseBodyBuffering) throws Exception { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("server.http.request.headers.read.timeout.enabled", true); wireMock.register(post(anyUrl()).willReturn(ok().withBody("Thank you next"))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .post(RequestBody.create("Simple POST request body".getBytes(StandardCharsets.UTF_8))) .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("Thank you next"); verifyResponseHeaders(response); } @Test void httpGetFailsDueToHeadersReadTimeout() throws Exception { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("server.http.request.headers.read.timeout.enabled", true); config.setProperty("server.http.request.headers.read.timeout", 100); Socket slowClient = new Socket("localhost", zuulServerPort); Thread.sleep(500); // end of stream reached because zuul closed the connection assertThat(slowClient.getInputStream().read()).isEqualTo(-1); slowClient.close(); } @ParameterizedTest @MethodSource("arguments") void httpGetFailsDueToMalformedResponseChunk( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()).willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); int expectedStatusCode = responseBodyBuffering ? 504 : 200; assertThat(response.code()).isEqualTo(expectedStatusCode); response.close(); } @ParameterizedTest @MethodSource("arguments") void zuulWillRetryHttpGetWhenOriginReturns500( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()).willReturn(aResponse().withStatus(500))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(500); assertThat(response.body().string()).isEqualTo(""); verify(2, getRequestedFor(anyUrl())); } @ParameterizedTest @MethodSource("arguments") void zuulWillRetryHttpGetWhenOriginReturns503( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()).willReturn(aResponse().withStatus(503))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(503); assertThat(response.body().string()).isEqualTo(""); verify(2, getRequestedFor(anyUrl())); } @Test void zuulWillRetryHttpGetWhenOriginReturns503AndRetryIsSuccessful() throws Exception { OkHttpClient okHttp = setupOkHttpClient(Protocol.HTTP_1_1); stubFor(get(anyUrl()) .inScenario("getRetry") .whenScenarioStateIs(Scenario.STARTED) .willSetStateTo("failFirst") .willReturn(aResponse().withStatus(200))); stubFor(get(anyUrl()) .inScenario("getRetry") .whenScenarioStateIs("failFirst") .willSetStateTo("allowSecond") .willReturn(aResponse().withStatus(503))); stubFor(get(anyUrl()) .inScenario("getRetry") .whenScenarioStateIs("allowSecond") .willReturn(aResponse().withStatus(200).withBody("yo"))); // first call just to ensure a pooled connection will be used later Request getRequest = setupRequestBuilder(false, false).get().build(); assertThat(okHttp.newCall(getRequest).execute().code()).isEqualTo(200); Request request = setupRequestBuilder(false, false).get().build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("yo"); verify(3, getRequestedFor(anyUrl())); } @Test void zuulWillRetryHttpPostWhenOriginReturns503AndBodyBuffered() throws Exception { OkHttpClient okHttp = setupOkHttpClient(Protocol.HTTP_1_1); // make sure a pooled connection will be used in ProxyEndpoint wireMock.register(get(anyUrl()).willReturn(aResponse().withStatus(200))); Request getRequest = setupRequestBuilder(false, false).get().build(); assertThat(okHttp.newCall(getRequest).execute().code()).isEqualTo(200); stubFor(post(anyUrl()) .inScenario("retry200") .whenScenarioStateIs(Scenario.STARTED) .willSetStateTo("secondPasses") .willReturn(aResponse().withStatus(503))); stubFor(post(anyUrl()) .inScenario("retry200") .whenScenarioStateIs("secondPasses") .willReturn(aResponse().withStatus(200).withBody("yo"))); StreamingRequestBody body = new StreamingRequestBody(); Request request = setupRequestBuilder(true, false).post(body).build(); body.write("yo"); body.write("yo1"); body.write("yo2"); body.write("yo3"); body.stop(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("yo"); verify(2, postRequestedFor(anyUrl())); } @ParameterizedTest @MethodSource("arguments") void httpGetReturnsStatus500DueToConnectionResetByPeer( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()).willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(500); assertThat(response.body().string()).isEqualTo(""); verify(1, getRequestedFor(anyUrl())); } @ParameterizedTest @MethodSource("arguments") void httpGet_ServerChunkedDribbleDelay( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody("Hello world, is anybody listening?") .withChunkedDribbleDelay(10, (int) CLIENT_READ_TIMEOUT.toMillis() + 500))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .build(); Response response = okHttp.newCall(request).execute(); int expectedStatusCode = responseBodyBuffering ? 504 : 200; assertThat(response.code()).isEqualTo(expectedStatusCode); response.close(); } @ParameterizedTest @MethodSource("arguments") void blockRequestWithMultipleHostHeaders( String description, OkHttpClient okHttp, boolean requestBodyBuffering, boolean responseBodyBuffering) throws Exception { wireMock.register(get(anyUrl()).willReturn(aResponse().withStatus(200))); Request request = setupRequestBuilder(requestBodyBuffering, responseBodyBuffering) .get() .addHeader("Host", "aaa.example.com") .addHeader("Host", "aaa.foobar.com") .build(); Response response = okHttp.newCall(request).execute(); assertThat(response.code()).isEqualTo(500); verify(0, anyRequestedFor(anyUrl())); response.close(); } @Test @Disabled void deflateOnly() throws Exception { String expectedResponseBody = TestUtil.COMPRESSIBLE_CONTENT; wireMock.register(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody(expectedResponseBody) .withHeader("Content-Type", TestUtil.COMPRESSIBLE_CONTENT_TYPE))); URL url = new URL(zuulBaseUri); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setAllowUserInteraction(false); connection.setRequestProperty("Accept-Encoding", "deflate"); InputStream inputStream = connection.getInputStream(); assertThat(connection.getResponseCode()).isEqualTo(200); assertThat(connection.getHeaderField("Content-Type")).isEqualTo("text/plain"); assertThat(connection.getHeaderField("Content-Encoding")).isEqualTo("deflate"); byte[] compressedData = IOUtils.toByteArray(inputStream); Inflater inflater = new Inflater(); inflater.setInput(compressedData); byte[] result = new byte[1000]; int nBytes = inflater.inflate(result); String text = new String(result, 0, nBytes, TestUtil.CHARSET); assertThat(text).isEqualTo(expectedResponseBody); inputStream.close(); connection.disconnect(); } @Test void gzipOnly() throws Exception { String expectedResponseBody = TestUtil.COMPRESSIBLE_CONTENT; wireMock.register(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody(expectedResponseBody) .withHeader("Content-Type", TestUtil.COMPRESSIBLE_CONTENT_TYPE))); URL url = new URL(zuulBaseUri); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setAllowUserInteraction(false); connection.setRequestProperty("Accept-Encoding", "gzip"); InputStream inputStream = connection.getInputStream(); assertThat(connection.getResponseCode()).isEqualTo(200); assertThat(connection.getHeaderField("Content-Type")).isEqualTo("text/plain"); assertThat(connection.getHeaderField("Content-Encoding")).isEqualTo("gzip"); GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream); byte[] data = IOUtils.toByteArray(gzipInputStream); String text = new String(data, TestUtil.CHARSET); assertThat(text).isEqualTo(expectedResponseBody); inputStream.close(); gzipInputStream.close(); connection.disconnect(); } @Test void brotliOnly() throws Throwable { Brotli.ensureAvailability(); String expectedResponseBody = TestUtil.COMPRESSIBLE_CONTENT; wireMock.register(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody(expectedResponseBody) .withHeader("Content-Type", TestUtil.COMPRESSIBLE_CONTENT_TYPE))); URL url = new URL(zuulBaseUri); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setAllowUserInteraction(false); connection.setRequestProperty("Accept-Encoding", "br"); InputStream inputStream = connection.getInputStream(); assertThat(connection.getResponseCode()).isEqualTo(200); assertThat(connection.getHeaderField("Content-Type")).isEqualTo("text/plain"); assertThat(connection.getHeaderField("Content-Encoding")).isEqualTo("br"); byte[] compressedData = IOUtils.toByteArray(inputStream); assertThat(compressedData.length > 0).isTrue(); DirectDecompress decompressResult = DirectDecompress.decompress(compressedData); assertThat(decompressResult.getResultStatus()).isEqualTo(DecoderJNI.Status.DONE); assertThat(new String(decompressResult.getDecompressedData(), TestUtil.CHARSET)) .isEqualTo("Hello Hello Hello Hello Hello"); inputStream.close(); connection.disconnect(); } @Test void noCompression() throws Exception { String expectedResponseBody = TestUtil.COMPRESSIBLE_CONTENT; wireMock.register(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody(expectedResponseBody) .withHeader("Content-Type", TestUtil.COMPRESSIBLE_CONTENT_TYPE))); URL url = new URL(zuulBaseUri); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setAllowUserInteraction(false); connection.setRequestProperty("Accept-Encoding", ""); // no compression InputStream inputStream = connection.getInputStream(); assertThat(connection.getResponseCode()).isEqualTo(200); assertThat(connection.getHeaderField("Content-Type")).isEqualTo("text/plain"); assertThat(connection.getHeaderField("Content-Encoding")).isNull(); byte[] data = IOUtils.toByteArray(inputStream); String text = new String(data, TestUtil.CHARSET); assertThat(text).isEqualTo(expectedResponseBody); inputStream.close(); connection.disconnect(); } @Test void jumboOriginResponseShouldBeChunked() throws Exception { String expectedResponseBody = TestUtil.JUMBO_RESPONSE_BODY; wireMock.register(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody(expectedResponseBody) .withHeader("Content-Type", TestUtil.COMPRESSIBLE_CONTENT_TYPE))); URL url = new URL(zuulBaseUri); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setAllowUserInteraction(false); connection.setRequestProperty("Accept-Encoding", ""); // no compression InputStream inputStream = connection.getInputStream(); assertThat(connection.getResponseCode()).isEqualTo(200); assertThat(connection.getHeaderField("Content-Type")).isEqualTo("text/plain"); assertThat(connection.getHeaderField("Content-Encoding")).isNull(); assertThat(connection.getHeaderField("Transfer-Encoding")).isEqualTo("chunked"); byte[] data = IOUtils.toByteArray(inputStream); String text = new String(data, TestUtil.CHARSET); assertThat(text).isEqualTo(expectedResponseBody); inputStream.close(); connection.disconnect(); } @Test @EnabledOnOs(value = {OS.LINUX}) void epollIsAvailableOnLinux() { if (Epoll.unavailabilityCause() != null) { Epoll.unavailabilityCause().printStackTrace(); } assertThat(Epoll.isAvailable()).isTrue(); } private static String randomPathSegment() { return UUID.randomUUID().toString(); } private static void verifyResponseHeaders(Response response) { assertThat(response.header(HeaderNames.REQUEST_ID)).startsWith("RQ-"); } private static class SimpleCallback implements Callback { private final CompletableFuture future = new CompletableFuture<>(); @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { future.completeExceptionally(e); } @Override public void onResponse(@NotNull Call call, @NotNull Response response) { future.complete(response); } public Response getResponse() { try { return future.get(5, TimeUnit.SECONDS); } catch (Exception e) { throw new AssertionError("did not get response in a reasonable amount of time"); } } } private static class StreamingRequestBody extends RequestBody { private static final String SENTINEL = "sentinel"; private final BlockingQueue data = new LinkedBlockingQueue<>(); void write(String text) { data.add(text); } void stop() { data.add(SENTINEL); } @Override public MediaType contentType() { return MediaType.get("text/plain"); } @Override public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException { String next; try { while (!(next = data.take()).equals(SENTINEL)) { bufferedSink.writeUtf8(next); bufferedSink.flush(); } } catch (InterruptedException e) { throw new RuntimeException(e); } } } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/MultiEventLoopIntegrationTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration; import org.junit.jupiter.api.extension.RegisterExtension; /** * Runs integration tests using multiple event loop threads to exercise less deterministic behavior around connection * pooling * * @author Justin Guerra * @since 6/9/25 */ public class MultiEventLoopIntegrationTest extends BaseIntegrationTest { @RegisterExtension static ZuulServerExtension ZUUL_EXTENSION = ZuulServerExtension.newBuilder() .withEventLoopThreads(4) .withOriginReadTimeout(ORIGIN_READ_TIMEOUT) .build(); public MultiEventLoopIntegrationTest() { super(ZUUL_EXTENSION.getServerPort()); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/SingleEventLoopIntegrationTest.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration; import org.junit.jupiter.api.extension.RegisterExtension; /** * Runs integration tests using a single event loop thread for deterministic results around connection pooling * * @author Justin Guerra * @since 6/9/25 */ public class SingleEventLoopIntegrationTest extends BaseIntegrationTest { @RegisterExtension static ZuulServerExtension ZUUL_EXTENSION = ZuulServerExtension.newBuilder() .withEventLoopThreads(1) .withOriginReadTimeout(ORIGIN_READ_TIMEOUT) .build(); public SingleEventLoopIntegrationTest() { super(ZUUL_EXTENSION.getServerPort()); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/ZuulServerExtension.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.config.ConfigurationManager; import com.netflix.zuul.integration.server.Bootstrap; import java.io.IOException; import java.net.ServerSocket; import java.time.Duration; import java.util.Objects; import org.apache.commons.configuration.AbstractConfiguration; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; /** * Simple extension for managing the lifecycle of a zuul server for use in integration testing * * @author Justin Guerra * @since 6/9/25 */ public class ZuulServerExtension implements AfterAllCallback, BeforeAllCallback { private final int eventLoopThreads; private final Duration originReadTimeout; private Bootstrap bootstrap; private int serverPort; private ZuulServerExtension(Builder builder) { this.eventLoopThreads = builder.eventLoopThreads; this.originReadTimeout = builder.originReadTimeout; } @Override public void beforeAll(ExtensionContext context) throws Exception { serverPort = findAvailableTcpPort(); AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.setProperty("zuul.server.netty.socket.force_nio", "true"); config.setProperty("zuul.server.netty.threads.worker", String.valueOf(eventLoopThreads)); config.setProperty("zuul.server.port.main", serverPort); config.setProperty("api.ribbon." + CommonClientConfigKey.ReadTimeout.key(), originReadTimeout.toMillis()); config.setProperty( "api.ribbon.NIWSServerListClassName", "com.netflix.zuul.integration.server.OriginServerList"); // short circuit graceful shutdown config.setProperty("server.outofservice.close.timeout", "0"); bootstrap = new Bootstrap(); bootstrap.start(); assertThat(bootstrap.isRunning()).isTrue(); } @Override public void afterAll(ExtensionContext context) throws Exception { if (bootstrap != null) { bootstrap.stop(); } } public int getServerPort() { return serverPort; } public static Builder newBuilder() { return new Builder(); } private static int findAvailableTcpPort() { try (ServerSocket sock = new ServerSocket(0)) { return sock.getLocalPort(); } catch (IOException e) { return -1; } } public static class Builder { private int eventLoopThreads = 1; private Duration originReadTimeout; public Builder withEventLoopThreads(int eventLoopThreads) { this.eventLoopThreads = eventLoopThreads; return this; } public Builder withOriginReadTimeout(Duration originReadTimeout) { this.originReadTimeout = originReadTimeout; return this; } public ZuulServerExtension build() { Objects.requireNonNull(originReadTimeout, "originReadTimeout cannot be null"); return new ZuulServerExtension(this); } } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/Bootstrap.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.spectator.api.Registry; import com.netflix.zuul.BasicRequestCompleteHandler; import com.netflix.zuul.DefaultFilterFactory; import com.netflix.zuul.StaticFilterLoader; import com.netflix.zuul.context.ZuulSessionContextDecorator; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.integration.server.filters.CrossThreadBoundaryFilter; import com.netflix.zuul.integration.server.filters.InboundRoutesFilter; import com.netflix.zuul.integration.server.filters.NeedsBodyBufferedInboundFilter; import com.netflix.zuul.integration.server.filters.NeedsBodyBufferedOutboundFilter; import com.netflix.zuul.integration.server.filters.RequestHeaderFilter; import com.netflix.zuul.integration.server.filters.ResponseHeaderFilter; import com.netflix.zuul.netty.server.ClientRequestReceiver; import com.netflix.zuul.netty.server.DirectMemoryMonitor; import com.netflix.zuul.netty.server.Server; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.origins.BasicNettyOriginManager; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Bootstrap { private static final Logger logger = LoggerFactory.getLogger(Bootstrap.class); private static final Set>> FILTER_TYPES; static { Set>> classes = new LinkedHashSet<>(); classes.add(InboundRoutesFilter.class); classes.add(NeedsBodyBufferedInboundFilter.class); classes.add(RequestHeaderFilter.class); classes.add(CrossThreadBoundaryFilter.class); classes.add(ResponseHeaderFilter.class); classes.add(NeedsBodyBufferedOutboundFilter.class); FILTER_TYPES = Collections.unmodifiableSet(classes); } private Server server; public void start() { long startNanos = System.nanoTime(); logger.info("Zuul: starting up."); try { Registry registry = new DefaultRegistry(); AccessLogPublisher accessLogPublisher = new AccessLogPublisher( "ACCESS", (channel, httpRequest) -> ClientRequestReceiver.getRequestFromChannel(channel) .getContext() .getUUID()); ServerStartup serverStartup = new ServerStartup( new NoOpServerStatusManager(), new StaticFilterLoader(new DefaultFilterFactory(), FILTER_TYPES), new ZuulSessionContextDecorator(new BasicNettyOriginManager(registry)), (f, s) -> {}, new BasicRequestCompleteHandler(), registry, new DirectMemoryMonitor(registry), new EventLoopGroupMetrics(registry), null, new ApplicationInfoManager(null, null, null), accessLogPublisher, new PushConnectionRegistry()); serverStartup.init(); server = serverStartup.server(); server.start(); long startupDuration = System.nanoTime() - startNanos; logger.info("Zuul: finished startup. Duration = {}ms", TimeUnit.NANOSECONDS.toMillis(startupDuration)); // server.awaitTermination(); } catch (Throwable t) { throw new RuntimeException(t); } } public Server getServer() { return this.server; } public boolean isRunning() { return (server != null) && (server.getListeningAddresses().size() > 0); } public void stop() { if (server != null) { server.stop(); } } private static class NoOpServerStatusManager extends ServerStatusManager { public NoOpServerStatusManager() { super(null); } @Override public void localStatus(InstanceInfo.InstanceStatus status) {} } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/HeaderNames.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server; public class HeaderNames { private HeaderNames() {} public static final String REQUEST_ID = "request-id"; } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/OriginServerList.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.netflix.loadbalancer.ConfigurationBasedServerList; import com.netflix.loadbalancer.Server; import java.util.List; public class OriginServerList extends ConfigurationBasedServerList { @Override protected List derive(String value) { List list = Lists.newArrayList(); if (!Strings.isNullOrEmpty(value)) { for (String s : value.split(",", -1)) { String[] hostAndPort = s.split(":", -1); String host = hostAndPort[0]; int port = Integer.parseInt(hostAndPort[1]); list.add(TestUtil.makeDiscoveryEnabledServer("", host, port)); } } return list; } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/ServerStartup.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.config.DynamicIntProperty; import com.netflix.discovery.EurekaClient; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.Registry; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.RequestCompleteHandler; import com.netflix.zuul.context.SessionContextDecorator; import com.netflix.zuul.netty.server.BaseServerStartup; import com.netflix.zuul.netty.server.DefaultEventLoopConfig; import com.netflix.zuul.netty.server.DirectMemoryMonitor; import com.netflix.zuul.netty.server.Http1MutualSslChannelInitializer; import com.netflix.zuul.netty.server.NamedSocketAddress; import com.netflix.zuul.netty.server.SocketAddressProperty; import com.netflix.zuul.netty.server.ZuulDependencyKeys; import com.netflix.zuul.netty.server.ZuulServerChannelInitializer; import com.netflix.zuul.netty.server.http2.Http2SslChannelInitializer; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.netty.ssl.BaseSslContextFactory; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; import io.netty.handler.codec.compression.CompressionOptions; import io.netty.handler.codec.http.HttpContentCompressor; import io.netty.handler.ssl.ClientAuth; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.io.File; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; import java.util.HashMap; import java.util.Map; @Singleton public class ServerStartup extends BaseServerStartup { enum ServerType { HTTP, HTTP2, HTTP_MUTUAL_TLS, WEBSOCKET, SSE } private static final String[] WWW_PROTOCOLS = new String[] {"TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3"}; private static final ServerType SERVER_TYPE = ServerType.HTTP; private final PushConnectionRegistry pushConnectionRegistry; // private final SamplePushMessageSenderInitializer pushSenderInitializer; @Inject public ServerStartup( ServerStatusManager serverStatusManager, FilterLoader filterLoader, SessionContextDecorator sessionCtxDecorator, FilterUsageNotifier usageNotifier, RequestCompleteHandler reqCompleteHandler, Registry registry, DirectMemoryMonitor directMemoryMonitor, EventLoopGroupMetrics eventLoopGroupMetrics, EurekaClient discoveryClient, ApplicationInfoManager applicationInfoManager, AccessLogPublisher accessLogPublisher, PushConnectionRegistry pushConnectionRegistry) { super( serverStatusManager, filterLoader, sessionCtxDecorator, usageNotifier, reqCompleteHandler, registry, directMemoryMonitor, eventLoopGroupMetrics, new DefaultEventLoopConfig(), discoveryClient, applicationInfoManager, accessLogPublisher); this.pushConnectionRegistry = pushConnectionRegistry; // this.pushSenderInitializer = pushSenderInitializer; } @Override protected Map> chooseAddrsAndChannels(ChannelGroup clientChannels) { Map> addrsToChannels = new HashMap<>(); SocketAddress sockAddr; String metricId; { int port = new DynamicIntProperty("zuul.server.port.main", 7001).get(); sockAddr = new SocketAddressProperty("zuul.server.addr.main", "=" + port).getValue(); if (sockAddr instanceof InetSocketAddress) { metricId = String.valueOf(((InetSocketAddress) sockAddr).getPort()); } else { // Just pick something. This would likely be a UDS addr or a LocalChannel addr. metricId = sockAddr.toString(); } } SocketAddress pushSockAddr; { int pushPort = new DynamicIntProperty("zuul.server.port.http.push", 7008).get(); pushSockAddr = new SocketAddressProperty("zuul.server.addr.http.push", "=" + pushPort).getValue(); } String mainListenAddressName = "main"; ServerSslConfig sslConfig; ChannelConfig channelConfig = defaultChannelConfig(mainListenAddressName); ChannelConfig channelDependencies = defaultChannelDependencies(mainListenAddressName); /* These settings may need to be tweaked depending if you're running behind an ELB HTTP listener, TCP listener, * or directly on the internet. */ switch (SERVER_TYPE) { /* The below settings can be used when running behind an ELB HTTP listener that terminates SSL for you * and passes XFF headers. */ case HTTP: channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.ALWAYS); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, false); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, false); addrsToChannels.put( new NamedSocketAddress("http", sockAddr), new ZuulServerChannelInitializer(metricId, channelConfig, channelDependencies, clientChannels) { @Override protected void addHttp1Handlers(ChannelPipeline pipeline) { super.addHttp1Handlers(pipeline); pipeline.addLast(new HttpContentCompressor((CompressionOptions[]) null)); } }); logAddrConfigured(sockAddr); break; /* The below settings can be used when running behind an ELB TCP listener with proxy protocol, terminating * SSL in Zuul. */ case HTTP2: sslConfig = ServerSslConfig.builder() .protocols(WWW_PROTOCOLS) .ciphers(ServerSslConfig.getDefaultCiphers()) .certChainFile(loadFromResources("server.cert")) .keyFile(loadFromResources("server.key")) .build(); channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.serverSslConfig, sslConfig); channelConfig.set( CommonChannelConfigKeys.sslContextFactory, new BaseSslContextFactory(registry, sslConfig)); addHttp2DefaultConfig(channelConfig, mainListenAddressName); addrsToChannels.put( new NamedSocketAddress("http2", sockAddr), new Http2SslChannelInitializer(metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr, sslConfig); break; /* The below settings can be used when running behind an ELB TCP listener with proxy protocol, terminating * SSL in Zuul. * * Can be tested using certs in resources directory: * curl https://localhost:7001/test -vk --cert src/main/resources/ssl/client.cert:zuul123 --key src/main/resources/ssl/client.key */ case HTTP_MUTUAL_TLS: sslConfig = ServerSslConfig.builder() .protocols(WWW_PROTOCOLS) .ciphers(ServerSslConfig.getDefaultCiphers()) .certChainFile(loadFromResources("server.cert")) .keyFile(loadFromResources("server.key")) .clientAuth(ClientAuth.REQUIRE) .clientAuthTrustStoreFile(loadFromResources("truststore.jks")) .clientAuthTrustStorePasswordFile(loadFromResources("truststore.key")) .build(); channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); channelConfig.set(CommonChannelConfigKeys.serverSslConfig, sslConfig); channelConfig.set( CommonChannelConfigKeys.sslContextFactory, new BaseSslContextFactory(registry, sslConfig)); addrsToChannels.put( new NamedSocketAddress("http_mtls", sockAddr), new Http1MutualSslChannelInitializer( metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr, sslConfig); break; /* Settings to be used when running behind an ELB TCP listener with proxy protocol as a Push notification * server using WebSockets */ case WEBSOCKET: channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); channelDependencies.set(ZuulDependencyKeys.pushConnectionRegistry, pushConnectionRegistry); /* addrsToChannels.put( new NamedSocketAddress("websocket", sockAddr), new SampleWebSocketPushChannelInitializer( metricId, channelConfig, channelDependencies, clientChannels)); */ logAddrConfigured(sockAddr); // port to accept push message from the backend, should be accessible on internal network only. // TODO ? addrsToChannels.put(new NamedSocketAddress("http.push", pushSockAddr), pushSenderInitializer); logAddrConfigured(pushSockAddr); break; /* Settings to be used when running behind an ELB TCP listener with proxy protocol as a Push notification * server using Server Sent Events (SSE) */ case SSE: channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); channelDependencies.set(ZuulDependencyKeys.pushConnectionRegistry, pushConnectionRegistry); /* addrsToChannels.put( new NamedSocketAddress("sse", sockAddr), new SampleSSEPushChannelInitializer( metricId, channelConfig, channelDependencies, clientChannels)); */ logAddrConfigured(sockAddr); // port to accept push message from the backend, should be accessible on internal network only. // todo ? addrsToChannels.put(new NamedSocketAddress("http.push", pushSockAddr), pushSenderInitializer); logAddrConfigured(pushSockAddr); break; } return Collections.unmodifiableMap(addrsToChannels); } private File loadFromResources(String s) { return new File(ClassLoader.getSystemResource("ssl/" + s).getFile()); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/TestUtil.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server; import com.netflix.appinfo.InstanceInfo; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.UUID; import org.apache.commons.lang3.StringUtils; public class TestUtil { private TestUtil() {} public static final Charset CHARSET = StandardCharsets.UTF_8; public static final String COMPRESSIBLE_CONTENT = "Hello Hello Hello Hello Hello"; public static final String COMPRESSIBLE_CONTENT_TYPE = "text/plain"; public static final String JUMBO_RESPONSE_BODY = StringUtils.repeat("abc", 1_000_000); public static DiscoveryEnabledServer makeDiscoveryEnabledServer(String appName, String ipAddress, int port) { InstanceInfo instanceInfo = new InstanceInfo( UUID.randomUUID().toString(), appName, appName, ipAddress, "sid123", new InstanceInfo.PortWrapper(true, port), null, null, null, null, null, null, null, 1, null, null, null, null, null, null, null, null, null, null, null, null); return new DiscoveryEnabledServer(instanceInfo, false, true); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/BodyUtil.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.message.http.HttpRequestMessage; public class BodyUtil { public static boolean needsRequestBodyBuffering(HttpRequestMessage request) { return request.getQueryParams().contains("bufferRequestBody", "true"); } public static boolean needsResponseBodyBuffering(HttpRequestMessage request) { return request.getQueryParams().contains("bufferResponseBody", "true"); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/CrossThreadBoundaryFilter.java ================================================ /* * Copyright 2025 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterSyncType; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpInboundFilter; import com.netflix.zuul.message.http.HttpRequestMessage; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import rx.Observable; /** * @author Justin Guerra * @since 5/20/25 */ @Filter(order = 30, type = FilterType.INBOUND, sync = FilterSyncType.ASYNC) public class CrossThreadBoundaryFilter extends HttpInboundFilter { private final ExecutorService executor; public CrossThreadBoundaryFilter() { executor = Executors.newSingleThreadExecutor(); } @Override public Observable applyAsync(HttpRequestMessage input) { // force a thread boundary change Future future = executor.submit(() -> input); return Observable.from(future); } @Override public boolean shouldFilter(HttpRequestMessage msg) { return true; } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/InboundRoutesFilter.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.endpoint.ProxyEndpoint; import com.netflix.zuul.filters.http.HttpInboundSyncFilter; import com.netflix.zuul.message.http.HttpRequestMessage; @Filter(order = 0, type = FilterType.INBOUND) public class InboundRoutesFilter extends HttpInboundSyncFilter { @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter(HttpRequestMessage msg) { return true; } @Override public HttpRequestMessage apply(HttpRequestMessage input) { // uncomment this line to trigger a resource leak // ByteBuf buffer = UnpooledByteBufAllocator.DEFAULT.buffer(); SessionContext context = input.getContext(); context.setEndpoint(ProxyEndpoint.class.getCanonicalName()); context.setRouteVIP("api"); return input; } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/NeedsBodyBufferedInboundFilter.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpInboundFilter; import com.netflix.zuul.message.http.HttpRequestMessage; import rx.Observable; @Filter(order = 20, type = FilterType.INBOUND) public class NeedsBodyBufferedInboundFilter extends HttpInboundFilter { @Override public boolean shouldFilter(HttpRequestMessage msg) { return msg.hasBody(); } @Override public boolean needsBodyBuffered(HttpRequestMessage message) { return BodyUtil.needsRequestBodyBuffering(message); } @Override public Observable applyAsync(HttpRequestMessage input) { return Observable.just(input); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/NeedsBodyBufferedOutboundFilter.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpOutboundFilter; import com.netflix.zuul.message.http.HttpResponseMessage; import rx.Observable; @Filter(order = 450, type = FilterType.OUTBOUND) public class NeedsBodyBufferedOutboundFilter extends HttpOutboundFilter { @Override public boolean shouldFilter(HttpResponseMessage msg) { return msg.hasBody(); } @Override public boolean needsBodyBuffered(HttpResponseMessage message) { return BodyUtil.needsResponseBodyBuffering(message.getOutboundRequest()); } @Override public Observable applyAsync(HttpResponseMessage response) { return Observable.just(response); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/RequestHeaderFilter.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpInboundFilter; import com.netflix.zuul.integration.server.HeaderNames; import com.netflix.zuul.message.http.HttpRequestMessage; import java.util.UUID; import rx.Observable; @Filter(order = 10, type = FilterType.INBOUND) public class RequestHeaderFilter extends HttpInboundFilter { @Override public boolean shouldFilter(HttpRequestMessage msg) { return true; } @Override public Observable applyAsync(HttpRequestMessage request) { request.getHeaders().set(HeaderNames.REQUEST_ID, "RQ-" + UUID.randomUUID()); request.storeInboundRequest(); return Observable.just(request); } } ================================================ FILE: zuul-integration-test/src/test/java/com/netflix/zuul/integration/server/filters/ResponseHeaderFilter.java ================================================ /* * Copyright 2022 Netflix, Inc. * * 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. */ package com.netflix.zuul.integration.server.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.http.HttpOutboundFilter; import com.netflix.zuul.integration.server.HeaderNames; import com.netflix.zuul.message.http.HttpResponseMessage; import rx.Observable; @Filter(order = 400, type = FilterType.OUTBOUND) public class ResponseHeaderFilter extends HttpOutboundFilter { @Override public boolean shouldFilter(HttpResponseMessage msg) { return true; } @Override public Observable applyAsync(HttpResponseMessage response) { String requestId = response.getInboundRequest().getHeaders().getFirst(HeaderNames.REQUEST_ID); if (requestId != null) { response.getHeaders().set(HeaderNames.REQUEST_ID, requestId); response.storeInboundResponse(); } return Observable.just(response); } } ================================================ FILE: zuul-integration-test/src/test/resources/log4j2-test.xml ================================================ ================================================ FILE: zuul-processor/build.gradle ================================================ apply plugin: "java" dependencies { implementation libraries.guava implementation project(":zuul-core") testImplementation libraries.jupiterApi, libraries.jupiterParams, libraries.jupiterEngine, libraries.junitPlatformLauncher, libraries.assertj testAnnotationProcessor project(":zuul-processor") } // Silences log statements during tests. This still allows normal failures to be printed. test { testLogging { showStandardStreams = false } } ================================================ FILE: zuul-processor/src/main/java/com/netflix/zuul/filters/processor/FilterProcessor.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.processor; import com.google.common.annotations.VisibleForTesting; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.tools.FileObject; import javax.tools.StandardLocation; @SupportedAnnotationTypes(FilterProcessor.FILTER_TYPE) @SupportedSourceVersion(SourceVersion.RELEASE_21) public final class FilterProcessor extends AbstractProcessor { static final String FILTER_TYPE = "com.netflix.zuul.Filter"; private final Set annotatedElements = new HashSet<>(); @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { Set annotated = roundEnv.getElementsAnnotatedWith( processingEnv.getElementUtils().getTypeElement(FILTER_TYPE)); for (Element el : annotated) { if (el.getModifiers().contains(Modifier.ABSTRACT)) { continue; } annotatedElements.add(processingEnv .getElementUtils() .getBinaryName((TypeElement) el) .toString()); } if (roundEnv.processingOver()) { try { addNewClasses(processingEnv.getFiler(), annotatedElements); } catch (IOException e) { throw new RuntimeException(e); } finally { annotatedElements.clear(); } } return false; } static void addNewClasses(Filer filer, Collection elements) throws IOException { String resourceName = "META-INF/zuul/allfilters"; List existing = Collections.emptyList(); try { FileObject existingFilters = filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceName); try (InputStream is = existingFilters.openInputStream(); InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { existing = readResourceFile(reader); } } catch (FileNotFoundException | NoSuchFileException e) { // Perhaps log this. } catch (IOException e) { throw new RuntimeException(e); } int sizeBefore = existing.size(); Set existingSet = new LinkedHashSet<>(existing); List newElements = new ArrayList<>(existingSet); for (String element : elements) { if (existingSet.add(element)) { newElements.add(element); } } if (newElements.size() == sizeBefore) { // nothing to do. return; } newElements.sort(String::compareTo); FileObject dest = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceName); try (OutputStream os = dest.openOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { writeResourceFile(osw, newElements); } } @VisibleForTesting static List readResourceFile(Reader reader) throws IOException { BufferedReader br = new BufferedReader(reader); String line; List lines = new ArrayList<>(); while ((line = br.readLine()) != null) { if (line.trim().isEmpty()) { continue; } lines.add(line); } return Collections.unmodifiableList(lines); } @VisibleForTesting static void writeResourceFile(Writer writer, Collection elements) throws IOException { BufferedWriter bw = new BufferedWriter(writer); for (Object element : elements) { bw.write(String.valueOf(element)); bw.newLine(); } bw.flush(); } } ================================================ FILE: zuul-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors ================================================ com.netflix.zuul.filters.processor.FilterProcessor,aggregating ================================================ FILE: zuul-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor ================================================ com.netflix.zuul.filters.processor.FilterProcessor ================================================ FILE: zuul-processor/src/test/java/com/netflix/zuul/filters/processor/FilterProcessorTest.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.processor; import static org.assertj.core.api.Assertions.assertThat; import com.netflix.zuul.StaticFilterLoader; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.filters.processor.override.SubpackageFilter; import com.netflix.zuul.filters.processor.subpackage.OverrideFilter; import java.util.Collection; import org.junit.jupiter.api.Test; /** * Tests for {@link FilterProcessor}. */ class FilterProcessorTest { @Test void allFilterClassedRecorded() throws Exception { Collection>> filters = StaticFilterLoader.loadFilterTypesFromResources(getClass().getClassLoader()); @SuppressWarnings("unchecked") Class>[] expected = new Class[] { OuterClassFilter.class, TopLevelFilter.class, TopLevelFilter.StaticSubclassFilter.class, TopLevelFilter.SubclassFilter.class, OverrideFilter.class, SubpackageFilter.class }; assertThat(filters).containsExactlyInAnyOrder(expected); } } ================================================ FILE: zuul-processor/src/test/java/com/netflix/zuul/filters/processor/TestFilter.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.processor; import com.netflix.zuul.exception.ZuulFilterConcurrencyExceededException; import com.netflix.zuul.filters.FilterSyncType; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.message.ZuulMessage; import io.netty.handler.codec.http.HttpContent; import rx.Observable; /** * A dummy filter which is used for testing. */ public abstract class TestFilter implements ZuulFilter { @Override public boolean isDisabled() { throw new UnsupportedOperationException(); } @Override public String filterName() { throw new UnsupportedOperationException(); } @Override public int filterOrder() { throw new UnsupportedOperationException(); } @Override public FilterType filterType() { throw new UnsupportedOperationException(); } @Override public boolean overrideStopFilterProcessing() { throw new UnsupportedOperationException(); } @Override public void incrementConcurrency() throws ZuulFilterConcurrencyExceededException { throw new UnsupportedOperationException(); } @Override public Observable applyAsync(ZuulMessage input) { throw new UnsupportedOperationException(); } @Override public void decrementConcurrency() { throw new UnsupportedOperationException(); } @Override public FilterSyncType getSyncType() { throw new UnsupportedOperationException(); } @Override public ZuulMessage getDefaultOutput(ZuulMessage input) { throw new UnsupportedOperationException(); } @Override public boolean needsBodyBuffered(ZuulMessage input) { throw new UnsupportedOperationException(); } @Override public HttpContent processContentChunk(ZuulMessage zuulMessage, HttpContent chunk) { throw new UnsupportedOperationException(); } @Override public boolean shouldFilter(ZuulMessage msg) { throw new UnsupportedOperationException(); } } ================================================ FILE: zuul-processor/src/test/java/com/netflix/zuul/filters/processor/TopLevelFilter.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.processor; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; /** * Used to test generated code. */ @Filter(order = 20, type = FilterType.INBOUND) final class TopLevelFilter extends TestFilter { @Filter(order = 21, type = FilterType.INBOUND) static final class StaticSubclassFilter extends TestFilter {} @SuppressWarnings("unused") // This should be ignored by the processor, since it is abstract @Filter(order = 22, type = FilterType.INBOUND) abstract static class AbstractSubclassFilter extends TestFilter {} @SuppressWarnings("InnerClassMayBeStatic") // The purpose of this test @Filter(order = 23, type = FilterType.INBOUND) final class SubclassFilter extends TestFilter {} static { // This should be ignored by the processor, since it is private. // See https://bugs.openjdk.java.net/browse/JDK-6587158 @SuppressWarnings("unused") @Filter(order = 23, type = FilterType.INBOUND) final class MethodClassFilter {} } } @Filter(order = 24, type = FilterType.INBOUND) final class OuterClassFilter extends TestFilter {} ================================================ FILE: zuul-processor/src/test/java/com/netflix/zuul/filters/processor/override/SubpackageFilter.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.processor.override; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.processor.TestFilter; @Filter(order = 30, type = FilterType.INBOUND) public final class SubpackageFilter extends TestFilter {} ================================================ FILE: zuul-processor/src/test/java/com/netflix/zuul/filters/processor/override/package-info.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ @com.netflix.zuul.Filter.FilterPackageName("MySubpackage") package com.netflix.zuul.filters.processor.override; ================================================ FILE: zuul-processor/src/test/java/com/netflix/zuul/filters/processor/subpackage/OverrideFilter.java ================================================ /* * Copyright 2020 Netflix, Inc. * * 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. */ package com.netflix.zuul.filters.processor.subpackage; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.FilterType; import com.netflix.zuul.filters.processor.TestFilter; @Filter(order = 30, type = FilterType.INBOUND) public final class OverrideFilter extends TestFilter {} ================================================ FILE: zuul-sample/build.gradle ================================================ apply plugin: "java" apply plugin: 'application' application { mainClass = "com.netflix.zuul.sample.Bootstrap" applicationDefaultJvmArgs = ["-DTZ=GMT", "-Darchaius.deployment.environment=test", "-Dcom.sun.management.jmxremote", "-Dcom.sun.management.jmxremote.local.only=false", "-Deureka.validateInstanceId=false", "-Deureka.mt.num_retries=1", "-Dlog4j.configurationFile=log4j2.xml"] } dependencies { implementation project(":zuul-core") implementation "com.netflix.eureka:eureka-client:2.0.4" implementation 'commons-configuration:commons-configuration:1.10' implementation "jakarta.inject:jakarta.inject-api:2.0.1" annotationProcessor project(":zuul-processor") implementation 'org.apache.logging.log4j:log4j-core:2.25.1' implementation 'org.apache.logging.log4j:log4j-api:2.25.1' implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.25.1' implementation 'org.slf4j:slf4j-simple:2.0.17' } /* * Run regular: ./gradlew run * Run benchmark: ./gradlew run -Pbench */ run { if (project.hasProperty('bench')) { println 'Running benchmark configuration...' jvmArgs "-Darchaius.deployment.environment=benchmark" } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/Bootstrap.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo.InstanceStatus; import com.netflix.config.ConfigurationManager; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.DefaultRegistry; import com.netflix.zuul.BasicFilterUsageNotifier; import com.netflix.zuul.BasicRequestCompleteHandler; import com.netflix.zuul.DefaultFilterFactory; import com.netflix.zuul.FilterFactory; import com.netflix.zuul.StaticFilterLoader; import com.netflix.zuul.context.ZuulSessionContextDecorator; import com.netflix.zuul.filters.ZuulFilter; import com.netflix.zuul.netty.server.ClientRequestReceiver; import com.netflix.zuul.netty.server.DirectMemoryMonitor; import com.netflix.zuul.netty.server.Server; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.origins.BasicNettyOriginManager; import com.netflix.zuul.sample.filters.Debug; import com.netflix.zuul.sample.filters.endpoint.Healthcheck; import com.netflix.zuul.sample.filters.inbound.DebugRequest; import com.netflix.zuul.sample.filters.inbound.Routes; import com.netflix.zuul.sample.filters.inbound.SampleServiceFilter; import com.netflix.zuul.sample.filters.outbound.ZuulResponseFilter; import com.netflix.zuul.sample.push.SamplePushMessageSenderInitializer; import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Bootstrap *

* Author: Arthur Gonigberg * Date: November 20, 2017 */ public class Bootstrap { private static final Logger logger = LoggerFactory.getLogger(Bootstrap.class); private static final Set>> FILTER_TYPES; static { Set>> classes = new LinkedHashSet<>(); classes.add(Healthcheck.class); classes.add(Debug.class); classes.add(Routes.class); classes.add(SampleServiceFilter.class); classes.add(ZuulResponseFilter.class); classes.add(DebugRequest.class); FILTER_TYPES = Collections.unmodifiableSet(classes); } public static void main(String[] args) { new Bootstrap().start(); } public void start() { long startNanos = System.nanoTime(); logger.info("Zuul Sample: starting up."); int exitCode = 0; Server server = null; try { ConfigurationManager.loadCascadedPropertiesFromResources("application"); AccessLogPublisher accessLogPublisher = new AccessLogPublisher( "ACCESS", (channel, httpRequest) -> ClientRequestReceiver.getRequestFromChannel(channel) .getContext() .getUUID()); ApplicationInfoManager instance = new ApplicationInfoManager(null, null, null); PushConnectionRegistry pushConnectionRegistry = new PushConnectionRegistry(); SamplePushMessageSenderInitializer pushMessageSenderInitializer = new SamplePushMessageSenderInitializer(pushConnectionRegistry); DefaultRegistry registry = new DefaultRegistry(); SampleServerStartup serverStartup = new SampleServerStartup( new ServerStatusManager(instance) { @Override public void localStatus(InstanceStatus status) {} }, new StaticFilterLoader(new SampleFilterFactory(), FILTER_TYPES), new ZuulSessionContextDecorator(new BasicNettyOriginManager(registry)), new BasicFilterUsageNotifier(registry), new BasicRequestCompleteHandler(), registry, new DirectMemoryMonitor(registry), new EventLoopGroupMetrics(registry), null, instance, accessLogPublisher, pushConnectionRegistry, pushMessageSenderInitializer); serverStartup.init(); server = serverStartup.server(); server.start(); long startupDuration = System.nanoTime() - startNanos; logger.info( "Zuul Sample: finished startup. Duration = {}ms", TimeUnit.NANOSECONDS.toMillis(startupDuration)); server.awaitTermination(); } catch (Throwable t) { // Don't use logger here, as we may be shutting down the JVM and the logs won't be printed. t.printStackTrace(); System.err.println("###############"); System.err.println("Zuul Sample: initialization failed. Forcing shutdown now."); System.err.println("###############"); exitCode = 1; } finally { // server shutdown if (server != null) { server.stop(); } System.exit(exitCode); } } private static class SampleFilterFactory implements FilterFactory { private final DefaultFilterFactory filterFactory; public SampleFilterFactory() { filterFactory = new DefaultFilterFactory(); } @Override public ZuulFilter newInstance(Class clazz) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException { if (clazz.equals(SampleServiceFilter.class)) { return new SampleServiceFilter(new SampleService()); } else { return filterFactory.newInstance(clazz); } } } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/SampleServerStartup.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.config.DynamicIntProperty; import com.netflix.discovery.EurekaClient; import com.netflix.netty.common.accesslog.AccessLogPublisher; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.netty.common.channel.config.CommonChannelConfigKeys; import com.netflix.netty.common.metrics.EventLoopGroupMetrics; import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler; import com.netflix.netty.common.ssl.ServerSslConfig; import com.netflix.netty.common.status.ServerStatusManager; import com.netflix.spectator.api.Registry; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.FilterUsageNotifier; import com.netflix.zuul.RequestCompleteHandler; import com.netflix.zuul.context.SessionContextDecorator; import com.netflix.zuul.netty.server.BaseServerStartup; import com.netflix.zuul.netty.server.DefaultEventLoopConfig; import com.netflix.zuul.netty.server.DirectMemoryMonitor; import com.netflix.zuul.netty.server.Http1MutualSslChannelInitializer; import com.netflix.zuul.netty.server.NamedSocketAddress; import com.netflix.zuul.netty.server.SocketAddressProperty; import com.netflix.zuul.netty.server.ZuulDependencyKeys; import com.netflix.zuul.netty.server.ZuulServerChannelInitializer; import com.netflix.zuul.netty.server.http2.Http2SslChannelInitializer; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.netty.ssl.BaseSslContextFactory; import com.netflix.zuul.sample.push.SamplePushMessageSenderInitializer; import com.netflix.zuul.sample.push.SampleSSEPushChannelInitializer; import com.netflix.zuul.sample.push.SampleWebSocketPushChannelInitializer; import io.netty.channel.ChannelInitializer; import io.netty.channel.group.ChannelGroup; import io.netty.handler.ssl.ClientAuth; import java.io.File; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Sample Server Startup - class that configures the Netty server startup settings *

* Author: Arthur Gonigberg * Date: November 20, 2017 */ public class SampleServerStartup extends BaseServerStartup { enum ServerType { HTTP, HTTP2, HTTP_MUTUAL_TLS, WEBSOCKET, SSE } private static final String[] WWW_PROTOCOLS = new String[] {"TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3"}; private static final ServerType SERVER_TYPE = ServerType.HTTP; private final PushConnectionRegistry pushConnectionRegistry; private final SamplePushMessageSenderInitializer pushSenderInitializer; public SampleServerStartup( ServerStatusManager serverStatusManager, FilterLoader filterLoader, SessionContextDecorator sessionCtxDecorator, FilterUsageNotifier usageNotifier, RequestCompleteHandler reqCompleteHandler, Registry registry, DirectMemoryMonitor directMemoryMonitor, EventLoopGroupMetrics eventLoopGroupMetrics, EurekaClient discoveryClient, ApplicationInfoManager applicationInfoManager, AccessLogPublisher accessLogPublisher, PushConnectionRegistry pushConnectionRegistry, SamplePushMessageSenderInitializer pushSenderInitializer) { super( serverStatusManager, filterLoader, sessionCtxDecorator, usageNotifier, reqCompleteHandler, registry, directMemoryMonitor, eventLoopGroupMetrics, new DefaultEventLoopConfig(), discoveryClient, applicationInfoManager, accessLogPublisher); this.pushConnectionRegistry = pushConnectionRegistry; this.pushSenderInitializer = pushSenderInitializer; } @Override protected Map> chooseAddrsAndChannels(ChannelGroup clientChannels) { Map> addrsToChannels = new HashMap<>(); SocketAddress sockAddr; String metricId; { int port = new DynamicIntProperty("zuul.server.port.main", 7001).get(); sockAddr = new SocketAddressProperty("zuul.server.addr.main", "=" + port).getValue(); if (sockAddr instanceof InetSocketAddress) { metricId = String.valueOf(((InetSocketAddress) sockAddr).getPort()); } else { // Just pick something. This would likely be a UDS addr or a LocalChannel addr. metricId = sockAddr.toString(); } } SocketAddress pushSockAddr; { int pushPort = new DynamicIntProperty("zuul.server.port.http.push", 7008).get(); pushSockAddr = new SocketAddressProperty("zuul.server.addr.http.push", "=" + pushPort).getValue(); } String mainListenAddressName = "main"; ServerSslConfig sslConfig; ChannelConfig channelConfig = defaultChannelConfig(mainListenAddressName); ChannelConfig channelDependencies = defaultChannelDependencies(mainListenAddressName); /* These settings may need to be tweaked depending if you're running behind an ELB HTTP listener, TCP listener, * or directly on the internet. */ switch (SERVER_TYPE) { /* The below settings can be used when running behind an ELB HTTP listener that terminates SSL for you * and passes XFF headers. */ case HTTP: channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.ALWAYS); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, false); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, false); addrsToChannels.put( new NamedSocketAddress("http", sockAddr), new ZuulServerChannelInitializer(metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr); break; /* The below settings can be used when running behind an ELB TCP listener with proxy protocol, terminating * SSL in Zuul. */ case HTTP2: sslConfig = ServerSslConfig.builder() .protocols(WWW_PROTOCOLS) .ciphers(ServerSslConfig.getDefaultCiphers()) .certChainFile(loadFromResources("server.cert")) .keyFile(loadFromResources("server.key")) .build(); channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.serverSslConfig, sslConfig); channelConfig.set( CommonChannelConfigKeys.sslContextFactory, new BaseSslContextFactory(registry, sslConfig)); addHttp2DefaultConfig(channelConfig, mainListenAddressName); addrsToChannels.put( new NamedSocketAddress("http2", sockAddr), new Http2SslChannelInitializer(metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr, sslConfig); break; /* The below settings can be used when running behind an ELB TCP listener with proxy protocol, terminating * SSL in Zuul. * * Can be tested using certs in resources directory: * curl https://localhost:7001/test -vk --cert src/main/resources/ssl/client.cert:zuul123 --key src/main/resources/ssl/client.key */ case HTTP_MUTUAL_TLS: sslConfig = ServerSslConfig.builder() .protocols(WWW_PROTOCOLS) .ciphers(ServerSslConfig.getDefaultCiphers()) .certChainFile(loadFromResources("server.cert")) .keyFile(loadFromResources("server.key")) .clientAuth(ClientAuth.REQUIRE) .clientAuthTrustStoreFile(loadFromResources("truststore.jks")) .clientAuthTrustStorePasswordFile(loadFromResources("truststore.key")) .build(); channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); channelConfig.set(CommonChannelConfigKeys.serverSslConfig, sslConfig); channelConfig.set( CommonChannelConfigKeys.sslContextFactory, new BaseSslContextFactory(registry, sslConfig)); addrsToChannels.put( new NamedSocketAddress("http_mtls", sockAddr), new Http1MutualSslChannelInitializer( metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr, sslConfig); break; /* Settings to be used when running behind an ELB TCP listener with proxy protocol as a Push notification * server using WebSockets */ case WEBSOCKET: channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); channelDependencies.set(ZuulDependencyKeys.pushConnectionRegistry, pushConnectionRegistry); addrsToChannels.put( new NamedSocketAddress("websocket", sockAddr), new SampleWebSocketPushChannelInitializer( metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr); // port to accept push message from the backend, should be accessible on internal network only. addrsToChannels.put(new NamedSocketAddress("http.push", pushSockAddr), pushSenderInitializer); logAddrConfigured(pushSockAddr); break; /* Settings to be used when running behind an ELB TCP listener with proxy protocol as a Push notification * server using Server Sent Events (SSE) */ case SSE: channelConfig.set( CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER); channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true); channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false); channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true); channelDependencies.set(ZuulDependencyKeys.pushConnectionRegistry, pushConnectionRegistry); addrsToChannels.put( new NamedSocketAddress("sse", sockAddr), new SampleSSEPushChannelInitializer( metricId, channelConfig, channelDependencies, clientChannels)); logAddrConfigured(sockAddr); // port to accept push message from the backend, should be accessible on internal network only. addrsToChannels.put(new NamedSocketAddress("http.push", pushSockAddr), pushSenderInitializer); logAddrConfigured(pushSockAddr); break; } return Collections.unmodifiableMap(addrsToChannels); } private File loadFromResources(String s) { return new File(ClassLoader.getSystemResource("ssl/" + s).getFile()); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/SampleService.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import rx.Observable; /** * Sample Service for demonstration in SampleServiceFilter. *

* Author: Arthur Gonigberg * Date: January 04, 2018 */ public class SampleService { private final AtomicBoolean status; public SampleService() { // change to true for demo this.status = new AtomicBoolean(false); } public boolean isHealthy() { return status.get(); } public Observable makeSlowRequest() { return Observable.just("test").delay(500, TimeUnit.MILLISECONDS); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/filters/Debug.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.filters; import com.netflix.zuul.Filter; import com.netflix.zuul.filters.http.HttpInboundSyncFilter; import com.netflix.zuul.message.http.HttpRequestMessage; /** * Determine if requests need to be debugged. * * In order to test this, set query parameter "debugRequest=true" * * Author: Arthur Gonigberg * Date: December 22, 2017 */ @Filter(order = 20) public class Debug extends HttpInboundSyncFilter { @Override public int filterOrder() { return 20; } @Override public boolean shouldFilter(HttpRequestMessage request) { return "true".equalsIgnoreCase(request.getQueryParams().getFirst("debugRequest")); } @Override public HttpRequestMessage apply(HttpRequestMessage request) { request.getContext().setDebugRequest(true); request.getContext().setDebugRouting(true); return request; } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/filters/endpoint/Healthcheck.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.filters.endpoint; import com.netflix.zuul.filters.http.HttpSyncEndpoint; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.message.http.HttpResponseMessageImpl; import com.netflix.zuul.stats.status.StatusCategoryUtils; import com.netflix.zuul.stats.status.ZuulStatusCategory; /** * Healthcheck Sample Endpoint * * Author: Arthur Gonigberg * Date: November 21, 2017 */ public class Healthcheck extends HttpSyncEndpoint { @Override public HttpResponseMessage apply(HttpRequestMessage request) { HttpResponseMessage resp = new HttpResponseMessageImpl(request.getContext(), request, 200); resp.setBodyAsText("healthy"); // need to set this manually since we are not going through the ProxyEndpoint StatusCategoryUtils.setStatusCategory(request.getContext(), ZuulStatusCategory.SUCCESS); return resp; } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/filters/inbound/DebugRequest.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.filters.inbound; import com.netflix.zuul.context.Debug; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.http.HttpInboundSyncFilter; import com.netflix.zuul.message.Header; import com.netflix.zuul.message.http.HttpRequestMessage; /** * Add debug request info to the context if request is marked as debug. * * Author: Arthur Gonigberg * Date: December 22, 2017 */ public class DebugRequest extends HttpInboundSyncFilter { @Override public int filterOrder() { return 21; } @Override public boolean shouldFilter(HttpRequestMessage request) { return request.getContext().debugRequest(); } @Override public boolean needsBodyBuffered(HttpRequestMessage request) { return shouldFilter(request); } @Override public HttpRequestMessage apply(HttpRequestMessage request) { SessionContext ctx = request.getContext(); Debug.addRequestDebug( ctx, "REQUEST:: " + request.getOriginalScheme() + " " + request.getOriginalHost() + ":" + request.getOriginalPort()); Debug.addRequestDebug( ctx, "REQUEST:: > " + request.getMethod() + " " + request.reconstructURI() + " " + request.getProtocol()); for (Header header : request.getHeaders().entries()) { Debug.addRequestDebug(ctx, "REQUEST:: > " + header.getName() + ":" + header.getValue()); } if (request.hasBody()) { Debug.addRequestDebug(ctx, "REQUEST:: > " + request.getBodyAsText()); } return request; } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/filters/inbound/Routes.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.filters.inbound; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.filters.endpoint.ProxyEndpoint; import com.netflix.zuul.filters.http.HttpInboundSyncFilter; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.sample.filters.endpoint.Healthcheck; /** * Routes configuration * * Author: Arthur Gonigberg * Date: November 21, 2017 */ public class Routes extends HttpInboundSyncFilter { @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter(HttpRequestMessage httpRequestMessage) { return true; } @Override public HttpRequestMessage apply(HttpRequestMessage request) { SessionContext context = request.getContext(); String path = request.getPath(); // Route healthchecks to the healthcheck endpoint. if (path.equalsIgnoreCase("/healthcheck")) { context.setEndpoint(Healthcheck.class.getCanonicalName()); } else { context.setEndpoint(ProxyEndpoint.class.getCanonicalName()); context.setRouteVIP("api"); } return request; } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/filters/inbound/SampleServiceFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.filters.inbound; import com.netflix.zuul.filters.http.HttpInboundFilter; import com.netflix.zuul.message.http.HttpRequestMessage; import com.netflix.zuul.sample.SampleService; import jakarta.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; /** * Sample Service Filter to demonstrate Guice injection of services and * making external requests to slow endpoints. * * Author: Arthur Gonigberg * Date: January 04, 2018 */ public class SampleServiceFilter extends HttpInboundFilter { private static final Logger log = LoggerFactory.getLogger(SampleServiceFilter.class); private final SampleService sampleService; @Inject public SampleServiceFilter(SampleService sampleService) { this.sampleService = sampleService; } @Override public int filterOrder() { return 500; } @Override public boolean shouldFilter(HttpRequestMessage msg) { return sampleService.isHealthy(); } @Override public Observable applyAsync(HttpRequestMessage request) { return sampleService.makeSlowRequest().map(response -> { log.info("Fetched sample service result: {}", response); return request; }); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/filters/outbound/ZuulResponseFilter.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.filters.outbound; import static com.netflix.zuul.constants.ZuulHeaders.CONNECTION; import static com.netflix.zuul.constants.ZuulHeaders.KEEP_ALIVE; import static com.netflix.zuul.constants.ZuulHeaders.X_ORIGINATING_URL; import static com.netflix.zuul.constants.ZuulHeaders.X_ZUUL; import static com.netflix.zuul.constants.ZuulHeaders.X_ZUUL_ERROR_CAUSE; import static com.netflix.zuul.constants.ZuulHeaders.X_ZUUL_INSTANCE; import static com.netflix.zuul.constants.ZuulHeaders.X_ZUUL_PROXY_ATTEMPTS; import static com.netflix.zuul.constants.ZuulHeaders.X_ZUUL_STATUS; import com.netflix.config.DynamicBooleanProperty; import com.netflix.zuul.context.Debug; import com.netflix.zuul.context.SessionContext; import com.netflix.zuul.exception.ZuulException; import com.netflix.zuul.filters.http.HttpOutboundSyncFilter; import com.netflix.zuul.message.Headers; import com.netflix.zuul.message.http.HttpResponseMessage; import com.netflix.zuul.niws.RequestAttempts; import com.netflix.zuul.passport.CurrentPassport; import com.netflix.zuul.stats.status.StatusCategory; import com.netflix.zuul.stats.status.StatusCategoryUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Sample Response Filter - adding custom response headers for better analysis of how the request was proxied * * Author: Arthur Gonigberg * Date: December 21, 2017 */ public class ZuulResponseFilter extends HttpOutboundSyncFilter { private static final Logger logger = LoggerFactory.getLogger(ZuulResponseFilter.class); private static final DynamicBooleanProperty SEND_RESPONSE_HEADERS = new DynamicBooleanProperty("zuul.responseFilter.send.headers", true); @Override public int filterOrder() { return 999; } @Override public boolean shouldFilter(HttpResponseMessage request) { return true; } @Override public HttpResponseMessage apply(HttpResponseMessage response) { SessionContext context = response.getContext(); if (SEND_RESPONSE_HEADERS.get()) { Headers headers = response.getHeaders(); StatusCategory statusCategory = StatusCategoryUtils.getStatusCategory(response); if (statusCategory != null) { headers.set(X_ZUUL_STATUS, statusCategory.name()); } RequestAttempts attempts = RequestAttempts.getFromSessionContext(response.getContext()); String headerStr = ""; if (attempts != null) { headerStr = attempts.toString(); } headers.set(X_ZUUL_PROXY_ATTEMPTS, headerStr); headers.set(X_ZUUL, "zuul"); headers.set( X_ZUUL_INSTANCE, System.getenv("EC2_INSTANCE_ID") != null ? System.getenv("EC2_INSTANCE_ID") : "unknown"); headers.set(CONNECTION, KEEP_ALIVE); headers.set(X_ORIGINATING_URL, response.getInboundRequest().reconstructURI()); if (response.getStatus() >= 400 && context.getError() != null) { Throwable error = context.getError(); headers.set( X_ZUUL_ERROR_CAUSE, error instanceof ZuulException ? ((ZuulException) error).getErrorCause() : "UNKNOWN_CAUSE"); } if (response.getStatus() >= 500) { logger.info("Passport: {}", CurrentPassport.fromSessionContext(context)); } if (logger.isDebugEnabled()) { logger.debug("Filter execution summary :: {}", context.getFilterExecutionSummary()); } } if (context.debugRequest()) { Debug.getRequestDebug(context).forEach(s -> logger.info("REQ_DEBUG: {}", s)); Debug.getRoutingDebug(context).forEach(s -> logger.info("ZUUL_DEBUG: {}", s)); } return response; } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SamplePushAuthHandler.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.push; import com.google.common.base.Strings; import com.netflix.zuul.message.http.Cookies; import com.netflix.zuul.netty.server.push.PushAuthHandler; import com.netflix.zuul.netty.server.push.PushUserAuth; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; /** * Takes cookie value of the cookie "userAuthCookie" as a customerId WITHOUT ANY actual validation. * For sample puprose only. In real life the cookies at minimum should be HMAC signed to prevent tampering/spoofing, * probably encrypted too if it can be exchanged on plain HTTP. * * Author: Susheel Aroskar * Date: 5/16/18 */ @ChannelHandler.Sharable public class SamplePushAuthHandler extends PushAuthHandler { public SamplePushAuthHandler(String path) { super(path, ".sample.netflix.com"); } /** * We support only cookie based auth in this sample */ @Override protected boolean isDelayedAuth(FullHttpRequest req, ChannelHandlerContext ctx) { return false; } @Override protected PushUserAuth doAuth(FullHttpRequest req, ChannelHandlerContext ctx) { Cookies cookies = parseCookies(req); for (Cookie c : cookies.getAll()) { if (c.name().equals("userAuthCookie")) { String customerId = c.value(); if (!Strings.isNullOrEmpty(customerId)) { return new SamplePushUserAuth(customerId); } } } return new SamplePushUserAuth(HttpResponseStatus.UNAUTHORIZED.code()); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SamplePushMessageSender.java ================================================ /** * Copyright 2018 Netflix, Inc. *

* 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. */ package com.netflix.zuul.sample.push; import com.google.common.base.Strings; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.netty.server.push.PushMessageSender; import com.netflix.zuul.netty.server.push.PushUserAuth; import io.netty.channel.ChannelHandler; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import jakarta.inject.Singleton; /** * Author: Susheel Aroskar * Date: 5/16/18 */ @Singleton @ChannelHandler.Sharable public class SamplePushMessageSender extends PushMessageSender { public SamplePushMessageSender(PushConnectionRegistry pushConnectionRegistry) { super(pushConnectionRegistry); } @Override protected PushUserAuth getPushUserAuth(FullHttpRequest request) { String cid = request.headers().get("X-CUSTOMER_ID"); if (Strings.isNullOrEmpty(cid)) { return new SamplePushUserAuth(HttpResponseStatus.UNAUTHORIZED.code()); } return new SamplePushUserAuth(cid); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SamplePushMessageSenderInitializer.java ================================================ /** * Copyright 2018 Netflix, Inc. *

* 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. */ package com.netflix.zuul.sample.push; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.netty.server.push.PushMessageSender; import com.netflix.zuul.netty.server.push.PushMessageSenderInitializer; import io.netty.channel.ChannelPipeline; import jakarta.inject.Inject; import jakarta.inject.Singleton; /** * Author: Susheel Aroskar * Date: 5/16/18 */ @Singleton public class SamplePushMessageSenderInitializer extends PushMessageSenderInitializer { private final PushMessageSender pushMessageSender; @Inject public SamplePushMessageSenderInitializer(PushConnectionRegistry pushConnectionRegistry) { super(); pushMessageSender = new SamplePushMessageSender(pushConnectionRegistry); } @Override protected void addPushMessageHandlers(ChannelPipeline pipeline) { pipeline.addLast(pushMessageSender); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SamplePushUserAuth.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.push; import com.netflix.zuul.netty.server.push.PushUserAuth; /** * Author: Susheel Aroskar * Date: 5/16/18 */ public class SamplePushUserAuth implements PushUserAuth { private final String customerId; private final int statusCode; private SamplePushUserAuth(String customerId, int statusCode) { this.customerId = customerId; this.statusCode = statusCode; } // Successful auth public SamplePushUserAuth(String customerId) { this(customerId, 200); } // Failed auth public SamplePushUserAuth(int statusCode) { this("", statusCode); } @Override public boolean isSuccess() { return statusCode == 200; } @Override public int statusCode() { return statusCode; } @Override public String getClientIdentity() { return customerId; } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SampleSSEPushChannelInitializer.java ================================================ /* * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.push; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.zuul.netty.server.ZuulDependencyKeys; import com.netflix.zuul.netty.server.push.PushAuthHandler; import com.netflix.zuul.netty.server.push.PushChannelInitializer; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.netty.server.push.PushProtocol; import com.netflix.zuul.netty.server.push.PushRegistrationHandler; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; /** * Author: Susheel Aroskar * Date: 6/8/18 */ public class SampleSSEPushChannelInitializer extends PushChannelInitializer { private final PushConnectionRegistry pushConnectionRegistry; private final PushAuthHandler pushAuthHandler; public SampleSSEPushChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { super(metricId, channelConfig, channelDependencies, channels); pushConnectionRegistry = channelDependencies.get(ZuulDependencyKeys.pushConnectionRegistry); pushAuthHandler = new SamplePushAuthHandler(PushProtocol.SSE.getPath()); } @Override protected void addPushHandlers(ChannelPipeline pipeline) { pipeline.addLast(PushAuthHandler.NAME, pushAuthHandler); pipeline.addLast(new PushRegistrationHandler(pushConnectionRegistry, PushProtocol.SSE)); pipeline.addLast(new SampleSSEPushClientProtocolHandler()); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SampleSSEPushClientProtocolHandler.java ================================================ /* * Copyright 2016 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.push; import com.netflix.config.CachedDynamicIntProperty; import com.netflix.zuul.netty.server.push.PushClientProtocolHandler; import com.netflix.zuul.netty.server.push.PushProtocol; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContentCompressor; import io.netty.handler.codec.http.HttpHeaders; 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.HttpVersion; /** * Created by saroskar on 10/10/16. */ public class SampleSSEPushClientProtocolHandler extends PushClientProtocolHandler { public static final CachedDynamicIntProperty SSE_RETRY_BASE_INTERVAL = new CachedDynamicIntProperty("zuul.push.sse.retry.base", 5000); @Override public void channelRead(ChannelHandlerContext ctx, Object mesg) throws Exception { if (mesg instanceof FullHttpRequest req) { if (req.method().equals(HttpMethod.GET) && PushProtocol.SSE.getPath().equals(req.uri())) { ctx.pipeline().fireUserEventTriggered(PushProtocol.SSE.getHandshakeCompleteEvent()); DefaultHttpResponse resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); HttpHeaders headers = resp.headers(); headers.add("Connection", "keep-alive"); headers.add("Content-Type", "text/event-stream"); headers.add("Transfer-Encoding", "chunked"); ChannelFuture cf = ctx.channel().writeAndFlush(resp); cf.addListener(future -> { if (future.isSuccess()) { ChannelPipeline pipeline = ctx.pipeline(); if (pipeline.get(HttpObjectAggregator.class) != null) { pipeline.remove(HttpObjectAggregator.class); } if (pipeline.get(HttpContentCompressor.class) != null) { pipeline.remove(HttpContentCompressor.class); } String reconnectInterval = "retry: " + SSE_RETRY_BASE_INTERVAL.get() + "\r\n\r\n"; ctx.writeAndFlush(reconnectInterval); } }); } } } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SampleWebSocketPushChannelInitializer.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.push; import com.netflix.netty.common.channel.config.ChannelConfig; import com.netflix.zuul.netty.server.ZuulDependencyKeys; import com.netflix.zuul.netty.server.push.PushAuthHandler; import com.netflix.zuul.netty.server.push.PushChannelInitializer; import com.netflix.zuul.netty.server.push.PushConnectionRegistry; import com.netflix.zuul.netty.server.push.PushProtocol; import com.netflix.zuul.netty.server.push.PushRegistrationHandler; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.ChannelGroup; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; /** * Author: Susheel Aroskar * Date: 5/16/18 */ public class SampleWebSocketPushChannelInitializer extends PushChannelInitializer { private final PushConnectionRegistry pushConnectionRegistry; private final PushAuthHandler pushAuthHandler; private static final int MAX_CONTENT_LENGTH = 65536; // 64KB public SampleWebSocketPushChannelInitializer( String metricId, ChannelConfig channelConfig, ChannelConfig channelDependencies, ChannelGroup channels) { super(metricId, channelConfig, channelDependencies, channels); pushConnectionRegistry = channelDependencies.get(ZuulDependencyKeys.pushConnectionRegistry); pushAuthHandler = new SamplePushAuthHandler(PushProtocol.WEBSOCKET.getPath()); } @Override protected void addPushHandlers(ChannelPipeline pipeline) { pipeline.addLast(PushAuthHandler.NAME, pushAuthHandler); pipeline.addLast(new WebSocketServerCompressionHandler(MAX_CONTENT_LENGTH)); pipeline.addLast(new WebSocketServerProtocolHandler(PushProtocol.WEBSOCKET.getPath(), null, true)); pipeline.addLast(new PushRegistrationHandler(pushConnectionRegistry, PushProtocol.WEBSOCKET)); pipeline.addLast(new SampleWebSocketPushClientProtocolHandler()); } } ================================================ FILE: zuul-sample/src/main/java/com/netflix/zuul/sample/push/SampleWebSocketPushClientProtocolHandler.java ================================================ /** * Copyright 2018 Netflix, Inc. * * 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. */ package com.netflix.zuul.sample.push; import com.netflix.zuul.netty.server.push.PushClientProtocolHandler; import com.netflix.zuul.netty.server.push.PushProtocol; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.util.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Author: Susheel Aroskar * Date: 5/16/18 */ public class SampleWebSocketPushClientProtocolHandler extends PushClientProtocolHandler { private static final Logger logger = LoggerFactory.getLogger(SampleWebSocketPushClientProtocolHandler.class); @Override public final void channelRead(ChannelHandlerContext ctx, Object msg) { try { if (!isAuthenticated()) { // Do not entertain ANY message from unauthenticated client PushProtocol.WEBSOCKET.sendErrorAndClose(ctx, 1007, "Missing authentication"); } else if (msg instanceof PingWebSocketFrame) { logger.debug("received ping frame"); ctx.writeAndFlush(new PongWebSocketFrame()); } else if (msg instanceof CloseWebSocketFrame) { logger.debug("received close frame"); ctx.close(); } else if (msg instanceof TextWebSocketFrame tf) { String text = tf.text(); logger.debug("received test frame: {}", text); if (text != null && text.startsWith("ECHO ")) { // echo protocol ctx.channel().writeAndFlush(tf.copy()); } } else if (msg instanceof BinaryWebSocketFrame) { logger.debug("received binary frame"); PushProtocol.WEBSOCKET.sendErrorAndClose(ctx, 1003, "Binary WebSocket frames not supported"); } } finally { ReferenceCountUtil.release(msg); } } } ================================================ FILE: zuul-sample/src/main/resources/application-benchmark.properties ================================================ ### Benchmark settings # disable safety throttles api.ribbon.MaxConnectionsPerHost=-1 api.netty.client.maxRequestsPerConnection=10000 api.netty.client.perServerWaterline=-1 zuul.origin.api.concurrency.protect.enabled=false # disable info headers zuul.responseFilter.send.headers=false ================================================ FILE: zuul-sample/src/main/resources/application-test.properties ================================================ # Test properties ================================================ FILE: zuul-sample/src/main/resources/application.properties ================================================ ### Instance env settings region=us-east-1 environment=test ### Eureka instance registration for this app #Name of the application to be identified by other services eureka.name=zuul #The port where the service will be running and serving requests eureka.port=7001 #Virtual host name by which the clients identifies this service eureka.vipAddress=${eureka.name}:${eureka.port} #For eureka clients running in eureka server, it needs to connect to servers in other zones eureka.preferSameZone=false # Don't register locally running instances. eureka.registration.enabled=false # By default don't validate eureka instance is from the cloud eureka.validateInstanceId=false # Loading Filters zuul.filters.packages=com.netflix.zuul.filters.common,\ com.netflix.zuul.sample.filters,\ com.netflix.zuul.sample.filters.endpoint,\ com.netflix.zuul.sample.filters.inbound,\ com.netflix.zuul.sample.filters.outbound ### Load balancing backends with Eureka eureka.shouldUseDns=true eureka.eurekaServer.context=discovery/v2 eureka.eurekaServer.domainName=discovery${environment}.netflix.net eureka.eurekaServer.gzipContent=true eureka.serviceUrl.default=http://${region}.${eureka.eurekaServer.domainName}:7001/${eureka.eurekaServer.context} api.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList api.ribbon.DeploymentContextBasedVipAddresses=api-test.netflix.net:7001 ### Load balancing backends without Eureka #eureka.shouldFetchRegistry=false #api.ribbon.listOfServers=100.66.23.88:7001,100.65.155.22:7001 #api.ribbon.client.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList #api.ribbon.DeploymentContextBasedVipAddresses=api-test.netflix.net:7001 # This has to be the last line @next=application-${environment}.properties ================================================ FILE: zuul-sample/src/main/resources/log4j2.xml ================================================ %d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5level [%t] %c: %msg%n ================================================ FILE: zuul-sample/src/main/resources/ssl/client.cert ================================================ -----BEGIN CERTIFICATE----- MIIE6jCCAtICAQEwDQYJKoZIhvcNAQEFBQAwOzELMAkGA1UEBhMCVVMxCzAJBgNV BAgMAkNBMQswCQYDVQQHDAJMRzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE3MTIx MjAwMzAzOVoXDTE4MTIxMjAwMzAzOVowOzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM AkNBMQswCQYDVQQHDAJMRzESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG 9w0BAQEFAAOCAg8AMIICCgKCAgEAvSVlDAU4ivsh/2V38cmsm7VvphaMe6v7xZzn elieZqxQj0b9NJ94q9dYkUvbI0JlJDfpRAiYHA32MW5GDkmIOeGZS3P+ystLnxu8 TIckP+dWqKez9qpXmW211vTeqbOvqOB8zyowStpVQHLF0bQ111ttFEW358ZXKFty gdcbqxJW4CXQ2DjTTGFy5EYB0+9e2PFo9KgKgWPsN1CU/o7/C0rUuotWRa9/WcqJ IHkdTKNM5Zpi/kkSWvLlPi8NJMLjOqnstE6GUBM2ur3H1akbF+uBT+sQszEScUlx 0hujtcDOBubc5iT3U3ZbWCPN+RW90J5WwFoWvMRDDT5OQYWBmJ5wmjWum5J0s0vs aupxM4oWdbeUdcjqQjI6euLZ+qc9VmoTP3f0O5bcKWvcRjnf8t9K+jkIEBjd8bFm XOuozG6vha+fl5730ihERN56iMkZv0SRMHPjrxxEWPhKmbVMoWXou/vHx/nyLpru bkHuPg4wVkno3UOmJ2GyHmLh1VRiYSsSBokwiOK+8iocYQwGcz4ayA5WYkghwMZV d9tLahflZkMhB+ikQwRHuspMcMoA/BimkpD2mtFPMsav6u6lbKrO27qU2SCrHdm0 zAHL/b/buPBw3JMTBHRKxbh4tafxgL06BGcLZAsYFdimpJXp5VoQr99xBkYukdcH 28NTjGECAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAvCzmIxiPxSDmAW1jF+9659xF aKo56pkWpQOSvBkrKRo4Ia1cVXPk5DfTlOffts6w3eSpSuaaW+4NyILrV21SThsy nPnM/JLwOLOgxVmRVDQe1a2MiCfNr7tsAej+6IxbRMHFwvdjA22nhYRGNyVbknq8 9fzLu/u3v0HphPbqc5DSKWtsZjH8CXnSjLwohjCqJsfWO/EwVEcksbOdwl5LWq7E VO5BMQ8Yyrm+qftnFNWF36o2vSLRWSh6vti+vcKR1tZm7J1XEN0Z/vKoQCiSF4iY Y5KUuDWjyij9UW8TJJOhn9XsSZrwdrbEbTXB3qpVAqaUEEzwUrK8G5w65bCW7zLr 627cFbDnorX90KmZ+7QMaJu6dCt1CZrPBM2a6oJid63NR7Fmt0jEJ3/zMf7qQQCZ nKhWlC2FqcA1NhPt21m8mKgYQKmd81LGyskJXVTaz/4zm5f1A1cRSDlzCIg2hi0e DQbiw8Ciz9YE3EXcecZu80PI1B/iaPWEmGPgxPfSFEw+v/9wLFaRpU3OsfCkDJX6 7M9QPJXMFyDCN+3qq6chvwJ1EI0vKfa08OM46Y3al4pL6Y9UQkJfS18LD5x54C0K RvXaxi7glFAyQf4HB7f3ecrqUgIv0WMW2XK+SG/w+tZ9p0jKGQq1FqAy7Hb2LWC7 fjDECaO9mbf8U3pr3u8= -----END CERTIFICATE----- ================================================ FILE: zuul-sample/src/main/resources/ssl/client.key ================================================ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,7381252110CA2BE7 wGH97s5ZDmRS0vDZcwewJ6WSIHGTP8Nr6ddqnFb0cr90LZDLnPr5IEXATbhBTHjw Q9YXVIUUOv16qOLy40jXk04d0GoinEMoxeOODIN98V0fNzZXaiaLoNU1Rhdui0f/ UlU8Hi5CgDCkvioWbSLNkmHPnqkfqn3YKGj+ybDblGMxvVIA3iPczhYthCcXVzcG ZZfrteKC8kFqaeWAmVyd+SWk6B8gdgdjcfiQY2wqxrpcNTKf3quC2LDiBG+qdvwN tmaCnCJ6Aa/hp3kc1HjpKeFprxOt0GQ8j++G6rnlrM/0itj+SsTo3XCdaDPxFepQ GB8NLGQ/8CEbjgmT+mmli7mOIbFzZn/S6BwNAtDZwB7w0KgTno8WhzAgF/grC55Q aot3PWFxAQyBRGLkh+8TO/rELYVc/3Kf1/ch9obl00KupuToK9LH/4woLWWkp2aM 7EQER0zufWiykctSqYA+CxtJXO83c7FJrI4PwNsHxzOKpodZ7iry82ntmEXTsDrH DEyeCbFMNMw+vKiqsaP2D/d4NDA7LFr2U0MfgDkbe/fRSDWZxPSUphAxfmUGLVrT mxzf7KixrFWd4PLvU8XZPsVgtbmAM5lxYZK5lYFMO/zrpibei/FJHmtnuH57OZOJ vBhtmoEh7JtwWmIfRqAspUYnhYyQIKnuB8ccNFzZm74PT/LrURnC4pm1bwl7t1FZ flg/SEeF1apdPSFkkd0snzgIXI0a8+u3KxxVvPNWbUzxilDS4ADyrZGnhSFOnP3o o5bG5F/Ufm8r/2YPCUpEqGQ/Kx+PmHNpnbUZRNJntFEwBPFdDg+cCOc3xkexJIKl QeaC+/7AKxEqV2xIQ0qlBgJFpW7I+TJZpwof4VjOWZWa93QUdl6iLE3G6auRusKT RxIct2jPuE1V+vu1PRxPtVARydzMMMCwR7jacOy8xqS9pSJyvVNOPuZeMdKmS4gO dbdrA2piMF3FCq6AujrP74YM3dIdXWMUItZtakC3NpRQXiHULOnHHhZhXrAQeIsU 45wP1lZljHXJauTQDWF43q8G/D30FiYw3wtGz5nTCWk9bNgLzcF85MdZD7UXlgYK WfkR8yuUoJmzuLuioL/3S8uVEhtFCzmW4VQ/2PSoauGkCz8PIvq+BBvlPcN3fDuM 8gAVn0XfIa8Qe753m60aTuJuLnNZhDJpckIpCM9A1+WctMlW5vliOSvTnu8leoE1 VEbSjMZR5llKSgRyv4ktnvE8bfZqIUhUPYdpDAiNThpc4+UeTsfv9zfWBPZ0HYSs KuSmYcn2pq8Jxbl5OFWFkfznOJozRUfTZd5Fr/U+KmlK7XepEuXZ9ZrnME0o3TxN q8aqmIb/MiIO5Wn5i1W6iX3z8sLmaY3asDqYWco4p1RgrkQAALPWAdg0LuWPKXGQ rsU2w1InsvjDOd+2GleA2MRJdSeu9RYVXNcPWH1XnuCirZE2TswjcuS+agAUfnM5 MYq1FhXTD7LNwXMZwaRyn21b/S55xe4c3Ge12FIMZsQtZX3fMOO14EF5EHy1RV/4 ilBc5n0F84KIgElpzUsH+D7bmheUWobn+CSCZwmZMao0SnSV43jXLl0Yg7xBk6OL UUfJuGOK1QB7KRvkbPHQByvtAVuhzsNpgYLcz4xOdx5gjq2RWxWRaCCn/3+2a/8j WzT9aDjGE4u58cBq+QLy3wfvSKPv3nXn7gz3BtkIzTx6yuri7ontuvh/zr4bCPc8 RWiZFvsuS477dngICIiTQsRzgB0cMfvWxjjqBR1e6yJ1cRZ5UTqinc4ABLJxWWu0 ThelmCkV1P5VsmocAdmHeX6SH4L+isnlLbc8RXJXdeJZ8XLiPJbiVCilG8CJV1Cf YD5j91v3GV6Ns/BC4pkpXmWEhcyISAWy5tSQRt4+lyvZIho8IEWMOly+nDi62PuW JQ0q/arCKpfsJ8fWBUv5q6/yBnYqZPLAbIn7zrm0xN0j7AsKc3rxdNv2pzVOny0E lXJVFVl4PvdHZ8WlnzYRWnsTESlgk3mFkoYjDnrruPrYCfNB8O6z2fRT/bZkckmM r0Raaybv7LSm2/YqP3NtvzbsrdKCuct/csINBMJT41Z2pZpAH4CYubtfcW/qQcQx zgANOlmY//VO3lVJYQTf79/vOrRPZA1DIm2JUFFg56lESYhDCOmos/4rNgsrfgvY DUORz3nBBwsb/78Y397MjzUW2ZryQ7UdFnfvhaR2hRoPUnau7ajgXx4JxFHkjpnR gGiXjHp9X+IgTxio1uCtPUbippJqI4H4WD12EFc9+UZ0o1fBmySUKhl/LXWPyvVQ RA5Qy2zVxbe8XtedZXDOpHadvwgdMCR20zcMP3Aq5WsTKUaZqgG4Os2xDWg5MlH1 d2MAlY92jlQxWzSTfTH2A1j+Uu1cyT8Q8r85ag447ZXbe47jfAQjbvXzGO+NVek+ ya+NTCz6qOaqkJNjcRptsmza1vX6mKFjwFvt+EMe3rm7NIskChDXiaJqDuEEpOCV KojSFMerIzqYrSpcGS5T7rZpW+rW/Ub+C1vqw2KC7+e3mnNqTtJ9yKFtVtuFE/iv BlvwPRhYJM/XN3to+zGPjgeKTes37jD8Yad9Cw6QoMd+Bd/trpUdwLtqlant6C3d XAN1FCiPHnTMCOP7zT53UCm13RiIKwWoRAEyLGA03xsIDwWLE7cZPOME4NC+tMFR w4lqJFanUiEW6KCu1XAWjGsa1G+PqAcj08nUCqPSszR84eEGLMm6Af3RLqPjOwXj 9QGHy/K3L+ktlb94qwGFEXplvGWmYay9uIbu4/K4ws+sT967JenYnn3suJMYBVwy LsXcPxqpIoRQ/dwZc0LPyX89GWhbiijPVzlb9WV4W5YHbZdw+mlga4CMGqdHXB+q e7aqG/hetCmnhgW0uWWTSVzG/h6TQ0fcufSAP/t8LMBFH0tVYLWRmWz6Kp6gkafw Wm5NbSb7qPlLZxpVB1IymI24+uFB6aReNU/hmJCnej2s9XtGkZpbQH5f9IjZOuuR EbktIRPJcuJDdqJA5HFjlABDOR61ujaXvkfi/Mn6r4zeUsbn6kpouPQpDNkPqiPC m9uPslBM2P8XQdAAb67pCBqRpMucNUsXz2xfztiARj2JCvuoIlHqn5TDi3a3kOit -----END RSA PRIVATE KEY----- ================================================ FILE: zuul-sample/src/main/resources/ssl/server.cert ================================================ -----BEGIN CERTIFICATE----- MIIFVjCCAz4CCQCOVDcKFZeqxzANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJV UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAkxHMQswCQYDVQQKDAJORjELMAkGA1UE CwwCRUUxEjAQBgNVBAMMCWxvY2FsaG9zdDEWMBQGCSqGSIb3DQEJARYHYUBiLmNv bTAeFw0xNzEyMTEyMzI0MDFaFw0xODEyMTEyMzI0MDFaMG0xCzAJBgNVBAYTAlVT MQswCQYDVQQIDAJDQTELMAkGA1UEBwwCTEcxCzAJBgNVBAoMAk5GMQswCQYDVQQL DAJFRTESMBAGA1UEAwwJbG9jYWxob3N0MRYwFAYJKoZIhvcNAQkBFgdhQGIuY29t MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw9Oud0mmtMWouKbguGq0 8vPP8gwGybDwRi/t4rmWUb/+xGHW0iqRYbVDwE4i8gCuM4g0HN3+ESglBFMvuxui BIPx60dR1sEP3IlX/22HcBv97Zj+vjchBMY/3AIP06nKgHGmnorKYYs5xczEEyxB D+0mZECeQhK+IbhoZdTphrQruOTQYpkGpkyEbpygX3u2chpGoGSxOTlz0WxBhpbR wr2jiz0IYxplLsIq2VL7XBlUd5yoaxOyrcc1to+uPH27R9sbrj5513LzZ22ebcWa dQJGtOdGfoSG+wH8k/94MSAr1bsBOJ1jq3XIuD38fbdkUyH1DPwkzCnSD6qD6sYt XMYTgfAysCYxxjw/GrcIEG1TNbys04YJCwRp5IB6alsya4ovbkqa40ToLCBAtC6v c59Ppo3ejWZPcpLSe5oIB4loRtWYlWjJnUf6kjGuCmyltTzRrgvng4P/VULq91QE FcCqsbKhI7/eoO20Pt/XYR7gDXc4z7YCuW/twaEpRsclrUiXY0E0Bq41FrhyW2rp Hl8x9kOJREGELMHp3hZycTh3FpxeN6Ti2znriz758dhNWKns5xAe8ZjSY/PpbcNq nHhDm2RQb1QiCw6ttJy4bMJmKofAEiEbE1Of4mPftRvM7zDM744nuOUPWZO1jQql p1stf6qKY5b6n771+qTQXJ0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAWe22QQmw +t2Cmau2i0dTdgHjmYU1KCwath5fCSdb95c8lrLHWhQHQt70DCkU1KmbyPY0y/2G g9Ofkc49eQCPrzl/FrcYKb6a9OIgByTMCrEAhDzSf4iB+eeCF/nMzqYciXYj3/DF VFenBhLw9ZfqePgamlkNZWf7RICOklQu5vHCgs+/xUXlenB6CXNm+9Qy8BD9FfnP vTpr3wJVz0UK6JZafuLKcGpjaVCpUTeMmToVlr1PxyyTlgVFBbK7JfKlTXh7cZRw 4FUarTAjgk3Vk3Q+Tdir6R8+DabqghJnTjOF/tETHeNt8msAc59VzRuzhy9ZeGkI vWq+LNqs+/+deaw5hsbkbD9AoDmHmV2U9Ki7zIYnRizl545f1nZQdPNeyYHO/QIl cP/U51pPid/bqlPiVwLg7eNGC1rI19cd0wZH4cjTwUtgqQhX/I3e5Boot9kdtvEr Tr0I656jmmzksjF8863wDKqW/RJeje8lzKXbo0vc2/zv2KX11zIGC4OJ7z404BF5 cEYnFmswIZQfR1mjTMN4ZfjCtBtWuc2uueDUUsgQ9YUss5bKpd8Xm1g9rguQYnBt M84aFXiDvbKhvI5t1Z5Xa9LN6FO0u0rIG+b8tOU8rlqhLp7SGYpRalb6UU2keZRh qNCCixZMahJMgVovuI8LkttwvdpTc7705W8= -----END CERTIFICATE----- ================================================ FILE: zuul-sample/src/main/resources/ssl/server.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDD0653Saa0xai4 puC4arTy88/yDAbJsPBGL+3iuZZRv/7EYdbSKpFhtUPATiLyAK4ziDQc3f4RKCUE Uy+7G6IEg/HrR1HWwQ/ciVf/bYdwG/3tmP6+NyEExj/cAg/TqcqAcaaeisphiznF zMQTLEEP7SZkQJ5CEr4huGhl1OmGtCu45NBimQamTIRunKBfe7ZyGkagZLE5OXPR bEGGltHCvaOLPQhjGmUuwirZUvtcGVR3nKhrE7KtxzW2j648fbtH2xuuPnnXcvNn bZ5txZp1Aka050Z+hIb7AfyT/3gxICvVuwE4nWOrdci4Pfx9t2RTIfUM/CTMKdIP qoPqxi1cxhOB8DKwJjHGPD8atwgQbVM1vKzThgkLBGnkgHpqWzJrii9uSprjROgs IEC0Lq9zn0+mjd6NZk9yktJ7mggHiWhG1ZiVaMmdR/qSMa4KbKW1PNGuC+eDg/9V Qur3VAQVwKqxsqEjv96g7bQ+39dhHuANdzjPtgK5b+3BoSlGxyWtSJdjQTQGrjUW uHJbaukeXzH2Q4lEQYQsweneFnJxOHcWnF43pOLbOeuLPvnx2E1YqeznEB7xmNJj 8+ltw2qceEObZFBvVCILDq20nLhswmYqh8ASIRsTU5/iY9+1G8zvMMzvjie45Q9Z k7WNCqWnWy1/qopjlvqfvvX6pNBcnQIDAQABAoICAQCNP1c9NYOoKlYLclruyhDg mNnpxaDzw8tbZODaQ3DYYHr73XJcv1WDu4I16GYuVi1QgDLOi5ThfSpOF057UHAp f550TUoLc8/kC7DMTY1+YMJkqZE9VHhdgD25jEcsLfEhelhrTMKzXv/52zumdKot OaoSb8V29RvtKJ0srkkO31AWGfzw0V3Jf7GaMyE+Hfa4EJnWwSpPk1Alw0b0ND7y j2SyXwB4syY+dtD/Vmp1wNN7PyT/rwKXc/QbTUGo4iu+pZ0urmOl8oT8mkXG+dvy AAVOIO9o/GB8Fq+/dGqWTJUxoaQ82NF2cAuqUROY/jm/+ONPTWOhW5znrd9e7Tjz yOgLA1ZlICHCzjJ8FDPtBlgr1ANhhg+axLkK02KJaHMohwxg6SrOentEzyryoYSE JMCHsH4E1dWI2mUclqp7XbLgOAcvz7MCg0hOQYi7VqdSf4egnkpESTpsx9WKjumO XwqAAeTos3AqsxBKAwHdoe2z9gjxaTRP6IbmsJU5iw56DGgqIUemhIv/fhjZGAYg PxX3FLxU/wSod+OcOy0NcN9wCi+zuSX8M2DDjHo6Ewz/rN/3tP+OH4mnnSiJ8WR8 9DfD31fLiKbvbuJeM1nRKtB/3WX2Rc0XWZFaUpHImzCzOdb18KFUHfzvA6GJrnk/ oV6f1jL91JQzSySL94gV5QKCAQEA8oLIeocN82F4O12dqR3pNRYM/CYEVJwWOUEm ukPvHziiVKl8Gnjd3tNu5N+vAeZEsTKs9fd4qVuP+STiP3F+5zUoVZHVvkL3LfSI r9HtnpRdzpFIA6RWTebd7snw44faDbeafCLt/0O2kaiEMGZwH+mqJ8CbEdc30OvN Y8F3mmrsFerTZnfl8gjgx5AWIpooVb4wE4gpfblT9ixKbiAPkkKlnuRuTrkVWD7r BdCmmEUYuprpejrsUwg55R1yguFnr1E6QVuVSOviVEBpdZXWX+M3Bx8+zl0Qouyk HTv5/CWq8iByE4MvIje6sup+Fz37t5Uu9tiXx1N11AMYcAJuLwKCAQEAzrglGQNW INyVCNGIXoPGmq6rOotArJjQAlmx+K29Mja6RvY/y7f8ZB3Uk39LXoQwzW6RtMTw fKru/HKc05JedMA00tsCBhwfiJ8gmEI8HwWOcALNCX9uw6ZOO9llg03G3CmwuIhN KBj+sZ4UXD9mzsGsWhAdn8/Y1EEyMQakaAZNLjinnDUu+LdTcy+0/uuyiTRmbklk qJYbdtGABC9Oib2/nvsKaTjhbFiOBSxE/8EYjEmWfT+WK+NffSvELQ3EVl7vLmgj qo08XFZx3AxHITe4Or22x2nfih8E9TRzW0LTsfydaVwyub5qnr3kNJ6qRsEX7Qkf S2/RMqWCtn8a8wKCAQBA473BC2IwPWRufh4xok9EZSIUVhfSi/FmYIh8TrEtKXpG LROIAc9cUDbcBv5NA9BdmbGuHwmqR1W+1J+1WikatJ6WRu9qeYCqS0RHx2RNimWP YFBkqRRuw9eejWpnd3JhOT+c97u3EedIEk9MpBxcbamZ+W+E1pGY1X+fsaTPLMz/ EFaAlJRyru12eJdzqswgJUO39jcj7PMKa89+qBWCjVLDsVvStLOBaVR5udrZ46M6 Szkt+5ZAoXLcW4TIgIe94X40/sxzNqrY4GNXk0BJaALRZQrpLP3GmotPRz0cuveC 0iu0DOYPwdmzBgu3LF6uQLzQUCRMsYhVsn5Xek8BAoIBAGWMLB0neG3YLhYQ6E6V qUBfQZoWwgSHZNdivHyOzHwYSlWFrj0i+ocr6Ds0sw+RHHAuOsF0ZTa4uYGlw8hj BKeRq+FQ2KOruQniMZ7aGrKahigcGCDsSrstvQzFdIqV8HRCvp9Hxa9G6AbUwue1 9YjntwTfGc5hygAqrr9KpgS747oq9ptTvOlNFV9mNiFsI14nMZJH13zBkGhD7gEg RBKB9dnhNHIQERyqO8nqv1JrxuVTWOvaCqkwnr3cfBgtxR8wr4o6ehrUGqy5gmE4 XtDAkG26uEkphzhQmJzj0S8pmti6YZFaS0jXc4Tbf3kh4D+1p003x/nEyh15FMcV lWUCggEBAKmXRrTQaY7C48drOyo60k5mh8puEpVK0K3VR7+5cOhr1bwbpOahocTh tj3JX00TS8q5h6DFmqLQOGRx+TW4E5ZuIHNv6WApqTy0WaXwNBfoiMIEs21NzCwq Tt3oa17kXJa+ih96MoJiFLwN2SOJmCEWRhNsDegEkrwhgoZj++F+/Pu2vR7rm5S/ 6xHMCk66RQoH9yGhJ+Ra6Zjqfa6xdHrGUMDdmewnU8g8dPHtURWZbF8Qd+7GaUdf 75Ar+B/phO8sK4Gv3na18LAIAYnHejq1aJ+90eDbiM33pxrYU9cGhzkFtzW5/u1u sT8eOekbbJpw4wdpOHysWuw/qdtn/ZY= -----END PRIVATE KEY----- ================================================ FILE: zuul-sample/src/main/resources/ssl/truststore.key ================================================ zuul123