Update Red Hat Copyright Notices
[nbdkit.git] / plugins / curl / curl.c
bloba40bd6bf6cbafa94bc4c72c26f2b621a267ef66e
1 /* nbdkit
2 * Copyright Red Hat
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
15 * * Neither the name of Red Hat nor the names of its contributors may be
16 * used to endorse or promote products derived from this software without
17 * specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
33 #include <config.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <stdarg.h>
38 #include <stdbool.h>
39 #include <stdint.h>
40 #include <inttypes.h>
41 #include <limits.h>
42 #include <string.h>
43 #include <unistd.h>
44 #include <errno.h>
45 #include <assert.h>
47 #include <curl/curl.h>
49 #include <nbdkit-plugin.h>
51 #include "cleanup.h"
53 #include "curldefs.h"
55 /* Plugin configuration. */
56 const char *url = NULL; /* required */
58 const char *cainfo = NULL;
59 const char *capath = NULL;
60 unsigned connections = 4;
61 char *cookie = NULL;
62 const char *cookiefile = NULL;
63 const char *cookiejar = NULL;
64 const char *cookie_script = NULL;
65 unsigned cookie_script_renew = 0;
66 bool followlocation = true;
67 struct curl_slist *headers = NULL;
68 const char *header_script = NULL;
69 unsigned header_script_renew = 0;
70 long http_version = CURL_HTTP_VERSION_NONE;
71 char *password = NULL;
72 #ifndef HAVE_CURLOPT_PROTOCOLS_STR
73 long protocols = CURLPROTO_ALL;
74 #else
75 const char *protocols = NULL;
76 #endif
77 const char *proxy = NULL;
78 char *proxy_password = NULL;
79 const char *proxy_user = NULL;
80 bool sslverify = true;
81 const char *ssl_cipher_list = NULL;
82 long ssl_version = CURL_SSLVERSION_DEFAULT;
83 const char *tls13_ciphers = NULL;
84 bool tcp_keepalive = false;
85 bool tcp_nodelay = true;
86 uint32_t timeout = 0;
87 const char *unix_socket_path = NULL;
88 const char *user = NULL;
89 const char *user_agent = NULL;
91 /* Use '-D curl.verbose=1' to set. */
92 NBDKIT_DLL_PUBLIC int curl_debug_verbose = 0;
94 static void
95 curl_load (void)
97 CURLcode r;
99 r = curl_global_init (CURL_GLOBAL_DEFAULT);
100 if (r != CURLE_OK) {
101 nbdkit_error ("libcurl initialization failed: %d", (int) r);
102 exit (EXIT_FAILURE);
106 static void
107 curl_unload (void)
109 free (cookie);
110 if (headers)
111 curl_slist_free_all (headers);
112 free (password);
113 free (proxy_password);
114 scripts_unload ();
115 free_all_handles ();
116 curl_global_cleanup ();
119 #ifndef HAVE_CURLOPT_PROTOCOLS_STR
120 /* See <curl/curl.h> */
121 static struct { const char *name; long bitmask; } curl_protocols[] = {
122 { "http", CURLPROTO_HTTP },
123 { "https", CURLPROTO_HTTPS },
124 { "ftp", CURLPROTO_FTP },
125 { "ftps", CURLPROTO_FTPS },
126 { "scp", CURLPROTO_SCP },
127 { "sftp", CURLPROTO_SFTP },
128 { "telnet", CURLPROTO_TELNET },
129 { "ldap", CURLPROTO_LDAP },
130 { "ldaps", CURLPROTO_LDAPS },
131 { "dict", CURLPROTO_DICT },
132 { "file", CURLPROTO_FILE },
133 { "tftp", CURLPROTO_TFTP },
134 { "imap", CURLPROTO_IMAP },
135 { "imaps", CURLPROTO_IMAPS },
136 { "pop3", CURLPROTO_POP3 },
137 { "pop3s", CURLPROTO_POP3S },
138 { "smtp", CURLPROTO_SMTP },
139 { "smtps", CURLPROTO_SMTPS },
140 { "rtsp", CURLPROTO_RTSP },
141 { "rtmp", CURLPROTO_RTMP },
142 { "rtmpt", CURLPROTO_RTMPT },
143 { "rtmpe", CURLPROTO_RTMPE },
144 { "rtmpte", CURLPROTO_RTMPTE },
145 { "rtmps", CURLPROTO_RTMPS },
146 { "rtmpts", CURLPROTO_RTMPTS },
147 { "gopher", CURLPROTO_GOPHER },
148 #ifdef CURLPROTO_SMB
149 { "smb", CURLPROTO_SMB },
150 #endif
151 #ifdef CURLPROTO_SMBS
152 { "smbs", CURLPROTO_SMBS },
153 #endif
154 #ifdef CURLPROTO_MQTT
155 { "mqtt", CURLPROTO_MQTT },
156 #endif
157 { NULL }
160 /* Parse the protocols parameter. */
161 static int
162 parse_protocols (const char *value)
164 size_t n, i;
166 protocols = 0;
168 while (*value) {
169 n = strcspn (value, ",");
170 for (i = 0; curl_protocols[i].name != NULL; ++i) {
171 if (strlen (curl_protocols[i].name) == n &&
172 strncmp (value, curl_protocols[i].name, n) == 0) {
173 protocols |= curl_protocols[i].bitmask;
174 goto found;
177 nbdkit_error ("protocols: protocol name not found: %.*s", (int) n, value);
178 return -1;
180 found:
181 value += n;
182 if (*value == ',')
183 value++;
186 if (protocols == 0) {
187 nbdkit_error ("protocols: empty list of protocols is not allowed");
188 return -1;
191 nbdkit_debug ("curl: protocols: %ld", protocols);
193 return 0;
195 #endif /* !HAVE_CURLOPT_PROTOCOLS_STR */
197 /* Called for each key=value passed on the command line. */
198 static int
199 curl_config (const char *key, const char *value)
201 int r;
203 if (strcmp (key, "cainfo") == 0) {
204 cainfo = value;
207 else if (strcmp (key, "capath") == 0) {
208 capath = value;
211 else if (strcmp (key, "connections") == 0) {
212 if (nbdkit_parse_unsigned ("connections", value,
213 &connections) == -1)
214 return -1;
215 if (connections == 0) {
216 nbdkit_error ("connections parameter must not be 0");
217 return -1;
221 else if (strcmp (key, "cookie") == 0) {
222 free (cookie);
223 if (nbdkit_read_password (value, &cookie) == -1)
224 return -1;
227 else if (strcmp (key, "cookiefile") == 0) {
228 /* Reject cookiefile=- because it will cause libcurl to try to
229 * read from stdin when we connect.
231 if (strcmp (value, "-") == 0) {
232 nbdkit_error ("cookiefile parameter cannot be \"-\"");
233 return -1;
235 cookiefile = value;
238 else if (strcmp (key, "cookiejar") == 0) {
239 /* Reject cookiejar=- because it will cause libcurl to try to
240 * write to stdout.
242 if (strcmp (value, "-") == 0) {
243 nbdkit_error ("cookiejar parameter cannot be \"-\"");
244 return -1;
246 cookiejar = value;
249 else if (strcmp (key, "cookie-script") == 0) {
250 cookie_script = value;
253 else if (strcmp (key, "cookie-script-renew") == 0) {
254 if (nbdkit_parse_unsigned ("cookie-script-renew", value,
255 &cookie_script_renew) == -1)
256 return -1;
259 else if (strcmp (key, "followlocation") == 0) {
260 r = nbdkit_parse_bool (value);
261 if (r == -1)
262 return -1;
263 followlocation = r;
266 else if (strcmp (key, "header") == 0) {
267 headers = curl_slist_append (headers, value);
268 if (headers == NULL) {
269 nbdkit_error ("curl_slist_append: %m");
270 return -1;
274 else if (strcmp (key, "header-script") == 0) {
275 header_script = value;
278 else if (strcmp (key, "header-script-renew") == 0) {
279 if (nbdkit_parse_unsigned ("header-script-renew", value,
280 &header_script_renew) == -1)
281 return -1;
284 else if (strcmp (key, "http-version") == 0) {
285 if (strcmp (value, "none") == 0)
286 http_version = CURL_HTTP_VERSION_NONE;
287 else if (strcmp (value, "1.0") == 0)
288 http_version = CURL_HTTP_VERSION_1_0;
289 else if (strcmp (value, "1.1") == 0)
290 http_version = CURL_HTTP_VERSION_1_1;
291 #ifdef HAVE_CURL_HTTP_VERSION_2_0
292 else if (strcmp (value, "2.0") == 0)
293 http_version = CURL_HTTP_VERSION_2_0;
294 #endif
295 #ifdef HAVE_CURL_HTTP_VERSION_2TLS
296 else if (strcmp (value, "2TLS") == 0)
297 http_version = CURL_HTTP_VERSION_2TLS;
298 #endif
299 #ifdef HAVE_CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE
300 else if (strcmp (value, "2-prior-knowledge") == 0)
301 http_version = CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE;
302 #endif
303 #ifdef HAVE_CURL_HTTP_VERSION_3
304 else if (strcmp (value, "3") == 0)
305 http_version = CURL_HTTP_VERSION_3;
306 #endif
307 #ifdef HAVE_CURL_HTTP_VERSION_3ONLY
308 else if (strcmp (value, "3only") == 0)
309 http_version = CURL_HTTP_VERSION_3ONLY;
310 #endif
311 else {
312 nbdkit_error ("unknown http-version: %s", value);
313 return -1;
317 else if (strcmp (key, "password") == 0) {
318 free (password);
319 if (nbdkit_read_password (value, &password) == -1)
320 return -1;
323 else if (strcmp (key, "protocols") == 0) {
324 #ifndef HAVE_CURLOPT_PROTOCOLS_STR
325 if (parse_protocols (value) == -1)
326 return -1;
327 #else
328 protocols = value;
329 #endif
332 else if (strcmp (key, "proxy") == 0) {
333 proxy = value;
336 else if (strcmp (key, "proxy-password") == 0) {
337 free (proxy_password);
338 if (nbdkit_read_password (value, &proxy_password) == -1)
339 return -1;
342 else if (strcmp (key, "proxy-user") == 0)
343 proxy_user = value;
345 else if (strcmp (key, "sslverify") == 0) {
346 r = nbdkit_parse_bool (value);
347 if (r == -1)
348 return -1;
349 sslverify = r;
352 else if (strcmp (key, "ssl-version") == 0) {
353 if (strcmp (value, "default") == 0)
354 ssl_version = CURL_SSLVERSION_DEFAULT;
355 else if (strcmp (value, "tlsv1") == 0)
356 ssl_version = CURL_SSLVERSION_TLSv1;
357 else if (strcmp (value, "sslv2") == 0)
358 ssl_version = CURL_SSLVERSION_SSLv2;
359 else if (strcmp (value, "sslv3") == 0)
360 ssl_version = CURL_SSLVERSION_SSLv3;
361 else if (strcmp (value, "tlsv1.0") == 0)
362 ssl_version = CURL_SSLVERSION_TLSv1_0;
363 else if (strcmp (value, "tlsv1.1") == 0)
364 ssl_version = CURL_SSLVERSION_TLSv1_1;
365 else if (strcmp (value, "tlsv1.2") == 0)
366 ssl_version = CURL_SSLVERSION_TLSv1_2;
367 else if (strcmp (value, "tlsv1.3") == 0)
368 ssl_version = CURL_SSLVERSION_TLSv1_3;
369 #ifdef HAVE_CURL_SSLVERSION_MAX_DEFAULT
370 else if (strcmp (value, "max-default") == 0)
371 ssl_version = CURL_SSLVERSION_MAX_DEFAULT;
372 #endif
373 #ifdef HAVE_CURL_SSLVERSION_MAX_TLSv1_0
374 else if (strcmp (value, "max-tlsv1.0") == 0)
375 ssl_version = CURL_SSLVERSION_MAX_TLSv1_0;
376 #endif
377 #ifdef HAVE_CURL_SSLVERSION_MAX_TLSv1_1
378 else if (strcmp (value, "max-tlsv1.1") == 0)
379 ssl_version = CURL_SSLVERSION_MAX_TLSv1_1;
380 #endif
381 #ifdef HAVE_CURL_SSLVERSION_MAX_TLSv1_2
382 else if (strcmp (value, "max-tlsv1.2") == 0)
383 ssl_version = CURL_SSLVERSION_MAX_TLSv1_2;
384 #endif
385 #ifdef HAVE_CURL_SSLVERSION_MAX_TLSv1_3
386 else if (strcmp (value, "max-tlsv1.3") == 0)
387 ssl_version = CURL_SSLVERSION_MAX_TLSv1_3;
388 #endif
389 else {
390 nbdkit_error ("unknown ssl-version: %s", value);
391 return -1;
395 else if (strcmp (key, "ssl-cipher-list") == 0)
396 ssl_cipher_list = value;
398 else if (strcmp (key, "tls13-ciphers") == 0)
399 tls13_ciphers = value;
401 else if (strcmp (key, "tcp-keepalive") == 0) {
402 r = nbdkit_parse_bool (value);
403 if (r == -1)
404 return -1;
405 tcp_keepalive = r;
408 else if (strcmp (key, "tcp-nodelay") == 0) {
409 r = nbdkit_parse_bool (value);
410 if (r == -1)
411 return -1;
412 tcp_nodelay = r;
415 else if (strcmp (key, "timeout") == 0) {
416 if (nbdkit_parse_uint32_t ("timeout", value, &timeout) == -1)
417 return -1;
418 #if LONG_MAX < UINT32_MAX
419 /* C17 5.2.4.2.1 requires that LONG_MAX is at least 2^31 - 1.
420 * However a large positive number might still exceed the limit.
422 if (timeout > LONG_MAX) {
423 nbdkit_error ("timeout is too large");
424 return -1;
426 #endif
429 else if (strcmp (key, "unix-socket-path") == 0 ||
430 strcmp (key, "unix_socket_path") == 0)
431 unix_socket_path = value;
433 else if (strcmp (key, "url") == 0)
434 url = value;
436 else if (strcmp (key, "user") == 0)
437 user = value;
439 else if (strcmp (key, "user-agent") == 0)
440 user_agent = value;
442 else {
443 nbdkit_error ("unknown parameter '%s'", key);
444 return -1;
447 return 0;
450 /* Check the user did pass a url parameter. */
451 static int
452 curl_config_complete (void)
454 if (url == NULL) {
455 nbdkit_error ("you must supply the url=<URL> parameter "
456 "after the plugin name on the command line");
457 return -1;
460 if (headers && header_script) {
461 nbdkit_error ("header and header-script cannot be used at the same time");
462 return -1;
465 if (!header_script && header_script_renew) {
466 nbdkit_error ("header-script-renew cannot be used without header-script");
467 return -1;
470 if (cookie && cookie_script) {
471 nbdkit_error ("cookie and cookie-script cannot be used at the same time");
472 return -1;
475 if (!cookie_script && cookie_script_renew) {
476 nbdkit_error ("cookie-script-renew cannot be used without cookie-script");
477 return -1;
480 return 0;
483 #define curl_config_help \
484 "cainfo=<CAINFO> Path to Certificate Authority file.\n" \
485 "capath=<CAPATH> Path to directory with CA certificates.\n" \
486 "connections=<N> Number of libcurl connections to use.\n" \
487 "cookie=<COOKIE> Set HTTP/HTTPS cookies.\n" \
488 "cookiefile= Enable cookie processing.\n" \
489 "cookiefile=<FILENAME> Read cookies from file.\n" \
490 "cookiejar=<FILENAME> Read and write cookies to jar.\n" \
491 "cookie-script=<SCRIPT> Script to set HTTP/HTTPS cookies.\n" \
492 "cookie-script-renew=<SECS> Time to renew HTTP/HTTPS cookies.\n" \
493 "followlocation=false Do not follow redirects.\n" \
494 "header=<HEADER> Set HTTP/HTTPS header.\n" \
495 "header-script=<SCRIPT> Script to set HTTP/HTTPS headers.\n" \
496 "header-script-renew=<SECS> Time to renew HTTP/HTTPS headers.\n" \
497 "http-version=none|... Force a particular HTTP protocol.\n" \
498 "password=<PASSWORD> The password for the user account.\n" \
499 "protocols=PROTO,PROTO,.. Limit protocols allowed.\n" \
500 "proxy=<PROXY> Set proxy URL.\n" \
501 "proxy-password=<PASSWORD> The proxy password.\n" \
502 "proxy-user=<USER> The proxy user.\n" \
503 "sslverify=false Do not verify SSL certificate of remote host.\n" \
504 "ssl-cipher-list=C1:C2:.. Specify TLS/SSL cipher suites to be used.\n" \
505 "ssl-version=<VERSION> Specify preferred TLS/SSL version.\n" \
506 "tcp-keepalive=true Enable TCP keepalives.\n" \
507 "tcp-nodelay=false Disable Nagle’s algorithm.\n" \
508 "timeout=<TIMEOUT> Set the timeout for requests (seconds).\n" \
509 "tls13-ciphers=C1:C2:.. Specify TLS 1.3 cipher suites to be used.\n" \
510 "unix-socket-path=<PATH> Open Unix domain socket instead of TCP/IP.\n" \
511 "url=<URL> (required) The disk image URL to serve.\n" \
512 "user=<USER> The user to log in as.\n" \
513 "user-agent=<USER-AGENT> Send user-agent header for HTTP/HTTPS."
515 /* Translate CURLcode to nbdkit_error. */
516 #define display_curl_error(ch, r, fs, ...) \
517 do { \
518 nbdkit_error ((fs ": %s: %s"), ## __VA_ARGS__, \
519 curl_easy_strerror ((r)), (ch)->errbuf); \
520 } while (0)
522 /* Create the per-connection handle. */
523 static void *
524 curl_open (int readonly)
526 struct handle *h;
528 h = calloc (1, sizeof *h);
529 if (h == NULL) {
530 nbdkit_error ("calloc: %m");
531 return NULL;
533 h->readonly = readonly;
535 return h;
538 /* Free up the per-connection handle. */
539 static void
540 curl_close (void *handle)
542 struct handle *h = handle;
544 free (h);
547 /* This plugin could support the parallel thread model. It currently
548 * uses serialize_requests because parallel has the unfortunate effect
549 * of pessimising common workloads. See:
550 * https://listman.redhat.com/archives/libguestfs/2023-February/030618.html
552 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_REQUESTS
554 /* Calls get_handle() ... put_handle() to get a handle for the length
555 * of the current scope.
557 #define GET_HANDLE_FOR_CURRENT_SCOPE(ch) \
558 CLEANUP_PUT_HANDLE struct curl_handle *ch = get_handle ();
559 #define CLEANUP_PUT_HANDLE __attribute__ ((cleanup (cleanup_put_handle)))
560 static void
561 cleanup_put_handle (void *chp)
563 struct curl_handle *ch = * (struct curl_handle **) chp;
565 if (ch != NULL)
566 put_handle (ch);
569 /* Get the file size. */
570 static int64_t
571 curl_get_size (void *handle)
573 GET_HANDLE_FOR_CURRENT_SCOPE (ch);
574 if (ch == NULL)
575 return -1;
577 return ch->exportsize;
580 /* Multi-conn is safe for read-only connections, but HTTP does not
581 * have any concept of flushing so we cannot use it for read-write
582 * connections.
584 static int
585 curl_can_multi_conn (void *handle)
587 struct handle *h = handle;
589 return !! h->readonly;
592 /* Read data from the remote server. */
593 static int
594 curl_pread (void *handle, void *buf, uint32_t count, uint64_t offset)
596 CURLcode r;
597 char range[128];
599 GET_HANDLE_FOR_CURRENT_SCOPE (ch);
600 if (ch == NULL)
601 return -1;
603 /* Run the scripts if necessary and set headers in the handle. */
604 if (do_scripts (ch) == -1) return -1;
606 /* Tell the write_cb where we want the data to be written. write_cb
607 * will update this if the data comes in multiple sections.
609 ch->write_buf = buf;
610 ch->write_count = count;
612 curl_easy_setopt (ch->c, CURLOPT_HTTPGET, 1L);
614 /* Make an HTTP range request. */
615 snprintf (range, sizeof range, "%" PRIu64 "-%" PRIu64,
616 offset, offset + count);
617 curl_easy_setopt (ch->c, CURLOPT_RANGE, range);
619 /* The assumption here is that curl will look after timeouts. */
620 r = curl_easy_perform (ch->c);
621 if (r != CURLE_OK) {
622 display_curl_error (ch, r, "pread: curl_easy_perform");
623 return -1;
626 /* Could use curl_easy_getinfo here to obtain further information
627 * about the connection.
630 /* As far as I understand the cURL API, this should never happen. */
631 assert (ch->write_count == 0);
633 return 0;
636 /* Write data to the remote server. */
637 static int
638 curl_pwrite (void *handle, const void *buf, uint32_t count, uint64_t offset)
640 CURLcode r;
641 char range[128];
643 GET_HANDLE_FOR_CURRENT_SCOPE (ch);
644 if (ch == NULL)
645 return -1;
647 /* Run the scripts if necessary and set headers in the handle. */
648 if (do_scripts (ch) == -1) return -1;
650 /* Tell the read_cb where we want the data to be read from. read_cb
651 * will update this if the data comes in multiple sections.
653 ch->read_buf = buf;
654 ch->read_count = count;
656 curl_easy_setopt (ch->c, CURLOPT_UPLOAD, 1L);
658 /* Make an HTTP range request. */
659 snprintf (range, sizeof range, "%" PRIu64 "-%" PRIu64,
660 offset, offset + count);
661 curl_easy_setopt (ch->c, CURLOPT_RANGE, range);
663 /* The assumption here is that curl will look after timeouts. */
664 r = curl_easy_perform (ch->c);
665 if (r != CURLE_OK) {
666 display_curl_error (ch, r, "pwrite: curl_easy_perform");
667 return -1;
670 /* Could use curl_easy_getinfo here to obtain further information
671 * about the connection.
674 /* As far as I understand the cURL API, this should never happen. */
675 assert (ch->read_count == 0);
677 return 0;
680 static struct nbdkit_plugin plugin = {
681 .name = "curl",
682 .version = PACKAGE_VERSION,
683 .load = curl_load,
684 .unload = curl_unload,
685 .config = curl_config,
686 .config_complete = curl_config_complete,
687 .config_help = curl_config_help,
688 .magic_config_key = "url",
689 .open = curl_open,
690 .close = curl_close,
691 .get_size = curl_get_size,
692 .can_multi_conn = curl_can_multi_conn,
693 .pread = curl_pread,
694 .pwrite = curl_pwrite,
697 NBDKIT_REGISTER_PLUGIN (plugin)