mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-01 20:29:00 +00:00
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 <jorge.gzm@gmail.com>
This commit is contained in:
parent
98f683dea8
commit
865393d419
5 changed files with 996 additions and 0 deletions
33
system/curl/CMakeLists.txt
Normal file
33
system/curl/CMakeLists.txt
Normal file
|
|
@ -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()
|
||||
42
system/curl/Kconfig
Normal file
42
system/curl/Kconfig
Normal file
|
|
@ -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
|
||||
25
system/curl/Make.defs
Normal file
25
system/curl/Make.defs
Normal file
|
|
@ -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
|
||||
36
system/curl/Makefile
Normal file
36
system/curl/Makefile
Normal file
|
|
@ -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
|
||||
860
system/curl/curl_main.c
Normal file
860
system/curl/curl_main.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue