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;
+}

Reply via email to