* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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