[vlc-devel] [PATCH] network: implement an API for HTTP requests.

Denis Charmet typx at dinauz.org
Wed Feb 5 12:01:29 CET 2014


Hi,

Since it's quite redundant with access/http, it would be good to avoid
code duplication.

Le lundi 03 février 2014 à 07:07:48, Felix Abecassis a écrit :
> +/* Read a line (ended by \r\n) from an HTTP response. */
> +static char* http_readline( vlc_object_t *p_this, int fd )
why not net_Get?
> +{
> +    size_t size = 64;
> +    char *str = malloc(sizeof(*str) * size);
no check
> +
> +    char c;
> +    size_t i = 0;
> +    while (net_Read(p_this, fd, NULL, &c, 1, false) > 0)
> +    {
> +        if (i == size - 1)
> +        {
> +            /* Ran out of space for storing the line, grow the buffer. */
> +            size *= 2;
> +            str = realloc(str, sizeof(*str) * size);
> +            if (!str)
> +                return NULL;
> +        }
> +        str[i++] = c;
> +        if (c == '\n')
> +            break;
> +    }
> +
> +    if (i == 0)
> +    {
> +        free(str);
> +        return NULL;
> +    }
> +
> +    str[i] = '\0';
> +    return str;
> +}
> +
> +/* Concatenate the given key-value pair to string headers. */
> +static char *http_add_header(char *headers, const char *key, const char *value)
> +{
> +    char *new_headers;
> +    int ret = asprintf(&new_headers, "%s%s: %s\r\n", headers, key, value);
> +    free(headers);
> +    return ret >= 0 ? new_headers : NULL;
> +}
> +
> +/* Split a header line with format "Key: Value" */
> +static int http_split_header_line(const char *line, char** key, char **value)
> +{
> +    *key = NULL;
> +    *value = NULL;
> +
> +    const char *key_begin = line;
> +    const char *key_end = strchr(key_begin, ':');
> +    *key = strndup(key_begin, key_end - key_begin);
> +    if (!*key)
> +        goto error;
> +
> +    const char *value_begin = key_end + 1;
Never trust a source if there is no ':' the following dereference is likely to segfault.
> +    while (*value_begin == ' ')
You may also skip potential '\t'
> +        ++value_begin;
> +    const char *value_end = strchr(value_begin, '\r');
> +    if (!value_end)
> +        goto error;
> +    /* If value_end == value_begin, the field is empty, this is valid. */
> +    if (value_end != value_begin)
> +    {
> +        *value = strndup(value_begin, value_end - value_begin);
> +        if (!*value)
> +            goto error;
> +    }
> +
> +    return 0;
> +
> +error:
> +    free(*key);
> +    free(*value);
> +    return -1;
> +}
> +
> +static vlc_http_response_t *http_get_response(vlc_object_t *p_this, int fd)
> +{
> +    vlc_http_response_t *response = calloc(1, sizeof(*response));
> +    if (!response)
> +        return NULL;
> +
> +    vlc_http_status_line_t *status_line = &response->status_line;
> +    vlc_http_header_t *header = &response->header;
> +    vlc_http_body_t *body = &response->body;
> +
> +    /* Parse status line. */
> +    char *line = http_readline(p_this, fd);
> +    int ret = sscanf(line, "HTTP/1.%d %3d", &status_line->http_version, &status_line->status_code);
You'll have to modify when HTTP/2.0 so why not already forsee the case?
> +    free(line);
> +    if (ret != 2)
> +    {
> +        msg_Err(p_this, "Received invalid HTTP status line.");
> +        goto error;
> +    }
> +
> +    /* Parse headers, line by line. */
> +    while ((line = http_readline(p_this, fd)))
> +    {
> +        /* Line with only \r\n: end of headers. */
> +        if (!strncmp(line, "\r\n", 2))
is it worth to call strncmp when you could test just line[0] == '\r' &&
line[1] == '\n' if there are 2 chars
> +        {
> +            free(line);
> +            break;
> +        }
> +
> +        /* Extract the key-value pair from the raw line. */
> +        char *key = NULL;
> +        char *value = NULL;
> +        ret = http_split_header_line(line, &key, &value);
> +        free(line);
> +        if (ret < 0)
> +            goto error;
> +
> +        /* Grow the dictionary and add the new key-value pair. */
> +        header->dictionary = realloc(header->dictionary, sizeof(*header->dictionary) * (header->size + 1));
You shouldn't use realloc like that. It can return NULL but
header->dictionary will still be allocated and will be leaked.
Why not using the array macros already available in vlc_arrays.h
> +        if (!header->dictionary)
> +        {
> +            free(key);
> +            free(value);
> +            goto error;
> +        }
> +        header->dictionary[header->size].key = key;
> +        header->dictionary[header->size].value = value;
> +        ++header->size;
> +    }
> +
> +    /* Try to find Content-Length entry in the header */
> +    for (size_t i = 0; i < header->size; ++i)
> +    {
> +        const char *key = header->dictionary[i].key;
> +        const char *value = header->dictionary[i].value;
> +        if (!strncasecmp(key, "Content-Length", 14))
This could be done in the previous loop
> +        {
> +            /* Get the field value. */
> +            errno = 0;
> +            body->size = strtoul(value, NULL, 10);
> +            if (errno)
> +            {
> +                msg_Err(p_this, "Invalid Content-Length in HTTP response.");
> +                goto error;
> +            }
> +            break;
> +        }
> +    }
> +
> +    /* Read the body of the response. */
> +    if (body->size > 0)
> +    {
> +        /* We add a terminal \0 for debugging convenience. */
> +        body->data = malloc(body->size + 1);
Should debug stuff be in the core?
> +        if (!body->data)
> +            goto error;
> +
> +        size_t size = net_Read(p_this, fd, NULL, body->data, body->size, true);
> +        if (size < body->size)
> +            goto error;
> +
> +        char* end = (char*)body->data + body->size;
> +        *end = '\0';
> +    }
> +    return response;
> +
> +error:
> +    msg_Err(p_this, "Error in %s", __FUNCTION__);
> +    http_response_Delete(response);
> +    return NULL;
> +}
> +
> +typedef struct http_method_s
> +{
> +    vlc_http_method_t id;
> +    const char        *string;
> +} http_method_t;
> +
> +static const struct http_method_s http_methods[] =
> +{
> +    { VLC_HTTP_OPTIONS, "OPTIONS" },
> +    { VLC_HTTP_GET, "GET" },
> +    { VLC_HTTP_HEAD, "HEAD" },
> +    { VLC_HTTP_POST, "POST" },
> +    { VLC_HTTP_PUT, "PUT" },
> +    { VLC_HTTP_DELETE, "DELETE" },
> +    { VLC_HTTP_TRACE, "TRACE" },
> +    { VLC_HTTP_CONNECT, "CONNECT" },
> +    { VLC_HTTP_PATCH, "PATCH" },
> +};
> +#define HTTP_METHODS_COUNT (sizeof(http_methods)/sizeof(http_methods[0]))
> +
> +#define HTTP_EXPECT_CONTINUE_THRESHOLD 1024
> +
> +#undef http_request_Send
> +vlc_http_response_t *http_request_Send(vlc_object_t *p_this, vlc_url_t *url,
> +                                       vlc_http_request_t *request)
> +{
> +    vlc_http_request_line_t *request_line = &request->request_line;
> +    vlc_http_header_t *request_header = &request->header;
> +    vlc_http_body_t *request_body = &request->body;
> +
> +    char *header_string = NULL;
> +    vlc_http_response_t *response = NULL;
> +
> +    int fd = net_ConnectTCP(p_this, url->psz_host, url->i_port);
> +    if (fd < 0)
> +    {
> +        msg_Err(p_this, "Could not connect to %s:%d", url->psz_host, url->i_port);
> +        return NULL;
> +    }
> +
> +    /* Lookup the string corresponding to the HTTP method. */
> +    const char* method_string = NULL;
> +    for (unsigned i = 0; i < HTTP_METHODS_COUNT; ++i)
> +    {
> +        if (http_methods[i].id == request_line->method)
> +        {
> +            method_string = http_methods[i].string;
> +            break;
> +        }
> +    }
<nitpicking>
You could use a const char** array instead of a for loop and using methods as
index or reorder your methods by probability of use like GET first
</nitpicking>
> +    if (!method_string)
> +        goto end;
> +
> +    /* Create the header string. */
> +    int ret;
> +    header_string = strdup("");
Why strdup("") when you could asprintf("%s %s HTTP/1.%d",...)
And you didn't check for result.

> +    for (size_t i = 0; i < request_header->size; ++i)
> +    {
> +        const char *key = request_header->dictionary[i].key;
> +        const char *value = request_header->dictionary[i].value;
> +        /* Discard unauthorized headers */
> +        if (!strncasecmp(key, "Content-Length", 14)
> +            || !strncasecmp(key, "Connection", 10))
Discard "\r\n" :)
> +        {
> +            msg_Err(p_this, "Unauthorized HTTP header: %s %s (overwritten by VLC)", key, value);
> +            continue;
> +        }
> +        /* Concatenate new key-value entry to the string. */
> +        header_string = http_add_header(header_string, key, value);
> +        if (!header_string)
> +            goto end;
> +    }
> +    /* If the body is larger than the threshold, we first ask the
> +       server if the headers are acceptable using Expect: 100-continue.
> +       This is only for HTTP 1.1. */
> +    bool use_expect_continue = request_line->http_version >= 1 && request_body->size >= HTTP_EXPECT_CONTINUE_THRESHOLD;
> +    if (use_expect_continue)
> +        if (!(header_string = http_add_header(header_string, "Expect", "100-continue")))
Maybe make Expect a forbidden header like "Content-Length" and
"Connection"
> +            goto end;
> +
> +    /* Force "Connection: close" mode. */
> +    if (!(header_string = http_add_header(header_string, "Connection", "close")))
Suddenly I wonder what happens when you have "Connection: close" and
"Expect: 100-continue"
In any case there might be cases where you could want the connection to
stay alive but fair enough for now.
> +        goto end;
> +
> +    /* Send request line and header. */
> +    ret = net_Printf(p_this, fd, NULL,
> +                     "%s %s HTTP/1.%d\r\n"
> +                     "%s"
> +                     "Content-Length: %zu\r\n"
> +                     "\r\n",
> +                     method_string, request_line->uri, request_line->http_version,
> +                     header_string, request_body->size);
> +    if (ret == -1)
> +        goto end;
> +
> +    if (use_expect_continue)
> +    {
> +        vlc_http_response_t *expect_response = http_get_response(p_this, fd);
> +        if (!expect_response) /* Error while reading the response. */
> +            goto end;
> +        int status_code = expect_response->status_line.status_code;
> +        http_response_Delete(expect_response);
> +        if (status_code != 100) /* Server did not accept the headers. */
> +        {
> +            msg_Err(p_this, "\"Expect: 100-continue\" request denied (status: %d).", status_code);
> +            goto end;
> +        }
> +    }
> +
> +    /* Send body. */
> +    if (request_body->size > 0)
> +    {
> +        ret = net_Write(p_this, fd, NULL, request_body->data, request_body->size);
> +        if (ret == -1)
> +        {
> +            msg_Err(p_this, "Could not send HTTP request body.");
> +            goto end;
> +        }
> +    }
> +
> +    response = http_get_response(p_this, fd);
> +
> +end:
> +    free(header_string);
> +    net_Close(fd);
> +    return response;
> +}
> +
> +void http_response_Delete(vlc_http_response_t *response)
> +{
> +    if (!response)
> +        return;
> +
> +    vlc_http_header_t *header = &response->header;
> +    vlc_http_body_t *body = &response->body;
> +
> +    for (size_t i = 0; i < header->size; ++i)
> +    {
> +        char *key = (char*)header->dictionary[i].key;
> +        char *value = (char*)header->dictionary[i].value;
> +        free(key);
> +        free(value);
> +    }
> +    free(header->dictionary);
> +    free(body->data);
> +    free(response);
> +}

Regards

-- 
Denis Charmet - TypX
Le mauvais esprit est un art de vivre



More information about the vlc-devel mailing list