mirror of
https://github.com/openharmony/third_party_sane-airscan.git
synced 2026-06-30 21:17:55 -04:00
3392 lines
86 KiB
C
3392 lines
86 KiB
C
/* AirScan (a.k.a. eSCL) backend for SANE
|
|
*
|
|
* Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com)
|
|
* See LICENSE for license terms and conditions
|
|
*
|
|
* HTTP Client
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <string.h>
|
|
|
|
#define NO_HTTP_STATUS
|
|
|
|
#include "airscan.h"
|
|
#include "http_parser.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <netdb.h>
|
|
#include <stdlib.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
#include <avahi-common/domain.h>
|
|
#include <gnutls/gnutls.h>
|
|
|
|
/******************** Constants ********************/
|
|
/* I/O buffer size
|
|
*/
|
|
#define HTTP_IOBUF_SIZE 65536
|
|
|
|
/* Limit of chained HTTP redirects
|
|
*/
|
|
#define HTTP_REDIRECT_LIMIT 8
|
|
|
|
/* Default query timeout, milliseconds
|
|
* Disabled, if negative
|
|
*/
|
|
#define HTTP_QUERY_TIMEOUT -1
|
|
|
|
/******************** Static variables ********************/
|
|
static gnutls_certificate_credentials_t gnutls_cred;
|
|
|
|
/******************** Forward declarations ********************/
|
|
typedef struct http_multipart http_multipart;
|
|
|
|
static http_data*
|
|
http_data_new(http_data *parent, const char *bytes, size_t size);
|
|
|
|
static void
|
|
http_data_set_content_type (http_data *data, const char *content_type);
|
|
|
|
static void
|
|
http_query_timeout_cancel (http_query *q);
|
|
|
|
static http_query*
|
|
http_query_by_ll_node (ll_node *node);
|
|
|
|
static void
|
|
http_query_complete (http_query *q, error err);
|
|
|
|
static void
|
|
http_query_connect (http_query *q, error err);
|
|
|
|
static void
|
|
http_query_disconnect (http_query *q);
|
|
|
|
static ssize_t
|
|
http_query_sock_send (http_query *q, const void *data, size_t size);
|
|
|
|
static ssize_t
|
|
http_query_sock_recv (http_query *q, void *data, size_t size);
|
|
|
|
static error
|
|
http_query_sock_err (http_query *q, int rc);
|
|
|
|
static void
|
|
http_query_cancel (http_query *q);
|
|
|
|
/******************** HTTP URI ********************/
|
|
/* Type http_uri represents HTTP URI
|
|
*/
|
|
struct http_uri {
|
|
struct http_parser_url parsed; /* Parsed URI */
|
|
const char *str; /* URI string */
|
|
const char *path; /* URI path */
|
|
const char *host; /* URI host */
|
|
HTTP_SCHEME scheme; /* URI scheme */
|
|
union { /* Host address*/
|
|
struct sockaddr sockaddr;
|
|
struct sockaddr_in in;
|
|
struct sockaddr_in6 in6;
|
|
} addr;
|
|
};
|
|
|
|
typedef struct {
|
|
const char *str;
|
|
size_t len;
|
|
} http_uri_field;
|
|
|
|
/* Make http_uri_field
|
|
*/
|
|
static http_uri_field
|
|
http_uri_field_make (const char *str)
|
|
{
|
|
http_uri_field field = {str, strlen(str)};
|
|
return field;
|
|
}
|
|
|
|
/* Check if URI has a particular field
|
|
*/
|
|
static inline bool
|
|
http_uri_field_present (const http_uri *uri, int num)
|
|
{
|
|
return (uri->parsed.field_set & (1 << num)) != 0;
|
|
}
|
|
|
|
/* Check if URI field is present and non-empty
|
|
*/
|
|
static inline bool
|
|
http_uri_field_nonempty (const http_uri *uri, int num)
|
|
{
|
|
return uri->parsed.field_data[num].len != 0;
|
|
}
|
|
|
|
/* Get first character of the field. Returns -1, if
|
|
* field is empty, or non-negative character code otherwise
|
|
*/
|
|
static inline int
|
|
http_uri_field_begin (const http_uri *uri, int num)
|
|
{
|
|
if (http_uri_field_nonempty(uri, num)) {
|
|
return (unsigned char) uri->str[uri->parsed.field_data[num].off];
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* Get field from URI
|
|
*/
|
|
static http_uri_field
|
|
http_uri_field_get (const http_uri *uri, int num)
|
|
{
|
|
http_uri_field field = {
|
|
uri->str + uri->parsed.field_data[num].off,
|
|
uri->parsed.field_data[num].len
|
|
};
|
|
|
|
return field;
|
|
}
|
|
|
|
/* Append field to buffer, and return pointer to
|
|
* updated buffer tail
|
|
*/
|
|
static inline char *
|
|
http_uri_field_append (http_uri_field field, char *buf)
|
|
{
|
|
memcpy(buf, field.str, field.len);
|
|
return buf + field.len;
|
|
}
|
|
|
|
/* Get field from URI and append to buffer
|
|
*/
|
|
static inline char*
|
|
http_uri_field_copy (const http_uri *uri, int num, char *buf)
|
|
{
|
|
return http_uri_field_append(http_uri_field_get(uri, num), buf);
|
|
}
|
|
|
|
/* Get and strdup the field
|
|
*/
|
|
static char*
|
|
http_uri_field_strdup (const http_uri *uri, int num)
|
|
{
|
|
http_uri_field field = http_uri_field_get(uri, num);
|
|
char *s = mem_new(char, field.len + 1);
|
|
|
|
memcpy(s, field.str, field.len);
|
|
s[field.len] = '\0';
|
|
|
|
return s;
|
|
}
|
|
|
|
/* Check are fields of two URIs are equal
|
|
*/
|
|
static bool
|
|
http_uri_field_equal (const http_uri *uri1, const http_uri *uri2,
|
|
int num, bool nocase)
|
|
{
|
|
http_uri_field f1 = http_uri_field_get(uri1, num);
|
|
http_uri_field f2 = http_uri_field_get(uri2, num);
|
|
|
|
if (f1.len != f2.len) {
|
|
return false;
|
|
}
|
|
|
|
if (nocase) {
|
|
return !strncasecmp(f1.str, f2.str, f1.len);
|
|
} else {
|
|
return !memcmp(f1.str, f2.str, f1.len);
|
|
}
|
|
}
|
|
|
|
/* Replace particular URI field with val[len] string
|
|
*/
|
|
static void
|
|
http_uri_field_replace_len (http_uri *uri, int num, const char *val, size_t len)
|
|
{
|
|
static const struct { char *pfx; int num; char *sfx; } fields[] = {
|
|
{"", UF_SCHEMA, "://"},
|
|
{"", UF_USERINFO, "@"},
|
|
{"", UF_HOST, ""},
|
|
{":", UF_PORT, ""},
|
|
{"", UF_PATH, ""},
|
|
{"?", UF_QUERY, ""},
|
|
{"#", UF_FRAGMENT, ""},
|
|
{NULL, -1, NULL}
|
|
};
|
|
|
|
int i;
|
|
char *buf = alloca(strlen(uri->str) + len + 4);
|
|
char *end = buf;
|
|
http_uri *uri2;
|
|
|
|
/* Rebuild URI string */
|
|
for (i = 0; fields[i].num != -1; i ++) {
|
|
http_uri_field field;
|
|
|
|
if (num == fields[i].num) {
|
|
field.str = val;
|
|
field.len = len;
|
|
} else {
|
|
field = http_uri_field_get(uri, fields[i].num);
|
|
}
|
|
|
|
if (field.len != 0) {
|
|
bool ip6_host = false;
|
|
|
|
if (fields[i].num == UF_HOST) {
|
|
ip6_host = memchr(field.str, ':', field.len) != NULL;
|
|
}
|
|
|
|
if (fields[i].pfx != NULL) {
|
|
http_uri_field pfx = http_uri_field_make(fields[i].pfx);
|
|
end = http_uri_field_append(pfx, end);
|
|
}
|
|
|
|
if (ip6_host) {
|
|
*end ++ = '[';
|
|
}
|
|
|
|
end = http_uri_field_append(field, end);
|
|
|
|
if (ip6_host) {
|
|
*end ++ = ']';
|
|
}
|
|
|
|
if (fields[i].sfx != NULL) {
|
|
http_uri_field sfx = http_uri_field_make(fields[i].sfx);
|
|
end = http_uri_field_append(sfx, end);
|
|
}
|
|
}
|
|
}
|
|
|
|
*end = '\0';
|
|
|
|
/* Reconstruct the URI */
|
|
uri2 = http_uri_new(buf, false);
|
|
log_assert(NULL, uri2 != NULL);
|
|
|
|
mem_free((char*) uri->str);
|
|
mem_free((char*) uri->path);
|
|
mem_free((char*) uri->host);
|
|
|
|
*uri = *uri2;
|
|
mem_free(uri2);
|
|
}
|
|
|
|
/* Replace particular URI field
|
|
*/
|
|
static void
|
|
http_uri_field_replace (http_uri *uri, int num, const char *val)
|
|
{
|
|
http_uri_field_replace_len (uri, num, val, strlen(val));
|
|
}
|
|
|
|
/* Append UF_PATH part of URI to buffer up to the final '/'
|
|
* character
|
|
*/
|
|
static char*
|
|
http_uri_field_copy_basepath (const http_uri *uri, char *buf)
|
|
{
|
|
http_uri_field path = http_uri_field_get(uri, UF_PATH);
|
|
const char *end = memrchr(path.str, '/', path.len);
|
|
|
|
path.len = end ? (size_t)(end - path.str) : 0;
|
|
|
|
buf = http_uri_field_append(path, buf);
|
|
*(buf ++) = '/';
|
|
|
|
return buf;
|
|
}
|
|
|
|
/* Check if sting has an scheme prefix, where scheme
|
|
* must be [a-zA-Z][a-zA-Z0-9+-.]*):
|
|
*
|
|
* If scheme is found, returns its length. 0 is returned otherwise
|
|
*/
|
|
static size_t
|
|
http_uri_parse_scheme (const char *str)
|
|
{
|
|
char c = *str;
|
|
size_t i;
|
|
|
|
if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
|
|
return 0;
|
|
}
|
|
|
|
i = 1;
|
|
for (;;) {
|
|
c = str[i ++];
|
|
|
|
if (('a' <= c && c <= 'z') ||
|
|
('A' <= c && c <= 'Z') ||
|
|
('0' <= c && c <= '9') ||
|
|
c == '+' ||
|
|
c == '-' ||
|
|
c == '.') {
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return c == ':' ? i : 0;
|
|
}
|
|
|
|
/* Return error, if UF_HOST field of URI is present and invalid
|
|
*/
|
|
static error
|
|
http_uri_parse_check_host (http_uri *uri)
|
|
{
|
|
http_uri_field field = http_uri_field_get(uri, UF_HOST);
|
|
char *host, *zone;
|
|
int rc;
|
|
struct in6_addr in6;
|
|
|
|
/* If UF_HOST is present and in square brackets, it must
|
|
* be valid IPv6 literal. Note, http_parser_parse_url()
|
|
* doesn't check it
|
|
*/
|
|
if (field.len == 0 || field.str == uri->str || field.str[-1] != '[') {
|
|
return NULL;
|
|
}
|
|
|
|
if (field.str[field.len] != ']') {
|
|
return ERROR("URI: missed ']' in IP6 address literal");
|
|
}
|
|
|
|
host = alloca(field.len + 1);
|
|
memcpy(host, field.str, field.len);
|
|
host[field.len] = '\0';
|
|
|
|
zone = strchr(host, '%');
|
|
if (zone != NULL) {
|
|
*zone = '\0';
|
|
}
|
|
|
|
rc = inet_pton(AF_INET6, host, &in6);
|
|
if (rc != 1) {
|
|
return ERROR("URI: invalid IP6 address literal");
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse URI in place. The parsed URI doesn't create
|
|
* a copy of URI string, and uses supplied string directly
|
|
*/
|
|
static error
|
|
http_uri_parse (http_uri *uri, const char *str)
|
|
{
|
|
size_t scheme_len = http_uri_parse_scheme(str);
|
|
const char *normalized = str;
|
|
const char *prefix = NULL;
|
|
size_t prefix_len = 0;
|
|
size_t path_skip = 0;
|
|
error err;
|
|
|
|
/* Note, github.com/nodejs/http-parser fails to properly
|
|
* parse relative URLs (URLs without scheme), so prepend
|
|
* fake scheme, then remove it
|
|
*/
|
|
if (scheme_len == 0) {
|
|
char *s;
|
|
|
|
if (str[0] == '/' && str[1] == '/') {
|
|
prefix = "s:";
|
|
} else if (str[0] == '/') {
|
|
prefix = "s://h";
|
|
} else {
|
|
prefix = "s://h/";
|
|
path_skip = 1;
|
|
}
|
|
|
|
prefix_len = strlen(prefix);
|
|
s = alloca(prefix_len + strlen(str) + 1);
|
|
memcpy(s, prefix, prefix_len);
|
|
strcpy(s + prefix_len, str);
|
|
|
|
normalized = s;
|
|
}
|
|
|
|
/* Parse URI */
|
|
memset(uri, 0, sizeof(*uri));
|
|
if (http_parser_parse_url(normalized, strlen(normalized),
|
|
0, &uri->parsed) != 0) {
|
|
return ERROR("URI: parse error");
|
|
}
|
|
|
|
uri->str = str;
|
|
|
|
/* Adjust offsets */
|
|
if (path_skip != 0) {
|
|
uri->parsed.field_data[UF_PATH].off ++;
|
|
uri->parsed.field_data[UF_PATH].len --;
|
|
}
|
|
|
|
if (prefix_len != 0) {
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < UF_MAX; i ++) {
|
|
if ((uri->parsed.field_set & (1 << i)) != 0) {
|
|
if (uri->parsed.field_data[i].off >= (uint16_t) prefix_len) {
|
|
uri->parsed.field_data[i].off -= (uint16_t) prefix_len;
|
|
} else {
|
|
uri->parsed.field_data[i].off = 0;
|
|
uri->parsed.field_data[i].len = 0;
|
|
uri->parsed.field_set &= ~ (1 << i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Decode scheme */
|
|
uri->scheme = HTTP_SCHEME_UNSET; /* For sanity */
|
|
if (scheme_len != 0) {
|
|
if (!strncasecmp(str, "http://", 7)) {
|
|
uri->scheme = HTTP_SCHEME_HTTP;
|
|
} else if (!strncasecmp(str, "https://", 8)) {
|
|
uri->scheme = HTTP_SCHEME_HTTPS;
|
|
} else if (!strncasecmp(str, "unix://", 7)) {
|
|
uri->scheme = HTTP_SCHEME_UNIX;
|
|
} else {
|
|
return ERROR("URI: invalid scheme");
|
|
}
|
|
}
|
|
|
|
/* Check UF_HOST */
|
|
err = http_uri_parse_check_host(uri);
|
|
if (err != NULL) {
|
|
return err;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Un-escape host name in place
|
|
*/
|
|
static void
|
|
http_uri_unescape_host (char *host)
|
|
{
|
|
char *zone = strstr(host, "%25");
|
|
if (zone != NULL) {
|
|
memmove(zone + 1, zone + 3, strlen(zone + 3) + 1);
|
|
}
|
|
}
|
|
|
|
/* Parse URI address
|
|
*/
|
|
static void
|
|
http_uri_parse_addr (http_uri *uri)
|
|
{
|
|
http_uri_field field;
|
|
char *host = NULL, *port = NULL;
|
|
uint16_t portnum;
|
|
int rc;
|
|
|
|
/* Reset address */
|
|
memset(&uri->addr, 0, sizeof(uri->addr));
|
|
|
|
/* Get host and port */
|
|
field = http_uri_field_get(uri, UF_HOST);
|
|
if (field.len != 0) {
|
|
host = alloca(field.len + 1);
|
|
memcpy(host, field.str, field.len);
|
|
host[field.len] = '\0';
|
|
http_uri_unescape_host(host);
|
|
}
|
|
|
|
field = http_uri_field_get(uri, UF_PORT);
|
|
if (field.len != 0) {
|
|
port = alloca(field.len + 1);
|
|
memcpy(port, field.str, field.len);
|
|
port[field.len] = '\0';
|
|
}
|
|
|
|
if (host == NULL) {
|
|
return;
|
|
}
|
|
|
|
/* Parse port number */
|
|
if (port != NULL) {
|
|
char *end;
|
|
unsigned long val = strtoul(port, &end, 10);
|
|
|
|
if (end == port || *end != '\0' || val > 0xffff) {
|
|
return;
|
|
}
|
|
|
|
portnum = htons((uint16_t) val);
|
|
} else {
|
|
switch (uri->scheme) {
|
|
case HTTP_SCHEME_HTTP:
|
|
portnum = htons(80);
|
|
break;
|
|
|
|
case HTTP_SCHEME_HTTPS:
|
|
portnum = htons(443);
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (strchr(host, ':') != NULL) {
|
|
struct in6_addr in6;
|
|
|
|
/* Strip zone suffix */
|
|
char *s = strchr(host, '%');
|
|
if (s != NULL) {
|
|
*s = '\0';
|
|
}
|
|
|
|
rc = inet_pton(AF_INET6, host, &in6);
|
|
if (rc != 1) {
|
|
return;
|
|
}
|
|
|
|
uri->addr.in6.sin6_family = AF_INET6;
|
|
uri->addr.in6.sin6_addr = in6;
|
|
uri->addr.in6.sin6_port = portnum;
|
|
} else {
|
|
struct in_addr in;
|
|
|
|
rc = inet_pton(AF_INET, host, &in);
|
|
if (rc != 1) {
|
|
return;
|
|
}
|
|
|
|
uri->addr.in.sin_family = AF_INET;
|
|
uri->addr.in.sin_addr = in;
|
|
uri->addr.in.sin_port = portnum;
|
|
}
|
|
}
|
|
|
|
/* Create new URI, by parsing URI string
|
|
*/
|
|
http_uri*
|
|
http_uri_new (const char *str, bool strip_fragment)
|
|
{
|
|
http_uri *uri = mem_new(http_uri, 1);
|
|
char *buf;
|
|
http_uri_field field;
|
|
|
|
/* Parse URI */
|
|
if (http_uri_parse(uri, str) != NULL) {
|
|
goto FAIL;
|
|
}
|
|
|
|
/* Don't allow relative URLs here */
|
|
if (uri->scheme == HTTP_SCHEME_UNSET) {
|
|
goto FAIL;
|
|
}
|
|
|
|
uri->str = buf = str_dup(str);
|
|
|
|
/* Honor strip_fragment flag */
|
|
if (strip_fragment && http_uri_field_present(uri, UF_FRAGMENT)) {
|
|
buf[uri->parsed.field_data[UF_FRAGMENT].off - 1] = '\0';
|
|
uri->parsed.field_set &= ~(1 << UF_FRAGMENT);
|
|
uri->parsed.field_data[UF_FRAGMENT].off = 0;
|
|
uri->parsed.field_data[UF_FRAGMENT].len = 0;
|
|
}
|
|
|
|
/* Prepare addr, path, etc */
|
|
http_uri_parse_addr(uri);
|
|
uri->path = http_uri_field_strdup(uri, UF_PATH);
|
|
|
|
field = http_uri_field_get(uri, UF_HOST);
|
|
if (memchr(field.str, ':', field.len) != NULL) {
|
|
char *host = mem_resize((char*) NULL, field.len + 2, 1);
|
|
|
|
host[0] = '[';
|
|
memcpy(host + 1, field.str, field.len);
|
|
host[field.len + 1] = ']';
|
|
host[field.len + 2] = '\0';
|
|
uri->host = host;
|
|
} else {
|
|
uri->host = http_uri_field_strdup(uri, UF_HOST);
|
|
}
|
|
|
|
return uri;
|
|
|
|
/* Error: cleanup and exit */
|
|
FAIL:
|
|
mem_free(uri);
|
|
return NULL;
|
|
}
|
|
|
|
/* Clone the URI
|
|
*/
|
|
http_uri*
|
|
http_uri_clone (const http_uri *old)
|
|
{
|
|
http_uri *uri = mem_new(http_uri, 1);
|
|
|
|
*uri = *old;
|
|
uri->str = str_dup(uri->str);
|
|
uri->path = str_dup(uri->path);
|
|
uri->host = str_dup(uri->host);
|
|
|
|
return uri;
|
|
}
|
|
|
|
/* Check that string, defined by begin and end pointers,
|
|
* has a specified prefix
|
|
*/
|
|
static bool
|
|
http_uri_str_prefix(const char *beg, const char *end, const char *pfx)
|
|
{
|
|
size_t len = end - beg;
|
|
size_t pfx_len = strlen(pfx);
|
|
|
|
return len >= pfx_len && !memcmp(beg, pfx, pfx_len);
|
|
}
|
|
|
|
/* Check that string, defined by begin and end pointers,
|
|
* equal to the specified pattern
|
|
*/
|
|
static bool
|
|
http_uri_str_equal(const char *beg, const char *end, const char *pattern)
|
|
{
|
|
size_t len = end - beg;
|
|
size_t plen = strlen(pattern);
|
|
|
|
return len == plen && !memcmp(beg, pattern, len);
|
|
}
|
|
|
|
/* Find 1st occurrence of the character in the string,
|
|
* defined by begin and end pointers
|
|
*/
|
|
static char *
|
|
http_uri_str_chr(const char *beg, const char *end, char c)
|
|
{
|
|
return memchr(beg, c, end - beg);
|
|
}
|
|
|
|
/* Find last occurrence of the character in the string,
|
|
* defined by begin and end pointers
|
|
*/
|
|
static char *
|
|
http_uri_str_rchr(const char *beg, const char *end, char c)
|
|
{
|
|
return memrchr(beg, c, end - beg);
|
|
}
|
|
|
|
/* Remove last path segment. Returns pointer to the
|
|
* path end
|
|
*/
|
|
static char*
|
|
http_uri_remove_last_segment (char *path, char *end)
|
|
{
|
|
char *s = http_uri_str_rchr(path, end, '/');
|
|
return s == NULL ? end : s;
|
|
}
|
|
|
|
/* Remove path dot segments, per rfc3986, 5.2.4.
|
|
*/
|
|
static char*
|
|
http_uri_remove_dot_segments (char *path, char *end)
|
|
{
|
|
char *input = path;
|
|
char *path_end = path;
|
|
|
|
while (input != end) {
|
|
/* A. If the input buffer begins with a prefix of "../" or "./",
|
|
* then remove that prefix from the input buffer; otherwise,
|
|
*/
|
|
if (http_uri_str_prefix(input, end, "../")) {
|
|
input += 3;
|
|
} else if (http_uri_str_prefix(input, end, "./")) {
|
|
input += 2;
|
|
/* B. if the input buffer begins with a prefix of "/./" or "/.",
|
|
* where "." is a complete path segment, then replace that
|
|
* prefix with "/" in the input buffer; otherwise,
|
|
*/
|
|
} else if (http_uri_str_prefix(input, end, "/./")) {
|
|
input += 2;
|
|
} else if (http_uri_str_equal(input, end, "/.")) {
|
|
input ++;
|
|
input[0] = '/';
|
|
/* C. if the input buffer begins with a prefix of "/../" or "/..",
|
|
* where ".." is a complete path segment, then replace that
|
|
* prefix with "/" in the input buffer and remove the last
|
|
* segment and its preceding "/" (if any) from the output
|
|
* buffer; otherwise,
|
|
*/
|
|
} else if (http_uri_str_prefix(input, end, "/../")) {
|
|
path_end = http_uri_remove_last_segment(path, path_end);
|
|
input += 3;
|
|
} else if (http_uri_str_equal(input, end, "/..")) {
|
|
path_end = http_uri_remove_last_segment(path, path_end);
|
|
input += 2;
|
|
input[0] = '/';
|
|
/* D. if the input buffer consists only of "." or "..", then remove
|
|
* that from the input buffer; otherwise,
|
|
*/
|
|
} else if (http_uri_str_equal(input, end, ".") ||
|
|
http_uri_str_equal(input, end, "..")) {
|
|
input = end;
|
|
/* E. move the first path segment in the input buffer to the end of
|
|
* the output buffer, including the initial "/" character (if
|
|
* any) and any subsequent characters up to, but not including,
|
|
* the next "/" character or the end of the input buffer.
|
|
*/
|
|
} else {
|
|
char *s = http_uri_str_chr(input + 1, end, '/');
|
|
size_t sz = s ? s - input : end - input;
|
|
|
|
memmove(path_end, input, sz);
|
|
path_end += sz;
|
|
input += sz;
|
|
}
|
|
}
|
|
|
|
return path_end;
|
|
}
|
|
|
|
/* Create URI, relative to base URI. If `path_only' is
|
|
* true, scheme, host and port are taken from the
|
|
* base URI
|
|
*/
|
|
http_uri*
|
|
http_uri_new_relative (const http_uri *base, const char *path,
|
|
bool strip_fragment, bool path_only)
|
|
{
|
|
char *buf = alloca(strlen(base->str) + strlen(path) + 1);
|
|
char *end = buf;
|
|
http_uri ref;
|
|
const http_uri *uri;
|
|
http_uri_field field;
|
|
char *path_beg;
|
|
|
|
if (http_uri_parse(&ref, path) != NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
/* If the base URI uses the unix:// scheme, don't allow a relative URI to
|
|
* change the scheme or host.
|
|
*/
|
|
if (base->scheme == HTTP_SCHEME_UNIX) {
|
|
path_only = true;
|
|
}
|
|
|
|
/* Set schema, userinfo, host and port */
|
|
if (path_only || !http_uri_field_present(&ref, UF_SCHEMA)) {
|
|
end = http_uri_field_copy(base, UF_SCHEMA, end);
|
|
} else {
|
|
end = http_uri_field_copy(&ref, UF_SCHEMA, end);
|
|
}
|
|
|
|
end = http_uri_field_append(http_uri_field_make("://"), end);
|
|
|
|
if (path_only || !http_uri_field_present(&ref, UF_HOST)) {
|
|
uri = base;
|
|
} else {
|
|
uri = &ref;
|
|
}
|
|
|
|
if (http_uri_field_present(uri, UF_USERINFO)) {
|
|
end = http_uri_field_copy(uri, UF_USERINFO, end);
|
|
end = http_uri_field_append(http_uri_field_make("@"), end);
|
|
}
|
|
|
|
field = http_uri_field_get(uri, UF_HOST);
|
|
if (memchr(field.str, ':', field.len) != NULL) {
|
|
*end ++ = '[';
|
|
end = http_uri_field_append(field, end);
|
|
*end ++ = ']';
|
|
} else {
|
|
end = http_uri_field_append(field, end);
|
|
}
|
|
|
|
if (http_uri_field_present(uri, UF_PORT)) {
|
|
end = http_uri_field_append(http_uri_field_make(":"), end);
|
|
end = http_uri_field_copy(uri, UF_PORT, end);
|
|
}
|
|
|
|
/* Set path */
|
|
path_beg = end;
|
|
if (!http_uri_field_nonempty(&ref, UF_PATH)) {
|
|
end = http_uri_field_copy(base, UF_PATH, end);
|
|
} else {
|
|
if (http_uri_field_begin(&ref, UF_PATH) != '/') {
|
|
end = http_uri_field_copy_basepath(base, end);
|
|
}
|
|
end = http_uri_field_copy(&ref, UF_PATH, end);
|
|
}
|
|
|
|
end = http_uri_remove_dot_segments(path_beg, end);
|
|
|
|
/* Query and fragment */
|
|
if (http_uri_field_present(&ref, UF_QUERY)) {
|
|
end = http_uri_field_append(http_uri_field_make("?"), end);
|
|
end = http_uri_field_copy(&ref, UF_QUERY, end);
|
|
}
|
|
|
|
if (!strip_fragment && http_uri_field_present(&ref, UF_FRAGMENT)) {
|
|
end = http_uri_field_append(http_uri_field_make("#"), end);
|
|
end = http_uri_field_copy(&ref, UF_FRAGMENT, end);
|
|
}
|
|
|
|
*end = '\0';
|
|
|
|
return http_uri_new(buf, false);
|
|
}
|
|
|
|
/* Free the URI
|
|
*/
|
|
#ifndef __clang_analyzer__
|
|
void
|
|
http_uri_free (http_uri *uri)
|
|
{
|
|
if (uri != NULL) {
|
|
mem_free((char*) uri->str);
|
|
mem_free((char*) uri->path);
|
|
mem_free((char*) uri->host);
|
|
mem_free(uri);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Get URI string
|
|
*/
|
|
const char*
|
|
http_uri_str (http_uri *uri)
|
|
{
|
|
return uri->str;
|
|
}
|
|
|
|
/* Get URI's host address. If Host address is not literal, returns NULL
|
|
*/
|
|
const struct sockaddr*
|
|
http_uri_addr (const http_uri *uri)
|
|
{
|
|
if (uri->addr.sockaddr.sa_family == AF_UNSPEC) {
|
|
return NULL;
|
|
}
|
|
|
|
return &uri->addr.sockaddr;
|
|
}
|
|
|
|
/* Get URI path
|
|
*
|
|
* Note, if URL has empty path (i.e., "http://1.2.3.4"), the
|
|
* empty string will be returned
|
|
*/
|
|
const char*
|
|
http_uri_get_path (const http_uri *uri)
|
|
{
|
|
return uri->path;
|
|
}
|
|
|
|
/* Set URI path
|
|
*/
|
|
void
|
|
http_uri_set_path (http_uri *uri, const char *path)
|
|
{
|
|
http_uri_field_replace(uri, UF_PATH, path);
|
|
}
|
|
|
|
/* Get URI host. It returns only host name, port number is
|
|
* not included.
|
|
*
|
|
* IPv6 literal addresses are returned in square brackets
|
|
* (i.e., [fe80::217:c8ff:fe7b:6a91%4])
|
|
*
|
|
* Note, the subsequent modifications of URI, such as http_uri_fix_host(),
|
|
* http_uri_fix_ipv6_zone() etc, may make the returned string invalid,
|
|
* so if you need to keep it for a long time, better make a copy
|
|
*/
|
|
const char*
|
|
http_uri_get_host (const http_uri *uri)
|
|
{
|
|
return uri->host;
|
|
}
|
|
|
|
/* http_uri_host_is checks if URI's host name is equal to the
|
|
* specified string.
|
|
*
|
|
* It does its best to compare domain names correctly, taking
|
|
* in account only significant difference (for example, the difference
|
|
* in upper/lower case * in domain names is not significant).
|
|
*/
|
|
bool
|
|
http_uri_host_is (const http_uri *uri, const char *host)
|
|
{
|
|
const char *uri_host = http_uri_get_host(uri);
|
|
|
|
if (http_uri_host_is_literal(uri)) {
|
|
return !strcasecmp(uri_host, host);
|
|
}
|
|
|
|
if (!avahi_is_valid_domain_name(uri_host)) {
|
|
return false;
|
|
}
|
|
|
|
if (!avahi_is_valid_domain_name(host)) {
|
|
return false;
|
|
}
|
|
|
|
return avahi_domain_equal(uri_host, host);
|
|
}
|
|
|
|
/* http_uri_host_is_literal returns true if URI uses literal
|
|
* IP address
|
|
*/
|
|
bool
|
|
http_uri_host_is_literal (const http_uri *uri)
|
|
{
|
|
return http_uri_addr(uri) != NULL;
|
|
}
|
|
|
|
/* Set URI host into the literal IP address.
|
|
*/
|
|
void
|
|
http_uri_set_host_addr (http_uri *uri, ip_addr addr)
|
|
{
|
|
ip_straddr straddr = ip_addr_to_straddr(addr, true);
|
|
char *host = straddr.text;
|
|
char *s;
|
|
|
|
/* Remove square brackets around IPv6 address */
|
|
if (host[0] == '[' && host[strlen(host) - 1] == ']') {
|
|
host[strlen(host) - 1] = '\0';
|
|
host ++;
|
|
}
|
|
|
|
/* Escape % character (zone suffix delimiter) */
|
|
s = strchr(host, '%');
|
|
if (s != NULL) {
|
|
/* Note, we replace '%' with '%25' plus don't forget
|
|
* the terminating '\0'. Hence strlen(host) + 3
|
|
*/
|
|
char *host2 = alloca(strlen(host) + 3);
|
|
size_t sz = s - host;
|
|
|
|
memcpy(host2, host, sz);
|
|
memcpy(host2 + sz, "%25", 3);
|
|
strcpy(host2 + sz + 3, s + 1);
|
|
host = host2;
|
|
}
|
|
|
|
/* Set the host */
|
|
http_uri_field_replace(uri, UF_HOST, host);
|
|
}
|
|
|
|
/* Fix URI host: if `match` is NULL or uri's host matches `match`,
|
|
* replace uri's host and port with values taken from the base_uri
|
|
*/
|
|
void
|
|
http_uri_fix_host (http_uri *uri, const http_uri *base_uri, const char *match)
|
|
{
|
|
http_uri_field schema, host, port;
|
|
|
|
if (match != NULL) {
|
|
host = http_uri_field_get(uri, UF_HOST);
|
|
if (strncasecmp(host.str, match, host.len)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
schema = http_uri_field_get(base_uri, UF_SCHEMA);
|
|
host = http_uri_field_get(base_uri, UF_HOST);
|
|
port = http_uri_field_get(base_uri, UF_PORT);
|
|
|
|
http_uri_field_replace_len(uri, UF_SCHEMA, schema.str, schema.len);
|
|
http_uri_field_replace_len(uri, UF_HOST, host.str, host.len);
|
|
http_uri_field_replace_len(uri, UF_PORT, port.str, port.len);
|
|
}
|
|
|
|
/* Fix IPv6 address zone suffix
|
|
*/
|
|
void
|
|
http_uri_fix_ipv6_zone (http_uri *uri, int ifindex)
|
|
{
|
|
http_uri_field field;
|
|
char *host;
|
|
|
|
/* Check if we need to change something */
|
|
if (uri->addr.sockaddr.sa_family != AF_INET6) {
|
|
return; /* Not IPv6 */
|
|
}
|
|
|
|
if (!ip_is_linklocal(AF_INET6, &uri->addr.in6.sin6_addr)) {
|
|
return; /* Not link-local */
|
|
}
|
|
|
|
field = http_uri_field_get(uri, UF_HOST);
|
|
if (memchr(field.str, '%', field.len)) {
|
|
return; /* Already has zone suffix */
|
|
}
|
|
|
|
/* Obtain writable copy of host name */
|
|
host = alloca(field.len + 64);
|
|
memcpy(host, field.str, field.len);
|
|
|
|
/* Append zone suffix */
|
|
sprintf(host + field.len, "%%25%d", ifindex);
|
|
|
|
/* Update URL's host */
|
|
http_uri_field_replace(uri, UF_HOST, host);
|
|
uri->addr.in6.sin6_scope_id = ifindex;
|
|
}
|
|
|
|
/* Strip zone suffix from literal IPv6 host address
|
|
*
|
|
* If address is not IPv6 or doesn't have zone suffix, it is
|
|
* not changed
|
|
*/
|
|
void
|
|
http_uri_strip_zone_suffux (http_uri *uri)
|
|
{
|
|
http_uri_field field;
|
|
const char *suffix;
|
|
size_t len;
|
|
char *host;
|
|
|
|
/* Check if we need to change something */
|
|
if (uri->addr.sockaddr.sa_family != AF_INET6) {
|
|
return; /* Not IPv6 */
|
|
}
|
|
|
|
field = http_uri_field_get(uri, UF_HOST);
|
|
suffix = memchr(field.str, '%', field.len);
|
|
if (suffix == NULL) {
|
|
return; /* No zone suffix */
|
|
}
|
|
|
|
len = suffix - field.str;
|
|
|
|
/* Update host */
|
|
host = alloca(len + 1);
|
|
memcpy(host, field.str, len);
|
|
host[len] = '\0';
|
|
|
|
http_uri_field_replace(uri, UF_HOST, host);
|
|
uri->addr.in6.sin6_scope_id = 0;
|
|
}
|
|
|
|
/* Make sure URI's path ends with the slash character
|
|
*/
|
|
void
|
|
http_uri_fix_end_slash (http_uri *uri)
|
|
{
|
|
const char *path = http_uri_get_path(uri);
|
|
if (!str_has_suffix(path, "/")) {
|
|
size_t len = strlen(path);
|
|
char *path2 = alloca(len + 2);
|
|
memcpy(path2, path, len);
|
|
path2[len] = '/';
|
|
path2[len+1] = '\0';
|
|
http_uri_set_path(uri, path2);
|
|
}
|
|
}
|
|
|
|
/* Check if 2 URIs are equal
|
|
*/
|
|
bool
|
|
http_uri_equal (const http_uri *uri1, const http_uri *uri2)
|
|
{
|
|
return uri1->scheme == uri2->scheme &&
|
|
http_uri_field_equal(uri1, uri2, UF_HOST, true) &&
|
|
http_uri_field_equal(uri1, uri2, UF_PORT, true) &&
|
|
http_uri_field_equal(uri1, uri2, UF_PATH, false) &&
|
|
http_uri_field_equal(uri1, uri2, UF_QUERY, false) &&
|
|
http_uri_field_equal(uri1, uri2, UF_FRAGMENT, false) &&
|
|
http_uri_field_equal(uri1, uri2, UF_USERINFO, false);
|
|
}
|
|
|
|
/******************** HTTP header ********************/
|
|
/* http_hdr represents HTTP header
|
|
*/
|
|
typedef struct {
|
|
ll_head fields; /* List of http_hdr_field */
|
|
} http_hdr;
|
|
|
|
/* http_hdr_field represents a single HTTP header field
|
|
*/
|
|
typedef struct {
|
|
char *name; /* Header name */
|
|
char *value; /* Header value, may be NULL */
|
|
ll_node chain; /* In http_hdr::fields */
|
|
} http_hdr_field;
|
|
|
|
/* Create http_hdr_field. Name can be NULL
|
|
*/
|
|
static http_hdr_field*
|
|
http_hdr_field_new (const char *name)
|
|
{
|
|
http_hdr_field *field = mem_new(http_hdr_field, 1);
|
|
field->name = name ? str_dup(name) : str_new();
|
|
return field;
|
|
}
|
|
|
|
/* Destroy http_hdr_field
|
|
*/
|
|
static void
|
|
http_hdr_field_free (http_hdr_field *field)
|
|
{
|
|
mem_free(field->name);
|
|
mem_free(field->value);
|
|
mem_free(field);
|
|
}
|
|
|
|
/* Initialize http_hdr in place
|
|
*/
|
|
static void
|
|
http_hdr_init (http_hdr *hdr)
|
|
{
|
|
ll_init(&hdr->fields);
|
|
}
|
|
|
|
/* Cleanup http_hdr in place
|
|
*/
|
|
static void
|
|
http_hdr_cleanup (http_hdr *hdr)
|
|
{
|
|
ll_node *node;
|
|
|
|
while ((node = ll_pop_beg(&hdr->fields)) != NULL) {
|
|
http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
http_hdr_field_free(field);
|
|
}
|
|
}
|
|
|
|
/* Write header to string buffer in wire format
|
|
*/
|
|
static char*
|
|
http_hdr_write (const http_hdr *hdr, char *out)
|
|
{
|
|
ll_node *node;
|
|
|
|
for (LL_FOR_EACH(node, &hdr->fields)) {
|
|
http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
out = str_append(out, field->name);
|
|
out = str_append(out, ": ");
|
|
out = str_append(out, field->value);
|
|
out = str_append(out, "\r\n");
|
|
}
|
|
|
|
return str_append(out, "\r\n");
|
|
}
|
|
|
|
/* Lookup field in the header
|
|
*/
|
|
static http_hdr_field*
|
|
http_hdr_lookup (const http_hdr *hdr, const char *name)
|
|
{
|
|
ll_node *node;
|
|
|
|
for (LL_FOR_EACH(node, &hdr->fields)) {
|
|
http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
if (!strcasecmp(field->name, name)) {
|
|
return field;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Get header field
|
|
*/
|
|
static const char*
|
|
http_hdr_get (const http_hdr *hdr, const char *name)
|
|
{
|
|
http_hdr_field *field = http_hdr_lookup(hdr, name);
|
|
|
|
if (field == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
if (field->value == NULL) {
|
|
return "";
|
|
}
|
|
|
|
return field->value;
|
|
}
|
|
|
|
/* Set header field
|
|
*/
|
|
static void
|
|
http_hdr_set (http_hdr *hdr, const char *name, const char *value)
|
|
{
|
|
http_hdr_field *field = http_hdr_lookup(hdr, name);
|
|
|
|
if (field == NULL) {
|
|
field = http_hdr_field_new(name);
|
|
ll_push_end(&hdr->fields, &field->chain);
|
|
}
|
|
|
|
if (field->value == NULL) {
|
|
field->value = str_dup(value);
|
|
} else {
|
|
field->value = str_assign(field->value, value);
|
|
}
|
|
}
|
|
|
|
/* Del header field
|
|
*/
|
|
static void
|
|
http_hdr_del (http_hdr *hdr, const char *name)
|
|
{
|
|
http_hdr_field *field = http_hdr_lookup(hdr, name);
|
|
|
|
if (field != NULL) {
|
|
ll_del(&field->chain);
|
|
http_hdr_field_free(field);
|
|
}
|
|
}
|
|
|
|
/* Handle HTTP parser on_header_field callback
|
|
* parser->data must point to the header being parsed
|
|
*/
|
|
static int
|
|
http_hdr_on_header_field (http_parser *parser,
|
|
const char *data, size_t size)
|
|
{
|
|
http_hdr *hdr = parser->data;
|
|
ll_node *node;
|
|
http_hdr_field *field = NULL;
|
|
|
|
/* Get last field */
|
|
node = ll_last(&hdr->fields);
|
|
if (node != NULL) {
|
|
field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
}
|
|
|
|
/* If there is no last field, or last field already
|
|
* has value, create a new field
|
|
*/
|
|
if (field == NULL || field->value != NULL) {
|
|
field = http_hdr_field_new(NULL);
|
|
ll_push_end(&hdr->fields, &field->chain);
|
|
}
|
|
|
|
/* Append data to the field name */
|
|
field->name = str_append_mem(field->name, data, size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Handle HTTP parser on_header_value callback
|
|
* parser->data must point to the header being parsed
|
|
*/
|
|
static int
|
|
http_hdr_on_header_value (http_parser *parser,
|
|
const char *data, size_t size)
|
|
{
|
|
http_hdr *hdr = parser->data;
|
|
ll_node *node;
|
|
http_hdr_field *field = NULL;
|
|
|
|
/* Get last field */
|
|
node = ll_last(&hdr->fields);
|
|
if (node != NULL) {
|
|
field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
}
|
|
|
|
/* If there is no last field, just ignore the data.
|
|
* Note, it actually should not happen
|
|
*/
|
|
if (field == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
/* Append data to field value */
|
|
if (field->value == NULL) {
|
|
field->value = str_new();
|
|
}
|
|
|
|
field->value = str_append_mem(field->value, data, size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Handle HTTP parser on_headers_complete callback
|
|
* This is used http_hdr_parse only and returns 1 to
|
|
* tell parser to don't attempt to parse message body
|
|
*/
|
|
static int
|
|
http_hdr_on_headers_complete (http_parser *parser)
|
|
{
|
|
(void) parser;
|
|
return 1;
|
|
}
|
|
|
|
|
|
/* Parse http_hdr from memory buffer
|
|
*
|
|
* If `skip_line' is true, first line is skipped, which
|
|
* is useful if first line contains HTTP request/status
|
|
* or a multipart boundary
|
|
*/
|
|
static error
|
|
http_hdr_parse (http_hdr *hdr, const char *data, size_t size, bool skip_line)
|
|
{
|
|
static http_parser_settings callbacks = {
|
|
.on_header_field = http_hdr_on_header_field,
|
|
.on_header_value = http_hdr_on_header_value,
|
|
.on_headers_complete = http_hdr_on_headers_complete
|
|
};
|
|
http_parser parser = {0};
|
|
static char prefix[] = "HTTP/1.1 200 OK\r\n";
|
|
|
|
/* Skip first line, if requested */
|
|
if (skip_line) {
|
|
const char *s = memchr(data, '\n', size);
|
|
if (s) {
|
|
size_t skip = (s - data) + 1;
|
|
data += skip;
|
|
size -= skip;
|
|
}
|
|
}
|
|
|
|
/* Initialize HTTP parser */
|
|
http_parser_init(&parser, HTTP_RESPONSE);
|
|
parser.data = hdr;
|
|
|
|
/* Note, http_parser unable to parse bare HTTP
|
|
* header, without request or status line, so
|
|
* we insert fake status line to make it happy
|
|
*/
|
|
http_parser_execute(&parser, &callbacks, prefix, sizeof(prefix) - 1);
|
|
http_parser_execute(&parser, &callbacks, data, size);
|
|
|
|
if (parser.http_errno != HPE_OK) {
|
|
return ERROR(http_errno_description(parser.http_errno));
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Check if character is special, for http_hdr_params_parse
|
|
*/
|
|
static bool
|
|
http_hdr_params_chr_isspec (char c)
|
|
{
|
|
if (0x20 < c && c < 0x7f) {
|
|
switch (c) {
|
|
case '(': case ')': case '<': case '>':
|
|
case '@': case ',': case ';': case ':':
|
|
case '\\': case '\"':
|
|
case '/': case '[': case ']': case '?':
|
|
case '=':
|
|
return true;
|
|
|
|
default:
|
|
return safe_isspace(c);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Check if character is space, for http_hdr_params_parse
|
|
*/
|
|
static bool
|
|
http_hdr_params_chr_isspace (char c)
|
|
{
|
|
switch ((unsigned char) c) {
|
|
case '\t': case '\n': case '\v': case '\f':
|
|
case '\r': case ' ': case 0x85: case 0xA0:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Parse HTTP header field with parameters.
|
|
* Result is saved into `params' as collection of name/value fields
|
|
*/
|
|
static error
|
|
http_hdr_params_parse (http_hdr *params, const char *name, const char *in)
|
|
{
|
|
enum {
|
|
/* params ::= param [';' params]
|
|
* param ::= SP1 NAME SP2 '=' SP3 value SP4
|
|
* value ::= TOKEN | STRING
|
|
*/
|
|
SP1, NAME, SP2, EQ, SP3, VALUE, TOKEN, STRING, STRING_BSLASH, SP4, END
|
|
} state = SP1;
|
|
char c;
|
|
http_hdr_field *field = NULL;
|
|
|
|
/* Parameters begin after ';' */
|
|
in = strchr(in, ';');
|
|
if (in == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
in ++;
|
|
|
|
while ((c = *in) != '\0') {
|
|
switch (state) {
|
|
case SP1: case SP2: case SP3: case SP4:
|
|
if (!http_hdr_params_chr_isspace(c)) {
|
|
state ++;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case NAME:
|
|
if (!http_hdr_params_chr_isspec(c)) {
|
|
if (field == NULL) {
|
|
field = http_hdr_field_new(NULL);
|
|
field->value = str_new();
|
|
ll_push_end(¶ms->fields, &field->chain);
|
|
}
|
|
field->name = str_append_c(field->name, c);
|
|
} else if (c == ';') {
|
|
state = SP1;
|
|
field = NULL;
|
|
} else {
|
|
state = SP2;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case EQ:
|
|
if (c == '=') {
|
|
state = SP3;
|
|
} else if (c == ';') {
|
|
state = SP1;
|
|
field = NULL;
|
|
} else {
|
|
return eloop_eprintf(
|
|
"http %s: expected '=' or ';', got: %s", name, in);
|
|
}
|
|
break;
|
|
|
|
case VALUE:
|
|
if (c == '"') {
|
|
state = STRING;
|
|
} else {
|
|
state = TOKEN;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case STRING:
|
|
if (c == '\\') {
|
|
state = STRING_BSLASH;
|
|
} else if (c == '"') {
|
|
state = SP4;
|
|
} else {
|
|
field->value = str_append_c(field->value, c);
|
|
}
|
|
break;
|
|
|
|
case STRING_BSLASH:
|
|
field->value = str_append_c(field->value, c);
|
|
state = STRING;
|
|
break;
|
|
|
|
case TOKEN:
|
|
if (c != ';' && c != '"' && !safe_isspace(c)) {
|
|
field->value = str_append_c(field->value, c);
|
|
} else {
|
|
state = SP4;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case END:
|
|
if (c != ';') {
|
|
return eloop_eprintf(
|
|
"http %s: expected ';', got: %s", name, in);
|
|
}
|
|
|
|
state = SP1;
|
|
field = NULL;
|
|
break;
|
|
}
|
|
|
|
in ++;
|
|
}
|
|
|
|
if (state == STRING || state == STRING_BSLASH) {
|
|
return eloop_eprintf( "http %s: unterminated quoted string");
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Call callback for each header field
|
|
*/
|
|
static void
|
|
hdr_for_each (const http_hdr *hdr,
|
|
void (*callback)(const char *name, const char *value, void *ptr),
|
|
void *ptr)
|
|
{
|
|
ll_node *node;
|
|
|
|
for (LL_FOR_EACH(node, &hdr->fields)) {
|
|
http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
|
|
if (field->value != NULL) {
|
|
callback(field->name, field->value, ptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
/******************** HTTP multipart ********************/
|
|
/* http_multipart represents a decoded multipart message
|
|
*/
|
|
struct http_multipart {
|
|
int count; /* Count of bodies */
|
|
http_data *data; /* Response data */
|
|
http_data **bodies; /* Multipart bodies, var-size */
|
|
};
|
|
|
|
/* Add multipart body
|
|
*/
|
|
static void
|
|
http_multipart_add_body (http_multipart *mp, http_data *body)
|
|
{
|
|
mp->bodies = mem_resize(mp->bodies, mp->count + 1, 0);
|
|
mp->bodies[mp->count ++] = body;
|
|
}
|
|
|
|
/* Find boundary within the multipart message data
|
|
*/
|
|
static const char*
|
|
http_multipart_find_boundary (const char *boundary, size_t boundary_len,
|
|
const char *data, size_t size) {
|
|
|
|
/* Note, per RFC 2046, "the boundary delimiter MUST occur at the beginning
|
|
* of a line, i.e., following a CRLF, and the initial CRLF is considered to
|
|
* be attached to the boundary delimiter line rather than part of the
|
|
* preceding part".
|
|
*
|
|
* However, Xerox WorkCentre 3025 violates this requirement, and
|
|
* puts boundary delimiter without preceding CRLF, so we must relax
|
|
* out expectations
|
|
*/
|
|
return memmem(data, size, boundary, boundary_len);
|
|
}
|
|
|
|
/* Adjust part of multipart message:
|
|
* 1) skip header
|
|
* 2) fetch content type
|
|
*/
|
|
static error
|
|
http_multipart_adjust_part (http_data *part)
|
|
{
|
|
const char *split;
|
|
http_hdr hdr;
|
|
size_t hdr_len;
|
|
error err;
|
|
|
|
/* Locate end of headers */
|
|
split = memmem(part->bytes, part->size, "\r\n\r\n", 4);
|
|
if (split == NULL) {
|
|
return ERROR("http multipart: can't locate end of part headers");
|
|
}
|
|
|
|
/* Parse headers and obtain content-type */
|
|
http_hdr_init(&hdr);
|
|
hdr_len = 4 + split - (char*) part->bytes;
|
|
err = http_hdr_parse(&hdr, part->bytes, hdr_len - 2, true);
|
|
|
|
if (err == NULL) {
|
|
const char *ct = http_hdr_get(&hdr, "Content-Type");
|
|
http_data_set_content_type(part, ct);
|
|
}
|
|
|
|
http_hdr_cleanup(&hdr);
|
|
if (err != NULL) {
|
|
return eloop_eprintf("http multipart: %s", ESTRING(err));
|
|
}
|
|
|
|
/* Cut of header */
|
|
split += 4;
|
|
part->size -= (split - (char*) part->bytes);
|
|
part->bytes = split;
|
|
|
|
/* Strip CR/LF preceding next boundary, if any */
|
|
if (split[part->size - 2] == '\r' && split[part->size - 1] == '\n') {
|
|
part->size -= 2;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Free http_multipart
|
|
*/
|
|
static void
|
|
http_multipart_free (http_multipart *mp)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < mp->count; i ++) {
|
|
http_data_unref(mp->bodies[i]);
|
|
}
|
|
|
|
mem_free(mp);
|
|
}
|
|
|
|
/* Parse MIME multipart message body
|
|
* Saves result into `out'. Result may be NULL, if no multipart
|
|
*/
|
|
static error
|
|
http_multipart_parse (http_multipart **out, log_ctx *log,
|
|
http_data *data, const char *content_type)
|
|
{
|
|
http_multipart *mp;
|
|
http_hdr params;
|
|
const char *boundary;
|
|
size_t boundary_len = 0;
|
|
const char *data_beg, *data_end, *data_prev;
|
|
error err;
|
|
ll_node *node;
|
|
|
|
/* Check MIME type */
|
|
*out = NULL;
|
|
if (strncmp(data->content_type, "multipart/", 10)) {
|
|
return NULL;
|
|
}
|
|
|
|
/* Obtain boundary */
|
|
http_hdr_init(¶ms);
|
|
err = http_hdr_params_parse(¶ms, "Content-Type", content_type);
|
|
if (err != NULL) {
|
|
http_hdr_cleanup(¶ms);
|
|
return err;
|
|
}
|
|
|
|
log_debug(log, "http multipart parameters:");
|
|
for (LL_FOR_EACH(node, ¶ms.fields)) {
|
|
http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain);
|
|
log_debug(log, " %s=\"%s\"", field->name, field->value);
|
|
}
|
|
|
|
boundary = http_hdr_get(¶ms, "boundary");
|
|
if (boundary) {
|
|
char *s;
|
|
|
|
boundary_len = strlen(boundary) + 2;
|
|
s = alloca(boundary_len + 1);
|
|
|
|
s[0] = '-';
|
|
s[1] = '-';
|
|
strcpy(s + 2, boundary);
|
|
boundary = s;
|
|
}
|
|
http_hdr_cleanup(¶ms);
|
|
|
|
if (!boundary) {
|
|
return ERROR("http multipart: missed boundary parameter");
|
|
}
|
|
|
|
/* Create http_multipart structure */
|
|
mp = mem_new(http_multipart, 1);
|
|
mp->data = http_data_ref(data);
|
|
|
|
/* Split data into parts */
|
|
data_beg = data->bytes;
|
|
data_end = data_beg + data->size;
|
|
data_prev = NULL;
|
|
|
|
while (data_beg != data_end) {
|
|
const char *part = http_multipart_find_boundary(boundary, boundary_len,
|
|
data_beg, data_end - data_beg);
|
|
const char *next = data_end;
|
|
|
|
if (part != NULL) {
|
|
if (data_prev != NULL) {
|
|
http_data *body = http_data_new(data,
|
|
data_prev, part - data_prev);
|
|
http_multipart_add_body(mp, body);
|
|
|
|
err = http_multipart_adjust_part(body);
|
|
if (err != NULL) {
|
|
http_multipart_free(mp);
|
|
return err;
|
|
}
|
|
}
|
|
|
|
data_prev = part;
|
|
|
|
const char *tail = part + boundary_len;
|
|
if (data_end - tail >= 2 && tail[0] == '\r' && tail[1] == '\n') {
|
|
next = tail + 2;
|
|
}
|
|
}
|
|
|
|
data_beg = next;
|
|
}
|
|
|
|
if (mp->count == 0) {
|
|
http_multipart_free(mp);
|
|
return ERROR("http multipart: no parts found");
|
|
}
|
|
|
|
if (data_beg != data_end) {
|
|
log_debug(log,
|
|
"http multipart: found %d bytes of garbage at the end of message",
|
|
(int)(data_end - data_beg));
|
|
}
|
|
|
|
*out = mp;
|
|
return NULL;
|
|
}
|
|
|
|
/******************** HTTP data ********************/
|
|
/* http_data + SoupBuffer
|
|
*/
|
|
typedef struct {
|
|
http_data data; /* HTTP data */
|
|
volatile unsigned int refcnt; /* Reference counter */
|
|
http_data *parent; /* Parent data buffer */
|
|
} http_data_ex;
|
|
|
|
|
|
/* Create new http_data
|
|
*
|
|
* If parent != NULL, supplied bytes buffer must be owned by
|
|
* parent. Otherwise, newly created http_data takes ownership
|
|
* on the supplied data buffer
|
|
*/
|
|
static http_data*
|
|
http_data_new(http_data *parent, const char *bytes, size_t size)
|
|
{
|
|
http_data_ex *data_ex = mem_new(http_data_ex, 1);
|
|
|
|
if (parent != NULL) {
|
|
log_assert(NULL, bytes >= (char*) parent->bytes);
|
|
log_assert(NULL,
|
|
(bytes + size) <= ((char*) parent->bytes + parent->size));
|
|
}
|
|
|
|
data_ex->data.content_type = str_new();
|
|
data_ex->data.bytes = bytes;
|
|
data_ex->data.size = size;
|
|
|
|
data_ex->refcnt = 1;
|
|
data_ex->parent = parent ? http_data_ref(parent) : NULL;
|
|
|
|
return &data_ex->data;
|
|
}
|
|
|
|
/* Set Content-type
|
|
*/
|
|
static void
|
|
http_data_set_content_type (http_data *data, const char *content_type)
|
|
{
|
|
mem_free((char*) data->content_type);
|
|
|
|
if (content_type == NULL) {
|
|
content_type = str_dup("text/plain");
|
|
} else {
|
|
char *s;
|
|
|
|
content_type = str_dup_tolower(content_type);
|
|
s = strchr(content_type, ';');
|
|
if (s != NULL) {
|
|
*s = '\0';
|
|
}
|
|
}
|
|
|
|
data->content_type = content_type;
|
|
}
|
|
|
|
/* Dummy http_data in case no data is present
|
|
*/
|
|
static http_data
|
|
http_data_empty = {
|
|
.content_type = "",
|
|
.bytes = "",
|
|
.size = 0
|
|
};
|
|
|
|
|
|
/* Ref http_data
|
|
*/
|
|
http_data*
|
|
http_data_ref (http_data *data)
|
|
{
|
|
if (data != NULL && data != &http_data_empty) {
|
|
http_data_ex *data_ex = OUTER_STRUCT(data, http_data_ex, data);
|
|
__sync_fetch_and_add(&data_ex->refcnt, 1);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/* Unref http_data
|
|
*/
|
|
void
|
|
http_data_unref (http_data *data)
|
|
{
|
|
if (data != NULL && data != &http_data_empty) {
|
|
http_data_ex *data_ex = OUTER_STRUCT(data, http_data_ex, data);
|
|
|
|
if (__sync_fetch_and_sub(&data_ex->refcnt, 1) == 1) {
|
|
if (data_ex->parent != NULL) {
|
|
http_data_unref(data_ex->parent);
|
|
} else {
|
|
mem_free((void*) data_ex->data.bytes);
|
|
}
|
|
|
|
mem_free((char*) data_ex->data.content_type);
|
|
mem_free(data_ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Append bytes to data. http_data must be owner of its
|
|
* own buffer, i.e. it must have no parent
|
|
*
|
|
* Returns true on success, false on OOM
|
|
*/
|
|
static bool
|
|
http_data_append (http_data *data, const char *bytes, size_t size)
|
|
{
|
|
http_data_ex *data_ex = OUTER_STRUCT(data, http_data_ex, data);
|
|
void *p;
|
|
|
|
log_assert(NULL, data_ex->parent == NULL);
|
|
|
|
p = mem_try_resize((char*) data->bytes, data->size + size, 0);
|
|
if (p == NULL) {
|
|
return false;
|
|
}
|
|
|
|
data->bytes = p;
|
|
memcpy((char*) data->bytes + data->size, bytes, size);
|
|
data->size += size;
|
|
|
|
return true;
|
|
}
|
|
|
|
/******************** HTTP data queue ********************/
|
|
/* http_data_queue represents a queue of http_data items
|
|
*/
|
|
struct http_data_queue {
|
|
http_data **items; /* Array of http_data items */
|
|
};
|
|
|
|
/* Create new http_data_queue
|
|
*/
|
|
http_data_queue*
|
|
http_data_queue_new (void)
|
|
{
|
|
http_data_queue *queue = mem_new(http_data_queue, 1);
|
|
queue->items = ptr_array_new(http_data*);
|
|
return queue;
|
|
}
|
|
|
|
/* Destroy http_data_queue
|
|
*/
|
|
void
|
|
http_data_queue_free (http_data_queue *queue)
|
|
{
|
|
http_data_queue_purge(queue);
|
|
mem_free(queue->items);
|
|
mem_free(queue);
|
|
}
|
|
|
|
/* Push item into the http_data_queue.
|
|
*/
|
|
void
|
|
http_data_queue_push (http_data_queue *queue, http_data *data)
|
|
{
|
|
queue->items = ptr_array_append(queue->items, data);
|
|
}
|
|
|
|
/* Pull an item from the http_data_queue. Returns NULL if queue is empty
|
|
*/
|
|
http_data*
|
|
http_data_queue_pull (http_data_queue *queue)
|
|
{
|
|
return ptr_array_del(queue->items, 0);
|
|
}
|
|
|
|
/* Get queue length
|
|
*/
|
|
int
|
|
http_data_queue_len (const http_data_queue *queue)
|
|
{
|
|
return (int) mem_len(queue->items);
|
|
}
|
|
|
|
/* Purge the queue
|
|
*/
|
|
void
|
|
http_data_queue_purge (http_data_queue *queue)
|
|
{
|
|
http_data *data;
|
|
|
|
while ((data = http_data_queue_pull(queue)) != NULL) {
|
|
http_data_unref(data);
|
|
}
|
|
}
|
|
|
|
/******************** HTTP client ********************/
|
|
/* Type http_client represents HTTP client instance
|
|
*/
|
|
struct http_client {
|
|
void *ptr; /* Callback's user data */
|
|
log_ctx *log; /* Logging context */
|
|
ll_head pending; /* Pending queries */
|
|
void (*onerror)( /* Callback to be called on transport error */
|
|
void *ptr, error err);
|
|
};
|
|
|
|
/* Create new http_client
|
|
*/
|
|
http_client*
|
|
http_client_new (log_ctx *log, void *ptr)
|
|
{
|
|
http_client *client = mem_new(http_client, 1);
|
|
|
|
client->ptr = ptr;
|
|
client->log = log;
|
|
ll_init(&client->pending);
|
|
|
|
return client;
|
|
}
|
|
|
|
/* Destroy http_client
|
|
*/
|
|
void
|
|
http_client_free (http_client *client)
|
|
{
|
|
log_assert(client->log, ll_empty(&client->pending));
|
|
|
|
mem_free(client);
|
|
}
|
|
|
|
/* Set on-error callback. If this callback is not NULL,
|
|
* in a case of transport error it will be called instead
|
|
* of the http_query callback
|
|
*/
|
|
void
|
|
http_client_onerror (http_client *client,
|
|
void (*onerror)(void *ptr, error err))
|
|
{
|
|
client->onerror = onerror;
|
|
}
|
|
|
|
/* Cancel all pending queries, if any
|
|
*/
|
|
void
|
|
http_client_cancel (http_client *client)
|
|
{
|
|
ll_node *node;
|
|
|
|
while ((node = ll_pop_beg(&client->pending)) != NULL) {
|
|
http_query *q;
|
|
q = http_query_by_ll_node(node);
|
|
http_query_cancel(q);
|
|
}
|
|
}
|
|
|
|
/* Set timeout of all pending queries, if any. Timeout is in milliseconds
|
|
*/
|
|
void
|
|
http_client_timeout (http_client *client, int timeout)
|
|
{
|
|
ll_node *node;
|
|
|
|
while ((node = ll_pop_beg(&client->pending)) != NULL) {
|
|
http_query *q;
|
|
q = http_query_by_ll_node(node);
|
|
http_query_timeout(q, timeout);
|
|
}
|
|
}
|
|
|
|
/* Check if client has pending queries
|
|
*/
|
|
bool
|
|
http_client_has_pending (const http_client *client)
|
|
{
|
|
return !ll_empty(&client->pending);
|
|
}
|
|
|
|
/******************** HTTP request handling ********************/
|
|
/* Type http_query represents HTTP query (both request and response)
|
|
*/
|
|
struct http_query {
|
|
/* URI and method */
|
|
http_uri *uri; /* Query URI */
|
|
http_uri *real_uri; /* Real URI, may be NULL */
|
|
const char *method; /* Request method */
|
|
|
|
/* Request and response headers */
|
|
http_hdr request_header; /* Request header */
|
|
http_hdr response_header; /* Response header */
|
|
bool host_inserted; /* Host: auto-inserted */
|
|
bool force_port; /* Host: always includes port */
|
|
|
|
/* HTTP redirects */
|
|
int redirect_count; /* Count of redirects */
|
|
http_uri *orig_uri; /* Original URI */
|
|
const char *orig_method; /* Original method */
|
|
|
|
/* Query timeout */
|
|
eloop_timer *timeout_timer; /* Timeout timer */
|
|
int timeout_value; /* In milliseconds */
|
|
|
|
/* Miscellaneous options */
|
|
bool no_need_response_body; /* Response body not needed */
|
|
|
|
/* Low-level I/O */
|
|
bool submitted; /* http_query_submit() called */
|
|
uint64_t eloop_callid; /* For eloop_call_cancel */
|
|
error err; /* Transport error */
|
|
struct addrinfo *addrs; /* Addresses to connect to */
|
|
bool addrs_freeaddrinfo; /* Use freeaddrinfo(addrs) */
|
|
struct addrinfo *addr_next; /* Next address to try */
|
|
int sock; /* HTTP socket */
|
|
gnutls_session_t tls; /* NULL if not TLS */
|
|
bool handshake; /* TLS handshake in progress */
|
|
bool sending; /* We are now sending */
|
|
eloop_fdpoll *fdpoll; /* Polls q->sock */
|
|
ip_straddr straddr; /* q->sock peer addr, for log */
|
|
|
|
char *rq_buf; /* Formatted request */
|
|
size_t rq_off; /* send() offset in request */
|
|
|
|
/* HTTP parser */
|
|
http_parser http_parser; /* HTTP parser structure */
|
|
bool http_headers_received; /* HTTP headers received */
|
|
bool http_parser_done; /* Message parsing done */
|
|
|
|
/* Data handling */
|
|
http_data *request_data; /* NULL if none */
|
|
http_data *response_data; /* NULL if none */
|
|
http_multipart *response_multipart; /* NULL if not multipart */
|
|
|
|
/* Callbacks and context */
|
|
timestamp timestamp; /* Submission timestamp */
|
|
uintptr_t uintptr; /* User-defined parameter */
|
|
void (*onerror) (void *ptr, /* On-error callback */
|
|
error err);
|
|
void (*onredir) (void *ptr, /* On-redirect callback */
|
|
http_uri *uri,
|
|
const http_uri *orig_uri);
|
|
void (*onrxhdr) (void *ptr, /* On-header reception */
|
|
http_query *q);
|
|
void (*callback) (void *ptr, /* Completion callback */
|
|
http_query *q);
|
|
|
|
/* Linkage to http_client */
|
|
http_client *client; /* Client that owns the query */
|
|
bool queued; /* Query is queued */
|
|
ll_node chain; /* In http_client::pending or
|
|
http_client::queued */
|
|
};
|
|
|
|
/* Get http_query* by pointer to its http_query::chain */
|
|
static http_query*
|
|
http_query_by_ll_node (ll_node *node)
|
|
{
|
|
return OUTER_STRUCT(node, http_query, chain);
|
|
}
|
|
|
|
/* Reset query into the state it had before http_query_submit()
|
|
*/
|
|
static void
|
|
http_query_reset (http_query *q)
|
|
{
|
|
if (q->host_inserted) {
|
|
http_hdr_del(&q->request_header, "Host");
|
|
q->host_inserted = false;
|
|
}
|
|
|
|
http_hdr_cleanup(&q->response_header);
|
|
|
|
if (q->addrs != NULL) {
|
|
if (q->addrs_freeaddrinfo) {
|
|
freeaddrinfo(q->addrs);
|
|
} else {
|
|
mem_free(q->addrs->ai_addr);
|
|
mem_free(q->addrs);
|
|
}
|
|
q->addrs = NULL;
|
|
q->addr_next = NULL;
|
|
}
|
|
|
|
q->handshake = q->sending = false;
|
|
|
|
http_query_disconnect(q);
|
|
|
|
str_trunc(q->rq_buf);
|
|
q->rq_off = 0;
|
|
|
|
q->http_headers_received = false;
|
|
q->http_parser_done = false;
|
|
|
|
http_data_unref(q->response_data);
|
|
q->response_data = NULL;
|
|
|
|
if (q->response_multipart != NULL) {
|
|
http_multipart_free(q->response_multipart);
|
|
q->response_multipart = NULL;
|
|
}
|
|
}
|
|
|
|
/* Free http_query
|
|
*/
|
|
static void
|
|
http_query_free (http_query *q)
|
|
{
|
|
http_query_reset(q);
|
|
|
|
http_query_timeout_cancel(q);
|
|
http_uri_free(q->uri);
|
|
http_uri_free(q->real_uri);
|
|
http_uri_free(q->orig_uri);
|
|
http_hdr_cleanup(&q->request_header);
|
|
|
|
mem_free(q->rq_buf);
|
|
|
|
http_data_unref(q->request_data);
|
|
|
|
mem_free(q);
|
|
}
|
|
|
|
/* Set Host header in HTTP request
|
|
*/
|
|
static void
|
|
http_query_set_host (http_query *q)
|
|
{
|
|
char *host, *end, *buf;
|
|
size_t len;
|
|
const struct sockaddr *addr = http_uri_addr(q->uri);
|
|
|
|
if (q->uri->scheme == HTTP_SCHEME_UNIX) {
|
|
http_query_set_request_header(q, "Host", "localhost");
|
|
return;
|
|
}
|
|
|
|
if (addr != NULL) {
|
|
ip_straddr s;
|
|
int dport;
|
|
|
|
switch (q->uri->scheme) {
|
|
case HTTP_SCHEME_HTTP:
|
|
dport = 80;
|
|
break;
|
|
|
|
case HTTP_SCHEME_HTTPS:
|
|
dport = 443;
|
|
break;
|
|
|
|
default:
|
|
dport = -1;
|
|
break;
|
|
}
|
|
if (q->force_port) {
|
|
dport = -1;
|
|
}
|
|
|
|
s = ip_straddr_from_sockaddr_dport(addr, dport, false, true);
|
|
http_query_set_request_header(q, "Host", s.text);
|
|
|
|
return;
|
|
}
|
|
|
|
host = strstr(http_uri_str(q->uri), "//") + 2;
|
|
end = strchr(host, '/');
|
|
len = end ? (size_t) (end - host) : strlen(host);
|
|
|
|
buf = alloca(len + 1);
|
|
memcpy(buf, host, len);
|
|
|
|
buf[len] = '\0';
|
|
|
|
http_query_set_request_header(q, "Host", buf);
|
|
}
|
|
|
|
/* Create new http_query
|
|
*
|
|
* Newly created http_query takes ownership on uri and body (if not NULL).
|
|
* The method and content_type assumed to be constant strings.
|
|
*/
|
|
http_query*
|
|
http_query_new (http_client *client, http_uri *uri, const char *method,
|
|
char *body, const char *content_type)
|
|
{
|
|
size_t len = body ? strlen(body) : 0;
|
|
return http_query_new_len(client, uri, method, body, len, content_type);
|
|
}
|
|
|
|
/* Create new http_query
|
|
*
|
|
* Works like http_query_new(), but request body length is specified
|
|
* explicitly
|
|
*/
|
|
http_query*
|
|
http_query_new_len (http_client *client, http_uri *uri, const char *method,
|
|
void *body, size_t body_len, const char *content_type)
|
|
{
|
|
http_query *q = mem_new(http_query, 1);
|
|
|
|
q->client = client;
|
|
q->uri = uri;
|
|
q->method = method;
|
|
|
|
http_hdr_init(&q->request_header);
|
|
http_hdr_init(&q->response_header);
|
|
|
|
q->sock = -1;
|
|
|
|
q->rq_buf = str_new();
|
|
|
|
q->onerror = client->onerror;
|
|
|
|
http_parser_init(&q->http_parser, HTTP_RESPONSE);
|
|
q->http_parser.data = &q->response_header;
|
|
|
|
/* Note, on Kyocera ECOSYS M2040dn connection keep-alive causes
|
|
* scanned job to remain in "Processing" state about 10 seconds
|
|
* after job has been actually completed, making scanner effectively
|
|
* busy.
|
|
*
|
|
* Looks like Kyocera firmware bug. Force connection to close
|
|
* as a workaround
|
|
*/
|
|
http_query_set_request_header(q, "Connection", "close");
|
|
|
|
/* Save request body and set Content-Type */
|
|
if (body != NULL) {
|
|
q->request_data = http_data_new(NULL, body, body_len);
|
|
if (content_type != NULL) {
|
|
http_query_set_request_header(q, "Content-Type", content_type);
|
|
http_data_set_content_type(q->request_data, content_type);
|
|
}
|
|
}
|
|
|
|
/* Set default timeout */
|
|
http_query_timeout(q, HTTP_QUERY_TIMEOUT);
|
|
|
|
return q;
|
|
}
|
|
|
|
/* Create new http_query, relative to base URI
|
|
*
|
|
* Newly created http_query takes ownership on body (if not NULL).
|
|
* The method and content_type assumed to be constant strings.
|
|
*/
|
|
http_query*
|
|
http_query_new_relative(http_client *client,
|
|
const http_uri *base_uri, const char *path,
|
|
const char *method, char *body, const char *content_type)
|
|
{
|
|
http_uri *uri = http_uri_new_relative(base_uri, path, true, false);
|
|
log_assert(client->log, uri != NULL);
|
|
return http_query_new(client, uri, method, body, content_type);
|
|
}
|
|
|
|
/* http_query_timeout callback
|
|
*/
|
|
static void
|
|
http_query_timeout_callback (void *p)
|
|
{
|
|
http_query *q = (http_query*) p;
|
|
|
|
q->timeout_timer = NULL; /* to prevent eloop_timer_cancel() */
|
|
http_query_complete(q, ERROR("timeout"));
|
|
}
|
|
|
|
/* Set query timeout, in milliseconds. Negative timeout means 'infinite'
|
|
*
|
|
* This function may be called multiple times (each subsequent call overrides
|
|
* a previous one)
|
|
*/
|
|
void
|
|
http_query_timeout (http_query *q, int timeout)
|
|
{
|
|
q->timeout_value = timeout;
|
|
|
|
if (q->submitted) {
|
|
http_query_timeout_cancel(q);
|
|
|
|
if (timeout >= 0) {
|
|
log_debug(q->client->log, "HTTP using timeout: %d ms",
|
|
q->timeout_value);
|
|
|
|
q->timeout_timer = eloop_timer_new(timeout,
|
|
http_query_timeout_callback, q);
|
|
} else {
|
|
log_debug(q->client->log, "HTTP using timeout: none");
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Set 'no_need_response_body' flag
|
|
*
|
|
* This flag notifies, that http_query issued is only interested
|
|
* in the HTTP response headers, not body
|
|
*
|
|
* If this flag is set, after successful reception of response
|
|
* HTTP header, errors in fetching response body is ignored
|
|
*/
|
|
void
|
|
http_query_no_need_response_body (http_query *q)
|
|
{
|
|
q->no_need_response_body = true;
|
|
}
|
|
|
|
/* Cancel query timeout timer
|
|
*/
|
|
static void
|
|
http_query_timeout_cancel (http_query *q)
|
|
{
|
|
if (q->timeout_timer != NULL) {
|
|
eloop_timer_cancel(q->timeout_timer);
|
|
q->timeout_timer = NULL;
|
|
}
|
|
}
|
|
|
|
/* Set forcing port to be added to the Host header for this query.
|
|
*
|
|
* This function may be called multiple times (each subsequent call overrides
|
|
* a previous one).
|
|
*/
|
|
void
|
|
http_query_force_port(http_query *q, bool force_port) {
|
|
q->force_port = force_port;
|
|
}
|
|
|
|
/* For this particular query override on-error callback, previously
|
|
* set by http_client_onerror()
|
|
*
|
|
* If canllback is NULL, the completion callback, specified on a
|
|
* http_query_submit() call, will be used even in a case of
|
|
* transport error.
|
|
*/
|
|
void
|
|
http_query_onerror (http_query *q, void (*onerror)(void *ptr, error err))
|
|
{
|
|
q->onerror = onerror;
|
|
}
|
|
|
|
/* Set on-redirect callback. It is called in a case of HTTP
|
|
* redirect and may modify the supplied URI
|
|
*/
|
|
void
|
|
http_query_onredir (http_query *q,
|
|
void (*onredir)(void *ptr, http_uri *uri, const http_uri *orig_uri))
|
|
{
|
|
q->onredir = onredir;
|
|
}
|
|
|
|
/* Set callback that will be called, when response headers reception
|
|
* is completed
|
|
*/
|
|
void
|
|
http_query_onrxhdr (http_query *q, void (*onrxhdr)(void *ptr, http_query *q))
|
|
{
|
|
q->onrxhdr = onrxhdr;
|
|
}
|
|
|
|
/* Choose HTTP redirect method, based on HTTP status code
|
|
* Returns NULL for non-redirection status code, and may
|
|
* be used to detect if status code implies redirection
|
|
*/
|
|
static const char*
|
|
http_query_redirect_method (const http_query *q)
|
|
{
|
|
const char *method = q->orig_method ? q->orig_method : q->method;
|
|
|
|
switch(http_query_status(q)) {
|
|
case 303:
|
|
if (!strcmp(method, "POST") || !strcmp(method, "PUT")) {
|
|
method = "GET";
|
|
}
|
|
break;
|
|
|
|
case 301: case 302: case 307: case 308:
|
|
break;
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
|
|
return method;
|
|
}
|
|
|
|
/* Handle HTTP redirection
|
|
*/
|
|
static error
|
|
http_query_redirect (http_query *q, const char *method)
|
|
{
|
|
const char *location;
|
|
http_uri *uri;
|
|
|
|
/* Check and parse location */
|
|
location = http_query_get_response_header(q, "Location");
|
|
if (location == NULL || *location == '\0') {
|
|
return ERROR("HTTP redirect: missed Location: field");
|
|
}
|
|
|
|
uri = http_uri_new_relative(q->uri, location, true, false);
|
|
if (uri == NULL) {
|
|
return ERROR("HTTP redirect: invalid Location: field");
|
|
}
|
|
|
|
/* Enforce redirects limit */
|
|
q->redirect_count ++;
|
|
if (q->redirect_count == HTTP_REDIRECT_LIMIT) {
|
|
return ERROR("HTTP redirect: too many redirects");
|
|
}
|
|
|
|
/* Save original URI and method at the first redirect */
|
|
if (q->redirect_count == 1) {
|
|
q->orig_uri = q->uri;
|
|
q->orig_method = q->method;
|
|
} else {
|
|
http_uri_free(q->uri);
|
|
q->uri = NULL; /* Just in case */
|
|
}
|
|
|
|
/* Issue log message */
|
|
log_debug(q->client->log, "HTTP redirect %d: %s %s",
|
|
q->redirect_count, method, http_uri_str(uri));
|
|
|
|
/* Call user hook, if any */
|
|
if (q->onredir != NULL) {
|
|
char *old_uri_str = alloca(strlen(uri->str) + 1);
|
|
|
|
strcpy(old_uri_str, uri->str);
|
|
q->onredir(q->client->ptr, uri, q->orig_uri);
|
|
if (strcmp(old_uri_str, uri->str)) {
|
|
log_debug(q->client->log, "HTTP redirect override: %s %s",
|
|
method, http_uri_str(uri));
|
|
}
|
|
}
|
|
|
|
/* Perform redirection */
|
|
http_query_reset(q);
|
|
|
|
q->method = method;
|
|
q->uri = uri;
|
|
|
|
http_query_submit(q, q->callback);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Check if I/O error should be ignored for this query
|
|
*/
|
|
static bool
|
|
http_query_ignore_error (http_query *q)
|
|
{
|
|
int status;
|
|
|
|
/* Headers must be received */
|
|
if (!q->http_headers_received) {
|
|
return false;
|
|
}
|
|
|
|
/* Depending on HTTP status, response body may be irrelevant */
|
|
status = q->http_parser.status_code;
|
|
switch (status / 100) {
|
|
case 1: case 3: case 4: case 5:
|
|
return true;
|
|
}
|
|
|
|
/* If `no_need_response_body' is set, ignore the error */
|
|
return q->no_need_response_body;
|
|
}
|
|
|
|
/* Complete query processing
|
|
*/
|
|
static void
|
|
http_query_complete (http_query *q, error err)
|
|
{
|
|
http_client *client = q->client;
|
|
|
|
/* Make sure latest response header field is terminated */
|
|
http_hdr_on_header_value(&q->http_parser, "", 0);
|
|
|
|
/* Unlink query from a client */
|
|
ll_del(&q->chain);
|
|
|
|
/* In some cases, I/O error can be ignored. Check for that */
|
|
if (err != NULL && http_query_ignore_error(q)) {
|
|
log_debug(client->log, "HTTP %s %s: %s (ignored)", q->method,
|
|
http_uri_str(q->uri), ESTRING(err));
|
|
err = NULL;
|
|
}
|
|
|
|
/* Issue log messages */
|
|
q->err = err;
|
|
if (err != NULL) {
|
|
log_debug(client->log, "HTTP %s %s: %s", q->method,
|
|
http_uri_str(q->uri), http_query_status_string(q));
|
|
} else {
|
|
log_debug(client->log, "HTTP %s %s: %d %s", q->method,
|
|
http_uri_str(q->uri),
|
|
http_query_status(q), http_query_status_string(q));
|
|
}
|
|
|
|
trace_http_query_hook(log_ctx_trace(client->log), q);
|
|
|
|
/* Handle redirection */
|
|
if (err == NULL) {
|
|
const char *method = http_query_redirect_method(q);
|
|
|
|
if (method != NULL) {
|
|
q->err = err = http_query_redirect(q, method);
|
|
if (err == NULL) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
log_debug(client->log, "HTTP %s %s: %s", q->method,
|
|
http_uri_str(q->uri), http_query_status_string(q));
|
|
}
|
|
|
|
/* Restore original method and URI, modified in case of redirection */
|
|
if (q->orig_uri != NULL) {
|
|
q->real_uri = q->uri;
|
|
q->uri = q->orig_uri;
|
|
q->method = q->orig_method;
|
|
|
|
q->orig_uri = NULL;
|
|
q->orig_method = NULL;
|
|
}
|
|
|
|
/* Call user callback */
|
|
if (err != NULL && q->onerror != NULL) {
|
|
q->onerror(client->ptr, err);
|
|
} else if (q->callback != NULL) {
|
|
q->callback(client->ptr, q);
|
|
}
|
|
|
|
http_query_free(q);
|
|
}
|
|
|
|
/* HTTP parser on_body callback
|
|
*/
|
|
static int
|
|
http_query_on_body_callback (http_parser *parser,
|
|
const char *data, size_t size)
|
|
{
|
|
http_query *q = OUTER_STRUCT(parser, http_query, http_parser);
|
|
|
|
if (size == 0) {
|
|
return 0; /* Just in case */
|
|
}
|
|
|
|
if (q->response_data == NULL) {
|
|
q->response_data = http_data_new(NULL, NULL, 0);
|
|
}
|
|
|
|
if (!http_data_append(q->response_data, data, size)) {
|
|
q->err = ERROR_ENOMEM;
|
|
}
|
|
|
|
return q->err ? 1 : 0;
|
|
}
|
|
|
|
/* HTTP parser on_headers_complete callback
|
|
*/
|
|
static int
|
|
http_query_on_headers_complete (http_parser *parser)
|
|
{
|
|
http_query *q = OUTER_STRUCT(parser, http_query, http_parser);
|
|
|
|
if (http_query_redirect_method(q) == NULL) {
|
|
log_debug(q->client->log,
|
|
"HTTP %s %s: got response headers (%d)",
|
|
q->method,
|
|
http_uri_str(q->uri),
|
|
http_query_status(q));
|
|
|
|
q->http_headers_received = true;
|
|
|
|
if (q->onrxhdr != NULL) {
|
|
q->onrxhdr(q->client->ptr, q);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* HTTP parser on_message_complete callback
|
|
*/
|
|
static int
|
|
http_query_on_message_complete (http_parser *parser)
|
|
{
|
|
http_query *q = OUTER_STRUCT(parser, http_query, http_parser);
|
|
|
|
if (q->response_data != NULL) {
|
|
const char *content_type;
|
|
|
|
content_type = http_query_get_response_header(q, "Content-Type");
|
|
if (content_type != NULL) {
|
|
http_data_set_content_type(q->response_data, content_type);
|
|
q->err = http_multipart_parse(
|
|
&q->response_multipart, q->client->log,
|
|
q->response_data, content_type);
|
|
}
|
|
}
|
|
|
|
q->http_parser_done = true;
|
|
|
|
return q->err ? 1 : 0;
|
|
}
|
|
|
|
/* HTTP parser callbacks
|
|
*/
|
|
static http_parser_settings
|
|
http_query_callbacks = {
|
|
.on_header_field = http_hdr_on_header_field,
|
|
.on_header_value = http_hdr_on_header_value,
|
|
.on_body = http_query_on_body_callback,
|
|
.on_headers_complete = http_query_on_headers_complete,
|
|
.on_message_complete = http_query_on_message_complete
|
|
};
|
|
|
|
/* Set http_query::fdpoll event mask
|
|
*/
|
|
static void
|
|
http_query_fdpoll_set_mask (http_query *q, ELOOP_FDPOLL_MASK mask)
|
|
{
|
|
ELOOP_FDPOLL_MASK old_mask = eloop_fdpoll_set_mask(q->fdpoll, mask);
|
|
log_debug(q->client->log, "HTTP fdpoll: %s -> %s",
|
|
eloop_fdpoll_mask_str(old_mask), eloop_fdpoll_mask_str(mask));
|
|
}
|
|
|
|
/* http_query::fdpoll callback
|
|
*/
|
|
static void
|
|
http_query_fdpoll_callback (int fd, void *data, ELOOP_FDPOLL_MASK mask)
|
|
{
|
|
http_query *q = data;
|
|
size_t len = mem_len(q->rq_buf) - q->rq_off;
|
|
ssize_t rc;
|
|
|
|
(void) fd;
|
|
(void) mask;
|
|
|
|
if (q->handshake) {
|
|
rc = gnutls_handshake(q->tls);
|
|
if (rc < 0) {
|
|
error err = http_query_sock_err(q, rc);
|
|
|
|
if (err == NULL) {
|
|
return;
|
|
}
|
|
|
|
log_debug(q->client->log, "HTTP %s: gnutls_handshake(): %s",
|
|
q->straddr.text, ESTRING(err));
|
|
|
|
/* TLS handshake failed, try another address, if any */
|
|
http_query_disconnect(q);
|
|
q->addr_next = q->addr_next->ai_next;
|
|
http_query_connect(q, err);
|
|
|
|
return;
|
|
}
|
|
|
|
log_debug(q->client->log, "HTTP done TLS handshake");
|
|
|
|
q->handshake = false;
|
|
http_query_fdpoll_set_mask(q, ELOOP_FDPOLL_BOTH);
|
|
} else if (q->sending) {
|
|
rc = http_query_sock_send(q, q->rq_buf + q->rq_off, len);
|
|
|
|
if (rc > 0) {
|
|
log_debug(q->client->log, "HTTP %d bytes sent", (int) rc);
|
|
trace_hexdump(log_ctx_trace(q->client->log), '>',
|
|
q->rq_buf + q->rq_off, rc);
|
|
}
|
|
|
|
if (rc < 0) {
|
|
error err = http_query_sock_err(q, rc);
|
|
|
|
if (err == NULL) {
|
|
return;
|
|
}
|
|
|
|
log_debug(q->client->log, "HTTP %s: send(): %s",
|
|
q->straddr.text, ESTRING(err));
|
|
|
|
http_query_disconnect(q);
|
|
|
|
if (q->rq_off == 0) {
|
|
/* None sent, try another address, if any */
|
|
q->addr_next = q->addr_next->ai_next;
|
|
http_query_connect(q, err);
|
|
} else {
|
|
/* Sending started and failed */
|
|
http_query_complete(q, err);
|
|
}
|
|
return;
|
|
}
|
|
|
|
q->rq_off += rc;
|
|
|
|
if (q->rq_off == mem_len(q->rq_buf)) {
|
|
log_debug(q->client->log, "HTTP done request sending");
|
|
|
|
q->sending = false;
|
|
http_query_fdpoll_set_mask(q, ELOOP_FDPOLL_BOTH);
|
|
|
|
/* Initialize HTTP parser */
|
|
http_parser_init(&q->http_parser, HTTP_RESPONSE);
|
|
q->http_parser.data = &q->response_header;
|
|
}
|
|
} else {
|
|
static char io_buf[HTTP_IOBUF_SIZE];
|
|
|
|
rc = http_query_sock_recv(q, io_buf, sizeof(io_buf));
|
|
if (rc > 0) {
|
|
log_debug(q->client->log, "HTTP %d bytes received", (int) rc);
|
|
trace_hexdump(log_ctx_trace(q->client->log), '<', io_buf, rc);
|
|
}
|
|
|
|
if (rc < 0) {
|
|
error err = http_query_sock_err(q, rc);
|
|
if (err != NULL) {
|
|
http_query_complete(q, err);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (rc == 0) {
|
|
log_debug(q->client->log, "HTTP end of input");
|
|
}
|
|
|
|
http_parser_execute(&q->http_parser, &http_query_callbacks,
|
|
io_buf, rc);
|
|
|
|
if (q->http_parser.http_errno != HPE_OK) {
|
|
error err = q->err;
|
|
if (err == NULL) {
|
|
err = ERROR(http_errno_description(q->http_parser.http_errno));
|
|
}
|
|
http_query_complete(q, err);
|
|
} else if (q->http_parser_done) {
|
|
log_debug(q->client->log, "HTTP done response reception");
|
|
http_query_complete(q, NULL);
|
|
} else if (rc == 0) {
|
|
error err = ERROR("connection closed by device");
|
|
http_query_complete(q, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Try to connect to the next address. The err parameter is a query
|
|
* completion error in a case there are no more addresses to try
|
|
*/
|
|
static void
|
|
http_query_connect (http_query *q, error err)
|
|
{
|
|
int rc;
|
|
|
|
/* Skip invalid addresses. Check that we have address to try */
|
|
AGAIN:
|
|
while (q->addr_next != NULL &&
|
|
q->addr_next->ai_family != AF_INET &&
|
|
q->addr_next->ai_family != AF_INET6 &&
|
|
q->addr_next->ai_family != AF_UNIX) {
|
|
q->addr_next = q->addr_next->ai_next;
|
|
}
|
|
|
|
if (q->addr_next == NULL) {
|
|
http_query_complete(q, err);
|
|
return;
|
|
}
|
|
|
|
q->straddr = ip_straddr_from_sockaddr(q->addr_next->ai_addr, true);
|
|
log_debug(q->client->log, "HTTP trying %s", q->straddr.text);
|
|
|
|
/* Create socket and try to connect */
|
|
log_assert(q->client->log, q->sock < 0);
|
|
q->sock = socket(q->addr_next->ai_family,
|
|
q->addr_next->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC,
|
|
q->addr_next->ai_protocol);
|
|
|
|
if (q->sock == -1) {
|
|
err = ERROR(strerror(errno));
|
|
log_debug(q->client->log, "HTTP %s: socket(): %s",
|
|
q->straddr.text, ESTRING(err));
|
|
|
|
q->addr_next = q->addr_next->ai_next;
|
|
goto AGAIN;
|
|
}
|
|
|
|
do {
|
|
rc = connect(q->sock, q->addr_next->ai_addr, q->addr_next->ai_addrlen);
|
|
} while (rc < 0 && errno == EINTR);
|
|
|
|
if (rc < 0 && errno != EINPROGRESS) {
|
|
err = ERROR(strerror(errno));
|
|
log_debug(q->client->log, "HTTP %s: connect(): %s",
|
|
q->straddr.text, ESTRING(err));
|
|
|
|
http_query_disconnect(q);
|
|
|
|
q->addr_next = q->addr_next->ai_next;
|
|
goto AGAIN;
|
|
}
|
|
|
|
/* Setup TLS, if required */
|
|
if (q->uri->scheme == HTTP_SCHEME_HTTPS) {
|
|
int rc = gnutls_init(&q->tls,
|
|
GNUTLS_CLIENT | GNUTLS_NONBLOCK | GNUTLS_NO_SIGNAL);
|
|
|
|
if (rc == GNUTLS_E_SUCCESS) {
|
|
rc = gnutls_set_default_priority(q->tls);
|
|
}
|
|
|
|
if (rc == GNUTLS_E_SUCCESS) {
|
|
rc = gnutls_credentials_set(q->tls, GNUTLS_CRD_CERTIFICATE,
|
|
gnutls_cred);
|
|
}
|
|
|
|
if (rc != GNUTLS_E_SUCCESS) {
|
|
err = ERROR(gnutls_strerror(rc));
|
|
http_query_disconnect(q);
|
|
http_query_complete(q, err);
|
|
return;
|
|
}
|
|
|
|
gnutls_transport_set_int(q->tls, q->sock);
|
|
}
|
|
|
|
/* Create fdpoll, and we are done */
|
|
q->fdpoll = eloop_fdpoll_new(q->sock, http_query_fdpoll_callback, q);
|
|
if (q->tls != NULL) {
|
|
q->handshake = true;
|
|
}
|
|
q->sending = true;
|
|
http_query_fdpoll_set_mask(q, ELOOP_FDPOLL_WRITE);
|
|
}
|
|
|
|
/* Close connection to the server, if any
|
|
*/
|
|
static void
|
|
http_query_disconnect (http_query *q)
|
|
{
|
|
if (q->fdpoll != NULL) {
|
|
eloop_fdpoll_free(q->fdpoll);
|
|
q->fdpoll = NULL;
|
|
}
|
|
|
|
if (q->tls != NULL) {
|
|
gnutls_deinit(q->tls);
|
|
q->tls = NULL;
|
|
}
|
|
|
|
if (q->sock >= 0) {
|
|
close(q->sock);
|
|
q->sock = -1;
|
|
}
|
|
}
|
|
|
|
/* Send data to socket (either via TCP or TLS)
|
|
* On a error, returns negative error code. Use
|
|
* http_query_sock_err() to decode it
|
|
*/
|
|
static ssize_t
|
|
http_query_sock_send (http_query *q, const void *data, size_t size)
|
|
{
|
|
ssize_t rc;
|
|
|
|
if (q->tls == NULL) {
|
|
rc = send(q->sock, data, size, MSG_NOSIGNAL);
|
|
if (rc < 0) {
|
|
rc = -errno;
|
|
}
|
|
} else {
|
|
rc = gnutls_record_send(q->tls, data, size);
|
|
if (rc < 0) {
|
|
gnutls_record_discard_queued(q->tls);
|
|
}
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Recv data from socket (either via TCP or TLS)
|
|
*/
|
|
static ssize_t
|
|
http_query_sock_recv (http_query *q, void *data, size_t size)
|
|
{
|
|
ssize_t rc;
|
|
|
|
if (q->tls == NULL) {
|
|
rc = recv(q->sock, data, size, MSG_NOSIGNAL);
|
|
if (rc < 0) {
|
|
rc = -errno;
|
|
}
|
|
} else {
|
|
rc = gnutls_record_recv(q->tls, data, size);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Get socket error. May return NULL if last operation
|
|
* has failed in recoverable manner
|
|
*/
|
|
static error
|
|
http_query_sock_err (http_query *q, int rc)
|
|
{
|
|
ELOOP_FDPOLL_MASK mask = 0;
|
|
error err = NULL;
|
|
|
|
if (q->tls == NULL) {
|
|
rc = -rc;
|
|
switch (rc) {
|
|
case EINTR:
|
|
break;
|
|
|
|
case EWOULDBLOCK:
|
|
mask = q->sending ? ELOOP_FDPOLL_WRITE : ELOOP_FDPOLL_READ;
|
|
break;
|
|
|
|
default:
|
|
err = ERROR(strerror(errno));
|
|
}
|
|
|
|
} else {
|
|
switch (rc) {
|
|
case GNUTLS_E_INTERRUPTED:
|
|
break;
|
|
|
|
case GNUTLS_E_AGAIN:
|
|
mask = gnutls_record_get_direction(q->tls) ?
|
|
ELOOP_FDPOLL_WRITE : ELOOP_FDPOLL_READ;
|
|
break;
|
|
|
|
default:
|
|
if (gnutls_error_is_fatal(rc)) {
|
|
err = ERROR(gnutls_strerror(rc));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mask != 0) {
|
|
http_query_fdpoll_set_mask(q, mask);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Start query processing. Called via eloop_call()
|
|
*/
|
|
static void
|
|
http_query_start_processing (void *p)
|
|
{
|
|
http_query *q = (http_query*) p;
|
|
http_uri_field field;
|
|
char *host, *port;
|
|
const char *path;
|
|
struct addrinfo hints;
|
|
int rc;
|
|
|
|
/* Get host name from the URI */
|
|
field = http_uri_field_get(q->uri, UF_HOST);
|
|
host = alloca(field.len + 1);
|
|
memcpy(host, field.str, field.len);
|
|
host[field.len] = '\0';
|
|
http_uri_unescape_host(host);
|
|
|
|
/* Get port name from the URI */
|
|
if (http_uri_field_nonempty(q->uri, UF_PORT)) {
|
|
field = http_uri_field_get(q->uri, UF_PORT);
|
|
port = alloca(field.len + 1);
|
|
memcpy(port, field.str, field.len);
|
|
port[field.len] = '\0';
|
|
} else {
|
|
port = q->uri->scheme == HTTP_SCHEME_HTTP ? "80" : "443";
|
|
}
|
|
|
|
/* Lookup target addresses */
|
|
if (q->uri->scheme != HTTP_SCHEME_UNIX) {
|
|
log_debug(q->client->log, "HTTP resolving %s %s", host, port);
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_flags = AI_ADDRCONFIG;
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_protocol = IPPROTO_TCP;
|
|
|
|
q->addrs_freeaddrinfo = true;
|
|
rc = getaddrinfo(host, port, &hints, &q->addrs);
|
|
if (rc != 0) {
|
|
http_query_complete(q, ERROR(gai_strerror(rc)));
|
|
return;
|
|
}
|
|
} else {
|
|
struct sockaddr_un *addr;
|
|
size_t pathlen = strlen(conf.socket_dir) + 1 /* for / */ + strlen(host);
|
|
char *path = alloca(pathlen + 1);
|
|
sprintf(path, "%s/%s", conf.socket_dir, host);
|
|
|
|
log_debug(q->client->log, "connecting to local socket %s", path);
|
|
q->addrs_freeaddrinfo = false;
|
|
q->addrs = mem_new(struct addrinfo, 1);
|
|
q->addrs->ai_family = AF_UNIX;
|
|
q->addrs->ai_socktype = SOCK_STREAM;
|
|
q->addrs->ai_protocol = 0;
|
|
|
|
addr = mem_new(struct sockaddr_un, 1);
|
|
addr->sun_family = AF_UNIX;
|
|
strncpy(addr->sun_path, path, sizeof(addr->sun_path)-1);
|
|
q->addrs->ai_addrlen = sizeof(struct sockaddr_un);
|
|
q->addrs->ai_addr = (struct sockaddr *)addr;
|
|
|
|
if (pathlen >= sizeof(addr->sun_path)) {
|
|
http_query_complete(q, ERROR("Socket path is too long."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
q->addr_next = q->addrs;
|
|
|
|
/* Set Host: header, if not set by user */
|
|
if (http_hdr_lookup(&q->request_header, "Host") == NULL) {
|
|
q->host_inserted = true;
|
|
http_query_set_host(q);
|
|
}
|
|
|
|
/* Obtain path. Note, URL format allows path to be empty,
|
|
* while HTTP request requires non-empty string
|
|
*/
|
|
path = http_uri_get_path(q->uri);
|
|
if (*path == '\0') {
|
|
path = "/";
|
|
}
|
|
|
|
/* Format HTTP request */
|
|
str_trunc(q->rq_buf);
|
|
q->rq_buf = str_append_printf(q->rq_buf, "%s %s HTTP/1.1\r\n",
|
|
q->method, path);
|
|
|
|
if (q->request_data != NULL) {
|
|
char buf[64];
|
|
sprintf(buf, "%zd", q->request_data->size);
|
|
http_hdr_set(&q->request_header, "Content-Length", buf);
|
|
}
|
|
|
|
q->rq_buf = http_hdr_write(&q->request_header, q->rq_buf);
|
|
|
|
if (q->request_data != NULL) {
|
|
q->rq_buf = str_append_mem(q->rq_buf,
|
|
q->request_data->bytes, q->request_data->size);
|
|
}
|
|
|
|
/* Connect to the host */
|
|
http_query_connect(q, ERROR("no host addresses available"));
|
|
}
|
|
|
|
/* Submit the query.
|
|
*
|
|
* When query is finished, callback will be called. After return from
|
|
* callback, memory, owned by http_query will be invalidated
|
|
*/
|
|
void
|
|
http_query_submit (http_query *q, void (*callback)(void *ptr, http_query *q))
|
|
{
|
|
q->callback = callback;
|
|
|
|
/* Issue log message, set timestamp and start timeout timer */
|
|
log_debug(q->client->log, "HTTP %s %s", q->method, http_uri_str(q->uri));
|
|
|
|
if (!q->submitted) {
|
|
q->submitted = true;
|
|
q->timestamp = timestamp_now();
|
|
|
|
if (q->timeout_value >= 0) {
|
|
http_query_timeout(q, q->timeout_value);
|
|
}
|
|
}
|
|
|
|
/* Submit the query */
|
|
log_assert(q->client->log, q->sock == -1);
|
|
ll_push_end(&q->client->pending, &q->chain);
|
|
|
|
q->eloop_callid = eloop_call(http_query_start_processing, q);
|
|
}
|
|
|
|
/* Cancel unfinished http_query. Callback will not be called and
|
|
* memory owned by the http_query will be released
|
|
*/
|
|
static void
|
|
http_query_cancel (http_query *q)
|
|
{
|
|
log_debug(q->client->log, "HTTP %s %s: Cancelled", q->method,
|
|
http_uri_str(q->uri));
|
|
|
|
ll_del(&q->chain);
|
|
eloop_call_cancel(q->eloop_callid);
|
|
|
|
http_query_free(q);
|
|
}
|
|
|
|
/* Get http_query timestamp. Timestamp is set when query is
|
|
* submitted. And this function should not be called before
|
|
* http_query_submit()
|
|
*/
|
|
timestamp
|
|
http_query_timestamp (const http_query *q)
|
|
{
|
|
return q->timestamp;
|
|
}
|
|
|
|
/* Set uintptr_t parameter, associated with query.
|
|
* Completion callback may later use http_query_get_uintptr()
|
|
* to fetch this value
|
|
*/
|
|
void
|
|
http_query_set_uintptr (http_query *q, uintptr_t u)
|
|
{
|
|
q->uintptr = u;
|
|
}
|
|
|
|
/* Get uintptr_t parameter, previously set by http_query_set_uintptr()
|
|
*/
|
|
uintptr_t
|
|
http_query_get_uintptr (http_query *q)
|
|
{
|
|
return q->uintptr;
|
|
}
|
|
|
|
/* Get query error, if any
|
|
*
|
|
* Both transport errors and erroneous HTTP response codes
|
|
* considered as errors here
|
|
*/
|
|
error
|
|
http_query_error (const http_query *q)
|
|
{
|
|
if (q->err == NULL) {
|
|
int status = http_query_status(q);
|
|
|
|
if (200 <= status && status < 300) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return ERROR(http_query_status_string(q));
|
|
}
|
|
|
|
/* Get query transport error, if any
|
|
*
|
|
* Only transport errors considered errors here
|
|
*/
|
|
error
|
|
http_query_transport_error (const http_query *q)
|
|
{
|
|
return q->err;
|
|
}
|
|
|
|
/* Get HTTP status code. Code not available, if query finished
|
|
* with transport error
|
|
*/
|
|
int
|
|
http_query_status (const http_query *q)
|
|
{
|
|
log_assert(q->client->log, q->err == NULL);
|
|
return q->http_parser.status_code;
|
|
}
|
|
|
|
/* Get HTTP status string
|
|
*/
|
|
const char*
|
|
http_query_status_string (const http_query *q)
|
|
{
|
|
if (q->err != NULL) {
|
|
return ESTRING(q->err);
|
|
}
|
|
|
|
return http_status_str(q->http_parser.status_code);
|
|
}
|
|
|
|
/* Get query URI
|
|
*
|
|
* It works as http_query_orig_uri() before query is submitted
|
|
* or after it is completed, and as http_query_real_uri() in
|
|
* between
|
|
*
|
|
* This function is deprecated, use http_query_orig_uri()
|
|
* or http_query_real_uri() instead
|
|
*/
|
|
http_uri*
|
|
http_query_uri (const http_query *q)
|
|
{
|
|
return q->uri;
|
|
}
|
|
|
|
/* Get original URI (the same as used when http_query was created)
|
|
*/
|
|
http_uri*
|
|
http_query_orig_uri (const http_query *q)
|
|
{
|
|
return q->orig_uri ? q->orig_uri : q->uri;
|
|
}
|
|
|
|
/* Get real URI, that can differ from the requested URI
|
|
* in a case of HTTP redirection
|
|
*/
|
|
http_uri*
|
|
http_query_real_uri (const http_query *q)
|
|
{
|
|
return q->real_uri ? q->real_uri : q->uri;
|
|
}
|
|
|
|
/* Get query method
|
|
*/
|
|
const char*
|
|
http_query_method (const http_query *q)
|
|
{
|
|
return q->method;
|
|
}
|
|
|
|
/* Set request header
|
|
*/
|
|
void
|
|
http_query_set_request_header (http_query *q, const char *name,
|
|
const char *value)
|
|
{
|
|
http_hdr_set(&q->request_header, name, value);
|
|
}
|
|
|
|
/* Get request header
|
|
*/
|
|
const char*
|
|
http_query_get_request_header (const http_query *q, const char *name)
|
|
{
|
|
return http_hdr_get(&q->request_header, name);
|
|
}
|
|
|
|
|
|
/* Get response header
|
|
*/
|
|
const char*
|
|
http_query_get_response_header(const http_query *q, const char *name)
|
|
{
|
|
return http_hdr_get(&q->response_header, name);
|
|
}
|
|
|
|
/* Get request data
|
|
*/
|
|
http_data*
|
|
http_query_get_request_data (const http_query *q)
|
|
{
|
|
return q->request_data ? q->request_data : &http_data_empty;
|
|
}
|
|
|
|
/* Get request data
|
|
*/
|
|
http_data*
|
|
http_query_get_response_data (const http_query *q)
|
|
{
|
|
return q->response_data ? q->response_data : &http_data_empty;
|
|
}
|
|
|
|
/* Get multipart response bodies. For non-multipart response
|
|
* returns NULL
|
|
*/
|
|
static http_multipart*
|
|
http_query_get_mp_response (const http_query *q)
|
|
{
|
|
return q->response_multipart;
|
|
}
|
|
|
|
/* Get count of parts of multipart response
|
|
*/
|
|
int
|
|
http_query_get_mp_response_count (const http_query *q)
|
|
{
|
|
http_multipart *mp = http_query_get_mp_response(q);
|
|
return mp ? mp->count : 0;
|
|
}
|
|
|
|
/* Get data of Nth part of multipart response
|
|
*/
|
|
http_data*
|
|
http_query_get_mp_response_data (const http_query *q, int n)
|
|
{
|
|
http_multipart *mp = http_query_get_mp_response(q);
|
|
|
|
if (mp == NULL || n < 0 || n >= mp->count) {
|
|
return NULL;
|
|
}
|
|
|
|
return mp->bodies[n];
|
|
}
|
|
|
|
/* Call callback for each request header
|
|
*/
|
|
void
|
|
http_query_foreach_request_header (const http_query *q,
|
|
void (*callback)(const char *name, const char *value, void *ptr),
|
|
void *ptr)
|
|
{
|
|
hdr_for_each(&q->request_header, callback, ptr);
|
|
}
|
|
|
|
/* Call callback for each response header
|
|
*/
|
|
void
|
|
http_query_foreach_response_header (const http_query *q,
|
|
void (*callback)(const char *name, const char *value, void *ptr),
|
|
void *ptr)
|
|
{
|
|
hdr_for_each(&q->response_header, callback, ptr);
|
|
}
|
|
|
|
/* Decode response part of the query.
|
|
* This function is intended for testing purposes, not for regular use
|
|
*/
|
|
error
|
|
http_query_test_decode_response (http_query *q, const void *data, size_t size)
|
|
{
|
|
http_parser_execute(&q->http_parser, &http_query_callbacks, data, size);
|
|
if (q->http_parser.http_errno == HPE_OK && !q->http_parser_done) {
|
|
http_parser_execute(&q->http_parser, &http_query_callbacks, data, 0);
|
|
}
|
|
|
|
if (q->http_parser.http_errno != HPE_OK) {
|
|
if (q->err != NULL) {
|
|
return q->err;
|
|
}
|
|
return ERROR(http_errno_description(q->http_parser.http_errno));
|
|
}
|
|
|
|
if (!q->http_parser_done) {
|
|
return ERROR("truncated response");
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/******************** HTTP initialization & cleanup ********************/
|
|
/* Initialize HTTP client
|
|
*/
|
|
SANE_Status
|
|
http_init (void)
|
|
{
|
|
int rc = gnutls_certificate_allocate_credentials(&gnutls_cred);
|
|
return rc == GNUTLS_E_SUCCESS ? SANE_STATUS_GOOD : SANE_STATUS_NO_MEM;
|
|
}
|
|
|
|
/* Initialize HTTP client
|
|
*/
|
|
void
|
|
http_cleanup (void)
|
|
{
|
|
if (gnutls_cred != NULL) {
|
|
gnutls_certificate_free_credentials(gnutls_cred);
|
|
gnutls_cred = NULL;
|
|
}
|
|
}
|
|
|
|
/* vim:ts=8:sw=4:et
|
|
*/
|