2 * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.transport.http;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.annotations.Beta;
13 import com.google.common.annotations.VisibleForTesting;
14 import io.netty.buffer.ByteBuf;
15 import io.netty.channel.ChannelHandlerContext;
16 import io.netty.channel.SimpleChannelInboundHandler;
17 import io.netty.handler.codec.http.DefaultFullHttpResponse;
18 import io.netty.handler.codec.http.FullHttpRequest;
19 import io.netty.handler.codec.http.FullHttpResponse;
20 import io.netty.handler.codec.http.HttpHeaderNames;
21 import io.netty.handler.codec.http.HttpHeaders;
22 import io.netty.handler.codec.http.HttpMethod;
23 import io.netty.handler.codec.http.HttpResponseStatus;
24 import io.netty.handler.codec.http.HttpUtil;
25 import io.netty.handler.codec.http.HttpVersion;
26 import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
27 import io.netty.util.AsciiString;
29 import java.net.URISyntaxException;
30 import java.util.Arrays;
32 import java.util.function.Function;
33 import java.util.stream.Collectors;
34 import org.eclipse.jdt.annotation.NonNull;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.opendaylight.netconf.transport.api.TransportChannelListener;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * Abstract base class for handling HTTP events on a {@link HTTPServer}'s channel pipeline. Users will typically add
43 * a subclass onto the corresponding {@link HTTPTransportChannel} when notified through
44 * {@link TransportChannelListener}.
46 * <p>Incoming requests are processed in four distinct stages:
48 * <li>request method binding, performed on the Netty thread via {@link #implementationOf(HttpMethod)}</li>
49 * <li>request path and header binding, performed on the Netty thread via
50 * {@link #prepareRequest(ImplementedMethod, URI, HttpHeaders)}</li>
51 * <li>request execution, performed in a dedicated thread, via
52 * {@link PendingRequest#execute(PendingRequestListener, java.io.InputStream)}</li>
53 * <li>response execution, performed in another dedicated thread</li>
55 * This split is done to off-load request and response body construction, so that it can result in a number of messages
56 * being sent down the Netty pipeline. That aspect is important when producing chunked-encoded response from a state
57 * snapshot -- which is the typical use case.
60 public abstract class HTTPServerSession extends SimpleChannelInboundHandler<FullHttpRequest> {
61 private static final Logger LOG = LoggerFactory.getLogger(HTTPServerSession.class);
62 private static final AsciiString STREAM_ID = ExtensionHeaderNames.STREAM_ID.text();
63 private static final Map<HttpMethod, ImplementedMethod> ALL_METHODS = Arrays.stream(ImplementedMethod.values())
64 .collect(Collectors.toUnmodifiableMap(ImplementedMethod::httpMethod, Function.identity()));
66 // FIXME: HTTP/1.1 and HTTP/2 behave differently w.r.t. incoming requests and their servicing:
67 // 1. HTTP/1.1 uses pipelining, therefore:
68 // - we cannot halt incoming pipeline
69 // - we may run request prepare() while a request is executing in the background, but
70 // - we MUST send responses out in the same order we have received them
71 // - SSE GET turns the session into a sender, i.e. no new requests can be processed
72 // 2. HTTP/2 uses concurrent execution, therefore
73 // - we need to track which streams are alive and support terminating pruning requests when client resets
75 // - SSE is nothing special
76 // - we have Http2Settings, which has framesize -- which we should use when streaming responses
77 // We support HTTP/1.1 -> HTTP/2 upgrade for the first request only -- hence we know before processing the first
78 // result which mode of operation is effective. We probably need to have two subclasses of this thing, with
79 // HTTP/1.1 and HTTP/2 specializations.
81 // FIXME: this heavily depends on the object model and is tied to HTTPServer using aggregators, so perhaps we should
82 // reconsider the design
84 private final HTTPScheme scheme;
86 // Only valid when the session is attached to a Channel
87 private ServerRequestExecutor executor;
89 protected HTTPServerSession(final HTTPScheme scheme) {
90 super(FullHttpRequest.class, false);
91 this.scheme = requireNonNull(scheme);
95 public final void handlerAdded(final ChannelHandlerContext ctx) {
96 final var channel = ctx.channel();
97 executor = new ServerRequestExecutor(channel.remoteAddress().toString());
98 LOG.debug("Threadpools for {} started", channel);
102 public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
103 shutdownTreadpools(ctx);
104 super.channelInactive(ctx);
108 public final void handlerRemoved(final ChannelHandlerContext ctx) {
109 shutdownTreadpools(ctx);
112 private void shutdownTreadpools(final ChannelHandlerContext ctx) {
114 LOG.debug("Threadpools for {} shut down", ctx.channel());
118 protected final void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest msg) {
119 final var headers = msg.headers();
120 // non-null indicates HTTP/2 request, which we need to propagate to any response
121 final var streamId = headers.getInt(STREAM_ID);
122 final var version = msg.protocolVersion();
124 // first things first:
125 // - HTTP semantics: we MUST have the equivalent of a Host header, as per
126 // https://www.rfc-editor.org/rfc/rfc9110#section-7.2
127 // - HTTP/1.1 protocol: it is a 400 Bad Request, as per
128 // https://www.rfc-editor.org/rfc/rfc9112#section-3.2, if
129 // - there are multiple values
130 // - the value is invalid
131 final var hostItr = headers.valueStringIterator(HttpHeaderNames.HOST);
132 if (!hostItr.hasNext()) {
133 LOG.debug("No Host header in request {}", msg);
135 respond(ctx, streamId, new DefaultFullHttpResponse(version, HttpResponseStatus.BAD_REQUEST));
138 final var host = hostItr.next();
139 if (hostItr.hasNext()) {
140 LOG.debug("Multiple Host header values in request {}", msg);
142 respond(ctx, streamId, new DefaultFullHttpResponse(version, HttpResponseStatus.BAD_REQUEST));
147 hostUri = scheme.hostUriOf(host);
148 } catch (URISyntaxException e) {
149 LOG.debug("Invalid Host header value '{}'", host, e);
151 respond(ctx, streamId, new DefaultFullHttpResponse(version, HttpResponseStatus.BAD_REQUEST));
156 // - check if we implement requested method
157 // - we do NOT implement CONNECT method, which is the only valid use of URIs in authority-form
158 final var nettyMethod = msg.method();
159 final var method = implementationOf(nettyMethod);
160 if (method == null) {
161 LOG.debug("Method {} not implemented", nettyMethod);
163 respond(ctx, streamId, new DefaultFullHttpResponse(version, HttpResponseStatus.NOT_IMPLEMENTED));
167 // next possibility: asterisk-form, as per https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4
168 // it is only applicable to server-wide OPTIONS https://www.rfc-editor.org/rfc/rfc9110#section-9.3.7, which
169 // should result in Allow: listing the contents of IMPLEMENTED_METHODS
170 final var uri = msg.uri();
171 if (HttpUtil.isAsteriskForm(uri)) {
173 respond(ctx, streamId, asteriskRequest(version, method));
177 // we are down to three possibilities:
178 // - origin-form, as per https://www.rfc-editor.org/rfc/rfc9112#section-3.2.1
179 // - absolute-form, as per https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2
180 // - authority-form, as per https://www.rfc-editor.org/rfc/rfc9112#section-3.2.3
181 // BUT but we do not implement the CONNECT method, so let's enlist URI's services first
182 final URI requestUri;
184 requestUri = new URI(uri);
185 } catch (URISyntaxException e) {
186 LOG.debug("Invalid request-target '{}'", uri, e);
188 respond(ctx, streamId, new DefaultFullHttpResponse(version, HttpResponseStatus.BAD_REQUEST));
192 // as per https://www.rfc-editor.org/rfc/rfc9112#section-3.3
194 if (requestUri.isAbsolute()) {
195 // absolute-form is the Target URI
196 targetUri = requestUri;
197 } else if (HttpUtil.isOriginForm(requestUri)) {
198 // origin-form needs to be combined with Host header
199 targetUri = hostUri.resolve(requestUri);
201 LOG.debug("Unsupported request-target '{}'", requestUri);
203 respond(ctx, streamId, new DefaultFullHttpResponse(version, HttpResponseStatus.BAD_REQUEST));
207 switch (prepareRequest(method, targetUri, msg.headers())) {
208 case CompletedRequest completed -> {
210 LOG.debug("Immediate response to {} {}", method, targetUri);
211 executor.respond(ctx, streamId, version, completed.asResponse());
213 case PendingRequest<?> pending -> {
214 LOG.debug("Scheduling execution of {} {}", method, targetUri);
215 executor.executeRequest(ctx, version, streamId, pending, msg.content());
221 static final @NonNull FullHttpResponse asteriskRequest(final HttpVersion version, final ImplementedMethod method) {
222 if (method == ImplementedMethod.OPTIONS) {
223 final var response = new DefaultFullHttpResponse(version, HttpResponseStatus.OK);
224 response.headers().set(HttpHeaderNames.ALLOW, "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT");
227 LOG.debug("Invalid use of '*' with method {}", method);
228 return new DefaultFullHttpResponse(version, HttpResponseStatus.BAD_REQUEST);
232 * Check whether this session implements a particular HTTP method. Default implementation supports all
233 * {@link ImplementedMethod}s.
235 * @param method an HTTP method
236 * @return an {@link ImplementedMethod}, or {@code null} if the method is not implemented
238 protected @Nullable ImplementedMethod implementationOf(final @NonNull HttpMethod method) {
239 return ALL_METHODS.get(method);
243 * Prepare an incoming HTTP request. The first two arguments are provided as context, which should be reflected back
244 * to {@link #respond(ChannelHandlerContext, Integer, FullHttpResponse)} when reponding to the request.
246 * <p>The ownership of request body is transferred to the implementation of this method. It is its responsibility to
247 * {@link ByteBuf#release()} it when no longer needed.
249 * @param method {@link ImplementedMethod} being requested
250 * @param targetUri URI of the target resource
251 * @param headers request {@link HttpHeaders}
254 protected abstract PreparedRequest prepareRequest(ImplementedMethod method, URI targetUri, HttpHeaders headers);
257 static final void respond(final ChannelHandlerContext ctx, final @Nullable Integer streamId,
258 final FullHttpResponse response) {
259 requireNonNull(response);
260 if (streamId != null) {
261 response.headers().setInt(STREAM_ID, streamId);
263 ctx.writeAndFlush(response);