This is an automated email from the ASF dual-hosted git repository.
jerpelea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git
The following commit(s) were added to refs/heads/master by this push:
new 865393d41 system/curl: add a small curl-like HTTP client command
865393d41 is described below
commit 865393d419994015bb60e74c859eb1341f163b4f
Author: Jorge Guzman <[email protected]>
AuthorDate: Sun Jul 19 13:46:50 2026 -0300
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 <[email protected]>
---
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(+)
diff --git a/system/curl/CMakeLists.txt b/system/curl/CMakeLists.txt
new file mode 100644
index 000000000..ff48d44a1
--- /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 000000000..ca2963aff
--- /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 000000000..4003825c7
--- /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 000000000..70469e218
--- /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 000000000..11837c625
--- /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 <nuttx/config.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <libgen.h>
+
+#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] <url>\n"
+ " -X <method> HTTP method (default GET, or POST with -d/-F).\n"
+ " -d <data> Request body. Use -d @file to read from a file.\n"
+ " --data-binary <d> Like -d but never interprets @-less data.\n"
+ " -F <name=content> multipart/form-data field. Use name=@file to\n"
+ " upload a file. Repeatable. Implies POST.\n"
+ " -H <header> Add a request header. Repeatable.\n"
+ " -o, --output <f> Write response body to <f> 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;
+}