1
0
mirror of https://gitlab.xiph.org/xiph/icecast-common.git synced 2024-07-14 02:54:14 -04:00
icecast-common/httpp/encoding.c
2018-10-12 11:27:17 +00:00

838 lines
24 KiB
C

/* encoding.c
**
** http transfer encoding library
** See RFC2616 section 3.6 for more details.
**
** Copyright (C) 2015-2018 Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Library General Public
** License as published by the Free Software Foundation; either
** version 2 of the License, or (at your option) any later version.
**
** This library 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
** Library General Public License for more details.
**
** You should have received a copy of the GNU Library General Public
** License along with this library; if not, write to the
** Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
** Boston, MA 02110-1301, USA.
**
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/types.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "encoding.h"
struct igloo_httpp_encoding_tag {
size_t refc;
ssize_t (*process_read)(igloo_httpp_encoding_t *self, void *buf, size_t len, ssize_t (*cb)(void*, void*, size_t), void *userdata);
ssize_t (*process_write)(igloo_httpp_encoding_t *self, const void *buf, size_t len, ssize_t (*cb)(void*, const void*, size_t), void *userdata);
httpp_meta_t *meta_read;
httpp_meta_t *meta_write;
void *buf_read_raw; /* input buffer */
size_t buf_read_raw_offset, buf_read_raw_len;
void *buf_read_decoded; /* decoded stuff */
size_t buf_read_decoded_offset, buf_read_decoded_len;
void *buf_write_raw; /* input buffer */
size_t buf_write_raw_offset, buf_write_raw_len;
void *buf_write_encoded; /* encoded output */
size_t buf_write_encoded_offset, buf_write_encoded_len;
/* backend specific stuff */
ssize_t bytes_till_eof;
size_t read_bytes_till_header;
};
/* Handlers, at end of file */
static ssize_t igloo___enc_identity_read(igloo_httpp_encoding_t *self, void *buf, size_t len, ssize_t (*cb)(void*, void*, size_t), void *userdata);
static ssize_t igloo___enc_identity_write(igloo_httpp_encoding_t *self, const void *buf, size_t len, ssize_t (*cb)(void*, const void*, size_t), void *userdata);
static ssize_t igloo___enc_chunked_read(igloo_httpp_encoding_t *self, void *buf, size_t len, ssize_t (*cb)(void*, void*, size_t), void *userdata);
static ssize_t igloo___enc_chunked_write(igloo_httpp_encoding_t *self, const void *buf, size_t len, ssize_t (*cb)(void*, const void*, size_t), void *userdata);
/* function to move some data out of our buffers */
ssize_t igloo___copy_buffer(void *dst, void **src, size_t *boffset, size_t *blen, size_t len)
{
void *p;
size_t have_len;
size_t todo;
if (!len)
return 0;
if (!dst || !src || !*src || !boffset || !blen)
return -1;
have_len = *blen - *boffset;
p = *src + *boffset;
todo = len < have_len ? len : have_len;
memcpy(dst, p, todo);
*boffset += todo;
if (*boffset == *blen) {
free(*src);
*src = NULL;
*boffset = 0;
*blen = 0;
}
return todo;
}
/* try to flush output buffers */
static inline void __flush_output(igloo_httpp_encoding_t *self, ssize_t (*cb)(void*, const void*, size_t), void *userdata)
{
if (cb && self->buf_write_encoded) {
ssize_t ret = cb(userdata,
self->buf_write_encoded + self->buf_write_encoded_offset,
self->buf_write_encoded_len - self->buf_write_encoded_offset);
if (ret > 0) {
self->buf_write_encoded_offset += ret;
if (self->buf_write_encoded_offset == self->buf_write_encoded_len) {
free(self->buf_write_encoded);
self->buf_write_encoded = NULL;
self->buf_write_encoded_offset = 0;
self->buf_write_encoded_len = 0;
}
}
}
}
/* meta data functions */
/* meta data is to be used in a encoding-specific way */
httpp_meta_t *igloo_httpp_encoding_meta_new(const char *key, const char *value)
{
httpp_meta_t *ret = calloc(1, sizeof(httpp_meta_t));
if (!ret)
return ret;
if (key)
if ((ret->key = strdup(key)) == NULL)
goto fail;
if (value) {
if ((ret->value = strdup(value)) == NULL)
goto fail;
ret->value_len = strlen(ret->value);
}
return ret;
fail:
igloo_httpp_encoding_meta_free(ret);
return NULL;
}
int igloo_httpp_encoding_meta_free(httpp_meta_t *self)
{
while (self) {
httpp_meta_t *cur = self;
self = self->next;
if (cur->key)
free(cur->key);
if (cur->value)
free(cur->value);
free(cur);
}
return 0;
}
int igloo_httpp_encoding_meta_append(httpp_meta_t **dst, httpp_meta_t *next)
{
httpp_meta_t **cur;
if (!dst)
return -1;
if (!next)
return 0;
cur = dst;
while (*cur)
cur = &((*cur)->next);
*cur = next;
return 0;
}
/* General setup */
igloo_httpp_encoding_t *igloo_httpp_encoding_new(const char *encoding) {
igloo_httpp_encoding_t *ret = calloc(1, sizeof(igloo_httpp_encoding_t));
if (!ret)
return NULL;
ret->refc = 1;
ret->bytes_till_eof = -1;
if (strcasecmp(encoding, igloo_HTTPP_ENCODING_IDENTITY) == 0) {
ret->process_read = igloo___enc_identity_read;
ret->process_write = igloo___enc_identity_write;
} else if (strcasecmp(encoding, igloo_HTTPP_ENCODING_CHUNKED) == 0) {
ret->process_read = igloo___enc_chunked_read;
ret->process_write = igloo___enc_chunked_write;
} else {
goto fail;
}
return ret;
fail:
igloo_httpp_encoding_release(ret);
return NULL;
}
int igloo_httpp_encoding_addref(igloo_httpp_encoding_t *self)
{
if (!self)
return -1;
self->refc++;
return 0;
}
int igloo_httpp_encoding_release(igloo_httpp_encoding_t *self)
{
if (!self)
return -1;
self->refc--;
if (self->refc)
return 0;
igloo_httpp_encoding_meta_free(self->meta_read);
igloo_httpp_encoding_meta_free(self->meta_write);
if (self->buf_read_raw)
free(self->buf_read_raw);
if (self->buf_read_decoded)
free(self->buf_read_decoded);
if (self->buf_write_raw)
free(self->buf_write_raw);
if (self->buf_write_encoded)
free(self->buf_write_encoded);
free(self);
return 0;
}
/* Read data from backend.
* if cb is NULL this will read from the internal buffer.
*/
ssize_t igloo_httpp_encoding_read(igloo_httpp_encoding_t *self, void *buf, size_t len, ssize_t (*cb)(void*, void*, size_t), void *userdata)
{
ssize_t done = 0;
ssize_t ret;
if (!self || !buf)
return -1;
if (!len)
return 0;
ret = igloo___copy_buffer(buf, &(self->buf_read_decoded), &(self->buf_read_decoded_offset), &(self->buf_read_decoded_len), len);
if (ret == (ssize_t)len)
return ret;
if (ret > 0) {
done += ret;
buf += ret;
len -= ret;
}
ret = self->process_read(self, buf, len, cb, userdata);
if (ret == -1 && done)
return done;
if (ret == -1)
return -1;
done += ret;
buf += ret;
len -= ret;
if (len) {
ret = igloo___copy_buffer(buf, &(self->buf_read_decoded), &(self->buf_read_decoded_offset), &(self->buf_read_decoded_len), len);
if (ret > 0) {
done += ret;
buf += ret;
len -= ret;
}
}
return done;
}
int igloo_httpp_encoding_eof(igloo_httpp_encoding_t *self, int (*cb)(void*), void *userdata)
{
if (!self)
return -1;
if (self->buf_read_decoded_len - self->buf_read_decoded_offset)
return 0;
if (self->bytes_till_eof == 0)
return 1;
if (cb)
return cb(userdata);
return 0;
}
/* Read any meta data that is in buffer.
* After a call to this function the meta data is released from the
* encoding object and the caller is responsible to free it.
*/
httpp_meta_t *igloo_httpp_encoding_get_meta(igloo_httpp_encoding_t *self)
{
httpp_meta_t *ret;
if (!self)
return NULL;
ret = self->meta_read;
self->meta_read = NULL;
return ret;
}
/* Write data to backend.
* If buf is NULL this will flush buffers.
* Depending on encoding flushing buffers may not be safe if not
* at end of stream.
*/
ssize_t igloo_httpp_encoding_write(igloo_httpp_encoding_t *self, const void *buf, size_t len, ssize_t (*cb)(void*, const void*, size_t), void *userdata)
{
ssize_t ret;
if (!self || !cb)
return -1;
/* first try to flush buffers */
__flush_output(self, cb, userdata);
/* now run the processor */
ret = self->process_write(self, buf, len, cb, userdata);
/* try to flush buffers again, maybe they are filled now! */
__flush_output(self, cb, userdata);
return ret;
}
/* Check if we have something to flush. */
ssize_t igloo_httpp_encoding_pending(igloo_httpp_encoding_t *self)
{
if (!self)
return -1;
if (!self->buf_write_encoded)
return 0;
return self->buf_write_encoded_len - self->buf_write_encoded_offset;
}
/* Attach meta data to the stream.
* this is to be written out as soon as the encoding supports.
*/
int igloo_httpp_encoding_append_meta(igloo_httpp_encoding_t *self, httpp_meta_t *meta)
{
if (!self)
return -1;
return igloo_httpp_encoding_meta_append(&(self->meta_write), meta);
}
/* handlers for encodings */
static ssize_t igloo___enc_identity_read(igloo_httpp_encoding_t *self, void *buf, size_t len, ssize_t (*cb)(void*, void*, size_t), void *userdata)
{
(void)self;
if (!cb)
return -1;
return cb(userdata, buf, len);
}
static ssize_t igloo___enc_identity_write(igloo_httpp_encoding_t *self, const void *buf, size_t len, ssize_t (*cb)(void*, const void*, size_t), void *userdata)
{
(void)self;
if (!cb)
return -1;
return cb(userdata, buf, len);
}
/* Here is what chunked encoding looks like:
*
* You have any number of chunks. They are just chained.
* The last chunk has a length of zero set.
* After the last chunk there are tailing headers and a final "\r\n".
*
* Each chunk looks like this:
* LENGTH as hex [";" extension [";" extention [...]]] "\r\n" BODY "\r\n".
* extension is: TOKEN ["=" data];
* data is: TOKEN | STRING as quoted.
*
*/
static void __enc_chunked_read_extentions(igloo_httpp_encoding_t *self, char *p, size_t len)
{
/* ok. If you want to ruin your day... go ahead and try to understand this. */
httpp_meta_t **parent;
httpp_meta_t *meta;
size_t value_len;
size_t value_decoded_len;
size_t key_len;
char *value;
char *c;
int in_quote;
if (self->meta_read) {
parent = &(self->meta_read);
while (*parent)
parent = &((*parent)->next);
} else {
parent = &(self->meta_read);
}
while (len) {
if (*p != ';') /* not a valid extension */
break;
p++;
len--;
*parent = meta = igloo_httpp_encoding_meta_new(NULL, NULL);
parent = &(meta->next);
for (key_len = 0; key_len < len && p[key_len] != '='; key_len++);
meta->key = malloc(key_len + 1);
if (!meta->key)
return;
memcpy(meta->key, p, key_len);
meta->key[key_len] = 0;
p += key_len;
len -= key_len;
if (!len)
break;
if (*p != '=') /* check if we have a value */
continue;
p++;
len--;
if (!len)
break;
in_quote = 0;
for (value_len = 0; value_len < len; value_len++) {
if (in_quote) {
if (p[value_len] == '\\') in_quote = 2;
else if (p[value_len] == '"') in_quote--;
if (in_quote)
continue;
value_len++;
break;
}
if (p[value_len] == '"') {
in_quote = 1;
} else if (p[value_len] == ';')
break;
}
value = meta->value = malloc(value_len + 1);
if (!meta->value)
return;
in_quote = 0;
for (c = p, p += value_len, len -= value_len, value_decoded_len = 0; value_len; value_len--, c++) {
if (in_quote) {
if (*c == '\\') in_quote = 2;
else if (*c == '"') {
in_quote--;
if (in_quote)
value[value_decoded_len++] = '"';
} else
value[value_decoded_len++] = *c;
if (in_quote)
continue;
break;
}
if (*c == '"') {
in_quote = 1;
} else if (p[value_len] == ';')
break;
else
value[value_decoded_len++] = *c;
}
value[value_decoded_len] = 0;
meta->value_len = value_decoded_len;
}
}
static ssize_t igloo___enc_chunked_read(igloo_httpp_encoding_t *self, void *buf, size_t len, ssize_t (*cb)(void*, void*, size_t), void *userdata)
{
ssize_t ret;
size_t buflen;
void *bufptr;
size_t i;
char *c;
int in_quote;
ssize_t offset_extentions;
ssize_t offset_CR;
ssize_t offset_LF;
long long unsigned int bodylen;
if (!cb)
return -1;
/* see if we have still a few bytes to go till the next header
* The 2 is the end of chunk mark that is not part of the body! */
if (self->read_bytes_till_header > 2) {
size_t todo = len > (self->read_bytes_till_header - 2) ? (self->read_bytes_till_header - 2) : len;
ret = cb(userdata, buf, todo);
if (ret < 1)
return ret;
self->read_bytes_till_header -= ret;
return ret;
}
/* ok. now we should have 2 or less bytes till next header.
* We just read a few bytes into our decoding buffer and see what we got.
*/
if (self->bytes_till_eof != -1) {
buflen = self->bytes_till_eof;
} else {
buflen = 1024;
}
if (self->buf_read_raw) {
bufptr = realloc(self->buf_read_raw, self->buf_read_raw_len + buflen);
} else {
bufptr = malloc(buflen);
self->buf_read_raw_offset = 0;
self->buf_read_raw_len = 0;
}
if (!bufptr)
return -1;
self->buf_read_raw = bufptr;
ret = cb(userdata, self->buf_read_raw + self->buf_read_raw_len, buflen);
if (ret < 1) {
if (!self->buf_read_raw_len) {
free(self->buf_read_raw);
self->buf_read_raw = NULL;
}
return ret;
}
self->buf_read_raw_len += ret;
/* now we should have some bytes in our buffer.
* Now skip the "\r\n" that may be left to do from self->read_bytes_till_header.
*/
if ((self->buf_read_raw_len - self->buf_read_raw_offset) == self->read_bytes_till_header) {
/* ok, that wasn't super big step forward. At least we got rid of self->read_bytes_till_header.
* now freeing buffers again...
*/
free(self->buf_read_raw);
self->buf_read_raw = NULL;
self->buf_read_raw_offset = 0;
self->buf_read_raw_len = 0;
self->read_bytes_till_header = 0;
return 0;
} else if ((self->buf_read_raw_len - self->buf_read_raw_offset) > self->read_bytes_till_header) {
/* ship the tailing "\r\n".
* We should check we really got that and not some other stuff, eh? Yes, I'm sure we should...
*/
self->buf_read_raw_offset += self->read_bytes_till_header;
self->read_bytes_till_header = 0;
}
/* Check if we reached EOF */
if (self->bytes_till_eof != -1) {
if (self->read_bytes_till_header == 0)
self->bytes_till_eof = 0;
if (self->bytes_till_eof == 0)
return 0;
}
/* ok. next we have at least a little bit if a header.
* Now we need to find out if the header is complet.
* If it is we will process it. If it isn't we will
* just return a short read and try again later!
*
* Hint: A complet header ends with "\r\n".
* But be aware! That is valid in any quoted string
* that is part of the header as well!
*/
in_quote = 0;
offset_extentions = -1;
offset_CR = -1;
offset_LF = -1;
for (i = self->buf_read_raw_offset, c = self->buf_read_raw + self->buf_read_raw_offset;
i < self->buf_read_raw_len;
i++, c++) {
if (in_quote) {
if (*c == '\\') in_quote = 2;
else if (*c == '"') in_quote--;
continue;
}
if (*c == '"') {
in_quote = 1;
} else if (*c == ';' && offset_extentions == -1) {
offset_extentions = i;
} else if (*c == '\r') {
offset_CR = i;
} else if (*c == '\n' && offset_CR == (ssize_t)(i - 1)) {
offset_LF = i;
break;
}
}
/* ok, now we know a lot more!:
* offset_extentions is the offset to the extentions if any.
* offset_LF is the offset to the byte before the body if any.
* if offset_LF is > -1 we know we have a complet header.
* if we don't just return a short read and try again later.
*/
if (offset_LF == -1)
return 0;
/* ok. Now we have a complet header.
* First pass the extentions to extention parser if any.
*/
if (offset_extentions != -1)
__enc_chunked_read_extentions(self, self->buf_read_raw + offset_extentions, offset_CR - offset_extentions);
/* ok. Next we parse the body length.
* We just replace whatever comes after the length by \0
* and try to parse the hex value.
*/
if (offset_extentions != -1) {
c = self->buf_read_raw + offset_extentions;
} else {
c = self->buf_read_raw + offset_CR;
}
*c = 0;
/* we hope that will work... */
if (sscanf(self->buf_read_raw + self->buf_read_raw_offset, "%llx", &bodylen) != 1)
return -1;
/* ok, Now we move the offset forward to the body. */
self->buf_read_raw_offset = offset_LF + 1;
if (!bodylen) {
self->bytes_till_eof = 2; /* 2 = tailing "\r\n" */
}
/* Do we still have some data in buffer?
* If not free the buffer and set the counters
* to point to the next header.
*/
if (self->buf_read_raw_offset == self->buf_read_raw_len) {
free(self->buf_read_raw);
self->buf_read_raw = NULL;
self->buf_read_raw_offset = 0;
self->buf_read_raw_len = 0;
self->read_bytes_till_header = bodylen + 2; /* 2 = tailing "\r\n" */
return 0;
}
/* ok, now we check if what we have in the buffer is less or equal than our bodylen. */
if ((self->buf_read_raw_len - self->buf_read_raw_offset) <= bodylen) {
/* ok, this is fantastic. The framework can do the rest for us! */
self->buf_read_decoded = self->buf_read_raw;
self->buf_read_decoded_offset = self->buf_read_raw_offset;
self->buf_read_decoded_len = self->buf_read_raw_len;
self->buf_read_raw = NULL;
self->buf_read_raw_offset = 0;
self->buf_read_raw_len = 0;
self->read_bytes_till_header = bodylen + 2 - (self->buf_read_decoded_len - self->buf_read_decoded_offset);
return 0;
}
/* ok, final case left:
* we have more than just the body in our buffer.
* What we do now is to allocate buffer, move the body over
* and let our internal structures point to the next header.
*/
self->buf_read_decoded = malloc(bodylen);
if (!self->buf_read_decoded) /* just retry later if we can not allocate a buffer */
return -1;
self->buf_read_decoded_offset = 0;
self->buf_read_decoded_len = bodylen;
memcpy(self->buf_read_decoded, self->buf_read_raw + self->buf_read_raw_offset, bodylen);
self->buf_read_raw_offset += bodylen;
self->read_bytes_till_header = 2; /* tailing "\r\n" */
if ((self->buf_read_raw_len - self->buf_read_raw_offset) >= 2) {
self->buf_read_raw_offset += 2;
self->read_bytes_till_header = 0;
if (self->bytes_till_eof != -1)
self->bytes_till_eof = 0;
if ((self->buf_read_raw_len - self->buf_read_raw_offset) == 0) {
free(self->buf_read_raw);
self->buf_read_raw = NULL;
self->buf_read_raw_len = 0;
self->buf_read_raw_offset = 0;
}
}
return 0;
}
static size_t __enc_chunked_write_extensions_valuelen(httpp_meta_t *cur)
{
size_t ret = cur->value_len;
size_t i;
char *p = cur->value;
if (!cur->value || !cur->value_len)
return 0;
for (i = 0; i < cur->value_len; i++, p++)
if (*p == '"')
ret++;
return ret;
}
static char *__enc_chunked_write_extensions(igloo_httpp_encoding_t *self)
{
size_t buflen;
void *buf;
char *p;
size_t len;
httpp_meta_t *cur;
if (!self->meta_write)
return NULL;
/* first find out how long the buffer must be. */
buflen = 1; /* tailing \0 */
cur = self->meta_write;
while (cur) {
if (!cur->key || (cur->value_len && !cur->value)) {
cur = cur->next;
continue;
}
buflen += 4; /* ; and = and two " */
buflen += strlen(cur->key);
buflen += __enc_chunked_write_extensions_valuelen(cur);
cur = cur->next;
}
p = buf = malloc(buflen);
if (!buf)
return NULL;
cur = self->meta_write;
while (cur) {
if (!cur->key || (cur->value_len && !cur->value)) {
cur = cur->next;
continue;
}
*(p++) = ';';
len = strlen(cur->key);
memcpy(p, cur->key, len);
p += len;
if (cur->value_len) {
const char *c;
size_t i;
*(p++) = '=';
*(p++) = '"';
for (i = 0, c = cur->value; i < cur->value_len; i++, c++) {
if (*c == '"')
*(p++) = '\\';
*(p++) = *c;
}
*(p++) = '"';
}
cur = cur->next;
}
*p = 0; /* terminate the string */
igloo_httpp_encoding_meta_free(self->meta_write);
self->meta_write = NULL;
return buf;
}
static ssize_t igloo___enc_chunked_write(igloo_httpp_encoding_t *self, const void *buf, size_t len, ssize_t (*cb)(void*, const void*, size_t), void *userdata)
{
char encoded_length[32];
char *extensions = NULL;
ssize_t total_chunk_size;
ssize_t header_length;
(void)cb, (void)userdata;
if (!buf)
len = 0;
/* refuse to write if we still have stuff to flush. */
if (igloo_httpp_encoding_pending(self) > 0)
return 0;
/* limit length to a bit more sane value */
if (len > 1048576)
len = 1048576;
snprintf(encoded_length, sizeof(encoded_length), "%lx", (long int)len);
extensions = __enc_chunked_write_extensions(self);
/* 2 = end of header and tailing "\r\n" */
header_length = strlen(encoded_length) + (extensions ? strlen(extensions) : 0) + 2;
total_chunk_size = header_length + len + 2;
if (!buf)
total_chunk_size += 2;
/* ok, we now allocate a huge buffer. We do it as if we would do it only when needed
* and it would fail we would end in bad state that can not be recovered */
self->buf_write_encoded = malloc(total_chunk_size);
if (!self->buf_write_encoded) {
if (extensions)
free(extensions);
return -1;
}
self->buf_write_encoded_offset = 0;
self->buf_write_encoded_len = total_chunk_size;
snprintf(self->buf_write_encoded, total_chunk_size, "%s%s\r\n", encoded_length, extensions ? extensions : "");
memcpy(self->buf_write_encoded + header_length, buf, len);
memcpy(self->buf_write_encoded + header_length + len, "\r\n\r\n", buf ? 2 : 4);
if (extensions)
free(extensions);
return len;
}