1dceb76c创建于 2024年5月30日历史提交
/* 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(&params->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(&params);
    err = http_hdr_params_parse(&params, "Content-Type", content_type);
    if (err != NULL) {
        http_hdr_cleanup(&params);
        return err;
    }

    log_debug(log, "http multipart parameters:");
    for (LL_FOR_EACH(node, &params.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(&params, "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(&params);

    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
 */