forked from openssl/openssl
-
Notifications
You must be signed in to change notification settings - Fork 2
/
quic-hq-interop.c
1039 lines (934 loc) · 35.2 KB
/
quic-hq-interop.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/**
* @file quic-hq-interop.c
* @brief QUIC client interoperability demo using OpenSSL.
*
* This file contains the implementation of a QUIC client that demonstrates
* interoperability with hq-interop servers. It handles connection setup,
* session caching, key logging, and sending HTTP GET requests over QUIC.
*
* The file includes functions for setting up SSL/TLS contexts, managing session
* caches, and handling I/O failures. It supports both IPv4 and IPv6 connections
* and uses non-blocking mode for QUIC operations.
*
* The client sends HTTP/1.0 GET requests for specified files and saves the
* responses to disk. It demonstrates OpenSSL's QUIC API, including ALPN protocol
* settings, peer address management, and SSL handshake and shutdown processes.
*
* @note This client is intended for demonstration purposes and may require
* additional error handling and robustness improvements for production use.
*
* USAGE
* quic-hq-interop <host> <port> <reqfile>
* host - The hostname of the server to contact
* port - The port that the server is listening on
* reqfile - a text file containing a space separated list of paths to fetch
* via http 1.0 GET requests
*
* ENVIRONMENT VARIABLES
* SSLKEYLOGFILE - set to a file path to record keylog exchange with server
* SSL_SESSION_FILE - set to a file path to record ssl sessions and restore
* said sessions on next invocation
* SSL_CERT_FILE - The ca file to use when validating a server
* SSL_CIPHER_SUITES - The list of cipher suites to use (see openssl-ciphers)
*/
#include <string.h>
/* Include the appropriate header file for SOCK_DGRAM */
#ifdef _WIN32 /* Windows */
# include <winsock2.h>
#else /* Linux/Unix */
# include <sys/socket.h>
# include <sys/select.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
static int handle_io_failure(SSL *ssl, int res);
#define REQ_STRING_SZ 1024
/**
* @brief A static pointer to a BIO object representing the session's BIO.
*
* This variable holds a reference to a BIO object used for network
* communication during the session. It is initialized to NULL and should
* be assigned a valid BIO object before use. The BIO object pointed to by
* this variable may be used throughout the session for reading and writing
* data.
*
* @note This variable is static, meaning it is only accessible within the
* file in which it is declared.
*/
static BIO *session_bio = NULL;
/**
* @brief Creates a BIO object for a UDP socket connection to a server.
*
* This function attempts to create a UDP socket and connect it to the server
* specified by the given hostname and port. The socket is wrapped in a BIO
* object, which allows for network communication via OpenSSL's BIO API.
* The function also returns the address of the connected peer.
*
* @param hostname The hostname of the server to connect to.
* @param port The port number of the server to connect to.
* @param family The desired address family (e.g., AF_INET for IPv4,
* AF_INET6 for IPv6).
* @param peer_addr A pointer to a BIO_ADDR pointer that will hold the address
* of the connected peer on success. The caller is responsible
* for freeing this memory using BIO_ADDR_free().
* @return A pointer to a BIO object on success, or NULL on failure.
* The returned BIO object will be associated with the connected socket.
* If the BIO object is successfully created, it will take ownership of
* the socket and automatically close it when the BIO is freed.
*
* @note The function uses OpenSSL's socket-related functions (e.g., BIO_socket,
* BIO_connect) or portability and to integrate with OpenSSL's error
* handling mechanisms.
* @note If no valid connection is established, the function returns NULL and
* ensures that any resources allocated during the process are properly
* freed.
*/
static BIO *create_socket_bio(const char *hostname, const char *port,
int family, BIO_ADDR **peer_addr)
{
int sock = -1;
BIO_ADDRINFO *res;
const BIO_ADDRINFO *ai = NULL;
BIO *bio;
/*
* Lookup IP address info for the server.
*/
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0,
&res))
return NULL;
/*
* Loop through all the possible addresses for the server and find one
* we can connect to.
*/
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
/*
* Create a UDP socket. We could equally use non-OpenSSL calls such
* as "socket" here for this and the subsequent connect and close
* functions. But for portability reasons and also so that we get
* errors on the OpenSSL stack in the event of a failure we use
* OpenSSL's versions of these functions.
*/
sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
if (sock == -1)
continue;
/* Connect the socket to the server's address */
if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
/*
* Set to nonblocking mode
* Note: This function returns a range of errors
* <= 0 if something goes wrong, so catch them all here
*/
if (BIO_socket_nbio(sock, 1) <= 0) {
BIO_closesocket(sock);
sock = -1;
continue;
}
break;
}
if (sock != -1) {
*peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai));
if (*peer_addr == NULL) {
BIO_closesocket(sock);
return NULL;
}
}
/* Free the address information resources we allocated earlier */
BIO_ADDRINFO_free(res);
/* If sock is -1 then we've been unable to connect to the server */
if (sock == -1)
return NULL;
/* Create a BIO to wrap the socket */
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
BIO_closesocket(sock);
return NULL;
}
/*
* Associate the newly created BIO with the underlying socket. By
* passing BIO_CLOSE here the socket will be automatically closed when
* the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which
* case you must close the socket explicitly when it is no longer
* needed.
*/
if (BIO_set_fd(bio, sock, BIO_CLOSE) <= 0) {
BIO_closesocket(sock);
BIO_free(bio);
return NULL;
}
return bio;
}
/**
* @brief Waits for activity on the SSL socket, either for reading or writing.
*
* This function monitors the underlying file descriptor of the given SSL
* connection to determine when it is ready for reading or writing, or both.
* It uses the select function to wait until the socket is either readable
* or writable, depending on what the SSL connection requires.
*
* @param ssl A pointer to the SSL object representing the connection.
*
* @note This function blocks until there is activity on the socket or
* until the timeout specified by OpenSSL is reached. In a real application,
* you might want to perform other tasks while waiting, such as updating a
* GUI or handling other connections.
*
* @note This function uses select for simplicity and portability. Depending
* on your application's requirements, you might consider using other
* mechanisms like poll or epoll for handling multiple file descriptors.
*/
static void wait_for_activity(SSL *ssl)
{
fd_set wfds, rfds;
int width, sock, isinfinite;
struct timeval tv;
struct timeval *tvp = NULL;
/* Get hold of the underlying file descriptor for the socket */
sock = SSL_get_fd(ssl);
FD_ZERO(&wfds);
FD_ZERO(&rfds);
/*
* Find out if we would like to write to the socket, or read from it (or
* both)
*/
if (SSL_net_write_desired(ssl))
FD_SET(sock, &wfds);
if (SSL_net_read_desired(ssl))
FD_SET(sock, &rfds);
width = sock + 1;
/*
* Find out when OpenSSL would next like to be called, regardless of
* whether the state of the underlying socket has changed or not.
*/
if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
tvp = &tv;
/*
* Wait until the socket is writeable or readable. We use select here
* for the sake of simplicity and portability, but you could equally use
* poll/epoll or similar functions
*
* NOTE: For the purposes of this demonstration code this effectively
* makes this demo block until it has something more useful to do. In a
* real application you probably want to go and do other work here (e.g.
* update a GUI, or service other connections).
*
* Let's say for example that you want to update the progress counter on
* a GUI every 100ms. One way to do that would be to use the timeout in
* the last parameter to "select" below. If the tvp value is greater
* than 100ms then use 100ms instead. Then, when select returns, you
* check if it did so because of activity on the file descriptors or
* because of the timeout. If the 100ms GUI timeout has expired but the
* tvp timeout has not then go and update the GUI and then restart the
* "select" (with updated timeouts).
*/
select(width, &rfds, &wfds, NULL, tvp);
}
/**
* @brief Handles I/O failures on an SSL connection based on the result code.
*
* This function processes the result of an SSL I/O operation and handles
* different types of errors that may occur during the operation. It takes
* appropriate actions such as retrying the operation, reporting errors, or
* returning specific status codes based on the error type.
*
* @param ssl A pointer to the SSL object representing the connection.
* @param res The result code from the SSL I/O operation.
* @return An integer indicating the outcome:
* - 1: Temporary failure, the operation should be retried.
* - 0: EOF, indicating the connection has been closed.
* - -1: A fatal error occurred or the connection has been reset.
*
* @note This function may block if a temporary failure occurs and
* wait_for_activity() is called.
*
* @note If the failure is due to an SSL verification error, additional
* information will be logged to stderr.
*/
static int handle_io_failure(SSL *ssl, int res)
{
switch (SSL_get_error(ssl, res)) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
/* Temporary failure. Wait until we can read/write and try again */
wait_for_activity(ssl);
return 1;
case SSL_ERROR_ZERO_RETURN:
/* EOF */
return 0;
case SSL_ERROR_SYSCALL:
return -1;
case SSL_ERROR_SSL:
/*
* Some stream fatal error occurred. This could be because of a
* stream reset - or some failure occurred on the underlying
* connection.
*/
switch (SSL_get_stream_read_state(ssl)) {
case SSL_STREAM_STATE_RESET_REMOTE:
fprintf(stderr, "Stream reset occurred\n");
/*
* The stream has been reset but the connection is still
* healthy.
*/
break;
case SSL_STREAM_STATE_CONN_CLOSED:
fprintf(stderr, "Connection closed\n");
/* Connection is already closed. */
break;
default:
fprintf(stderr, "Unknown stream failure\n");
break;
}
/*
* If the failure is due to a verification error we can get more
* information about it from SSL_get_verify_result().
*/
if (SSL_get_verify_result(ssl) != X509_V_OK)
fprintf(stderr, "Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
return -1;
default:
return -1;
}
}
/**
* @brief A static integer indicating whether the session is cached.
*
* This variable is used to track the state of session caching. It is
* initialized to 0, meaning no session is cached. The value may be updated
* to indicate that a session has been successfully cached.
*
* @note This variable is static, meaning it is only accessible within the
* file in which it is declared.
*/
static int session_cached = 0;
/**
* @brief Caches a new SSL session if one is not already cached.
*
* This function writes a new SSL session to the session BIO and caches it.
* It ensures that only one session is cached at a time by checking the
* `session_cached` flag. If a session is already cached, the function
* returns without caching the new session.
*
* @param ssl A pointer to the SSL object associated with the session.
* @param sess A pointer to the SSL_SESSION object to be cached.
* @return 1 if the session is successfully cached, 0 otherwise.
*
* @note This function only allows one session to be cached. Subsequent
* sessions will not be cached unless `session_cached` is reset.
*/
static int cache_new_session(struct ssl_st *ssl, SSL_SESSION *sess)
{
if (session_cached == 1)
return 0;
/* Just write the new session to our bio */
if (!PEM_write_bio_SSL_SESSION(session_bio, sess))
return 0;
(void)BIO_flush(session_bio);
/* only cache one session */
session_cached = 1;
return 1;
}
/**
* @brief Sets up the session cache for the SSL connection.
*
* This function configures session caching for the given SSL connection
* and context. It attempts to load a session from the specified cache file
* or creates a new one if the file does not exist. It also configures the
* session cache mode and disables stateless session tickets.
*
* @param ssl A pointer to the SSL object for the connection.
* @param ctx A pointer to the SSL_CTX object representing the context.
* @param filename The name of the file used to store the session cache.
* @return 1 on success, 0 on failure.
*
* @note If the cache file does not exist, a new file is created and the
* session cache is initialized. If a session is successfully loaded from
* the file, it is added to the context and set for the SSL connection.
* If an error occurs during setup, the session BIO is freed.
*/
static int setup_session_cache(SSL *ssl, SSL_CTX *ctx, const char *filename)
{
SSL_SESSION *sess = NULL;
int rc = 0;
int new_cache = 0;
/*
* Because we cache sessions to a file in this client, we don't
* actualy need to internally store sessions, because we restore them
* from the file with SSL_set_session below, but we want to ensure
* that caching is enabled so that the session cache callbacks get called
* properly. The documentation is a bit unclear under what conditions
* the callback is made, so play it safe here, by enforcing enablement
*/
if (!SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT |
SSL_SESS_CACHE_NO_INTERNAL_STORE |
SSL_SESS_CACHE_NO_AUTO_CLEAR))
return rc;
/* open our cache file */
session_bio = BIO_new_file(filename, "r+");
if (session_bio == NULL) {
/* file might need to be created */
session_bio = BIO_new_file(filename, "w+");
if (session_bio == NULL)
return rc;
new_cache = 1;
}
if (new_cache == 0) {
/* read in our cached session */
if (PEM_read_bio_SSL_SESSION(session_bio, &sess, NULL, NULL)) {
/* set our session */
if (!SSL_set_session(ssl, sess))
goto err;
}
} else {
/* Set the callback to store new sessions */
SSL_CTX_sess_set_new_cb(ctx, cache_new_session);
}
rc = 1;
err:
if (rc == 0)
BIO_free(session_bio);
return rc;
}
/**
* @brief Pointer to a list of SSL polling items.
*
* This static variable holds the reference to a dynamically allocated list
* of SSL_POLL_ITEM structures used for SSL polling operations. It is
* initialized to NULL and will be populated as needed.
*/
static SSL_POLL_ITEM *poll_list = NULL;
/**
* @brief Pointer to an array of BIO objects for output.
*
* This static variable holds the reference to a dynamically allocated array
* of BIO structures, which are used for handling output in SSL operations.
* It is initialized to NULL and will be set when needed. This array holds
* the out bio's for all received data from GET requests
*/
static BIO **outbiolist = NULL;
/**
* @brief Pointer to an array of output names.
*
* This static variable holds the reference to a dynamically allocated array
* of strings, representing output names. It is initialized to NULL and
* populated as required during operation. This array holds the names of the
* output files from http GET requests. Indicies are correlated with the
* corresponding outbiolist and poll_list arrays
*/
static char **outnames = NULL;
/**
* @brief Counter for the number of poll items.
*
* This static variable holds the count of items in the poll_list. It is
* initialized to 0 and updated as items are added or removed from the list.
*/
static size_t poll_count = 0;
/**
* @brief Pointer to an array of request strings.
*
* This static variable holds the reference to a dynamically allocated array
* of strings, representing requests. It is initialized to NULL and populated
* as requests are added during execution.
*/
static char **req_array = NULL;
/**
* @brief Counter for the total number of requests.
*
* This static variable tracks the total number of parsed from reqfile. It is
* initialized to 0 and incremented as new requests are processed.
*/
static size_t total_requests = 0;
/**
* @brief Index for the current request in the request array.
*
* This static variable keeps track of the index of the current request being
* processed in the request array. It is initialized to 0 and updated as
* requests are handled.
*/
static size_t req_idx = 0;
/**
* @brief Builds and processes a set of SSL poll requests.
*
* This function creates a new set of SSL poll requests based on the current
* request array. It allocates and manages memory for poll lists, BIO output
* files, and associated request names. Each request sends an HTTP GET to the
* corresponding peer. The function processes the requests until a batch limit
* or error is encountered.
*
* @param ssl A pointer to the SSL object to use for creating new streams.
*
* @return The number of poll requests successfully built, or 0 on error.
*/
static size_t build_request_set(SSL *ssl)
{
size_t poll_idx;
char *req;
char outfilename[REQ_STRING_SZ];
char req_string[REQ_STRING_SZ];
SSL *new_stream;
size_t written;
/*
* Free any previous poll list
*/
for (poll_idx = 0; poll_idx < poll_count; poll_idx++) {
(void)BIO_flush(outbiolist[poll_idx]);
BIO_free(outbiolist[poll_idx]);
SSL_free(poll_list[poll_idx].desc.value.ssl);
}
/*
* Reset out lists and poll_count
*/
OPENSSL_free(outbiolist);
OPENSSL_free(outnames);
OPENSSL_free(poll_list);
outnames = NULL;
poll_list = NULL;
outbiolist = NULL;
poll_count = 0;
/*
* Iterate through our parsed lists of requests
* note req_idx may start at a non-zero value if
* multiple calls to build_request_list are made
*/
while (req_idx < total_requests) {
req = req_array[req_idx];
/* Up our poll count and set our poll_list index */
poll_count++;
poll_idx = poll_count - 1;
/*
* Expand our poll_list, outbiolist, and outnames arrays
*/
poll_list = OPENSSL_realloc(poll_list,
sizeof(SSL_POLL_ITEM) * poll_count);
if (poll_list == NULL) {
fprintf(stderr, "Unable to realloc poll_list\n");
goto err;
}
outbiolist = OPENSSL_realloc(outbiolist,
sizeof(BIO *) * poll_count);
if (outbiolist == NULL) {
fprintf(stderr, "Unable to realloc outbiolist\n");
goto err;
}
outnames = OPENSSL_realloc(outnames, sizeof(char *) * poll_count);
if (outnames == NULL) {
fprintf(stderr, "Unable to realloc outnames\n");
goto err;
}
/* set the output file name for this index */
outnames[poll_idx] = req;
/* Format the http request */
BIO_snprintf(req_string, REQ_STRING_SZ, "GET /%s\r\n", req);
/* build the outfile request path */
memset(outfilename, 0, REQ_STRING_SZ);
BIO_snprintf(outfilename, REQ_STRING_SZ, "/downloads/%s", req);
/* open a bio to write the file */
outbiolist[poll_idx] = BIO_new_file(outfilename, "w+");
if (outbiolist[poll_idx] == NULL) {
fprintf(stderr, "Failed to open outfile %s\n", outfilename);
goto err;
}
/* create a request stream */
new_stream = NULL;
/*
* We don't strictly have to do this check, but our quic client limits
* our max data streams to 100, so we're just batching in groups of 100
* for now
*/
if (poll_count <= 99)
new_stream = SSL_new_stream(ssl, 0);
if (new_stream == NULL) {
/*
* We ran out of new streams to allocate
* return and process this batch before getting more
*/
poll_count--;
return poll_count;
}
/*
* Create a poll descriptor for this stream
*/
poll_list[poll_idx].desc = SSL_as_poll_descriptor(new_stream);
poll_list[poll_idx].revents = 0;
poll_list[poll_idx].events = SSL_POLL_EVENT_R;
/* Write an HTTP GET request to the peer */
while (!SSL_write_ex2(poll_list[poll_idx].desc.value.ssl,
req_string, strlen(req_string),
SSL_WRITE_FLAG_CONCLUDE, &written)) {
if (handle_io_failure(poll_list[poll_idx].desc.value.ssl, 0) == 1)
continue; /* Retry */
fprintf(stderr, "Failed to write start of HTTP request\n");
goto err; /* Cannot retry: error */
}
req_idx++;
}
return poll_count;
err:
for (poll_idx = 0; poll_idx < poll_count; poll_idx++) {
BIO_free(outbiolist[poll_idx]);
SSL_free(poll_list[poll_idx].desc.value.ssl);
}
OPENSSL_free(poll_list);
OPENSSL_free(outbiolist);
poll_list = NULL;
outbiolist = NULL;
poll_count = 0;
return poll_count;
}
/**
* @brief Static pointer to a BIO_ADDR structure representing the peer's address.
*
* This variable is used to store the address of a peer for network communication.
* It is statically allocated and should be initialized appropriately.
*/
static BIO_ADDR *peer_addr = NULL;
/**
* @brief Set up a TLS/QUIC connection to the specified hostname and port.
*
* This function creates and configures an SSL context for a client connection
* using the QUIC client method. It sets up the necessary certificates,
* performs host verification, configures ALPN, and establishes a non-blocking
* connection.
*
* @param hostname Hostname to connect to.
* @param port Port to connect to.
* @param ipv6 Whether to use IPv6 (non-zero for IPv6, zero for IPv4).
* @param ctx Pointer to an SSL_CTX object, which will be created.
* @param ssl Pointer to an SSL object, which will be created.
*
* @return Returns 0 on success, 1 on error.
*/
static int setup_connection(char *hostname, char *port, int ipv6,
SSL_CTX **ctx, SSL **ssl)
{
unsigned char alpn[] = {10, 'h', 'q', '-', 'i', 'n', 't', 'e', 'r', 'o', 'p'};
int ret = 0;
BIO *bio = NULL;
/*
* Create an SSL_CTX which we can use to create SSL objects from. We
* want an SSL_CTX for creating clients so we use
* OSSL_QUIC_client_method() here.
*/
*ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (*ctx == NULL) {
fprintf(stderr, "Failed to create the SSL_CTX\n");
goto end;
}
/*
* Configure the client to abort the handshake if certificate
* verification fails. Virtually all clients should do this unless you
* really know what you are doing.
*/
SSL_CTX_set_verify(*ctx, SSL_VERIFY_PEER, NULL);
/*
* Use the default trusted certificate store
* Note: The store is read from SSL_CERT_DIR and SSL_CERT_FILE
* environment variables in the default case, so users can set those
* When running this application to direct where the store is loaded from
*/
if (!SSL_CTX_set_default_verify_paths(*ctx)) {
fprintf(stderr, "Failed to set the default trusted certificate store\n");
goto end;
}
/*
* If the SSL_CIPHER_SUITES env variable is set, assign those
* ciphers to the context
*/
if (getenv("SSL_CIPHER_SUITES") != NULL) {
if (!SSL_CTX_set_ciphersuites(*ctx, getenv("SSL_CIPHER_SUITES"))) {
fprintf(stderr, "Failed to set cipher suites for connection\n");
goto end;
}
}
/* Create an SSL object to represent the TLS connection */
*ssl = SSL_new(*ctx);
if (*ssl == NULL) {
fprintf(stderr, "Failed to create the SSL object\n");
goto end;
}
if (getenv("SSL_SESSION_FILE") != NULL) {
if (!setup_session_cache(*ssl, *ctx, getenv("SSL_SESSION_FILE"))) {
fprintf(stderr, "Unable to setup session cache\n");
goto end;
}
}
/*
* Create the underlying transport socket/BIO and associate it with the
* connection.
*/
bio = create_socket_bio(hostname, port, ipv6 ? AF_INET6 : AF_INET,
&peer_addr);
if (bio == NULL) {
fprintf(stderr, "Failed to crete the BIO\n");
goto end;
}
SSL_set_bio(*ssl, bio, bio);
/*
* Tell the server during the handshake which hostname we are attempting
* to connect to in case the server supports multiple hosts.
*/
if (!SSL_set_tlsext_host_name(*ssl, hostname)) {
fprintf(stderr, "Failed to set the SNI hostname\n");
goto end;
}
/*
* Ensure we check during certificate verification that the server has
* supplied a certificate for the hostname that we were expecting.
* Virtually all clients should do this unless you really know what you
* are doing.
*/
if (!SSL_set1_host(*ssl, hostname)) {
fprintf(stderr, "Failed to set the certificate verification hostname");
goto end;
}
/* SSL_set_alpn_protos returns 0 for success! */
if (SSL_set_alpn_protos(*ssl, alpn, sizeof(alpn)) != 0) {
fprintf(stderr, "Failed to set the ALPN for the connection\n");
goto end;
}
/* Set the IP address of the remote peer */
if (!SSL_set1_initial_peer_addr(*ssl, peer_addr)) {
fprintf(stderr, "Failed to set the initial peer address\n");
goto end;
}
/*
* The underlying socket is always nonblocking with QUIC, but the default
* behaviour of the SSL object is still to block. We set it for nonblocking
* mode in this demo.
*/
if (!SSL_set_blocking_mode(*ssl, 0)) {
fprintf(stderr, "Failed to turn off blocking mode\n");
goto end;
}
/* Do the handshake with the server */
while ((ret = SSL_connect(*ssl)) != 1) {
if (handle_io_failure(*ssl, ret) == 1)
continue; /* Retry */
fprintf(stderr, "Failed to connect to server\n");
goto end; /* Cannot retry: error */
}
return 1;
end:
SSL_CTX_free(*ctx);
SSL_free(*ssl);
BIO_ADDR_free(peer_addr);
return 0;
}
/**
* @brief Entry point for the QUIC hq-interop client demo application.
*
* This function sets up an SSL/TLS connection using QUIC, sends HTTP GET
* requests for files specified in the command-line arguments, and saves
* the responses to disk. It handles various configurations such as IPv6
* support, session caching, and key logging.
*
* @param argc The number of command-line arguments.
* @param argv The array of command-line arguments. The expected format is
* "[-6] hostname port file".
* @return EXIT_SUCCESS on success, or EXIT_FAILURE on error.
*
* @note The function performs the following main tasks:
* - Parses command-line arguments and configures IPv6 if specified.
* - Reads the list of requests from the specified file.
* - Sets up the SSL context and configures certificate verification.
* - Optionally enables key logging and session caching.
* - Establishes a non-blocking QUIC connection to the server.
* - Sends an HTTP GET request for each file and writes the response
* to the corresponding output file.
* - Gracefully shuts down the SSL connection and frees resources.
* - Prints any OpenSSL error stack information on failure.
*/
int main(int argc, char *argv[])
{
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
BIO *req_bio = NULL;
int res = EXIT_FAILURE;
int ret;
size_t readbytes = 0;
char buf[160];
int eof = 0;
int argnext = 1;
char *reqfile = NULL;
char *reqnames = OPENSSL_zalloc(1025);
size_t read_offset = 0;
size_t bytes_read = 0;
size_t poll_idx = 0;
size_t poll_done = 0;
size_t result_count = 0;
struct timeval poll_timeout;
size_t this_poll_count = 0;
char *req = NULL;
char *hostname, *port;
int ipv6 = 0;
if (argc < 4) {
fprintf(stderr, "Usage: quic-hq-interop [-6] hostname port reqfile\n");
goto end;
}
if (!strcmp(argv[argnext], "-6")) {
if (argc < 5) {
fprintf(stderr, "Usage: quic-hq-interop [-6] hostname port reqfile\n");
goto end;
}
ipv6 = 1;
argnext++;
}
hostname = argv[argnext++];
port = argv[argnext++];
reqfile = argv[argnext];
req_bio = BIO_new_file(reqfile, "r");
if (req_bio == NULL) {
fprintf(stderr, "Failed to open request file %s\n", reqfile);
goto end;
}
/* Get the list of requests */
while (!BIO_eof(req_bio)) {
if (!BIO_read_ex(req_bio, &reqnames[read_offset], REQ_STRING_SZ, &bytes_read)) {
fprintf(stderr, "Failed to read some data from request file\n");
goto end;
}
read_offset += bytes_read;
reqnames = OPENSSL_realloc(reqnames, read_offset + REQ_STRING_SZ);
if (reqnames == NULL) {
fprintf(stderr, "Realloc failure\n");
goto end;
}
}
BIO_free(req_bio);
req_bio = NULL;
reqnames[read_offset + 1] = '\0';
if (!setup_connection(hostname, port, ipv6, &ctx, &ssl)) {
fprintf(stderr, "Unable to establish connection\n");
goto end;
}
req = strtok(reqnames, " ");
while (req != NULL) {
total_requests++;
req_array = OPENSSL_realloc(req_array, sizeof(char *) * total_requests);
req_array[total_requests - 1] = req;
req = strtok(NULL, " ");
}
/* get a list of requests to poll */
this_poll_count = build_request_set(ssl);
/*
* Now poll all our descriptors for events
*/
while (this_poll_count != 0 && poll_done < this_poll_count) {
result_count = 0;
poll_timeout.tv_sec = 0;
poll_timeout.tv_usec = 0;
if (!SSL_poll(poll_list, this_poll_count, sizeof(SSL_POLL_ITEM),
&poll_timeout, 0, &result_count)) {
fprintf(stderr, "Failed to poll\n");
goto end;
}
/* Iterate over our poll array looking for ready SSL's */
for (poll_idx = 0; poll_idx < this_poll_count; poll_idx++) {
/*
* If we have visited the number of SSL's that SSL_poll
* indicated were ready, we can go poll again
*/
if (result_count == 0)
break;
if (poll_list[poll_idx].revents == SSL_POLL_EVENT_R) {
/*
* We found an SSL that we can read, drop our result count
*/
result_count--;
/* And clear the revents for the next poll */
poll_list[poll_idx].revents = 0;
/*
* Get up to sizeof(buf) bytes of the response. We keep reading until
* the server closes the connection.
*/
eof = 0;
/* Read our data, and handle any errors/eof conditions */
if (!SSL_read_ex(poll_list[poll_idx].desc.value.ssl, buf,
sizeof(buf), &readbytes)) {
switch (handle_io_failure(poll_list[poll_idx].desc.value.ssl,
0)) {
case 1:
eof = 0;
break; /* Retry on next poll */
case 0:
eof = 1;
break;
case -1:
default:
fprintf(stderr, "Failed reading remaining data\n");
goto end; /* Cannot retry: error */
}
}
/*
* If error handling indicated that this SSL is in an EOF state
* we mark the SSL as not needing any more polling, and up our
* poll_done count. Otherwise, just write to the outbio
*/
if (!eof) {
BIO_write(outbiolist[poll_idx], buf, readbytes);
} else {
fprintf(stderr, "completed %s\n", outnames[poll_idx]);
/* This file is done, take it out of polling contention */
poll_list[poll_idx].events = 0;
poll_done++;
}
}
}
/*
* If we've completed this poll set, try get another one
*/
if (poll_done == this_poll_count) {
this_poll_count = build_request_set(ssl);
poll_done = 0;
}
}