3e677476fbb3a55b6b7bbf2e8c167bf662ab92d7
[netconf.git] /
1 /*
2  * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.transport.http;
9
10 import static io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames.SCHEME;
11 import static io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames.STREAM_ID;
12
13 import com.google.common.util.concurrent.FutureCallback;
14 import io.netty.channel.Channel;
15 import io.netty.channel.ChannelHandlerContext;
16 import io.netty.handler.codec.http.FullHttpRequest;
17 import io.netty.handler.codec.http.FullHttpResponse;
18 import io.netty.handler.codec.http.HttpScheme;
19 import io.netty.handler.ssl.SslHandler;
20 import java.util.Map;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.atomic.AtomicInteger;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 /**
27  * Client side {@link RequestDispatcher} implementation for HTTP 2.
28  *
29  * <p>Serves as gateway to Netty {@link Channel}, performs sending requests to server, returns server responses
30  * associated. Uses request to response mapping by stream identifier.
31  */
32 final class ClientHttp2RequestDispatcher extends ClientRequestDispatcher {
33     private static final Logger LOG = LoggerFactory.getLogger(ClientHttp2RequestDispatcher.class);
34
35     // TODO: we access the queue only from Netty callbacks: can we use a plain HashMap?
36     private final Map<Integer, FutureCallback<FullHttpResponse>> map = new ConcurrentHashMap<>();
37     // identifier for streams initiated from client require to be odd-numbered, 1 is reserved
38     // see https://datatracker.ietf.org/doc/html/rfc7540#section-5.1.1
39     private final AtomicInteger streamIdCounter = new AtomicInteger(3);
40
41     private boolean ssl = false;
42
43     @Override
44     public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
45         ssl = ctx.pipeline().get(SslHandler.class) != null;
46         super.handlerAdded(ctx);
47     }
48
49     @Override
50     public void dispatch(final Channel channel, final FullHttpRequest request,
51             final FutureCallback<FullHttpResponse> callback) {
52         final var streamId = nextStreamId();
53         request.headers()
54             .setInt(STREAM_ID.text(), streamId)
55             .set(SCHEME.text(), ssl ? HttpScheme.HTTPS.name() : HttpScheme.HTTP.name());
56
57         // Map has to be populated first, simply because a response may arrive sooner than the successful callback
58         map.put(streamId, callback);
59         channel.writeAndFlush(request).addListener(sent -> {
60             final var cause = sent.cause();
61             if (cause != null && map.remove(streamId, callback)) {
62                 callback.onFailure(cause);
63             }
64         });
65     }
66
67     int nextStreamId() {
68         return streamIdCounter.getAndAdd(2);
69     }
70
71     @Override
72     protected void channelRead0(final ChannelHandlerContext ctx, final FullHttpResponse response) {
73         final var streamId = response.headers().getInt(STREAM_ID.text());
74         if (streamId == null) {
75             LOG.warn("Unexpected response with no stream ID -- Dropping response object {}", response);
76             return;
77         }
78         final var callback = map.remove(streamId);
79         if (callback != null) {
80             callback.onSuccess(response);
81         } else {
82             LOG.warn("Unexpected response with unknown or expired stream ID {} -- Dropping response object {}",
83                 streamId, response);
84         }
85     }
86 }