Skip to content

Commit 0df52c8

Browse files
Netty4 HTTP authn enhancements (#92220) (#96703)
This is a backport of multiple work items related to authentication enhancements for HTTP, which were originally merged in the 8.8 - 8.9 releases. Hence, the HTTP (only the netty4-based implementation (default), not the NIO one) authentication implementation gets a throughput boost (especially for requests failing authn). Relates to: ES-6188 #92220 #95112
1 parent fe18a67 commit 0df52c8

File tree

67 files changed

+3914
-618
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+3914
-618
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.http.netty4;
10+
11+
import io.netty.buffer.Unpooled;
12+
import io.netty.channel.ChannelHandlerContext;
13+
import io.netty.channel.ChannelInboundHandlerAdapter;
14+
import io.netty.handler.codec.DecoderResult;
15+
import io.netty.handler.codec.http.HttpContent;
16+
import io.netty.handler.codec.http.HttpObject;
17+
import io.netty.handler.codec.http.HttpRequest;
18+
import io.netty.handler.codec.http.LastHttpContent;
19+
import io.netty.util.ReferenceCountUtil;
20+
21+
import org.elasticsearch.action.ActionListener;
22+
import org.elasticsearch.action.support.ContextPreservingActionListener;
23+
import org.elasticsearch.common.util.concurrent.ThreadContext;
24+
import org.elasticsearch.http.netty4.internal.HttpValidator;
25+
import org.elasticsearch.transport.Transports;
26+
27+
import java.util.ArrayDeque;
28+
29+
import static org.elasticsearch.http.netty4.Netty4HttpHeaderValidator.State.DROPPING_DATA_PERMANENTLY;
30+
import static org.elasticsearch.http.netty4.Netty4HttpHeaderValidator.State.DROPPING_DATA_UNTIL_NEXT_REQUEST;
31+
import static org.elasticsearch.http.netty4.Netty4HttpHeaderValidator.State.FORWARDING_DATA_UNTIL_NEXT_REQUEST;
32+
import static org.elasticsearch.http.netty4.Netty4HttpHeaderValidator.State.QUEUEING_DATA;
33+
import static org.elasticsearch.http.netty4.Netty4HttpHeaderValidator.State.WAITING_TO_START;
34+
35+
public class Netty4HttpHeaderValidator extends ChannelInboundHandlerAdapter {
36+
37+
private final HttpValidator validator;
38+
private final ThreadContext threadContext;
39+
private ArrayDeque<HttpObject> pending = new ArrayDeque<>(4);
40+
private State state = WAITING_TO_START;
41+
42+
public Netty4HttpHeaderValidator(HttpValidator validator, ThreadContext threadContext) {
43+
this.validator = validator;
44+
this.threadContext = threadContext;
45+
}
46+
47+
State getState() {
48+
return state;
49+
}
50+
51+
@SuppressWarnings("fallthrough")
52+
@Override
53+
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
54+
assert msg instanceof HttpObject;
55+
final HttpObject httpObject = (HttpObject) msg;
56+
57+
switch (state) {
58+
case WAITING_TO_START:
59+
assert pending.isEmpty();
60+
pending.add(ReferenceCountUtil.retain(httpObject));
61+
requestStart(ctx);
62+
assert state == QUEUEING_DATA;
63+
break;
64+
case QUEUEING_DATA:
65+
pending.add(ReferenceCountUtil.retain(httpObject));
66+
break;
67+
case FORWARDING_DATA_UNTIL_NEXT_REQUEST:
68+
assert pending.isEmpty();
69+
if (httpObject instanceof LastHttpContent) {
70+
state = WAITING_TO_START;
71+
}
72+
ctx.fireChannelRead(httpObject);
73+
break;
74+
case DROPPING_DATA_UNTIL_NEXT_REQUEST:
75+
assert pending.isEmpty();
76+
if (httpObject instanceof LastHttpContent) {
77+
state = WAITING_TO_START;
78+
}
79+
// fall-through
80+
case DROPPING_DATA_PERMANENTLY:
81+
assert pending.isEmpty();
82+
ReferenceCountUtil.release(httpObject); // consume without enqueuing
83+
break;
84+
}
85+
86+
setAutoReadForState(ctx, state);
87+
}
88+
89+
private void requestStart(ChannelHandlerContext ctx) {
90+
assert state == WAITING_TO_START;
91+
92+
if (pending.isEmpty()) {
93+
return;
94+
}
95+
96+
final HttpObject httpObject = pending.getFirst();
97+
final HttpRequest httpRequest;
98+
if (httpObject instanceof HttpRequest && httpObject.decoderResult().isSuccess()) {
99+
// a properly decoded HTTP start message is expected to begin validation
100+
// anything else is probably an error that the downstream HTTP message aggregator will have to handle
101+
httpRequest = (HttpRequest) httpObject;
102+
} else {
103+
httpRequest = null;
104+
}
105+
106+
state = QUEUEING_DATA;
107+
108+
if (httpRequest == null) {
109+
// this looks like a malformed request and will forward without validation
110+
ctx.channel().eventLoop().submit(() -> forwardFullRequest(ctx));
111+
} else {
112+
Transports.assertDefaultThreadContext(threadContext);
113+
// this prevents thread-context changes to propagate to the validation listener
114+
// atm, the validation listener submits to the event loop executor, which doesn't know about the ES thread-context,
115+
// so this is just a defensive play, in case the code inside the listener changes to not use the event loop executor
116+
ContextPreservingActionListener<Void> contextPreservingActionListener = new ContextPreservingActionListener<>(
117+
threadContext.wrapRestorable(threadContext.newStoredContext(false)),
118+
ActionListener.wrap(aVoid ->
119+
// Always use "Submit" to prevent reentrancy concerns if we are still on event loop
120+
ctx.channel().eventLoop().submit(() -> forwardFullRequest(ctx)),
121+
e -> ctx.channel().eventLoop().submit(() -> forwardRequestWithDecoderExceptionAndNoContent(ctx, e))
122+
)
123+
);
124+
// this prevents thread-context changes to propagate beyond the validation, as netty worker threads are reused
125+
try (ThreadContext.StoredContext ignore = threadContext.newStoredContext(false)) {
126+
validator.validate(httpRequest, ctx.channel(), contextPreservingActionListener);
127+
}
128+
}
129+
}
130+
131+
private void forwardFullRequest(ChannelHandlerContext ctx) {
132+
Transports.assertDefaultThreadContext(threadContext);
133+
assert ctx.channel().eventLoop().inEventLoop();
134+
assert ctx.channel().config().isAutoRead() == false;
135+
assert state == QUEUEING_DATA;
136+
137+
boolean fullRequestForwarded = forwardData(ctx, pending);
138+
139+
assert fullRequestForwarded || pending.isEmpty();
140+
if (fullRequestForwarded) {
141+
state = WAITING_TO_START;
142+
requestStart(ctx);
143+
} else {
144+
state = FORWARDING_DATA_UNTIL_NEXT_REQUEST;
145+
}
146+
147+
assert state == WAITING_TO_START || state == QUEUEING_DATA || state == FORWARDING_DATA_UNTIL_NEXT_REQUEST;
148+
setAutoReadForState(ctx, state);
149+
}
150+
151+
private void forwardRequestWithDecoderExceptionAndNoContent(ChannelHandlerContext ctx, Exception e) {
152+
Transports.assertDefaultThreadContext(threadContext);
153+
assert ctx.channel().eventLoop().inEventLoop();
154+
assert ctx.channel().config().isAutoRead() == false;
155+
assert state == QUEUEING_DATA;
156+
157+
HttpObject messageToForward = pending.getFirst();
158+
boolean fullRequestDropped = dropData(pending);
159+
if (messageToForward instanceof HttpContent) {
160+
// if the request to forward contained data (which got dropped), replace with empty data
161+
messageToForward = ((HttpContent) messageToForward).replace(Unpooled.EMPTY_BUFFER);
162+
}
163+
messageToForward.setDecoderResult(DecoderResult.failure(e));
164+
ctx.fireChannelRead(messageToForward);
165+
166+
assert fullRequestDropped || pending.isEmpty();
167+
if (fullRequestDropped) {
168+
state = WAITING_TO_START;
169+
requestStart(ctx);
170+
} else {
171+
state = DROPPING_DATA_UNTIL_NEXT_REQUEST;
172+
}
173+
174+
assert state == WAITING_TO_START || state == QUEUEING_DATA || state == DROPPING_DATA_UNTIL_NEXT_REQUEST;
175+
setAutoReadForState(ctx, state);
176+
}
177+
178+
@Override
179+
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
180+
state = DROPPING_DATA_PERMANENTLY;
181+
while (true) {
182+
if (dropData(pending) == false) {
183+
break;
184+
}
185+
}
186+
super.channelInactive(ctx);
187+
}
188+
189+
private static boolean forwardData(ChannelHandlerContext ctx, ArrayDeque<HttpObject> pending) {
190+
final int pendingMessages = pending.size();
191+
try {
192+
HttpObject toForward;
193+
while ((toForward = pending.poll()) != null) {
194+
ctx.fireChannelRead(toForward);
195+
ReferenceCountUtil.release(toForward); // reference cnt incremented when enqueued
196+
if (toForward instanceof LastHttpContent) {
197+
return true;
198+
}
199+
}
200+
return false;
201+
} finally {
202+
maybeResizePendingDown(pendingMessages, pending);
203+
}
204+
}
205+
206+
private static boolean dropData(ArrayDeque<HttpObject> pending) {
207+
final int pendingMessages = pending.size();
208+
try {
209+
HttpObject toDrop;
210+
while ((toDrop = pending.poll()) != null) {
211+
ReferenceCountUtil.release(toDrop, 2); // 1 for enqueuing, 1 for consuming
212+
if (toDrop instanceof LastHttpContent) {
213+
return true;
214+
}
215+
}
216+
return false;
217+
} finally {
218+
maybeResizePendingDown(pendingMessages, pending);
219+
}
220+
}
221+
222+
private static void maybeResizePendingDown(int largeSize, ArrayDeque<HttpObject> pending) {
223+
if (pending.size() <= 4 && largeSize > 32) {
224+
// Prevent the ArrayDeque from becoming forever large due to a single large message.
225+
ArrayDeque<HttpObject> old = pending;
226+
pending = new ArrayDeque<>(4);
227+
pending.addAll(old);
228+
}
229+
}
230+
231+
private static void setAutoReadForState(ChannelHandlerContext ctx, State state) {
232+
ctx.channel().config().setAutoRead((state == QUEUEING_DATA || state == DROPPING_DATA_PERMANENTLY) == false);
233+
}
234+
235+
enum State {
236+
WAITING_TO_START,
237+
QUEUEING_DATA,
238+
FORWARDING_DATA_UNTIL_NEXT_REQUEST,
239+
DROPPING_DATA_UNTIL_NEXT_REQUEST,
240+
DROPPING_DATA_PERMANENTLY
241+
}
242+
}

0 commit comments

Comments
 (0)