Coverage Report

Created: 2025-07-12 06:32

/src/h2o/lib/common/httpclient.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2018 Ichito Nagata, Fastly, Inc.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining a copy
5
 * of this software and associated documentation files (the "Software"), to
6
 * deal in the Software without restriction, including without limitation the
7
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8
 * sell copies of the Software, and to permit persons to whom the Software is
9
 * furnished to do so, subject to the following conditions:
10
 *
11
 * The above copyright notice and this permission notice shall be included in
12
 * all copies or substantial portions of the Software.
13
 *
14
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20
 * IN THE SOFTWARE.
21
 */
22
23
#include "h2o/httpclient.h"
24
25
const char h2o_httpclient_error_is_eos[] = "end of stream";
26
const char h2o_httpclient_error_refused_stream[] = "refused stream";
27
const char h2o_httpclient_error_unknown_alpn_protocol[] = "unknown alpn protocol";
28
const char h2o_httpclient_error_io[] = "I/O error";
29
const char h2o_httpclient_error_connect_timeout[] = "connection timeout";
30
const char h2o_httpclient_error_first_byte_timeout[] = "first byte timeout";
31
const char h2o_httpclient_error_io_timeout[] = "I/O timeout";
32
const char h2o_httpclient_error_invalid_content_length[] = "invalid content-length";
33
const char h2o_httpclient_error_flow_control[] = "flow control error";
34
const char h2o_httpclient_error_http1_line_folding[] = "line folding of header fields is not supported";
35
const char h2o_httpclient_error_http1_unexpected_transfer_encoding[] = "unexpected type of transfer-encoding";
36
const char h2o_httpclient_error_http1_parse_failed[] = "failed to parse the response";
37
const char h2o_httpclient_error_protocol_violation[] = "protocol violation";
38
const char h2o_httpclient_error_internal[] = "internal error";
39
const char h2o_httpclient_error_malformed_frame[] = "malformed HTTP frame";
40
const char h2o_httpclient_error_unexpected_101[] = "received unexpected 101 response";
41
42
/**
43
 * Used to indicate that the HTTP request is to be "upgraded" into a CONNECT tunnel.
44
 */
45
const char h2o_httpclient_upgrade_to_connect[] = "\nCONNECT / CONNECT-UDP method";
46
47
void h2o_httpclient_connection_pool_init(h2o_httpclient_connection_pool_t *connpool, h2o_socketpool_t *sockpool)
48
1
{
49
1
    connpool->socketpool = sockpool;
50
1
    h2o_linklist_init_anchor(&connpool->http2.conns);
51
1
    h2o_linklist_init_anchor(&connpool->http3.conns);
52
1
}
53
54
static void close_client(h2o_httpclient_t *client)
55
0
{
56
0
    if (client->_connect_req != NULL) {
57
0
        h2o_socketpool_cancel_connect(client->_connect_req);
58
0
        client->_connect_req = NULL;
59
0
    }
60
61
0
    if (h2o_timer_is_linked(&client->_timeout))
62
0
        h2o_timer_unlink(&client->_timeout);
63
64
0
    free(client);
65
0
}
66
67
static void on_connect_error(h2o_httpclient_t *client, const char *errstr)
68
0
{
69
0
    assert(errstr != NULL);
70
0
    client->_cb.on_connect(client, errstr, NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL);
71
0
    close_client(client);
72
0
}
73
74
static void on_connect_timeout(h2o_timer_t *entry)
75
0
{
76
0
    h2o_httpclient_t *client = H2O_STRUCT_FROM_MEMBER(h2o_httpclient_t, _timeout, entry);
77
0
    on_connect_error(client, h2o_httpclient_error_connect_timeout);
78
0
}
79
80
static void do_cancel(h2o_httpclient_t *_client)
81
0
{
82
0
    h2o_httpclient_t *client = (void *)_client;
83
0
    close_client(client);
84
0
}
85
86
static h2o_httpclient_t *create_client(h2o_httpclient_t **_client, h2o_mem_pool_t *pool, void *data, h2o_httpclient_ctx_t *ctx,
87
                                       h2o_httpclient_connection_pool_t *connpool, const char *upgrade_to,
88
                                       h2o_httpclient_connect_cb on_connect)
89
468
{
90
468
#define SZ_MAX(x, y) ((x) > (y) ? (x) : (y))
91
468
    size_t sz = SZ_MAX(h2o_httpclient__h1_size, h2o_httpclient__h2_size);
92
468
#undef SZ_MAX
93
468
    h2o_httpclient_t *client = h2o_mem_alloc(sz);
94
468
    memset(client, 0, sz);
95
468
    client->pool = pool;
96
468
    client->ctx = ctx;
97
468
    client->data = data;
98
468
    client->upgrade_to = upgrade_to;
99
468
    client->connpool = connpool;
100
468
    client->cancel = do_cancel;
101
468
    client->_cb.on_connect = on_connect;
102
468
    client->_timeout.cb = on_connect_timeout;
103
468
    client->timings.start_at = h2o_gettimeofday(ctx->loop);
104
105
468
    if (_client != NULL)
106
468
        *_client = client;
107
108
468
    return client;
109
468
}
110
111
static void on_pool_connect(h2o_socket_t *sock, const char *errstr, void *data, h2o_url_t *origin)
112
468
{
113
468
    h2o_httpclient_t *client = data;
114
115
468
    h2o_timer_unlink(&client->_timeout);
116
117
468
    client->_connect_req = NULL;
118
119
468
    if (sock == NULL) {
120
0
        assert(errstr != NULL);
121
0
        on_connect_error(client, errstr);
122
0
        return;
123
0
    }
124
125
468
    h2o_iovec_t alpn_proto;
126
468
    if (client->ctx->force_cleartext_http2 && sock->ssl == NULL && client->ctx->protocol_selector.ratio.http2 == 100) {
127
        /* The client has prior knowledge and wants to use http2 without
128
         * discovery or upgrade. Make sure that we're using a 100%
129
         * H2 context, to avoid having this connection reused for H1
130
         * accidentaly */
131
0
        goto ForceH2;
132
468
    } else if (sock->ssl == NULL || (alpn_proto = h2o_socket_ssl_get_selected_protocol(sock)).len == 0) {
133
468
        h2o_httpclient__h1_on_connect(client, sock, origin);
134
468
    } else {
135
0
        if (h2o_memis(alpn_proto.base, alpn_proto.len, H2O_STRLIT("h2"))) {
136
0
        ForceH2:
137
            /* detach this socket from the socketpool to count the number of h1 connections correctly */
138
0
            h2o_socketpool_detach(client->connpool->socketpool, sock);
139
0
            h2o_httpclient__h2_on_connect(client, sock, origin);
140
0
        } else if (memcmp(alpn_proto.base, "http/1.1", alpn_proto.len) == 0) {
141
0
            h2o_httpclient__h1_on_connect(client, sock, origin);
142
0
        } else {
143
0
            on_connect_error(client, h2o_httpclient_error_unknown_alpn_protocol);
144
0
        }
145
0
    }
146
468
}
147
148
enum {
149
    /**
150
     * indicates that H1 should be chosen
151
     */
152
    PROTOCOL_SELECTOR_H1,
153
    /**
154
     * indicates that H2 should be chosen (though the server might fallback to H1)
155
     */
156
    PROTOCOL_SELECTOR_H2,
157
    /**
158
     * indicates that H3 should be chosen
159
     */
160
    PROTOCOL_SELECTOR_H3,
161
    /**
162
     * used when ratio.http2 < 0; see h2o_httpclient_ctx_t::protocol_selector.ratio.http2
163
     */
164
    PROTOCOL_SELECTOR_SERVER_DRIVEN,
165
    /**
166
     * total number
167
     */
168
    PROTOCOL_SELECTOR_COUNT
169
};
170
171
static size_t select_protocol(struct st_h2o_httpclient_protocol_selector_t *selector)
172
468
{
173
468
    H2O_BUILD_ASSERT(PTLS_ELEMENTSOF(selector->_deficits) == PROTOCOL_SELECTOR_COUNT);
174
175
    /* update the deficits */
176
468
    if (selector->ratio.http2 < 0) {
177
0
        selector->_deficits[PROTOCOL_SELECTOR_SERVER_DRIVEN] += 100 - selector->ratio.http3;
178
468
    } else {
179
468
        selector->_deficits[PROTOCOL_SELECTOR_H1] += 100 - selector->ratio.http2 - selector->ratio.http3;
180
468
        selector->_deficits[PROTOCOL_SELECTOR_H2] += selector->ratio.http2;
181
468
    }
182
468
    selector->_deficits[PROTOCOL_SELECTOR_H3] += selector->ratio.http3;
183
184
    /* select one with the highest value */
185
468
    size_t result = 0;
186
1.87k
    for (size_t i = 1; i < PROTOCOL_SELECTOR_COUNT; ++i) {
187
1.40k
        if (selector->_deficits[result] < selector->_deficits[i])
188
0
            result = i;
189
1.40k
    }
190
191
    /* decrement the one being selected */
192
468
    selector->_deficits[result] -= 100;
193
194
468
    return result;
195
468
}
196
197
static struct st_h2o_httpclient__h2_conn_t *find_h2conn(h2o_httpclient_connection_pool_t *pool, h2o_url_t *target)
198
0
{
199
0
    int should_check_target = h2o_socketpool_is_global(pool->socketpool);
200
201
0
    for (h2o_linklist_t *l = pool->http2.conns.next; l != &pool->http2.conns; l = l->next) {
202
0
        struct st_h2o_httpclient__h2_conn_t *conn = H2O_STRUCT_FROM_MEMBER(struct st_h2o_httpclient__h2_conn_t, link, l);
203
0
        if (should_check_target && !(conn->origin_url.scheme == target->scheme &&
204
0
                                     h2o_memis(conn->origin_url.authority.base, conn->origin_url.authority.len,
205
0
                                               target->authority.base, target->authority.len)))
206
0
            continue;
207
0
        if (conn->num_streams >= h2o_httpclient__h2_get_max_concurrent_streams(conn))
208
0
            continue;
209
0
        return conn;
210
0
    }
211
212
0
    return NULL;
213
0
}
214
215
static void connect_using_socket_pool(h2o_httpclient_t **_client, h2o_mem_pool_t *pool, void *data, h2o_httpclient_ctx_t *ctx,
216
                                      h2o_httpclient_connection_pool_t *connpool, h2o_url_t *origin, const char *upgrade_to,
217
                                      h2o_httpclient_connect_cb on_connect, h2o_iovec_t alpn_protos)
218
468
{
219
468
    h2o_httpclient_t *client = create_client(_client, pool, data, ctx, connpool, upgrade_to, on_connect);
220
468
    h2o_timer_link(client->ctx->loop, client->ctx->connect_timeout, &client->_timeout);
221
468
    h2o_socketpool_connect(&client->_connect_req, connpool->socketpool, origin, ctx->loop, ctx->getaddr_receiver, alpn_protos,
222
468
                           on_pool_connect, client);
223
468
}
224
225
static void connect_using_h2conn(h2o_httpclient_t **_client, h2o_mem_pool_t *pool, void *data,
226
                                 struct st_h2o_httpclient__h2_conn_t *conn, h2o_httpclient_connection_pool_t *connpool,
227
                                 const char *upgrade_to, h2o_httpclient_connect_cb on_connect)
228
0
{
229
0
    h2o_httpclient_t *client = create_client(_client, pool, data, conn->ctx, connpool, upgrade_to, on_connect);
230
0
    h2o_httpclient__h2_on_connect(client, conn->sock, &conn->origin_url);
231
0
}
232
233
void h2o_httpclient_connect(h2o_httpclient_t **_client, h2o_mem_pool_t *pool, void *data, h2o_httpclient_ctx_t *ctx,
234
                            h2o_httpclient_connection_pool_t *connpool, h2o_url_t *origin, const char *upgrade_to,
235
                            h2o_httpclient_connect_cb on_connect)
236
468
{
237
468
    static const h2o_iovec_t no_protos = {}, both_protos = {H2O_STRLIT("\x02"
238
468
                                                                       "h2"
239
468
                                                                       "\x08"
240
468
                                                                       "http/1.1")};
241
468
    assert(connpool != NULL);
242
243
468
    size_t selected_protocol = select_protocol(&ctx->protocol_selector);
244
245
    /* adjust selected protocol if the attempt is to create a tunnel */
246
468
    if (upgrade_to != NULL) {
247
        /* TODO provide a knob to map each upgrade token to some, all, or no HTTP version. Until that is done, upgrade other than to
248
         * a CONNECT and CONNECT-UDP tunnel is directed to H1. */
249
0
        if (upgrade_to != h2o_httpclient_upgrade_to_connect && strcmp(upgrade_to, "connect-udp") != 0)
250
0
            selected_protocol = PROTOCOL_SELECTOR_H1;
251
0
    }
252
253
468
    switch (selected_protocol) {
254
468
    case PROTOCOL_SELECTOR_H1:
255
        /* H1: use the socket pool to obtain a connection, without any ALPN */
256
468
        connect_using_socket_pool(_client, pool, data, ctx, connpool, origin, upgrade_to, on_connect, no_protos);
257
468
        break;
258
0
    case PROTOCOL_SELECTOR_H2: {
259
        /* H2: use existing H2 connection (if any) or create a new connection offering both H1 and H2 */
260
0
        struct st_h2o_httpclient__h2_conn_t *h2conn = find_h2conn(connpool, origin);
261
0
        if (h2conn != NULL) {
262
0
            connect_using_h2conn(_client, pool, data, h2conn, connpool, upgrade_to, on_connect);
263
0
        } else {
264
0
            connect_using_socket_pool(_client, pool, data, ctx, connpool, origin, upgrade_to, on_connect, both_protos);
265
0
        }
266
0
    } break;
267
0
    case PROTOCOL_SELECTOR_H3:
268
0
        h2o_httpclient__connect_h3(_client, pool, data, ctx, connpool, origin, upgrade_to, on_connect);
269
0
        break;
270
0
    case PROTOCOL_SELECTOR_SERVER_DRIVEN: {
271
        /* offer H2 the server, but evenly distribute the load among existing H1 and H2 connections */
272
0
        struct st_h2o_httpclient__h2_conn_t *h2conn = find_h2conn(connpool, origin);
273
0
        if (h2conn != NULL && connpool->socketpool->_shared.pooled_count != 0) {
274
            /* both of h1 and h2 connections exist, compare in-use ratio */
275
0
            double http1_ratio = (double)(connpool->socketpool->_shared.count - connpool->socketpool->_shared.pooled_count) /
276
0
                                 connpool->socketpool->_shared.count;
277
0
            double http2_ratio = (double)h2conn->num_streams / h2o_httpclient__h2_get_max_concurrent_streams(h2conn);
278
0
            if (http2_ratio <= http1_ratio) {
279
0
                connect_using_h2conn(_client, pool, data, h2conn, connpool, upgrade_to, on_connect);
280
0
            } else {
281
0
                connect_using_socket_pool(_client, pool, data, ctx, connpool, origin, upgrade_to, on_connect, no_protos);
282
0
            }
283
0
        } else if (h2conn != NULL) {
284
            /* h2 connection exists */
285
0
            connect_using_h2conn(_client, pool, data, h2conn, connpool, upgrade_to, on_connect);
286
0
        } else if (connpool->socketpool->_shared.pooled_count != 0) {
287
            /* h1 connection exists */
288
0
            connect_using_socket_pool(_client, pool, data, ctx, connpool, origin, upgrade_to, on_connect, no_protos);
289
0
        } else {
290
            /* no connections, connect using ALPN */
291
0
            connect_using_socket_pool(_client, pool, data, ctx, connpool, origin, upgrade_to, on_connect, both_protos);
292
0
        }
293
0
    } break;
294
468
    }
295
468
}
296
297
void h2o_httpclient_set_conn_properties_of_socket(h2o_socket_t *sock, h2o_httpclient_conn_properties_t *properties)
298
468
{
299
468
    properties->ssl.protocol_version = h2o_socket_get_ssl_protocol_version(sock);
300
468
    properties->ssl.session_reused = h2o_socket_get_ssl_session_reused(sock);
301
468
    properties->ssl.cipher = h2o_socket_get_ssl_cipher(sock);
302
468
    properties->ssl.cipher_bits = h2o_socket_get_ssl_cipher_bits(sock);
303
468
    properties->sock = sock;
304
468
}