Line data Source code
1 : #define _GNU_SOURCE
2 : #include "fd_sshttp_private.h"
3 : #include "fd_ssarchive.h"
4 :
5 : #include "../../../third_party/picohttpparser/picohttpparser.h"
6 : #include "../../../util/log/fd_log.h"
7 : #include "../../../util/fd_util.h"
8 : #include "../../../waltz/http/fd_http.h"
9 : #include "../../../waltz/http/fd_url.h"
10 : #include "../../../ballet/ed25519/fd_x25519.h"
11 :
12 : FD_STATIC_ASSERT( FD_HASH_FOOTPRINT==32UL, resolved_hash_sz );
13 :
14 : #include <unistd.h>
15 : #include <errno.h>
16 : #include <stdlib.h>
17 :
18 : #include <sys/epoll.h>
19 : #include <sys/socket.h>
20 : #include <sys/uio.h>
21 : #include <sys/random.h>
22 : #include <netinet/in.h>
23 :
24 : _Bool fd_sshttp_fuzz = 0;
25 :
26 : static void
27 0 : fd_sshttp_tls_init( fd_tls_t * tls, fd_sshttp_t * http ) {
28 0 : fd_memset( tls, 0, sizeof(fd_tls_t) );
29 :
30 : /* Seed the CSPRNG that fd_tls draws handshake randomness from */
31 :
32 0 : uchar rng_key[ FD_CHACHA_KEY_SZ ];
33 0 : if( FD_UNLIKELY( !fd_rng_secure( rng_key, sizeof(rng_key) ) ) ) FD_LOG_CRIT(( "fd_rng_secure failed" ));
34 0 : fd_chacha_rng_init( http->rng, rng_key, FD_CHACHA_RNG_ALGO_CHACHA8 );
35 0 : fd_memzero_explicit( rng_key, sizeof(rng_key) );
36 0 : tls->rng = http->rng;
37 :
38 0 : static uchar const alpn[] = { 8, 'h', 't', 't', 'p', '/', '1', '.', '1' };
39 0 : fd_memcpy( tls->alpn, alpn, sizeof(alpn) );
40 0 : tls->alpn_sz = sizeof(alpn);
41 :
42 0 : tls->quic = 0;
43 :
44 0 : tls->ca_store = &http->ca_store;
45 0 : }
46 :
47 : static int
48 0 : http_init_tls( fd_sshttp_t * http ) {
49 0 : ulong hostname_len = strlen( http->hostname );
50 0 : if( FD_UNLIKELY( hostname_len >= sizeof(http->tls.server_name) ) ) {
51 0 : FD_LOG_WARNING(( "hostname too long for SNI: %s", http->hostname ));
52 0 : return -1;
53 0 : }
54 0 : fd_memcpy( http->tls.server_name, http->hostname, hostname_len );
55 0 : http->tls.server_name[ hostname_len ] = '\0';
56 0 : http->tls.server_name_len = (ushort)hostname_len;
57 :
58 : /* Generate a fresh ephemeral X25519 key for this handshake */
59 :
60 0 : if( FD_UNLIKELY( !fd_rng_secure( http->tls.kex_private_key, 32UL ) ) ) FD_LOG_CRIT(( "fd_rng_secure failed" ));
61 0 : fd_x25519_public( http->tls.kex_public_key, http->tls.kex_private_key );
62 :
63 0 : fd_tlsrec_conn_init( &http->tls_conn, &http->tls, 0 );
64 0 : fd_tlsrec_sock_init( http->tls_sock );
65 :
66 0 : return 0;
67 0 : }
68 :
69 : static int
70 : http_connect_tls( fd_sshttp_t * http,
71 0 : long now ) {
72 0 : if( FD_UNLIKELY( now > http->deadline ) ) {
73 0 : FD_LOG_WARNING(( "TLS handshake timeout" ));
74 0 : fd_sshttp_cancel( http );
75 0 : return FD_SSHTTP_ADVANCE_ERROR;
76 0 : }
77 :
78 : /* Flush any buffered outgoing data (e.g., ClientHello from a
79 : previous call where the TCP connect was still in progress). */
80 :
81 0 : int flush = fd_tlsrec_sock_flush( http->tls_sock, http->sockfd );
82 0 : if( flush<0 ) { fd_sshttp_cancel( http ); return FD_SSHTTP_ADVANCE_ERROR; }
83 0 : if( flush>0 ) return FD_SSHTTP_ADVANCE_AGAIN;
84 :
85 : /* Drive the TLS handshake */
86 :
87 0 : ulong tcp_rx_sz;
88 0 : int err = fd_tlsrec_sock_rx( http->tls_sock, &http->tls_conn, http->sockfd, &tcp_rx_sz );
89 0 : if( FD_UNLIKELY( err ) ) {
90 0 : if( err==FD_TLSREC_SOCK_ERR_EOF ) {
91 0 : FD_LOG_WARNING(( "peer " FD_IP4_ADDR_FMT ":%hu closed the connection during TLS handshake",
92 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
93 0 : } else {
94 0 : FD_LOG_WARNING(( "TLS handshake failed (%d-%s)", err, fd_tlsrec_sock_strerror( err ) ));
95 0 : }
96 0 : fd_sshttp_cancel( http );
97 0 : return FD_SSHTTP_ADVANCE_ERROR;
98 0 : }
99 :
100 : /* Transition to request state once handshake completes */
101 :
102 0 : if( fd_tlsrec_conn_is_ready( &http->tls_conn ) ) {
103 0 : http->state = FD_SSHTTP_STATE_REQ;
104 0 : http->deadline = now + FD_SSHTTP_DEADLINE_NANOS;
105 0 : return FD_SSHTTP_ADVANCE_AGAIN;
106 0 : }
107 :
108 0 : if( fd_tlsrec_conn_is_failed( &http->tls_conn ) ) {
109 0 : FD_LOG_WARNING(( "TLS handshake failed" ));
110 0 : fd_sshttp_cancel( http );
111 0 : return FD_SSHTTP_ADVANCE_ERROR;
112 0 : }
113 :
114 0 : return FD_SSHTTP_ADVANCE_AGAIN;
115 0 : }
116 :
117 : static long
118 : http_send_tls( fd_sshttp_t * http,
119 : void * buf,
120 0 : ulong bufsz ) {
121 0 : int flush = fd_tlsrec_sock_flush( http->tls_sock, http->sockfd );
122 0 : if( flush<0 ) return FD_SSHTTP_ADVANCE_ERROR;
123 0 : if( flush>0 ) return FD_SSHTTP_ADVANCE_AGAIN;
124 :
125 0 : ulong consumed;
126 0 : if( FD_UNLIKELY( fd_tlsrec_sock_tx( http->tls_sock, &http->tls_conn, http->sockfd, buf, bufsz, &consumed ) ) )
127 0 : return FD_SSHTTP_ADVANCE_ERROR;
128 0 : return (long)consumed;
129 0 : }
130 :
131 : static ulong
132 : tls_rx_pop_iov( fd_sshttp_t * http,
133 : struct iovec const * iov,
134 0 : ulong iov_cnt ) {
135 0 : ulong n = 0UL;
136 0 : for( ulong i=0UL; i<iov_cnt; i++ ) {
137 0 : ulong sz = fd_tlsrec_sock_rx_pop( http->tls_sock, iov[i].iov_base, iov[i].iov_len );
138 0 : n += sz;
139 0 : if( sz<iov[i].iov_len ) break;
140 0 : }
141 0 : return n;
142 0 : }
143 :
144 : static long
145 : http_recv_tls( fd_sshttp_t * http,
146 : struct iovec const * iov,
147 0 : ulong iov_cnt ) {
148 :
149 : /* Drain any buffered plaintext from a previous call */
150 :
151 0 : ulong n = tls_rx_pop_iov( http, iov, iov_cnt );
152 0 : if( n ) {
153 0 : return (long)n;
154 0 : }
155 :
156 : /* Drain pending ciphertext before generating any new records */
157 :
158 0 : int flush = fd_tlsrec_sock_flush( http->tls_sock, http->sockfd );
159 0 : if( flush<0 ) return FD_SSHTTP_ADVANCE_ERROR;
160 0 : if( flush>0 ) return FD_SSHTTP_ADVANCE_AGAIN;
161 :
162 : /* Read and decrypt */
163 :
164 0 : ulong tcp_rx_sz;
165 0 : int err = fd_tlsrec_sock_rx( http->tls_sock, &http->tls_conn, http->sockfd, &tcp_rx_sz );
166 0 : if( FD_UNLIKELY( err ) ) {
167 0 : if( err==FD_TLSREC_SOCK_ERR_EOF ) {
168 0 : FD_LOG_WARNING(( "peer " FD_IP4_ADDR_FMT ":%hu closed the connection mid-response",
169 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
170 0 : } else if( err==FD_TLSREC_SOCK_ERR_RECV ) {
171 0 : FD_LOG_WARNING(( "recv() failed (%d-%s) from " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
172 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
173 0 : }
174 0 : return FD_SSHTTP_ADVANCE_ERROR;
175 0 : }
176 0 : if( !tcp_rx_sz ) return FD_SSHTTP_ADVANCE_AGAIN;
177 :
178 0 : n = tls_rx_pop_iov( http, iov, iov_cnt );
179 0 : if( !n ) return FD_SSHTTP_ADVANCE_AGAIN;
180 0 : return (long)n;
181 0 : }
182 :
183 : static int
184 : setup_redirect_tls( fd_sshttp_t * http,
185 0 : long now ) {
186 0 : fd_sshttp_cancel( http );
187 0 : if( FD_UNLIKELY( fd_sshttp_init( http, http->addr, http->hostname, http->is_https, http->location, http->location_len, ULONG_MAX, now ) ) ) {
188 0 : return FD_SSHTTP_ADVANCE_ERROR;
189 0 : }
190 0 : return FD_SSHTTP_ADVANCE_AGAIN;
191 0 : }
192 :
193 : void
194 0 : fd_sshttp_load_ca_store( fd_sshttp_t * sshttp ) {
195 0 : if( FD_UNLIKELY( fd_x509_ca_store_load_system( &sshttp->ca_store )<0L ) ) {
196 0 : FD_LOG_WARNING(( "No CA certificate bundle found, HTTPS snapshot sources will be refused" ));
197 0 : return;
198 0 : }
199 0 : sshttp->ca_store_loaded = 1;
200 0 : }
201 :
202 : FD_FN_CONST ulong
203 0 : fd_sshttp_align( void ) {
204 0 : return alignof(fd_sshttp_t);
205 0 : }
206 :
207 : FD_FN_CONST ulong
208 0 : fd_sshttp_footprint( void ) {
209 0 : ulong l;
210 0 : l = FD_LAYOUT_INIT;
211 0 : l = FD_LAYOUT_APPEND( l, alignof(fd_sshttp_t), sizeof(fd_sshttp_t) );
212 0 : return FD_LAYOUT_FINI( l, fd_sshttp_align() );
213 0 : }
214 :
215 : void *
216 : fd_sshttp_new( void * shmem,
217 0 : int epoll_fd ) {
218 0 : if( FD_UNLIKELY( !shmem ) ) {
219 0 : FD_LOG_WARNING(( "NULL shmem" ));
220 0 : return NULL;
221 0 : }
222 :
223 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shmem, fd_sshttp_align() ) ) ) {
224 0 : FD_LOG_WARNING(( "unaligned shmem" ));
225 0 : return NULL;
226 0 : }
227 :
228 0 : if( FD_UNLIKELY( epoll_fd==-1 ) ) {
229 0 : FD_LOG_WARNING(( "invalid epoll_fd" ));
230 0 : return NULL;
231 0 : }
232 :
233 0 : FD_SCRATCH_ALLOC_INIT( l, shmem );
234 0 : fd_sshttp_t * sshttp = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_sshttp_t), sizeof(fd_sshttp_t) );
235 :
236 0 : sshttp->state = FD_SSHTTP_STATE_INIT;
237 0 : sshttp->sockfd = -1;
238 0 : sshttp->epoll_fd = epoll_fd;
239 0 : sshttp->content_len = 0UL;
240 0 : fd_cstr_fini( sshttp->snapshot_name );
241 0 : sshttp->resolved_slot = 0UL;
242 0 : fd_memset( sshttp->resolved_hash, 0, FD_HASH_FOOTPRINT );
243 :
244 : /* Initialize native TLS config */
245 0 : fd_tlsrec_sock_init( sshttp->tls_sock );
246 0 : fd_sshttp_tls_init( &sshttp->tls, sshttp );
247 :
248 : /* CA trust store — loaded via fd_sshttp_load_ca_store() during
249 : privileged_init, before the seccomp sandbox locks down. */
250 0 : sshttp->ca_store_loaded = 0;
251 :
252 0 : FD_COMPILER_MFENCE();
253 0 : sshttp->magic = FD_SSHTTP_MAGIC;
254 0 : FD_COMPILER_MFENCE();
255 :
256 0 : return (void *)sshttp;
257 0 : }
258 :
259 : fd_sshttp_t *
260 0 : fd_sshttp_join( void * shhttp ) {
261 0 : if( FD_UNLIKELY( !shhttp ) ) {
262 0 : FD_LOG_WARNING(( "NULL shhttp" ));
263 0 : return NULL;
264 0 : }
265 :
266 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shhttp, fd_sshttp_align() ) ) ) {
267 0 : FD_LOG_WARNING(( "misaligned shhttp" ));
268 0 : return NULL;
269 0 : }
270 :
271 0 : fd_sshttp_t * sshttp = (fd_sshttp_t *)shhttp;
272 :
273 0 : if( FD_UNLIKELY( sshttp->magic!=FD_SSHTTP_MAGIC ) ) {
274 0 : FD_LOG_WARNING(( "bad magic" ));
275 0 : return NULL;
276 0 : }
277 :
278 0 : return sshttp;
279 0 : }
280 :
281 : /* http_init_ssl removed — replaced by http_init_tls (native fd_tls) */
282 :
283 : int
284 : fd_sshttp_init( fd_sshttp_t * http,
285 : fd_ip4_port_t addr,
286 : char const * hostname,
287 : int is_https,
288 : char const * path,
289 : ulong path_len,
290 : ulong hops,
291 0 : long now ) {
292 0 : FD_TEST( http->state==FD_SSHTTP_STATE_INIT );
293 :
294 0 : ulong hostname_len = strnlen( hostname, sizeof(http->hostname) );
295 0 : if( FD_UNLIKELY( hostname_len==sizeof(http->hostname) ) ) {
296 0 : FD_LOG_WARNING(( "snapshot hostname too long" ));
297 0 : return -1;
298 0 : }
299 : /* Redirects reinitialize using our own hostname buffer. */
300 0 : memmove( http->hostname, hostname, hostname_len+1UL );
301 0 : http->is_https = is_https;
302 :
303 0 : if( FD_LIKELY( is_https ) ) {
304 0 : if( FD_UNLIKELY( !http->ca_store_loaded ) ) {
305 0 : FD_LOG_WARNING(( "Refusing HTTPS snapshot download from %s: no CA trust store loaded", hostname ));
306 0 : return -1;
307 0 : }
308 0 : if( FD_UNLIKELY( http_init_tls( http ) ) ) return -1;
309 0 : }
310 :
311 0 : if( hops!=ULONG_MAX ) {
312 0 : http->hops = hops;
313 0 : fd_cstr_fini( http->snapshot_name );
314 0 : http->resolved_slot = 0UL;
315 0 : fd_memset( http->resolved_hash, 0, FD_HASH_FOOTPRINT );
316 0 : }
317 0 : http->request_sent = 0UL;
318 0 : int fmt_ok;
319 0 : if( FD_LIKELY( hostname[0] ) ) {
320 0 : fmt_ok = fd_cstr_printf_check( http->request, sizeof(http->request), &http->request_len,
321 0 : "GET %.*s HTTP/1.1\r\n"
322 0 : "User-Agent: Firedancer\r\n"
323 0 : "Accept: */*\r\n"
324 0 : "Accept-Encoding: identity\r\n"
325 0 : "Host: %s:%u\r\n\r\n",
326 0 : (int)path_len, path, hostname, fd_ushort_bswap( addr.port ) );
327 0 : } else {
328 0 : fmt_ok = fd_cstr_printf_check( http->request, sizeof(http->request), &http->request_len,
329 0 : "GET %.*s HTTP/1.1\r\n"
330 0 : "User-Agent: Firedancer\r\n"
331 0 : "Accept: */*\r\n"
332 0 : "Accept-Encoding: identity\r\n"
333 0 : "Host: " FD_IP4_ADDR_FMT ":%u\r\n\r\n",
334 0 : (int)path_len, path, FD_IP4_ADDR_FMT_ARGS( addr.addr ), fd_ushort_bswap( addr.port ) );
335 0 : }
336 0 : if( FD_UNLIKELY( !fmt_ok ) ) {
337 0 : FD_LOG_WARNING(( "HTTP request too long for %.*s", (int)path_len, path ));
338 0 : return -1;
339 0 : }
340 :
341 0 : http->response_len = 0UL;
342 0 : http->content_len = 0UL;
343 0 : http->content_read = 0UL;
344 :
345 0 : http->addr = addr;
346 0 : http->sockfd = socket( AF_INET, SOCK_STREAM|SOCK_NONBLOCK, 0 );
347 0 : if( FD_UNLIKELY( -1==http->sockfd ) ) {
348 0 : FD_LOG_WARNING(( "socket() failed (%d-%s) for " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
349 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
350 0 : return -1;
351 0 : }
352 :
353 0 : int so_rcvbuf = (int)FD_SSHTTP_SO_RCVBUF;
354 0 : if( FD_UNLIKELY( -1==setsockopt( http->sockfd, SOL_SOCKET, SO_RCVBUF, &so_rcvbuf, sizeof(int) ) ) ) {
355 0 : FD_LOG_ERR(( "setsockopt(SOL_SOCKET,SO_RCVBUF,%i) failed (%i-%s)", so_rcvbuf, errno, fd_io_strerror( errno ) ));
356 0 : }
357 :
358 0 : struct sockaddr_in addr_in = {
359 0 : .sin_family = AF_INET,
360 0 : .sin_port = addr.port,
361 0 : .sin_addr = { .s_addr = addr.addr }
362 0 : };
363 :
364 0 : if( FD_LIKELY( -1==connect( http->sockfd, fd_type_pun_const( &addr_in ), sizeof(addr_in) ) ) ) {
365 0 : if( FD_UNLIKELY( errno!=EINPROGRESS ) ) {
366 0 : FD_LOG_WARNING(( "connect() failed (%d-%s) to " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
367 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
368 0 : if( FD_UNLIKELY( -1==close( http->sockfd ) ) ) FD_LOG_ERR(( "close() failed (%d-%s) for " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
369 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
370 0 : http->sockfd = -1;
371 0 : return -1;
372 0 : }
373 0 : }
374 :
375 0 : http->epoll_events = EPOLLIN|EPOLLOUT;
376 0 : struct epoll_event ev = { .events = http->epoll_events, .data.fd = http->sockfd };
377 0 : if( FD_UNLIKELY( -1==epoll_ctl( http->epoll_fd, EPOLL_CTL_ADD, http->sockfd, &ev ) ) ) FD_LOG_ERR(( "epoll_ctl(ADD) failed (%d-%s)", errno, fd_io_strerror( errno ) ));
378 :
379 0 : if( FD_LIKELY( is_https ) ) {
380 0 : http->state = FD_SSHTTP_STATE_CONNECT;
381 0 : http->deadline = now + FD_SSHTTP_DEADLINE_NANOS;
382 0 : } else {
383 0 : http->state = FD_SSHTTP_STATE_REQ;
384 0 : http->deadline = now + FD_SSHTTP_DEADLINE_NANOS;
385 0 : }
386 :
387 0 : return 0;
388 0 : }
389 :
390 : long
391 0 : fd_sshttp_deadline( fd_sshttp_t const * http ) {
392 0 : switch( http->state ) {
393 0 : case FD_SSHTTP_STATE_INIT: return LONG_MAX;
394 0 : case FD_SSHTTP_STATE_REDIRECT:
395 0 : case FD_SSHTTP_STATE_DONE: return 0L; /* non-I/O work pending */
396 0 : case FD_SSHTTP_STATE_DL:
397 : /* Body bytes already buffered (response residual or decrypted TLS
398 : plaintext) don't keep the socket readable: run now. Otherwise
399 : the caller bounds download progress. */
400 0 : if( http->response_len || (http->is_https && fd_tlsrec_sock_rx_avail( http->tls_sock )) ) return 0L;
401 0 : return LONG_MAX;
402 0 : default: return http->deadline;
403 0 : }
404 0 : }
405 :
406 : void
407 0 : fd_sshttp_cancel( fd_sshttp_t * http ) {
408 0 : if( FD_LIKELY( http->state!=FD_SSHTTP_STATE_INIT && -1!=http->sockfd ) ) {
409 0 : if( FD_UNLIKELY( -1==epoll_ctl( http->epoll_fd, EPOLL_CTL_DEL, http->sockfd, NULL ) ) ) FD_LOG_ERR(( "epoll_ctl(DEL) failed (%d-%s)", errno, fd_io_strerror( errno ) ));
410 0 : if( FD_UNLIKELY( -1==close( http->sockfd ) ) ) FD_LOG_ERR(( "close() failed (%d-%s) for " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
411 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
412 0 : http->sockfd = -1;
413 0 : }
414 0 : http->state = FD_SSHTTP_STATE_INIT;
415 :
416 : /* Clean up native TLS state */
417 0 : fd_tlsrec_conn_fini( &http->tls_conn );
418 0 : fd_memzero_explicit( http->tls.kex_private_key, 32UL );
419 0 : fd_tlsrec_sock_init( http->tls_sock );
420 0 : }
421 :
422 : static long
423 : http_send( fd_sshttp_t * http,
424 : void * buf,
425 0 : ulong bufsz ) {
426 0 : if( FD_LIKELY( http->is_https ) )
427 0 : return http_send_tls( http, buf, bufsz );
428 :
429 0 : long sent = sendto( http->sockfd, buf, bufsz, MSG_NOSIGNAL, NULL, 0 );
430 0 : if( FD_UNLIKELY( -1==sent && errno==EAGAIN ) ) {
431 0 : return FD_SSHTTP_ADVANCE_AGAIN;
432 0 : } else if( FD_UNLIKELY( -1==sent ) ) {
433 0 : FD_LOG_WARNING(( "sendto() failed (%d-%s) to " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
434 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
435 0 : fd_sshttp_cancel( http );
436 0 : return FD_SSHTTP_ADVANCE_ERROR;
437 0 : }
438 :
439 0 : return sent;
440 0 : }
441 :
442 : /* http_recv receives at most max bytes scattered into iov. */
443 :
444 : static long
445 : http_recv( fd_sshttp_t * http,
446 : struct iovec const * iov,
447 : ulong iov_cnt,
448 0 : ulong max ) {
449 0 : struct iovec clamped[ FD_SSHTTP_IOV_MAX ];
450 0 : ulong cnt = 0UL;
451 0 : for( ; cnt<iov_cnt && max; cnt++ ) {
452 0 : ulong len = fd_ulong_min( iov[ cnt ].iov_len, max );
453 0 : clamped[ cnt ].iov_base = iov[ cnt ].iov_base;
454 0 : clamped[ cnt ].iov_len = len;
455 0 : max -= len;
456 0 : }
457 0 : if( FD_UNLIKELY( !cnt ) ) return FD_SSHTTP_ADVANCE_AGAIN;
458 :
459 0 : if( FD_LIKELY( http->is_https ) )
460 0 : return http_recv_tls( http, clamped, cnt );
461 :
462 0 : long read = readv( http->sockfd, clamped, (int)cnt );
463 0 : if( FD_UNLIKELY( -1==read && errno==EAGAIN ) ) {
464 0 : return FD_SSHTTP_ADVANCE_AGAIN;
465 0 : } else if( FD_UNLIKELY( -1==read ) ) {
466 0 : FD_LOG_WARNING(( "readv() failed (%d-%s) from " FD_IP4_ADDR_FMT ":%hu", errno, fd_io_strerror( errno ),
467 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
468 0 : fd_sshttp_cancel( http );
469 0 : return FD_SSHTTP_ADVANCE_ERROR;
470 0 : } else if( FD_UNLIKELY( !read ) ) {
471 0 : FD_LOG_WARNING(( "peer " FD_IP4_ADDR_FMT ":%hu closed the connection mid-response",
472 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
473 0 : fd_sshttp_cancel( http );
474 0 : return FD_SSHTTP_ADVANCE_ERROR;
475 0 : }
476 :
477 0 : return read;
478 0 : }
479 :
480 : static int
481 : send_request( fd_sshttp_t * http,
482 0 : long now ) {
483 0 : if( FD_UNLIKELY( now>http->deadline ) ) {
484 0 : FD_LOG_WARNING(( "timeout sending request to " FD_IP4_ADDR_FMT ":%hu",
485 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
486 0 : fd_sshttp_cancel( http );
487 0 : return FD_SSHTTP_ADVANCE_ERROR;
488 0 : }
489 :
490 0 : long sent = http_send( http, http->request+http->request_sent, http->request_len-http->request_sent );
491 0 : if( FD_UNLIKELY( sent<=0 ) ) return (int)sent;
492 :
493 0 : http->request_sent += (ulong)sent;
494 0 : if( FD_UNLIKELY( http->request_sent==http->request_len ) ) {
495 0 : http->state = FD_SSHTTP_STATE_RESP;
496 0 : http->response_len = 0UL;
497 0 : http->deadline = now + FD_SSHTTP_DEADLINE_NANOS;
498 0 : }
499 :
500 0 : return FD_SSHTTP_ADVANCE_AGAIN;
501 0 : }
502 :
503 : /* Accepts a full URL redirect to another port on the same host. */
504 :
505 : static char const *
506 : parse_redirect_url( fd_sshttp_t const * http,
507 : char const * url,
508 : ulong * len,
509 0 : ushort * port ) {
510 0 : fd_url_t parsed[ 1 ];
511 0 : _Bool is_ssl;
512 0 : if( FD_UNLIKELY( fd_url_parse_endpoint( parsed, url, *len, port, &is_ssl, "redirect location" ) ) ) return NULL;
513 0 : if( FD_UNLIKELY( is_ssl!=(_Bool)http->is_https ) ) return NULL;
514 0 : if( FD_UNLIKELY( !parsed->tail_len || parsed->tail[ 0 ]!='/' ) ) return NULL;
515 :
516 0 : char host[ FD_FQDN_BUF_MAX ];
517 0 : fd_memcpy( host, parsed->host, parsed->host_len );
518 0 : host[ parsed->host_len ] = '\0';
519 0 : uint ip4;
520 0 : int same_ip = fd_cstr_to_ip4_addr( host, &ip4 ) && ip4==http->addr.addr;
521 0 : int same_name = http->hostname[ 0 ] && !strcasecmp( host, http->hostname );
522 0 : if( FD_UNLIKELY( !same_ip && !same_name ) ) return NULL;
523 :
524 0 : *len = parsed->tail_len;
525 0 : return parsed->tail;
526 0 : }
527 :
528 : static int
529 : follow_redirect( fd_sshttp_t * http,
530 : struct phr_header * headers,
531 : ulong header_cnt,
532 0 : long now ) {
533 0 : if( FD_UNLIKELY( !http->hops ) ) {
534 0 : FD_LOG_WARNING(( "too many redirects (remaining %lu) from " FD_IP4_ADDR_FMT ":%hu", http->hops,
535 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
536 0 : fd_sshttp_cancel( http );
537 0 : return FD_SSHTTP_ADVANCE_ERROR;
538 0 : }
539 : /* The check above guarantees hops>0. */
540 0 : http->hops--;
541 :
542 0 : ulong location_len = 0UL;
543 0 : char const * location = NULL;
544 0 : ushort port = fd_ushort_bswap( http->addr.port );
545 :
546 0 : for( ulong i=0UL; i<header_cnt; i++ ) {
547 0 : if( FD_UNLIKELY( headers[ i ].name_len == 8 && !strncasecmp( headers[ i ].name, "location", headers[ i ].name_len ) ) ) {
548 0 : location_len = headers[ i ].value_len;
549 0 : location = headers[ i ].value;
550 0 : if( location_len && location[ 0 ]!='/' ) location = parse_redirect_url( http, location, &location_len, &port );
551 0 : if( FD_UNLIKELY( !location || !location_len ) ) {
552 0 : FD_LOG_WARNING(( "invalid location header `%.*s` from " FD_IP4_ADDR_FMT ":%hu", (int)headers[ i ].value_len, headers[ i ].value,
553 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
554 0 : fd_sshttp_cancel( http );
555 0 : return FD_SSHTTP_ADVANCE_ERROR;
556 0 : }
557 :
558 0 : if( FD_UNLIKELY( location_len>=PATH_MAX-1UL ) ) {
559 0 : FD_LOG_WARNING(( "location header too long `%.*s` from " FD_IP4_ADDR_FMT ":%hu", (int)location_len, location,
560 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
561 0 : fd_sshttp_cancel( http );
562 0 : return FD_SSHTTP_ADVANCE_ERROR;
563 0 : }
564 :
565 : /* The rpc tile sends the generic path on to the snapshot server,
566 : which then names the file. Only a file name is resolved. */
567 0 : char const * file = location+1UL;
568 0 : ulong file_len = location_len-1UL;
569 0 : int generic = ( file_len==16UL && ( !memcmp( file, "snapshot.tar.bz2", 16UL ) || !memcmp( file, "snapshot.tar.zst", 16UL ) ) ) ||
570 0 : ( file_len==28UL && ( !memcmp( file, "incremental-snapshot.tar.bz2", 28UL ) || !memcmp( file, "incremental-snapshot.tar.zst", 28UL ) ) );
571 0 : if( generic ) break;
572 :
573 0 : char snapshot_name[ PATH_MAX ];
574 0 : fd_memcpy( snapshot_name, file, file_len );
575 0 : snapshot_name[ file_len ] = '\0';
576 :
577 0 : int is_zstd;
578 0 : ulong full_entry_slot, incremental_entry_slot;
579 0 : uchar decoded_hash[ FD_HASH_FOOTPRINT ];
580 0 : int err = fd_ssarchive_parse_filename( snapshot_name, &full_entry_slot, &incremental_entry_slot, decoded_hash, &is_zstd );
581 :
582 0 : if( FD_UNLIKELY( err || !is_zstd ) ) {
583 0 : FD_LOG_WARNING(( "unrecognized snapshot file `%s` in redirect location header from " FD_IP4_ADDR_FMT ":%hu", snapshot_name,
584 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
585 0 : fd_sshttp_cancel( http );
586 0 : return FD_SSHTTP_ADVANCE_ERROR;
587 0 : }
588 :
589 0 : http->resolved_slot = (incremental_entry_slot!=ULONG_MAX)
590 0 : ? incremental_entry_slot : full_entry_slot;
591 0 : fd_memcpy( http->resolved_hash, decoded_hash, FD_HASH_FOOTPRINT );
592 :
593 0 : char encoded_hash[ FD_BASE58_ENCODED_32_SZ ];
594 0 : fd_base58_encode_32( decoded_hash, NULL, encoded_hash );
595 :
596 0 : if( FD_LIKELY( incremental_entry_slot!=ULONG_MAX ) ) {
597 0 : FD_TEST( fd_cstr_printf_check( http->snapshot_name, PATH_MAX, NULL, "incremental-snapshot-%lu-%lu-%s.tar.zst", full_entry_slot, incremental_entry_slot, encoded_hash ) );
598 0 : } else {
599 0 : FD_TEST( fd_cstr_printf_check( http->snapshot_name, PATH_MAX, NULL, "snapshot-%lu-%s.tar.zst", full_entry_slot, encoded_hash ) );
600 0 : }
601 0 : break;
602 0 : }
603 0 : }
604 :
605 0 : if( FD_UNLIKELY( !location_len ) ) {
606 0 : FD_LOG_WARNING(( "no location header in redirect response from " FD_IP4_ADDR_FMT ":%hu",
607 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
608 0 : fd_sshttp_cancel( http );
609 0 : return FD_SSHTTP_ADVANCE_ERROR;
610 0 : }
611 :
612 : /* Pre-validate that the redirect request will fit in the request
613 : buffer. The request is rebuilt from scratch by fd_sshttp_init
614 : during the redirect, but the format must match so that a path
615 : accepted here will not overflow in fd_sshttp_init. */
616 0 : int pre_check;
617 0 : if( FD_LIKELY( http->hostname[0] ) ) {
618 0 : pre_check = fd_cstr_printf_check( http->request, sizeof(http->request), &http->request_len,
619 0 : "GET %.*s HTTP/1.1\r\n"
620 0 : "User-Agent: Firedancer\r\n"
621 0 : "Accept: */*\r\n"
622 0 : "Accept-Encoding: identity\r\n"
623 0 : "Host: %s:%u\r\n\r\n",
624 0 : (int)location_len, location, http->hostname, port );
625 0 : } else {
626 0 : pre_check = fd_cstr_printf_check( http->request, sizeof(http->request), &http->request_len,
627 0 : "GET %.*s HTTP/1.1\r\n"
628 0 : "User-Agent: Firedancer\r\n"
629 0 : "Accept: */*\r\n"
630 0 : "Accept-Encoding: identity\r\n"
631 0 : "Host: " FD_IP4_ADDR_FMT ":%u\r\n\r\n",
632 0 : (int)location_len, location, FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), port );
633 0 : }
634 0 : if( FD_UNLIKELY( !pre_check ) ) {
635 0 : FD_LOG_WARNING(( "redirect request too long `%.*s` from " FD_IP4_ADDR_FMT ":%hu", (int)location_len, location,
636 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
637 0 : fd_sshttp_cancel( http );
638 0 : return FD_SSHTTP_ADVANCE_ERROR;
639 0 : }
640 :
641 0 : http->addr.port = fd_ushort_bswap( port );
642 0 : FD_LOG_INFO(( "following redirect to %s://" FD_IP4_ADDR_FMT ":%hu%.*s", http->is_https ? "https" : "http",
643 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ),
644 0 : (int)location_len, location ));
645 :
646 0 : if( FD_UNLIKELY( http->is_https ) ) {
647 0 : http->state = FD_SSHTTP_STATE_REDIRECT;
648 0 : http->location_len = location_len;
649 0 : FD_TEST( location_len<PATH_MAX-1UL );
650 0 : fd_memcpy( http->location, location, location_len );
651 0 : http->location[ location_len ] = '\0';
652 0 : } else {
653 0 : if( FD_LIKELY( !fd_sshttp_fuzz ) ) {
654 0 : fd_sshttp_cancel( http );
655 0 : if( FD_UNLIKELY( fd_sshttp_init( http, http->addr, http->hostname, http->is_https, location, location_len, ULONG_MAX, now ) ) ) {
656 0 : return FD_SSHTTP_ADVANCE_ERROR;
657 0 : }
658 0 : } else {
659 0 : http->state = FD_SSHTTP_STATE_RESP;
660 0 : http->response_len = 0UL;
661 0 : }
662 0 : }
663 :
664 0 : return FD_SSHTTP_ADVANCE_AGAIN;
665 0 : }
666 :
667 : static int
668 : read_response( fd_sshttp_t * http,
669 : struct iovec const * iov,
670 : ulong * data_len,
671 0 : long now ) {
672 0 : if( FD_UNLIKELY( now>http->deadline ) ) {
673 0 : FD_LOG_WARNING(( "timeout reading response from " FD_IP4_ADDR_FMT ":%hu",
674 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
675 0 : fd_sshttp_cancel( http );
676 0 : return FD_SSHTTP_ADVANCE_ERROR;
677 0 : }
678 :
679 0 : if( FD_UNLIKELY( http->response_len>=sizeof(http->response) ) ) {
680 0 : FD_LOG_WARNING(( "response headers too large from " FD_IP4_ADDR_FMT ":%hu",
681 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
682 0 : fd_sshttp_cancel( http );
683 0 : return FD_SSHTTP_ADVANCE_ERROR;
684 0 : }
685 :
686 0 : struct iovec resp_iov = { .iov_base = http->response+http->response_len, .iov_len = sizeof(http->response)-http->response_len };
687 0 : long read = http_recv( http, &resp_iov, 1UL, resp_iov.iov_len );
688 0 : if( FD_UNLIKELY( read<=0 ) ) return (int)read;
689 :
690 0 : http->response_len += (ulong)read;
691 :
692 0 : int minor_version;
693 0 : int status;
694 0 : const char * message;
695 0 : ulong message_len;
696 0 : struct phr_header headers[ 128UL ];
697 0 : ulong header_cnt = 128UL;
698 0 : int parsed = phr_parse_response( http->response,
699 0 : http->response_len,
700 0 : &minor_version,
701 0 : &status,
702 0 : &message,
703 0 : &message_len,
704 0 : headers,
705 0 : &header_cnt,
706 0 : http->response_len - (ulong)read );
707 0 : if( FD_UNLIKELY( parsed==-1 ) ) {
708 0 : FD_LOG_WARNING(( "malformed response headers from " FD_IP4_ADDR_FMT ":%hu",
709 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
710 0 : fd_sshttp_cancel( http );
711 0 : return FD_SSHTTP_ADVANCE_ERROR;
712 0 : } else if( parsed==-2 ) {
713 0 : return FD_SSHTTP_ADVANCE_AGAIN;
714 0 : }
715 :
716 0 : int is_redirect = (status==301) | (status==302) | (status==303) | (status==307) | (status==308);
717 0 : if( FD_UNLIKELY( is_redirect ) ) {
718 0 : return follow_redirect( http, headers, header_cnt, now );
719 0 : }
720 :
721 0 : if( FD_UNLIKELY( status!=200 ) ) {
722 0 : FD_LOG_WARNING(( "unexpected response status %d %.*s from " FD_IP4_ADDR_FMT ":%hu", status, (int)message_len, message,
723 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
724 0 : fd_sshttp_cancel( http );
725 0 : return FD_SSHTTP_ADVANCE_ERROR;
726 0 : }
727 :
728 0 : http->content_read = 0UL;
729 0 : http->content_len = ULONG_MAX;
730 0 : for( ulong i=0UL; i<header_cnt; i++ ) {
731 0 : if( FD_LIKELY( headers[i].name_len!=14UL ) ) continue;
732 0 : if( FD_LIKELY( strncasecmp( headers[i].name, "content-length", 14UL ) ) ) continue;
733 :
734 0 : ulong val = 0UL;
735 0 : if( FD_UNLIKELY( fd_http_parse_content_len( headers[i].value, (ulong)headers[i].value_len, &val ) || val==0UL ) ) {
736 0 : FD_LOG_WARNING(( "invalid content-length in response from " FD_IP4_ADDR_FMT ":%hu", FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
737 0 : fd_sshttp_cancel( http );
738 0 : return FD_SSHTTP_ADVANCE_ERROR;
739 0 : }
740 0 : http->content_len = val;
741 0 : break;
742 0 : }
743 :
744 0 : if( FD_UNLIKELY( http->content_len==ULONG_MAX ) ) {
745 0 : FD_LOG_WARNING(( "no content-length header in response from " FD_IP4_ADDR_FMT ":%hu",
746 0 : FD_IP4_ADDR_FMT_ARGS( http->addr.addr ), fd_ushort_bswap( http->addr.port ) ));
747 0 : fd_sshttp_cancel( http );
748 0 : return FD_SSHTTP_ADVANCE_ERROR;
749 0 : }
750 :
751 0 : http->state = FD_SSHTTP_STATE_DL;
752 0 : if( FD_UNLIKELY( (ulong)parsed<http->response_len ) ) {
753 : /* Body bytes past the caller's buffer are kept in response, with
754 : response_len repurposed as the residual length, drained by
755 : read_body before it reads the socket again. */
756 0 : ulong leftover = fd_ulong_min( http->response_len - (ulong)parsed, http->content_len );
757 0 : ulong copy_len = fd_ulong_min( leftover, iov[0].iov_len );
758 0 : fd_memcpy( iov[0].iov_base, http->response+parsed, copy_len );
759 0 : memmove( http->response, http->response+(ulong)parsed+copy_len, leftover-copy_len );
760 0 : http->response_len = leftover-copy_len;
761 0 : http->content_read += copy_len;
762 0 : *data_len = copy_len;
763 0 : return FD_SSHTTP_ADVANCE_DATA;
764 0 : } else {
765 0 : FD_TEST( http->response_len==(ulong)parsed );
766 0 : http->response_len = 0UL;
767 0 : return FD_SSHTTP_ADVANCE_AGAIN;
768 0 : }
769 0 : }
770 :
771 : static int
772 : read_body( fd_sshttp_t * http,
773 : struct iovec const * iov,
774 : ulong iov_cnt,
775 : ulong * data_len,
776 0 : long now ) {
777 0 : if( FD_UNLIKELY( http->content_read>=http->content_len ) ) {
778 0 : if( FD_UNLIKELY( http->is_https ) ) {
779 0 : http->state = FD_SSHTTP_STATE_DONE;
780 0 : http->deadline = now + FD_SSHTTP_DEADLINE_NANOS;
781 0 : return FD_SSHTTP_ADVANCE_AGAIN;
782 0 : } else {
783 0 : fd_sshttp_cancel( http );
784 0 : http->state = FD_SSHTTP_STATE_INIT;
785 0 : return FD_SSHTTP_ADVANCE_DONE;
786 0 : }
787 0 : }
788 :
789 0 : FD_TEST( http->content_read<http->content_len );
790 :
791 0 : if( FD_UNLIKELY( http->response_len ) ) { /* residual body bytes from read_response */
792 0 : ulong copy_len = fd_ulong_min( http->response_len, iov[0].iov_len );
793 0 : fd_memcpy( iov[0].iov_base, http->response, copy_len );
794 0 : memmove( http->response, http->response+copy_len, http->response_len-copy_len );
795 0 : http->response_len -= copy_len;
796 0 : http->content_read += copy_len;
797 0 : *data_len = copy_len;
798 0 : return FD_SSHTTP_ADVANCE_DATA;
799 0 : }
800 :
801 0 : long read = http_recv( http, iov, iov_cnt, http->content_len-http->content_read );
802 0 : if( FD_UNLIKELY( read<=0 ) ) return (int)read;
803 :
804 0 : *data_len = (ulong)read;
805 0 : http->content_read += (ulong)read;
806 :
807 0 : return FD_SSHTTP_ADVANCE_DATA;
808 0 : }
809 :
810 : char const *
811 0 : fd_sshttp_snapshot_name( fd_sshttp_t const * http ) {
812 0 : return http->snapshot_name;
813 0 : }
814 :
815 : ulong
816 0 : fd_sshttp_content_len( fd_sshttp_t const * http ) {
817 0 : return http->content_len;
818 0 : }
819 :
820 : ulong
821 0 : fd_sshttp_resolved_slot( fd_sshttp_t const * http ) {
822 0 : return http->resolved_slot;
823 0 : }
824 :
825 : uchar const *
826 0 : fd_sshttp_resolved_hash( fd_sshttp_t const * http ) {
827 0 : return http->resolved_hash;
828 0 : }
829 :
830 : int
831 : fd_sshttp_advance( fd_sshttp_t * http,
832 : struct iovec const * iov,
833 : ulong iov_cnt,
834 : ulong * data_len,
835 : int * downloading,
836 0 : long now ) {
837 0 : FD_TEST( iov_cnt>=1UL && iov_cnt<=FD_SSHTTP_IOV_MAX );
838 0 : *downloading = 0;
839 0 : int res;
840 0 : switch( http->state ) {
841 0 : case FD_SSHTTP_STATE_INIT: return FD_SSHTTP_ADVANCE_AGAIN;
842 0 : case FD_SSHTTP_STATE_CONNECT: res = http_connect_tls( http, now ); break;
843 0 : case FD_SSHTTP_STATE_REDIRECT: res = setup_redirect_tls( http, now ); break;
844 0 : case FD_SSHTTP_STATE_REQ: res = send_request( http, now ); break;
845 0 : case FD_SSHTTP_STATE_RESP: res = read_response( http, iov, data_len, now ); break;
846 0 : case FD_SSHTTP_STATE_DL: *downloading = 1; res = read_body( http, iov, iov_cnt, data_len, now ); break;
847 0 : case FD_SSHTTP_STATE_DONE:
848 0 : fd_sshttp_cancel( http );
849 0 : http->state = FD_SSHTTP_STATE_INIT;
850 0 : return FD_SSHTTP_ADVANCE_DONE;
851 0 : default: return FD_SSHTTP_ADVANCE_ERROR;
852 0 : }
853 :
854 : /* Only wait for writability while there is something to send,
855 : otherwise a connected socket is always ready and the tile spins. */
856 0 : if( FD_LIKELY( http->sockfd!=-1 ) ) {
857 0 : int out = http->state==FD_SSHTTP_STATE_REQ || (http->is_https && fd_tlsrec_sock_tx_pending( http->tls_sock ));
858 0 : uint events = EPOLLIN | (out ? EPOLLOUT : 0U);
859 0 : if( FD_UNLIKELY( events!=http->epoll_events ) ) {
860 0 : struct epoll_event ev = { .events = events, .data.fd = http->sockfd };
861 0 : if( FD_UNLIKELY( -1==epoll_ctl( http->epoll_fd, EPOLL_CTL_MOD, http->sockfd, &ev ) ) ) FD_LOG_ERR(( "epoll_ctl(MOD) failed (%d-%s)", errno, fd_io_strerror( errno ) ));
862 0 : http->epoll_events = events;
863 0 : }
864 0 : }
865 0 : return res;
866 0 : }
|