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

Felix Abecassis felix.abecassis at gmail.com
Mon Feb 3 19:07:48 CET 2014


We would like to provide a simple API for sending HTTP requests and
for parsing the response from the server. Many modules are currently
handcrafting GET/POST requests by manipulating strings.

Comments are welcome concerning the API and the features that should
be implemented.

Difference from last draft:
- Now correctly split headers.
- Now automatically use Expect: 100-continue for large request bodies
  if HTTP version is 1.1.
- Moved declaration to vlc_http.h and definition to a new file.

To discuss:
- HTTP 2.0: no official standard yet. What should we do?
- For courmisch: you've mentioned special FD polling in LUA. Could you give me more details?
---
 include/vlc_http.h      |  68 ++++++++++
 modules/misc/Modules.am |   4 +
 src/Makefile.am         |   1 +
 src/libvlccore.sym      |   2 +
 src/network/http.c      | 349 ++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 424 insertions(+)
 create mode 100644 src/network/http.c

diff --git a/include/vlc_http.h b/include/vlc_http.h
index ddde13e..a64e61f 100644
--- a/include/vlc_http.h
+++ b/include/vlc_http.h
@@ -64,4 +64,72 @@ VLC_API char *http_auth_FormatAuthorizationHeader
               const char *, const char *,
               const char *, const char * ) VLC_USED;
 
+typedef enum
+{
+    VLC_HTTP_OPTIONS,
+    VLC_HTTP_GET,
+    VLC_HTTP_HEAD,
+    VLC_HTTP_POST,
+    VLC_HTTP_PUT,
+    VLC_HTTP_DELETE,
+    VLC_HTTP_TRACE,
+    VLC_HTTP_CONNECT,
+    VLC_HTTP_PATCH
+} vlc_http_method_t;
+
+typedef struct
+{
+    vlc_http_method_t method;
+    const char*       uri;
+    int               http_version;
+} vlc_http_request_line_t;
+
+typedef struct
+{
+    const char* key;
+    const char* value;
+} vlc_http_header_pair_t;
+
+/* Headers are stored as a dictionary.
+   For instance 'User-agent: foo' is stored as { "User-agent", "foo" }.
+*/
+typedef struct
+{
+    vlc_http_header_pair_t *dictionary;
+    size_t                  size;
+} vlc_http_header_t;
+
+typedef struct
+{
+    void*  data;
+    size_t size;
+} vlc_http_body_t;
+
+typedef struct
+{
+    vlc_http_request_line_t request_line;
+    vlc_http_header_t       header;
+    vlc_http_body_t         body;
+} vlc_http_request_t;
+
+typedef struct
+{
+    int http_version;
+    int status_code;
+    /* Reason-Phrase not saved. */
+} vlc_http_status_line_t;
+
+typedef struct
+{
+    vlc_http_status_line_t  status_line;
+    vlc_http_header_t       header;
+    vlc_http_body_t         body;
+} vlc_http_response_t;
+
+VLC_API void http_response_Delete(vlc_http_response_t *response);
+
+VLC_API vlc_http_response_t *http_request_Send(vlc_object_t *p_this, vlc_url_t *url,
+                                               vlc_http_request_t *request);
+#define http_request_Send(a,b,c) http_request_Send(VLC_OBJECT(a),b,c)
+
 #endif /* VLC_HTTP_H */
diff --git a/modules/misc/Modules.am b/modules/misc/Modules.am
index f0b69d4..dd71f28 100644
--- a/modules/misc/Modules.am
+++ b/modules/misc/Modules.am
@@ -6,6 +6,10 @@ libaudioscrobbler_plugin_la_SOURCES = audioscrobbler.c
 libaudioscrobbler_plugin_la_LIBADD = $(SOCKET_LIBS) $(LIBPTHREAD)
 misc_LTLIBRARIES += libaudioscrobbler_plugin.la
 
+libhttprequest_plugin_la_SOURCES = httprequest.c
+libhttprequest_plugin_la_LIBADD = $(SOCKET_LIBS) $(LIBPTHREAD)
+misc_LTLIBRARIES += libhttprequest_plugin.la
+
 libexport_plugin_la_SOURCES = \
 	playlist/html.c \
 	playlist/m3u.c \
diff --git a/src/Makefile.am b/src/Makefile.am
index 474da25..9d46adf 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -424,6 +424,7 @@ SOURCES_libvlc_common = \
 	network/udp.c \
 	network/rootbind.c \
 	network/tls.c \
+	network/http.c \
 	text/charset.c \
 	text/strings.c \
 	text/unicode.c \
diff --git a/src/libvlccore.sym b/src/libvlccore.sym
index 3fc39d7..6fd95d8 100644
--- a/src/libvlccore.sym
+++ b/src/libvlccore.sym
@@ -144,6 +144,8 @@ http_auth_Reset
 http_auth_ParseWwwAuthenticateHeader
 http_auth_ParseAuthenticationInfoHeader
 http_auth_FormatAuthorizationHeader
+http_request_Send
+http_response_Delete
 httpd_ClientIP
 httpd_FileDelete
 httpd_FileNew
diff --git a/src/network/http.c b/src/network/http.c
new file mode 100644
index 0000000..8326533
--- /dev/null
+++ b/src/network/http.c
@@ -0,0 +1,349 @@
+/*****************************************************************************
+ * http.c: functions to send HTTP requests and receive responses.
+ *****************************************************************************
+ * Copyright (C) 2014 VLC authors and VideoLAN
+ *
+ * Authors: Felix Abecassis <felix.abecassis at gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_common.h>
+#include <vlc_url.h>
+#include <vlc_network.h>
+#include <vlc_http.h>
+
+#include <errno.h>
+
+/* Read a line (ended by \r\n) from an HTTP response. */
+static char* http_readline( vlc_object_t *p_this, int fd )
+{
+    size_t size = 64;
+    char *str = malloc(sizeof(*str) * size);
+
+    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;
+    while (*value_begin == ' ')
+        ++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);
+    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))
+        {
+            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));
+        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))
+        {
+            /* 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);
+        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;
+        }
+    }
+    if (!method_string)
+        goto end;
+
+    /* Create the header string. */
+    int ret;
+    header_string = strdup("");
+    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))
+        {
+            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")))
+            goto end;
+
+    /* Force "Connection: close" mode. */
+    if (!(header_string = http_add_header(header_string, "Connection", "close")))
+        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);
+}
-- 
1.8.3.2




More information about the vlc-devel mailing list