Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse HTTP response with PicoHTTPParser #671

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ openfortivpn_SOURCES = src/config.c src/config.h src/hdlc.c src/hdlc.h \
src/tunnel.h src/main.c src/ssl.h src/xml.c \
src/xml.h src/userinput.c src/userinput.h \
src/openssl_hostname_validation.c \
src/openssl_hostname_validation.h
src/openssl_hostname_validation.h \
src/picohttpparser.c src/picohttpparser.h
openfortivpn_CFLAGS = -Wall -pedantic
openfortivpn_CPPFLAGS = -DSYSCONFDIR=\"$(sysconfdir)\" \
-DPPP_PATH=\"@PPP_PATH@\" \
Expand Down
166 changes: 131 additions & 35 deletions src/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "ipv4.h"
#include "userinput.h"
#include "log.h"
#include "picohttpparser.h"

#include <unistd.h>
#include <arpa/inet.h>
Expand All @@ -42,6 +43,9 @@
/*
* URL-encodes a string for HTTP requests.
*
* RFC 3986 describes the percent-encoding mechanism:
* https://www.rfc-editor.org/info/rfc3986
*
* The dest buffer size MUST be at least strlen(str) * 3 + 1.
*
* @param[out] dest the buffer to write the URL-encoded string
Expand Down Expand Up @@ -145,6 +149,57 @@ static const char *find_header(
}


static inline int header_name_cmp(const struct phr_header header, const char *s)
{
return strncmp(header.name, s, header.name_len);
}


static inline int header_value_cmp(const struct phr_header header, const char *s)
{
return strncmp(header.value, s, header.value_len);
}


/*
* Extract "Content-Size" and "Transfer-Encoding: chunked" from headers.
*
* @param[out] content_size
* @param[out] chunked
* @return 1 in case of success
* < 0 in case of error
*/
static int parse_response_header(const struct phr_header *headers, size_t num_headers,
uint32_t *content_size, int *chunked)
{
uint32_t content_length = 0;
int transfer_encoding_chunked = 0;

for (int i = 0; i != num_headers; i++) {
if (header_name_cmp(headers[i], "Content-Length") == 0) {
long l = strtol(headers[i].name, NULL, 10);

if (errno || l < 0)
return ERR_HTTP_INVALID;
else if (l > UINT32_MAX)
return ERR_HTTP_TOO_LONG;
content_length = l;
}
if (header_name_cmp(headers[i], "Transfer-Encoding") == 0) {
if (header_value_cmp(headers[i], "chunked") == 0)
transfer_encoding_chunked = 1;
else
return ERR_HTTP_INVALID;
}
}
if (content_length && transfer_encoding_chunked)
return ERR_HTTP_INVALID;
*content_size = content_length;
*chunked = transfer_encoding_chunked;
return 1;
}


/*
* Receives data from the HTTP server.
*
Expand All @@ -162,18 +217,25 @@ int http_receive(
{
uint32_t capacity = HTTP_BUFFER_SIZE;
char *buffer;
uint32_t bytes_read = 0;
uint32_t bytes_read = 0, last_bytes_read = 0;
uint32_t header_size = 0;
uint32_t content_size = 0;
int chunked = 0;
struct phr_chunked_decoder decoder = {0}; // zero-clear

buffer = malloc(capacity + 1); // room for terminal '\0'
if (buffer == NULL)
return ERR_HTTP_NO_MEM;

while (1) {
int n;
int minor_version, status;
const char *msg;
size_t msg_len;
struct phr_header headers[100]; // FIXME
size_t num_headers = ARRAY_SIZE(headers);

/* read SSL stream into buffer as long as ERR_SSL_AGAIN */
while ((n = safe_ssl_read(tunnel->ssl_handle,
(uint8_t *) buffer + bytes_read,
capacity - bytes_read)) == ERR_SSL_AGAIN)
Expand All @@ -184,47 +246,81 @@ int http_receive(
free(buffer);
return ERR_HTTP_SSL;
}
last_bytes_read = bytes_read;
bytes_read += n;

log_debug_details("%s:\n%s\n", __func__, buffer);

if (!header_size) {
/* Have we reached the end of the HTTP header? */
static const char EOH[4] = "\r\n\r\n";
const char *eoh = memmem(buffer, bytes_read,
EOH, sizeof(EOH));

if (eoh) {
header_size = eoh - buffer + sizeof(EOH);

/* Get the body size. */
const char *header = find_header(buffer,
"Content-Length: ",
header_size);

if (header)
content_size = atoi(header);

if (find_header(buffer,
"Transfer-Encoding: chunked",
header_size))
chunked = 1;
if (content_size == 0 && !chunked) {
/* parse the HTTP response header */
n = phr_parse_response(buffer, bytes_read,
&minor_version, &status,
&msg, &msg_len,
headers, &num_headers,
last_bytes_read);
if (n > 0) {
header_size = n;
log_error("*** header_size: %u\n", header_size);
n = parse_response_header(headers, num_headers,
&content_size, &chunked);
if (content_size)
log_error("*** content_size: %u\n", content_size);
if (chunked)
log_error("*** chunked: %d\n", chunked);
if (n < 0) {
free(buffer);
return n;
}
if (content_size > UINT32_MAX - header_size) {
free(buffer);
return ERR_HTTP_TOO_LONG;
} else if (content_size == 0 && !chunked) {
free(buffer);
return ERR_HTTP_INVALID;
}
} else if (n == -2) {
/* response is partial, continue the loop */
} else {
/* failed to parse the response header */
assert(n == -1);
free(buffer);
return ERR_HTTP_INVALID;
}
}

if (header_size) {
/* Have we reached the end of the HTTP body? */
if (chunked) {
static const char EOB[7] = "\r\n0\r\n\r\n";

/* Last chunk terminator. Done naively. */
if (bytes_read >= sizeof(EOB) &&
!memcmp(&buffer[bytes_read - sizeof(EOB)],
EOB, sizeof(EOB)))
break;
if (chunked) {
size_t bufsz = bytes_read - header_size;

n = phr_decode_chunked(&decoder,
&buffer[header_size], &bufsz);

if (n >= 0) {
/* body is complete */
if (n > 0) {
/* garbage after body */
log_warn("Garbage after body (%d bytes).\n",
n);
}
} else if (n == -2) {
/* body is incomplete, continue the loop */
} else {
if (bytes_read >= header_size + content_size)
break;
/* failed to parse the chunked body */
assert(n == -1);
free(buffer);
return ERR_HTTP_INVALID;
}
//~ /* parse the HTTP response chunked body */
//~ static const char EOB[7] = "\r\n0\r\n\r\n";

//~ /* Last chunk terminator. Done naively. */
//~ if (bytes_read >= sizeof(EOB) &&
//~ !memcmp(&buffer[bytes_read - sizeof(EOB)],
//~ EOB, sizeof(EOB)))
//~ break;
} else if (content_size > 0) {
/* read until the end of the HTTP response body */
if (bytes_read >= header_size + content_size) {
buffer[bytes_read] = '\0';
break;
}
}

Expand Down
Loading