2 * Copyright (C) 2014-2023 Red Hat Inc.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
47 #include <curl/curl.h>
49 #include <nbdkit-plugin.h>
55 /* Plugin configuration. */
56 const char *url
= NULL
; /* required */
58 const char *cainfo
= NULL
;
59 const char *capath
= NULL
;
61 const char *cookiefile
= NULL
;
62 const char *cookiejar
= NULL
;
63 const char *cookie_script
= NULL
;
64 unsigned cookie_script_renew
= 0;
65 bool followlocation
= true;
66 struct curl_slist
*headers
= NULL
;
67 const char *header_script
= NULL
;
68 unsigned header_script_renew
= 0;
69 char *password
= NULL
;
70 #ifndef HAVE_CURLOPT_PROTOCOLS_STR
71 long protocols
= CURLPROTO_ALL
;
73 const char *protocols
= NULL
;
75 const char *proxy
= NULL
;
76 char *proxy_password
= NULL
;
77 const char *proxy_user
= NULL
;
78 bool sslverify
= true;
79 const char *ssl_cipher_list
= NULL
;
80 const char *ssl_version
= NULL
;
81 const char *tls13_ciphers
= NULL
;
82 bool tcp_keepalive
= false;
83 bool tcp_nodelay
= true;
85 const char *unix_socket_path
= NULL
;
86 const char *user
= NULL
;
87 const char *user_agent
= NULL
;
89 /* Use '-D curl.verbose=1' to set. */
90 NBDKIT_DLL_PUBLIC
int curl_debug_verbose
= 0;
97 r
= curl_global_init (CURL_GLOBAL_DEFAULT
);
99 nbdkit_error ("libcurl initialization failed: %d", (int) r
);
109 curl_slist_free_all (headers
);
111 free (proxy_password
);
114 curl_global_cleanup ();
117 #ifndef HAVE_CURLOPT_PROTOCOLS_STR
118 /* See <curl/curl.h> */
119 static struct { const char *name
; long bitmask
; } curl_protocols
[] = {
120 { "http", CURLPROTO_HTTP
},
121 { "https", CURLPROTO_HTTPS
},
122 { "ftp", CURLPROTO_FTP
},
123 { "ftps", CURLPROTO_FTPS
},
124 { "scp", CURLPROTO_SCP
},
125 { "sftp", CURLPROTO_SFTP
},
126 { "telnet", CURLPROTO_TELNET
},
127 { "ldap", CURLPROTO_LDAP
},
128 { "ldaps", CURLPROTO_LDAPS
},
129 { "dict", CURLPROTO_DICT
},
130 { "file", CURLPROTO_FILE
},
131 { "tftp", CURLPROTO_TFTP
},
132 { "imap", CURLPROTO_IMAP
},
133 { "imaps", CURLPROTO_IMAPS
},
134 { "pop3", CURLPROTO_POP3
},
135 { "pop3s", CURLPROTO_POP3S
},
136 { "smtp", CURLPROTO_SMTP
},
137 { "smtps", CURLPROTO_SMTPS
},
138 { "rtsp", CURLPROTO_RTSP
},
139 { "rtmp", CURLPROTO_RTMP
},
140 { "rtmpt", CURLPROTO_RTMPT
},
141 { "rtmpe", CURLPROTO_RTMPE
},
142 { "rtmpte", CURLPROTO_RTMPTE
},
143 { "rtmps", CURLPROTO_RTMPS
},
144 { "rtmpts", CURLPROTO_RTMPTS
},
145 { "gopher", CURLPROTO_GOPHER
},
147 { "smb", CURLPROTO_SMB
},
149 #ifdef CURLPROTO_SMBS
150 { "smbs", CURLPROTO_SMBS
},
152 #ifdef CURLPROTO_MQTT
153 { "mqtt", CURLPROTO_MQTT
},
158 /* Parse the protocols parameter. */
160 parse_protocols (const char *value
)
167 n
= strcspn (value
, ",");
168 for (i
= 0; curl_protocols
[i
].name
!= NULL
; ++i
) {
169 if (strlen (curl_protocols
[i
].name
) == n
&&
170 strncmp (value
, curl_protocols
[i
].name
, n
) == 0) {
171 protocols
|= curl_protocols
[i
].bitmask
;
175 nbdkit_error ("protocols: protocol name not found: %.*s", (int) n
, value
);
184 if (protocols
== 0) {
185 nbdkit_error ("protocols: empty list of protocols is not allowed");
189 nbdkit_debug ("curl: protocols: %ld", protocols
);
193 #endif /* !HAVE_CURLOPT_PROTOCOLS_STR */
195 /* Called for each key=value passed on the command line. */
197 curl_config (const char *key
, const char *value
)
201 if (strcmp (key
, "cainfo") == 0) {
205 else if (strcmp (key
, "capath") == 0) {
209 else if (strcmp (key
, "cookie") == 0) {
211 if (nbdkit_read_password (value
, &cookie
) == -1)
215 else if (strcmp (key
, "cookiefile") == 0) {
216 /* Reject cookiefile=- because it will cause libcurl to try to
217 * read from stdin when we connect.
219 if (strcmp (value
, "-") == 0) {
220 nbdkit_error ("cookiefile parameter cannot be \"-\"");
226 else if (strcmp (key
, "cookiejar") == 0) {
227 /* Reject cookiejar=- because it will cause libcurl to try to
230 if (strcmp (value
, "-") == 0) {
231 nbdkit_error ("cookiejar parameter cannot be \"-\"");
237 else if (strcmp (key
, "cookie-script") == 0) {
238 cookie_script
= value
;
241 else if (strcmp (key
, "cookie-script-renew") == 0) {
242 if (nbdkit_parse_unsigned ("cookie-script-renew", value
,
243 &cookie_script_renew
) == -1)
247 else if (strcmp (key
, "followlocation") == 0) {
248 r
= nbdkit_parse_bool (value
);
254 else if (strcmp (key
, "header") == 0) {
255 headers
= curl_slist_append (headers
, value
);
256 if (headers
== NULL
) {
257 nbdkit_error ("curl_slist_append: %m");
262 else if (strcmp (key
, "header-script") == 0) {
263 header_script
= value
;
266 else if (strcmp (key
, "header-script-renew") == 0) {
267 if (nbdkit_parse_unsigned ("header-script-renew", value
,
268 &header_script_renew
) == -1)
272 else if (strcmp (key
, "password") == 0) {
274 if (nbdkit_read_password (value
, &password
) == -1)
278 else if (strcmp (key
, "protocols") == 0) {
279 #ifndef HAVE_CURLOPT_PROTOCOLS_STR
280 if (parse_protocols (value
) == -1)
287 else if (strcmp (key
, "proxy") == 0) {
291 else if (strcmp (key
, "proxy-password") == 0) {
292 free (proxy_password
);
293 if (nbdkit_read_password (value
, &proxy_password
) == -1)
297 else if (strcmp (key
, "proxy-user") == 0)
300 else if (strcmp (key
, "sslverify") == 0) {
301 r
= nbdkit_parse_bool (value
);
307 else if (strcmp (key
, "ssl-version") == 0)
310 else if (strcmp (key
, "ssl-cipher-list") == 0)
311 ssl_cipher_list
= value
;
313 else if (strcmp (key
, "tls13-ciphers") == 0)
314 tls13_ciphers
= value
;
316 else if (strcmp (key
, "tcp-keepalive") == 0) {
317 r
= nbdkit_parse_bool (value
);
323 else if (strcmp (key
, "tcp-nodelay") == 0) {
324 r
= nbdkit_parse_bool (value
);
330 else if (strcmp (key
, "timeout") == 0) {
331 if (nbdkit_parse_uint32_t ("timeout", value
, &timeout
) == -1)
333 #if LONG_MAX < UINT32_MAX
334 /* C17 5.2.4.2.1 requires that LONG_MAX is at least 2^31 - 1.
335 * However a large positive number might still exceed the limit.
337 if (timeout
> LONG_MAX
) {
338 nbdkit_error ("timeout is too large");
344 else if (strcmp (key
, "unix-socket-path") == 0 ||
345 strcmp (key
, "unix_socket_path") == 0)
346 unix_socket_path
= value
;
348 else if (strcmp (key
, "url") == 0)
351 else if (strcmp (key
, "user") == 0)
354 else if (strcmp (key
, "user-agent") == 0)
358 nbdkit_error ("unknown parameter '%s'", key
);
365 /* Check the user did pass a url parameter. */
367 curl_config_complete (void)
370 nbdkit_error ("you must supply the url=<URL> parameter "
371 "after the plugin name on the command line");
375 if (headers
&& header_script
) {
376 nbdkit_error ("header and header-script cannot be used at the same time");
380 if (!header_script
&& header_script_renew
) {
381 nbdkit_error ("header-script-renew cannot be used without header-script");
385 if (cookie
&& cookie_script
) {
386 nbdkit_error ("cookie and cookie-script cannot be used at the same time");
390 if (!cookie_script
&& cookie_script_renew
) {
391 nbdkit_error ("cookie-script-renew cannot be used without cookie-script");
398 #define curl_config_help \
399 "cainfo=<CAINFO> Path to Certificate Authority file.\n" \
400 "capath=<CAPATH> Path to directory with CA certificates.\n" \
401 "cookie=<COOKIE> Set HTTP/HTTPS cookies.\n" \
402 "cookiefile= Enable cookie processing.\n" \
403 "cookiefile=<FILENAME> Read cookies from file.\n" \
404 "cookiejar=<FILENAME> Read and write cookies to jar.\n" \
405 "cookie-script=<SCRIPT> Script to set HTTP/HTTPS cookies.\n" \
406 "cookie-script-renew=<SECS> Time to renew HTTP/HTTPS cookies.\n" \
407 "followlocation=false Do not follow redirects.\n" \
408 "header=<HEADER> Set HTTP/HTTPS header.\n" \
409 "header-script=<SCRIPT> Script to set HTTP/HTTPS headers.\n" \
410 "header-script-renew=<SECS> Time to renew HTTP/HTTPS headers.\n" \
411 "password=<PASSWORD> The password for the user account.\n" \
412 "protocols=PROTO,PROTO,.. Limit protocols allowed.\n" \
413 "proxy=<PROXY> Set proxy URL.\n" \
414 "proxy-password=<PASSWORD> The proxy password.\n" \
415 "proxy-user=<USER> The proxy user.\n" \
416 "timeout=<TIMEOUT> Set the timeout for requests (seconds).\n" \
417 "sslverify=false Do not verify SSL certificate of remote host.\n" \
418 "ssl-version=<VERSION> Specify preferred TLS/SSL version.\n " \
419 "ssl-cipher-list=C1:C2:.. Specify TLS/SSL cipher suites to be used.\n" \
420 "tls13-ciphers=C1:C2:.. Specify TLS 1.3 cipher suites to be used.\n" \
421 "tcp-keepalive=true Enable TCP keepalives.\n" \
422 "tcp-nodelay=false Disable Nagle’s algorithm.\n" \
423 "unix-socket-path=<PATH> Open Unix domain socket instead of TCP/IP.\n" \
424 "url=<URL> (required) The disk image URL to serve.\n" \
425 "user=<USER> The user to log in as.\n" \
426 "user-agent=<USER-AGENT> Send user-agent header for HTTP/HTTPS."
428 /* Translate CURLcode to nbdkit_error. */
429 #define display_curl_error(ch, r, fs, ...) \
431 nbdkit_error ((fs ": %s: %s"), ## __VA_ARGS__, \
432 curl_easy_strerror ((r)), (ch)->errbuf); \
435 /* Create the per-connection handle. */
437 curl_open (int readonly
)
441 h
= calloc (1, sizeof *h
);
443 nbdkit_error ("calloc: %m");
446 h
->readonly
= readonly
;
451 /* Free up the per-connection handle. */
453 curl_close (void *handle
)
455 struct handle
*h
= handle
;
460 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_REQUESTS
462 /* Calls get_handle() ... put_handle() to get a handle for the length
463 * of the current scope.
465 #define GET_HANDLE_FOR_CURRENT_SCOPE(ch) \
466 CLEANUP_PUT_HANDLE struct curl_handle *ch = get_handle ();
467 #define CLEANUP_PUT_HANDLE __attribute__((cleanup (cleanup_put_handle)))
469 cleanup_put_handle (void *chp
)
471 struct curl_handle
*ch
= * (struct curl_handle
**) chp
;
477 /* Get the file size. */
479 curl_get_size (void *handle
)
481 GET_HANDLE_FOR_CURRENT_SCOPE (ch
);
485 return ch
->exportsize
;
488 /* Multi-conn is safe for read-only connections, but HTTP does not
489 * have any concept of flushing so we cannot use it for read-write
493 curl_can_multi_conn (void *handle
)
495 struct handle
*h
= handle
;
497 return !! h
->readonly
;
500 /* Read data from the remote server. */
502 curl_pread (void *handle
, void *buf
, uint32_t count
, uint64_t offset
)
507 GET_HANDLE_FOR_CURRENT_SCOPE (ch
);
511 /* Run the scripts if necessary and set headers in the handle. */
512 if (do_scripts (ch
) == -1) return -1;
514 /* Tell the write_cb where we want the data to be written. write_cb
515 * will update this if the data comes in multiple sections.
518 ch
->write_count
= count
;
520 curl_easy_setopt (ch
->c
, CURLOPT_HTTPGET
, 1L);
522 /* Make an HTTP range request. */
523 snprintf (range
, sizeof range
, "%" PRIu64
"-%" PRIu64
,
524 offset
, offset
+ count
);
525 curl_easy_setopt (ch
->c
, CURLOPT_RANGE
, range
);
527 /* The assumption here is that curl will look after timeouts. */
528 r
= curl_easy_perform (ch
->c
);
530 display_curl_error (ch
, r
, "pread: curl_easy_perform");
534 /* Could use curl_easy_getinfo here to obtain further information
535 * about the connection.
538 /* As far as I understand the cURL API, this should never happen. */
539 assert (ch
->write_count
== 0);
544 /* Write data to the remote server. */
546 curl_pwrite (void *handle
, const void *buf
, uint32_t count
, uint64_t offset
)
551 GET_HANDLE_FOR_CURRENT_SCOPE (ch
);
555 /* Run the scripts if necessary and set headers in the handle. */
556 if (do_scripts (ch
) == -1) return -1;
558 /* Tell the read_cb where we want the data to be read from. read_cb
559 * will update this if the data comes in multiple sections.
562 ch
->read_count
= count
;
564 curl_easy_setopt (ch
->c
, CURLOPT_UPLOAD
, 1L);
566 /* Make an HTTP range request. */
567 snprintf (range
, sizeof range
, "%" PRIu64
"-%" PRIu64
,
568 offset
, offset
+ count
);
569 curl_easy_setopt (ch
->c
, CURLOPT_RANGE
, range
);
571 /* The assumption here is that curl will look after timeouts. */
572 r
= curl_easy_perform (ch
->c
);
574 display_curl_error (ch
, r
, "pwrite: curl_easy_perform");
578 /* Could use curl_easy_getinfo here to obtain further information
579 * about the connection.
582 /* As far as I understand the cURL API, this should never happen. */
583 assert (ch
->read_count
== 0);
588 static struct nbdkit_plugin plugin
= {
590 .version
= PACKAGE_VERSION
,
592 .unload
= curl_unload
,
593 .config
= curl_config
,
594 .config_complete
= curl_config_complete
,
595 .config_help
= curl_config_help
,
596 .magic_config_key
= "url",
599 .get_size
= curl_get_size
,
600 .can_multi_conn
= curl_can_multi_conn
,
602 .pwrite
= curl_pwrite
,
605 NBDKIT_REGISTER_PLUGIN(plugin
)