From b44d6fe8e384d0e98aee622f3782847b832518b3 Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Sun, 19 Jul 2026 13:46:50 -0300 Subject: [PATCH] system/curl: add a small curl-like HTTP client command Add the "curl" NSH command: a command-line HTTP client built on top of the netutils webclient library. It implements a subset of the real curl options: GET and POST (and other methods via -X), custom request headers (-H), a raw request body (-d, including -d @file), multipart/form-data file uploads (-F name=@file), saving the response body to a file (-o) and verbose output (-v). HTTP only (no HTTPS). Signed-off-by: Jorge Guzman --- system/curl/CMakeLists.txt | 33 ++ system/curl/Kconfig | 42 ++ system/curl/Make.defs | 25 ++ system/curl/Makefile | 36 ++ system/curl/curl_main.c | 860 +++++++++++++++++++++++++++++++++++++ 5 files changed, 996 insertions(+) create mode 100644 system/curl/CMakeLists.txt create mode 100644 system/curl/Kconfig create mode 100644 system/curl/Make.defs create mode 100644 system/curl/Makefile create mode 100644 system/curl/curl_main.c diff --git a/system/curl/CMakeLists.txt b/system/curl/CMakeLists.txt new file mode 100644 index 00000000000..ff48d44a14a --- /dev/null +++ b/system/curl/CMakeLists.txt @@ -0,0 +1,33 @@ +# ############################################################################## +# apps/system/curl/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## + +if(CONFIG_SYSTEM_CURL) + nuttx_add_application( + NAME + ${CONFIG_SYSTEM_CURL_PROGNAME} + SRCS + curl_main.c + STACKSIZE + ${CONFIG_SYSTEM_CURL_STACKSIZE} + PRIORITY + ${CONFIG_SYSTEM_CURL_PRIORITY}) +endif() diff --git a/system/curl/Kconfig b/system/curl/Kconfig new file mode 100644 index 00000000000..ca2963affcf --- /dev/null +++ b/system/curl/Kconfig @@ -0,0 +1,42 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config SYSTEM_CURL + tristate "curl HTTP command" + default n + depends on NETUTILS_WEBCLIENT + ---help--- + Enable the "curl" command: a small command-line HTTP client + supporting GET and POST (and other methods via -X), custom + headers (-H), a raw request body (-d, including -d @file) and + multipart/form-data file uploads (-F name=@file), built on top + of the netutils webclient library. HTTP only (no HTTPS). + +if SYSTEM_CURL + +config SYSTEM_CURL_PROGNAME + string "Program name" + default "curl" + ---help--- + This is the name of the program that will be used when the + NSH ELF program is installed. + +config SYSTEM_CURL_PRIORITY + int "curl task priority" + default 100 + +config SYSTEM_CURL_STACKSIZE + int "curl stack size" + default DEFAULT_TASK_STACKSIZE + +config SYSTEM_CURL_BUFFERSIZE + int "curl transfer buffer size" + default 512 + ---help--- + Size of the buffer used to hold the request/response headers and + to stream body data. It must be large enough to hold the whole + request header line and the whole response header. + +endif diff --git a/system/curl/Make.defs b/system/curl/Make.defs new file mode 100644 index 00000000000..4003825c70e --- /dev/null +++ b/system/curl/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/system/curl/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_SYSTEM_CURL),) +CONFIGURED_APPS += $(APPDIR)/system/curl +endif diff --git a/system/curl/Makefile b/system/curl/Makefile new file mode 100644 index 00000000000..70469e2180c --- /dev/null +++ b/system/curl/Makefile @@ -0,0 +1,36 @@ +############################################################################ +# apps/system/curl/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +# curl built-in application info + +PROGNAME = $(CONFIG_SYSTEM_CURL_PROGNAME) +PRIORITY = $(CONFIG_SYSTEM_CURL_PRIORITY) +STACKSIZE = $(CONFIG_SYSTEM_CURL_STACKSIZE) +MODULE = $(CONFIG_SYSTEM_CURL) + +# curl command + +MAINSRC = curl_main.c + +include $(APPDIR)/Application.mk diff --git a/system/curl/curl_main.c b/system/curl/curl_main.c new file mode 100644 index 00000000000..11837c625a7 --- /dev/null +++ b/system/curl/curl_main.c @@ -0,0 +1,860 @@ +/**************************************************************************** + * apps/system/curl/curl_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netutils/webclient.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifndef CONFIG_SYSTEM_CURL_BUFFERSIZE +# define CONFIG_SYSTEM_CURL_BUFFERSIZE 512 +#endif + +/* Limits for user-provided -H headers and -F form fields. */ + +#define CURL_MAX_HEADERS 12 +#define CURL_MAX_FORMS 8 + +/* Each form field expands to at most 3 body chunks (part header, content, + * trailing CRLF), plus one final closing-boundary chunk. + */ + +#define CURL_MAX_CHUNKS (CURL_MAX_FORMS * 3 + 1) + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* A single piece of the request body. It is either a block of memory + * (fd < 0, data in "mem") or the contents of an open file (fd >= 0). + */ + +struct curl_chunk_s +{ + int fd; /* >= 0: read from this file; < 0: use "mem" */ + FAR char *mem; /* Heap buffer owned by this chunk (freed on cleanup) */ + size_t len; /* Total number of bytes in this chunk */ + size_t off; /* Bytes already consumed */ +}; + +/* The full request body, streamed chunk by chunk. */ + +struct curl_body_s +{ + struct curl_chunk_s chunks[CURL_MAX_CHUNKS]; + int nchunks; + int cur; +}; + +/* Destination for the response body. */ + +struct curl_sink_s +{ + int fd; +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: curl_guess_mime + * + * Description: + * Very small extension-to-MIME map for the common cases. Anything else + * defaults to application/octet-stream, exactly like curl's fallback. + * + ****************************************************************************/ + +static FAR const char *curl_guess_mime(FAR const char *name) +{ + FAR const char *dot = strrchr(name, '.'); + + if (dot != NULL) + { + if (strcasecmp(dot, ".png") == 0) + { + return "image/png"; + } + + if (strcasecmp(dot, ".jpg") == 0 || strcasecmp(dot, ".jpeg") == 0) + { + return "image/jpeg"; + } + + if (strcasecmp(dot, ".gif") == 0) + { + return "image/gif"; + } + + if (strcasecmp(dot, ".json") == 0) + { + return "application/json"; + } + + if (strcasecmp(dot, ".txt") == 0) + { + return "text/plain"; + } + } + + return "application/octet-stream"; +} + +/**************************************************************************** + * Name: curl_add_mem_chunk + * + * Description: + * Append a memory chunk. Takes ownership of "mem" (heap allocated). + * + ****************************************************************************/ + +static int curl_add_mem_chunk(FAR struct curl_body_s *body, FAR char *mem, + size_t len) +{ + FAR struct curl_chunk_s *c; + + if (body->nchunks >= CURL_MAX_CHUNKS) + { + return -ENOSPC; + } + + c = &body->chunks[body->nchunks++]; + c->fd = -1; + c->mem = mem; + c->len = len; + c->off = 0; + return OK; +} + +/**************************************************************************** + * Name: curl_add_file_chunk + * + * Description: + * Append a file chunk. Takes ownership of the open descriptor "fd". + * + ****************************************************************************/ + +static int curl_add_file_chunk(FAR struct curl_body_s *body, int fd, + size_t len) +{ + FAR struct curl_chunk_s *c; + + if (body->nchunks >= CURL_MAX_CHUNKS) + { + return -ENOSPC; + } + + c = &body->chunks[body->nchunks++]; + c->fd = fd; + c->mem = NULL; + c->len = len; + c->off = 0; + return OK; +} + +/**************************************************************************** + * Name: curl_add_str_chunk + * + * Description: + * Convenience wrapper: duplicate a string and append it as a memory + * chunk. + * + ****************************************************************************/ + +static int curl_add_str_chunk(FAR struct curl_body_s *body, + FAR const char *str, size_t len) +{ + FAR char *dup = strdup(str); + int ret; + + if (dup == NULL) + { + return -ENOMEM; + } + + ret = curl_add_mem_chunk(body, dup, len); + if (ret < 0) + { + free(dup); + } + + return ret; +} + +/**************************************************************************** + * Name: curl_body_cleanup + ****************************************************************************/ + +static void curl_body_cleanup(FAR struct curl_body_s *body) +{ + int i; + + for (i = 0; i < body->nchunks; i++) + { + FAR struct curl_chunk_s *c = &body->chunks[i]; + + if (c->fd >= 0) + { + close(c->fd); + } + + free(c->mem); + } + + body->nchunks = 0; +} + +/**************************************************************************** + * Name: curl_body_total + ****************************************************************************/ + +static size_t curl_body_total(FAR const struct curl_body_s *body) +{ + size_t total = 0; + int i; + + for (i = 0; i < body->nchunks; i++) + { + total += body->chunks[i].len; + } + + return total; +} + +/**************************************************************************** + * Name: curl_body_callback + * + * Description: + * webclient request-body provider. Fills "buffer" with up to *sizep bytes + * pulled sequentially from the chunk list. Called repeatedly by webclient + * until exactly bodylen bytes have been produced. + * + ****************************************************************************/ + +static int curl_body_callback(FAR void *buffer, FAR size_t *sizep, + FAR const void * FAR *datap, size_t reqsize, + FAR void *arg) +{ + FAR struct curl_body_s *body = (FAR struct curl_body_s *)arg; + FAR char *out = (FAR char *)buffer; + size_t cap = *sizep; + size_t done = 0; + + (void)reqsize; + (void)datap; + + while (done < cap && body->cur < body->nchunks) + { + FAR struct curl_chunk_s *c = &body->chunks[body->cur]; + size_t remain = c->len - c->off; + size_t want; + + if (remain == 0) + { + body->cur++; + continue; + } + + want = cap - done; + if (want > remain) + { + want = remain; + } + + if (c->fd < 0) + { + memcpy(out + done, c->mem + c->off, want); + c->off += want; + done += want; + } + else + { + ssize_t n = read(c->fd, out + done, want); + if (n < 0) + { + return -errno; + } + + if (n == 0) + { + /* Short file: the pre-computed Content-Length can no longer + * be honoured, so fail rather than send a truncated body. + */ + + return -EIO; + } + + c->off += n; + done += n; + } + } + + *sizep = done; + return OK; +} + +/**************************************************************************** + * Name: curl_sink_callback + * + * Description: + * webclient response-body consumer. Writes each chunk to the destination + * file descriptor (stdout or the -o file). + * + ****************************************************************************/ + +static int curl_sink_callback(FAR char **buffer, int offset, int datend, + FAR int *buflen, FAR void *arg) +{ + FAR struct curl_sink_s *sink = (FAR struct curl_sink_s *)arg; + int len = datend - offset; + + (void)buflen; + + while (len > 0) + { + ssize_t n = write(sink->fd, *buffer + offset, len); + if (n < 0) + { + return -errno; + } + + offset += n; + len -= n; + } + + return OK; +} + +/**************************************************************************** + * Name: curl_header_callback + ****************************************************************************/ + +static int curl_header_callback(FAR const char *line, bool truncated, + FAR void *arg) +{ + (void)truncated; + (void)arg; + + fprintf(stderr, "< %s\n", line); + return OK; +} + +/**************************************************************************** + * Name: curl_add_form + * + * Description: + * Parse a single -F "name=value" or -F "name=@path[;type=mime]" spec and + * append the corresponding multipart chunks to the body. + * + ****************************************************************************/ + +static int curl_add_form(FAR struct curl_body_s *body, + FAR const char *boundary, FAR const char *spec) +{ + char hdr[256]; + FAR const char *eq; + FAR const char *value; + FAR char name[64]; + size_t namelen; + int len; + int ret; + + eq = strchr(spec, '='); + if (eq == NULL) + { + fprintf(stderr, "curl: bad -F spec '%s' (want name=value)\n", spec); + return -EINVAL; + } + + namelen = eq - spec; + if (namelen >= sizeof(name)) + { + return -E2BIG; + } + + memcpy(name, spec, namelen); + name[namelen] = '\0'; + value = eq + 1; + + if (value[0] == '@') + { + /* File upload: name=@path[;type=mime] */ + + FAR const char *path = value + 1; + FAR const char *mime; + FAR char *typesep; + char pathbuf[CONFIG_WEBCLIENT_MAXFILENAME]; + struct stat st; + int fd; + + strlcpy(pathbuf, path, sizeof(pathbuf)); + typesep = strchr(pathbuf, ';'); + if (typesep != NULL && strncmp(typesep, ";type=", 6) == 0) + { + *typesep = '\0'; + mime = typesep + 6; + } + else + { + mime = curl_guess_mime(pathbuf); + } + + fd = open(pathbuf, O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + fprintf(stderr, "curl: cannot open '%s': %d\n", pathbuf, errno); + return -errno; + } + + if (fstat(fd, &st) < 0) + { + fprintf(stderr, "curl: stat '%s' failed: %d\n", pathbuf, errno); + close(fd); + return -errno; + } + + len = snprintf(hdr, sizeof(hdr), + "--%s\r\n" + "Content-Disposition: form-data; name=\"%s\"; " + "filename=\"%s\"\r\n" + "Content-Type: %s\r\n\r\n", + boundary, name, basename(pathbuf), mime); + if (len < 0 || len >= sizeof(hdr)) + { + close(fd); + return -E2BIG; + } + + ret = curl_add_str_chunk(body, hdr, len); + if (ret < 0) + { + close(fd); + return ret; + } + + ret = curl_add_file_chunk(body, fd, st.st_size); + if (ret < 0) + { + close(fd); + return ret; + } + } + else + { + /* Plain text field: name=value */ + + len = snprintf(hdr, sizeof(hdr), + "--%s\r\n" + "Content-Disposition: form-data; name=\"%s\"\r\n\r\n", + boundary, name); + if (len < 0 || len >= sizeof(hdr)) + { + return -E2BIG; + } + + ret = curl_add_str_chunk(body, hdr, len); + if (ret < 0) + { + return ret; + } + + ret = curl_add_str_chunk(body, value, strlen(value)); + if (ret < 0) + { + return ret; + } + } + + /* Trailing CRLF that terminates this part's content. */ + + return curl_add_str_chunk(body, "\r\n", 2); +} + +/**************************************************************************** + * Name: curl_usage + ****************************************************************************/ + +static void curl_usage(FAR const char *progname) +{ + fprintf(stderr, + "Usage: %s [options] \n" + " -X HTTP method (default GET, or POST with -d/-F).\n" + " -d Request body. Use -d @file to read from a file.\n" + " --data-binary Like -d but never interprets @-less data.\n" + " -F multipart/form-data field. Use name=@file to\n" + " upload a file. Repeatable. Implies POST.\n" + " -H
Add a request header. Repeatable.\n" + " -o, --output Write response body to instead of stdout.\n" + " -v Verbose: print response headers to stderr.\n" + " -h Show this help.\n" + "\n" + "Only http:// URLs are supported (no HTTPS). A scheme-less URL such\n" + "as host:port/path is treated as http://host:port/path.\n", + progname); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + FAR const char *headers[CURL_MAX_HEADERS]; + FAR const char *forms[CURL_MAX_FORMS]; + FAR const char *method = NULL; + FAR const char *data = NULL; + FAR const char *rawurl = NULL; + FAR const char *outfile = NULL; + struct webclient_context ctx; + struct curl_body_s body; + struct curl_sink_s sink; + char boundary[48]; + + /* Sized so the compiler can prove the Content-Type snprintf never + * truncates: the fixed prefix (including its NUL) plus the largest + * possible boundary string. + */ + + char ctype[sizeof("Content-Type: multipart/form-data; boundary=") + + sizeof(boundary)]; + char urlbuf[CONFIG_WEBCLIENT_MAXHOSTNAME + CONFIG_WEBCLIENT_MAXFILENAME + + 16]; + FAR const char *url; + FAR char *buffer = NULL; + unsigned int nheaders = 0; + unsigned int nforms = 0; + bool have_ctype = false; + bool verbose = false; + int ret; + int opt; + unsigned int i; + + struct option long_options[] = + { + {"data", required_argument, NULL, 'd'}, + {"data-binary", required_argument, NULL, 'B'}, + {"form", required_argument, NULL, 'F'}, + {"header", required_argument, NULL, 'H'}, + {"request", required_argument, NULL, 'X'}, + {"output", required_argument, NULL, 'o'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + body.nchunks = 0; + body.cur = 0; + + while ((opt = getopt_long(argc, argv, "X:d:F:H:o:vh", + long_options, NULL)) != ERROR) + { + switch (opt) + { + case 'X': + method = optarg; + break; + + case 'd': + case 'B': + data = optarg; + break; + + case 'F': + if (nforms >= CURL_MAX_FORMS) + { + fprintf(stderr, "curl: too many -F fields (max %d)\n", + CURL_MAX_FORMS); + return EXIT_FAILURE; + } + + forms[nforms++] = optarg; + break; + + case 'H': + if (nheaders >= CURL_MAX_HEADERS - 1) + { + fprintf(stderr, "curl: too many -H headers (max %d)\n", + CURL_MAX_HEADERS - 1); + return EXIT_FAILURE; + } + + if (strncasecmp(optarg, "Content-Type:", 13) == 0) + { + have_ctype = true; + } + + headers[nheaders++] = optarg; + break; + + case 'o': + outfile = optarg; + break; + + case 'v': + verbose = true; + break; + + case 'h': + curl_usage(argv[0]); + return EXIT_SUCCESS; + + case '?': + default: + curl_usage(argv[0]); + return EXIT_FAILURE; + } + } + + if (optind >= argc) + { + fprintf(stderr, "curl: missing URL\n"); + curl_usage(argv[0]); + return EXIT_FAILURE; + } + + rawurl = argv[optind]; + + if (data != NULL && nforms > 0) + { + fprintf(stderr, "curl: -d and -F cannot be combined\n"); + return EXIT_FAILURE; + } + + /* Prepend a default http:// scheme when the URL has none. */ + + if (strstr(rawurl, "://") == NULL) + { + snprintf(urlbuf, sizeof(urlbuf), "http://%s", rawurl); + url = urlbuf; + } + else + { + url = rawurl; + } + + /* Default method: POST when sending a body, GET otherwise. */ + + if (method == NULL) + { + method = (data != NULL || nforms > 0) ? "POST" : "GET"; + } + + /* Build the request body. */ + + if (nforms > 0) + { + /* multipart/form-data: derive a boundary and add the Content-Type + * header ourselves. + */ + + snprintf(boundary, sizeof(boundary), "----NuttXCurl%08x%08x", + getpid(), nforms); + + for (i = 0; i < nforms; i++) + { + ret = curl_add_form(&body, boundary, forms[i]); + if (ret < 0) + { + goto errout_body; + } + } + + /* Closing boundary. */ + + ret = curl_add_str_chunk(&body, "--", 2); + if (ret >= 0) + { + ret = curl_add_str_chunk(&body, boundary, strlen(boundary)); + } + + if (ret >= 0) + { + ret = curl_add_str_chunk(&body, "--\r\n", 4); + } + + if (ret < 0) + { + goto errout_body; + } + + if (nheaders < CURL_MAX_HEADERS && !have_ctype) + { + snprintf(ctype, sizeof(ctype), + "Content-Type: multipart/form-data; boundary=%s", + boundary); + headers[nheaders++] = ctype; + have_ctype = true; + } + } + else if (data != NULL && data[0] == '@') + { + /* -d @file : stream the file as the raw request body. */ + + struct stat st; + int fd = open(data + 1, O_RDONLY | O_CLOEXEC); + + if (fd < 0) + { + fprintf(stderr, "curl: cannot open '%s': %d\n", data + 1, errno); + ret = -errno; + goto errout_body; + } + + if (fstat(fd, &st) < 0) + { + fprintf(stderr, "curl: stat '%s' failed: %d\n", data + 1, errno); + ret = -errno; + close(fd); + goto errout_body; + } + + ret = curl_add_file_chunk(&body, fd, st.st_size); + if (ret < 0) + { + close(fd); + goto errout_body; + } + } + else if (data != NULL) + { + /* -d 'literal' : the string itself is the body. */ + + ret = curl_add_str_chunk(&body, data, strlen(data)); + if (ret < 0) + { + goto errout_body; + } + } + + /* curl adds a form Content-Type by default for -d without an explicit + * one. JSON callers are expected to pass -H "Content-Type: ...". + */ + + if (data != NULL && !have_ctype && nheaders < CURL_MAX_HEADERS) + { + headers[nheaders++] = + "Content-Type: application/x-www-form-urlencoded"; + } + + buffer = malloc(CONFIG_SYSTEM_CURL_BUFFERSIZE); + if (buffer == NULL) + { + fprintf(stderr, "curl: out of memory\n"); + ret = -ENOMEM; + goto errout_body; + } + + /* Open the destination for the response body. */ + + if (outfile != NULL) + { + sink.fd = open(outfile, + O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (sink.fd < 0) + { + fprintf(stderr, "curl: cannot open '%s': %d\n", outfile, errno); + ret = -errno; + goto errout_buffer; + } + } + else + { + sink.fd = STDOUT_FILENO; + } + + /* Assemble and run the request. */ + + webclient_set_defaults(&ctx); + ctx.method = method; + ctx.url = url; + ctx.buffer = buffer; + ctx.buflen = CONFIG_SYSTEM_CURL_BUFFERSIZE; + ctx.sink_callback = curl_sink_callback; + ctx.sink_callback_arg = &sink; + + if (nheaders > 0) + { + ctx.headers = headers; + ctx.nheaders = nheaders; + } + + if (verbose) + { + ctx.header_callback = curl_header_callback; + + fprintf(stderr, "> %s %s\n", method, url); + for (i = 0; i < nheaders; i++) + { + fprintf(stderr, "> %s\n", headers[i]); + } + } + + if (body.nchunks > 0) + { + ctx.body_callback = curl_body_callback; + ctx.body_callback_arg = &body; + ctx.bodylen = curl_body_total(&body); + } + + ret = webclient_perform(&ctx); + if (ret < 0) + { + fprintf(stderr, "curl: request failed: %d\n", ret); + } + else + { + fprintf(stderr, "curl: HTTP %u\n", ctx.http_status); + } + + if (sink.fd != STDOUT_FILENO) + { + close(sink.fd); + } + +errout_buffer: + free(buffer); + +errout_body: + curl_body_cleanup(&body); + return ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +}