fc7aa92b57
Some improvements could be made by anyone. For example, I know the main routine in perform() shall be split into several parts for readability. I apologize in advance for this kind of spaghetti code, but I was short on time to refactor it. Also chunked HTTP transfer encoding would be a nice contribution from anyone interested. It is detected but not yet supported.
643 lines
23 KiB
C
643 lines
23 KiB
C
/****************************************************************************
|
|
* apps/netutils/libcurl4nx/curl4nx_easy_perform.c
|
|
* Implementation of the HTTP client, cURL like interface.
|
|
*
|
|
* Copyright (C) 2019 Gregory Nutt. All rights reserved.
|
|
* Author: Sebastien Lorquet <sebastien@lorquet.fr>
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following
|
|
* disclaimer in the documentation and/or other materials provided
|
|
* with the distribution.
|
|
* 3. The name of the author may not be used to endorse or promote
|
|
* products derived from this software without specific prior
|
|
* written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
|
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
****************************************************************************/
|
|
|
|
/****************************************************************************
|
|
* Included Files
|
|
****************************************************************************/
|
|
|
|
#include <nuttx/config.h>
|
|
#include <nuttx/compiler.h>
|
|
#include <debug.h>
|
|
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <netdb.h>
|
|
#include <strings.h>
|
|
#include <errno.h>
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
|
|
#include <nuttx/version.h>
|
|
|
|
#include "netutils/netlib.h"
|
|
#include "netutils/curl4nx.h"
|
|
#include "curl4nx_private.h"
|
|
|
|
#if defined(CONFIG_NETUTILS_CODECS)
|
|
# if defined(CONFIG_CODECS_URLCODE)
|
|
# define WGET_USE_URLENCODE 1
|
|
# include "netutils/urldecode.h"
|
|
# endif
|
|
# if defined(CONFIG_CODECS_BASE64)
|
|
# include "netutils/base64.h"
|
|
# endif
|
|
#else
|
|
# undef CONFIG_CODECS_URLCODE
|
|
# undef CONFIG_CODECS_BASE64
|
|
#endif
|
|
|
|
/****************************************************************************
|
|
* Pre-processor Definitions
|
|
****************************************************************************/
|
|
|
|
#define CURL4NX_STATE_STATUSLINE 0
|
|
#define CURL4NX_STATE_STATUSCODE 1
|
|
#define CURL4NX_STATE_STATUSREASON 2
|
|
#define CURL4NX_STATE_HEADERS 3
|
|
#define CURL4NX_STATE_DATA_NORMAL 4
|
|
#define CURL4NX_STATE_DATA_CHUNKED 5
|
|
|
|
/****************************************************************************
|
|
* Private Functions
|
|
****************************************************************************/
|
|
|
|
/****************************************************************************
|
|
* Name: curl4nx_easy_perform()
|
|
*
|
|
* Description:
|
|
* Resolve the hostname to an IP address. There are 3 cases:
|
|
* - direct IP: inet_aton is used
|
|
* - using internal curl4nx resolution
|
|
* - DNS resolution, if available
|
|
****************************************************************************/
|
|
|
|
/****************************************************************************
|
|
* Name: curl4nx_resolve
|
|
* Description:
|
|
* Translate a host name to an IP addres (V4 only for the moment)
|
|
* - either the host is a string with an IP
|
|
* - either the known hosts were defined by CURL4NXOPT_
|
|
****************************************************************************/
|
|
|
|
static int curl4nx_resolve(FAR struct curl4nx_s *handle, FAR char *hostname,
|
|
FAR struct in_addr *ip)
|
|
{
|
|
int ret = inet_aton(hostname, ip);
|
|
if (ret != 0)
|
|
{
|
|
return CURL4NXE_OK; /* IP address is valid */
|
|
}
|
|
|
|
curl4nx_warn("Not a valid IP address, trying hostname resolution\n");
|
|
return CURL4NXE_COULDNT_RESOLVE_HOST;
|
|
}
|
|
|
|
/****************************************************************************
|
|
* Name: curl4nx_is_header
|
|
* Description:
|
|
* Return TRUE if buf contains header, then update off to point at the
|
|
* beginning of header value
|
|
****************************************************************************/
|
|
|
|
static int curl4nx_is_header(FAR char *buf, int len,
|
|
FAR const char *header, FAR int *off)
|
|
{
|
|
if (strncasecmp(buf, header, strlen(header)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
*off = strlen(header);
|
|
while ((*off) < len && buf[*off] == ' ')
|
|
{
|
|
(*off)++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/****************************************************************************
|
|
* Public Functions
|
|
****************************************************************************/
|
|
|
|
/****************************************************************************
|
|
* Name: curl4nx_easy_perform()
|
|
****************************************************************************/
|
|
|
|
int curl4nx_easy_perform(FAR struct curl4nx_s *handle)
|
|
{
|
|
struct sockaddr_in server;
|
|
FILE * stream; /* IOSTREAM used to printf in the socket TODO avoid */
|
|
int rxoff; /* Current offset within RX buffer */
|
|
char tmpbuf[16]; /* Buffer to hold small strings */
|
|
int tmplen; /* Number of bytes used in tmpbuffer */
|
|
int state; /* Current state of the parser */
|
|
int ret; /* Return value from internal calls */
|
|
char * headerbuf;
|
|
int headerlen;
|
|
char * end;
|
|
int cret = CURL4NXE_OK; /* Public return value */
|
|
bool redirected = false; /* Boolean to manage HTTP redirections */
|
|
bool chunked = false;
|
|
unsigned long long done = 0;
|
|
int redircount = 0;
|
|
|
|
curl4nx_info("started\n");
|
|
|
|
stream = NULL;
|
|
headerbuf = malloc(CONFIG_LIBCURL4NX_MAXHEADERLINE);
|
|
if (!headerbuf)
|
|
{
|
|
cret = CURL4NXE_OUT_OF_MEMORY;
|
|
goto abort;
|
|
}
|
|
|
|
if (handle->host[0] == 0 || handle->port == 0)
|
|
{
|
|
/* URL has not been set */
|
|
|
|
cret = CURL4NXE_URL_MALFORMAT;
|
|
goto freebuf;
|
|
}
|
|
|
|
/* TODO: check that host and port have changed or are the same, so we can
|
|
* recycle the socket for the next request.
|
|
*/
|
|
|
|
do
|
|
{
|
|
handle->sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (handle->sockfd < 0)
|
|
{
|
|
/* socket failed. It will set the errno appropriately */
|
|
|
|
curl4nx_err("ERROR: socket failed: %d\n", errno);
|
|
cret = CURL4NXE_COULDNT_CONNECT;
|
|
goto freebuf;
|
|
}
|
|
|
|
/* TODO: timeouts */
|
|
|
|
server.sin_family = AF_INET;
|
|
server.sin_port = htons(handle->port);
|
|
cret = curl4nx_resolve(handle, handle->host, &server.sin_addr);
|
|
if (cret != CURL4NXE_OK)
|
|
{
|
|
/* Could not resolve host (or malformed IP address) */
|
|
|
|
curl4nx_err("ERROR: Failed to resolve hostname\n");
|
|
cret = CURL4NXE_COULDNT_RESOLVE_HOST;
|
|
goto freebuf;
|
|
}
|
|
|
|
/* Connect to server. First we have to set some fields in the
|
|
* 'server' address structure. The system will assign me an arbitrary
|
|
* local port that is not in use.
|
|
*/
|
|
|
|
ret = connect(handle->sockfd,
|
|
(struct sockaddr *)&server, sizeof(struct sockaddr_in));
|
|
if (ret < 0)
|
|
{
|
|
curl4nx_err("ERROR: connect failed: %d\n", errno);
|
|
cret = CURL4NXE_COULDNT_CONNECT;
|
|
goto close;
|
|
}
|
|
|
|
curl4nx_info("Connected...\n");
|
|
|
|
stream = fdopen(handle->sockfd, "wb");
|
|
|
|
/* Send request */
|
|
|
|
fprintf(stream, "%s %s HTTP/%d.%d\r\n",
|
|
handle->method,
|
|
handle->path,
|
|
handle->version >> 4,
|
|
handle->version & 0x0f);
|
|
|
|
/* Send headers */
|
|
|
|
fprintf(stream, "Host: %s\r\n", handle->host);
|
|
|
|
/* For the moment we do not support compression */
|
|
|
|
fprintf(stream, "Content-encoding: identity\r\n");
|
|
|
|
/* Send more headers */
|
|
|
|
/* End of headers */
|
|
|
|
fprintf(stream, "\r\n");
|
|
|
|
/* TODO send data */
|
|
|
|
fflush(stream);
|
|
curl4nx_info("Request sent\n");
|
|
|
|
/* Now wait for the result */
|
|
|
|
redirected = false; /* for now */
|
|
state = CURL4NX_STATE_STATUSLINE;
|
|
tmplen = 0;
|
|
|
|
while (1)
|
|
{
|
|
ret = recv(handle->sockfd, handle->rxbuf, handle->rxbufsize, 0);
|
|
if (ret < 0)
|
|
{
|
|
curl4nx_err("RECV failed, errno=%d\n", errno);
|
|
cret = CURL4NXE_RECV_ERROR;
|
|
goto close;
|
|
}
|
|
else if (ret == 0)
|
|
{
|
|
curl4nx_err("Connection lost\n");
|
|
cret = CURL4NXE_GOT_NOTHING;
|
|
goto close;
|
|
}
|
|
|
|
curl4nx_info("Received %d bytes\n", ret);
|
|
rxoff = 0;
|
|
|
|
if (state == CURL4NX_STATE_STATUSLINE)
|
|
{
|
|
/* Accumulate HTTP/x.y until space */
|
|
|
|
while ((tmplen < sizeof(tmpbuf)) && (rxoff < ret))
|
|
{
|
|
if (handle->rxbuf[rxoff] == ' ')
|
|
{
|
|
/* found space after http version */
|
|
|
|
tmpbuf[tmplen] = 0; /* Finish pending string */
|
|
rxoff++;
|
|
curl4nx_info("received version: [%s]\n", tmpbuf);
|
|
tmplen = 0; /* reset buffer to look at code */
|
|
state = CURL4NX_STATE_STATUSCODE;
|
|
break;
|
|
}
|
|
|
|
/* Not a space: accumulate chars */
|
|
|
|
tmpbuf[tmplen] = handle->rxbuf[rxoff];
|
|
tmplen++;
|
|
rxoff++;
|
|
}
|
|
|
|
/* Check for overflow, version code should not fill the tmpbuf */
|
|
|
|
if (tmplen == sizeof(tmpbuf))
|
|
{
|
|
/* extremely long http version -> invalid response */
|
|
|
|
curl4nx_err("Buffer overflow while reading version\n");
|
|
cret = CURL4NXE_RECV_ERROR;
|
|
goto close;
|
|
}
|
|
|
|
/* No overflow and no space found: wait for next buffer */
|
|
}
|
|
|
|
/* NO ELSE HERE, state may have changed and require new management
|
|
* for the same rx buffer.
|
|
*/
|
|
|
|
if (state == CURL4NX_STATE_STATUSCODE)
|
|
{
|
|
/* Accumulate response code until space */
|
|
|
|
while ((tmplen < sizeof(tmpbuf)) && (rxoff < ret))
|
|
{
|
|
if (handle->rxbuf[rxoff] == ' ')
|
|
{
|
|
/* Found space after http version */
|
|
|
|
tmpbuf[tmplen] = 0; /* Finish pending string */
|
|
rxoff++;
|
|
curl4nx_info("received code: [%s]\n", tmpbuf);
|
|
handle->status = strtol(tmpbuf, &end, 10);
|
|
|
|
if (*end != 0)
|
|
{
|
|
curl4nx_err("Bad status code [%s]\n", tmpbuf);
|
|
cret = CURL4NXE_RECV_ERROR;
|
|
goto close;
|
|
}
|
|
|
|
tmplen = 0; /* reset buffer to look at code */
|
|
state = CURL4NX_STATE_STATUSREASON;
|
|
break;
|
|
}
|
|
|
|
/* Not a space: accumulate chars */
|
|
|
|
tmpbuf[tmplen] = handle->rxbuf[rxoff];
|
|
tmplen++;
|
|
rxoff++;
|
|
}
|
|
|
|
/* Check for overflow, version code should not fill the tmpbuf */
|
|
|
|
if (tmplen == sizeof(tmpbuf))
|
|
{
|
|
/* Extremely long http code -> invalid response */
|
|
|
|
curl4nx_err("Buffer overflow while reading code\n");
|
|
cret = CURL4NXE_RECV_ERROR;
|
|
goto close;
|
|
}
|
|
|
|
/* No overflow and no space found: wait for next buffer */
|
|
}
|
|
|
|
if (state == CURL4NX_STATE_STATUSREASON)
|
|
{
|
|
/* Accumulate response code until CRLF */
|
|
|
|
while (rxoff < ret)
|
|
{
|
|
if (handle->rxbuf[rxoff] == 0x0d ||
|
|
handle->rxbuf[rxoff] == 0x0a)
|
|
{
|
|
/* Accumulate all contiguous CR and LF in any order */
|
|
|
|
tmpbuf[tmplen] = handle->rxbuf[rxoff];
|
|
tmplen++;
|
|
if (tmplen == 2)
|
|
{
|
|
if (tmpbuf[0] == 0x0d && tmpbuf[1] == 0x0a)
|
|
{
|
|
headerlen = 0;
|
|
handle->content_length = 0;
|
|
state = CURL4NX_STATE_HEADERS;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
tmplen = 0; /* Reset search for CRLF */
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* This char is not interesting: reset storage */
|
|
|
|
tmplen = 0;
|
|
|
|
/* curl4nx_info("-> %c\n", handle->rxbuf[rxoff]); */
|
|
}
|
|
|
|
rxoff++;
|
|
}
|
|
}
|
|
|
|
if (state == CURL4NX_STATE_HEADERS)
|
|
{
|
|
while (rxoff < ret)
|
|
{
|
|
if (handle->rxbuf[rxoff] == 0x0d ||
|
|
handle->rxbuf[rxoff] == 0x0a)
|
|
{
|
|
/* Accumulate all contiguous CR and LF in any order */
|
|
|
|
tmpbuf[tmplen] = handle->rxbuf[rxoff];
|
|
tmplen++;
|
|
if (tmplen == 2)
|
|
{
|
|
if (tmpbuf[0] == 0x0d && tmpbuf[1] == 0x0a)
|
|
{
|
|
if (headerlen == 0) /* Found an empty header */
|
|
{
|
|
curl4nx_info("<End of headers>\n");
|
|
rxoff++; /* Skip this char */
|
|
state = chunked ?
|
|
CURL4NX_STATE_DATA_CHUNKED :
|
|
CURL4NX_STATE_DATA_NORMAL;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
int off;
|
|
curl4nx_iofunc_f func;
|
|
headerbuf[headerlen] = 0;
|
|
func = handle->headerfunc;
|
|
if (func == NULL)
|
|
{
|
|
func = handle->writefunc;
|
|
}
|
|
|
|
func(headerbuf, headerlen, 1,
|
|
handle->headerdata);
|
|
|
|
/* Find the content-length */
|
|
|
|
if (curl4nx_is_header(headerbuf, headerlen,
|
|
"content-length:",
|
|
&off))
|
|
{
|
|
handle->content_length =
|
|
strtoull(headerbuf + off, &end, 10);
|
|
if (*end != 0)
|
|
{
|
|
curl4nx_err("Stray chars after "
|
|
"content length!\n");
|
|
cret = CURL4NXE_RECV_ERROR;
|
|
goto close;
|
|
}
|
|
|
|
curl4nx_info("Found content length: "
|
|
"%llu\n",
|
|
handle->content_length);
|
|
}
|
|
|
|
/* Find the transfer encoding */
|
|
|
|
if (curl4nx_is_header(headerbuf, headerlen,
|
|
"transfer-encoding:",
|
|
&off))
|
|
{
|
|
chunked = !strncasecmp(headerbuf + off,
|
|
"chunked", 7);
|
|
if (chunked)
|
|
{
|
|
curl4nx_info("Transfer using "
|
|
"chunked format\n");
|
|
}
|
|
}
|
|
|
|
/* Find the location */
|
|
|
|
if (curl4nx_is_header(headerbuf, headerlen,
|
|
"location:",
|
|
&off))
|
|
{
|
|
/* Parse the new URL if we get a
|
|
* redirection code.
|
|
*/
|
|
|
|
if (handle->status >= 300 &&
|
|
handle->status < 400)
|
|
{
|
|
if (handle->flags &
|
|
CURL4NX_FLAGS_FOLLOWLOCATION)
|
|
{
|
|
if ((handle->max_redirs > 0) &&
|
|
(redircount >=
|
|
handle->max_redirs))
|
|
{
|
|
curl4nx_info(
|
|
"Too many redirections\n");
|
|
cret = CURL4NXE_TOO_MANY_REDIRECTS;
|
|
goto close;
|
|
}
|
|
|
|
cret =
|
|
curl4nx_easy_setopt(handle,
|
|
CURL4NXOPT_URL,
|
|
headerbuf + off);
|
|
if (cret != CURL4NXE_OK)
|
|
{
|
|
goto close;
|
|
}
|
|
|
|
redirected = true;
|
|
redircount += 1;
|
|
curl4nx_info("REDIRECTION (%d) -> %s\n",
|
|
redircount,
|
|
headerbuf + off);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Prepare for next header */
|
|
|
|
headerlen = 0;
|
|
tmplen = 0; /* Reset search for CRLF */
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tmplen = 0; /* Reset search for CRLF */
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tmplen = 0; /* Reset CRLF detection */
|
|
|
|
/* Plus one for final zero */
|
|
|
|
if (headerlen < (CONFIG_LIBCURL4NX_MAXHEADERLINE - 1))
|
|
{
|
|
headerbuf[headerlen] = handle->rxbuf[rxoff];
|
|
headerlen++;
|
|
}
|
|
else
|
|
{
|
|
curl4nx_warn("prevented header overload\n");
|
|
}
|
|
}
|
|
|
|
rxoff++;
|
|
}
|
|
}
|
|
|
|
if (state == CURL4NX_STATE_DATA_NORMAL)
|
|
{
|
|
if ((handle->flags & CURL4NX_FLAGS_FAILONERROR) &&
|
|
handle->status >= 400)
|
|
{
|
|
cret = CURL4NXE_HTTP_RETURNED_ERROR;
|
|
goto close;
|
|
}
|
|
|
|
curl4nx_info("now in normal data state, rxoff=%d rxlen=%d\n",
|
|
rxoff, ret);
|
|
done += (ret - rxoff);
|
|
|
|
handle->writefunc(handle->rxbuf + rxoff, ret - rxoff, 1,
|
|
handle->writedata);
|
|
if (handle->content_length != 0)
|
|
{
|
|
curl4nx_info("Done %llu of %llu\n",
|
|
done, handle->content_length);
|
|
|
|
if (handle->progressfunc)
|
|
{
|
|
handle->progressfunc(handle->progressdata,
|
|
handle->content_length,
|
|
done, 0, 0);
|
|
}
|
|
|
|
if (handle->content_length == done)
|
|
{
|
|
/* Transfer is complete */
|
|
|
|
goto close;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state == CURL4NX_STATE_DATA_CHUNKED)
|
|
{
|
|
if ((handle->flags & CURL4NX_FLAGS_FAILONERROR) &&
|
|
handle->status >= 400)
|
|
{
|
|
cret = CURL4NXE_HTTP_RETURNED_ERROR;
|
|
goto close;
|
|
}
|
|
|
|
curl4nx_info("now in chunked data state, rxoff=%d rxlen=%d\n",
|
|
rxoff, ret);
|
|
curl4nx_err("Not supported yet.\n");
|
|
goto close;
|
|
}
|
|
}
|
|
|
|
/* Done with this connection - this will also close the socket */
|
|
|
|
close:
|
|
curl4nx_info("Closing\n");
|
|
if (stream)
|
|
{
|
|
fclose(stream);
|
|
}
|
|
}
|
|
while (redirected);
|
|
|
|
freebuf:
|
|
free(headerbuf);
|
|
|
|
abort:
|
|
curl4nx_info("done\n");
|
|
return cret;
|
|
}
|