/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 | } |