Skip to content
Open
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
67 changes: 54 additions & 13 deletions src/cmd.c
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,22 @@ cmd_select_format(struct http_client *client, struct cmd *cmd,
const char *uri, size_t uri_len, formatting_fun *f_format) {

const char *ext;
int ext_len = -1;
const char *suffix;
int ext_len = 0;
int suffix_len = 0;
int parsed_uri_len = uri_len;
unsigned int i;
int found = 0; /* did we match it to a predefined format? */
int found = 0; /* did we match the extension and/or the suffix to a predefined format? */

/* those are the available reply formats */
struct reply_format {
/* those are the available content-type formats */
struct content_type_format {
const char *s;
size_t sz;
formatting_fun f;
const char *ct;
};
struct reply_format funs[] = {
{.s = "json", .sz = 4, .f = json_reply, .ct = "application/json"},
struct content_type_format funs[] = {
{.s = "json", .sz = 4, . f = json_reply, .ct = "application/json"},
{.s = "raw", .sz = 3, .f = raw_reply, .ct = "binary/octet-stream"},

#ifdef MSGPACK
Expand All @@ -341,26 +344,55 @@ cmd_select_format(struct http_client *client, struct cmd *cmd,
{.s = "jpeg", .sz = 4, .f = custom_type_reply, .ct = "image/jpeg"},

{.s = "js", .sz = 2, .f = json_reply, .ct = "application/javascript"},
{.s = "css", .sz = 3, .f = custom_type_reply, .ct = "text/css"},
{.s = "css", .sz = 3, .f = custom_type_reply, .ct = "text/css"}
};

/* those are the available content-encoding formats */
struct content_encoding_format {
const char *s;
size_t sz;
formatting_fun f;
};
struct content_encoding_format sfxs[] = {
{.s = "gzip", .sz = 4, . f = custom_type_reply},
{.s = "br", .sz = 2, . f = custom_type_reply},
{.s = "zstd", .sz = 4, . f = custom_type_reply}
};

/* default */
*f_format = json_reply;

/* find extension */
/* find extension and/or suffix */
for(ext = uri + uri_len - 1; ext != uri && *ext != '/'; --ext) {
if(*ext == '.') {
ext++;
ext_len = uri + uri_len - ext;
suffix = ext + 1;
suffix_len = uri + uri_len - suffix;

for(ext = ext - 1; ext != uri && *ext != '/'; --ext) {

Check notice

Code scanning / CodeQL

For loop variable changed in body

Loop counters should not be modified in the body of the [loop](1).

Check notice

Code scanning / CodeQL

For loop variable changed in body

Loop counters should not be modified in the body of the [loop](1).
if(*ext == '.') {
ext++;

Check notice

Code scanning / CodeQL

For loop variable changed in body

Loop counters should not be modified in the body of the [loop](1). Loop counters should not be modified in the body of the [loop](2).
ext_len = suffix - ext - 1;
break;
}

}
break;
}
}
if(!ext_len) return uri_len; /* nothing found */
if(!suffix_len) return uri_len; /* nothing found */

if(ext_len) { /* both extension and suffix are found, as in 'key.ext.suffix' */
parsed_uri_len = uri_len - ext_len - 1 - suffix_len - 1;
} else {
ext = suffix;
ext_len = suffix_len;

parsed_uri_len = uri_len - ext_len - 1;
}

/* find function for the given extension */
for(i = 0; i < sizeof(funs)/sizeof(funs[0]); ++i) {
if(ext_len == (int)funs[i].sz && strncmp(ext, funs[i].s, ext_len) == 0) {

if(cmd->mime_free) free(cmd->mime);
cmd->mime = (char*)funs[i].ct;
cmd->mime_free = 0;
Expand All @@ -376,8 +408,17 @@ cmd_select_format(struct http_client *client, struct cmd *cmd,
/* /!\ we don't copy cmd->mime, this is done soon after in cmd_setup */
}

/* override function if suffix matches a known content-encoding */
for(i = 0; i < sizeof(sfxs)/sizeof(sfxs[0]); ++i) {
if(suffix_len == (int)sfxs[i].sz && strncmp(suffix, sfxs[i].s, suffix_len) == 0) {
cmd->content_encoding = (char *)sfxs[i].s;
*f_format = sfxs[i].f;
found = 1;
}
}

if(found) {
return uri_len - ext_len - 1;
return parsed_uri_len;
} else {
/* no matching format, use default output with the full argument, extension included. */
return uri_len;
Expand Down
2 changes: 2 additions & 0 deletions src/cmd.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ struct cmd {
char *mime; /* forced output content-type */
int mime_free; /* need to free mime buffer */

char *content_encoding; /* forced output content-encoding */

char *filename; /* content-disposition attachment */

char *if_none_match; /* used with ETags */
Expand Down
4 changes: 4 additions & 0 deletions src/formats/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ format_send_reply(struct cmd *cmd, const char *p, size_t sz, const char *content

int free_cmd = 1;
const char *ct = cmd->mime?cmd->mime:content_type;
const char *ce = cmd->content_encoding;
struct http_response *resp;

if(cmd->is_websocket) {
Expand Down Expand Up @@ -113,6 +114,9 @@ format_send_reply(struct cmd *cmd, const char *p, size_t sz, const char *content
}
http_response_set_header(resp, "Content-Type", ct, HEADER_COPY_VALUE);
http_response_set_header(resp, "ETag", etag, HEADER_COPY_VALUE);
if(ce) {
http_response_set_header(resp, "Content-Encoding", ce, HEADER_COPY_VALUE);
}
http_response_set_body(resp, p, sz);
}
resp->http_version = cmd->http_version;
Expand Down
30 changes: 29 additions & 1 deletion tests/basic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/python3
import urllib.request, urllib.error, urllib.parse, unittest, json, hashlib, threading, uuid, time
import urllib.request, urllib.error, urllib.parse, unittest, json, hashlib, threading, uuid, time, gzip
from functools import wraps
try:
import msgpack
Expand All @@ -19,6 +19,10 @@ def query(self, url, data = None, headers={}):
r = urllib.request.Request(self.wrap(url), data, headers)
return urllib.request.urlopen(r)

def put(self, url, data):
r = urllib.request.Request(self.wrap(url), data=data, method='PUT')
return urllib.request.urlopen(r)

class TestBasics(TestWebdis):

def test_crossdomain(self):
Expand Down Expand Up @@ -72,6 +76,30 @@ def test_list(self):
self.assertTrue(f.getheader('ETag') == '"622e51f547a480bef7cf5452fb7782db"')
self.assertTrue(f.read() == b'{"LRANGE":["abc","def"]}')

def test_encoding_with_extension_and_suffix(self):
"success type (+OK)"
self.query('DEL/world')
# NOTE: the gzip implementation in python generates OS dependent headers.
# > gzip.compress(('{"user_id": 1234}').encode('utf-8'), mtime=0)
gzipped_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xabV*-N-\x8a\xcfLQ\xb2R0426\xa9\x05\x00\x07\xb91\xf2\x11\x00\x00\x00'
self.put('SET/world', gzipped_data)
f = self.query('GET/world.json.gzip')
self.assertTrue(f.getheader('Content-Type') == 'application/json')
self.assertTrue(f.getheader('Content-Encoding') == 'gzip')
self.assertTrue(f.getheader('ETag') == '"8c50e25769b3ee8892d466d536a6ce2f"')
self.assertTrue(gzip.decompress(f.read()) == b'{"user_id": 1234}')

def test_encoding_with_suffix(self):
"success type (+OK)"
self.query('DEL/world')
gzipped_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xabV*-N-\x8a\xcfLQ\xb2R0426\xa9\x05\x00\x07\xb91\xf2\x11\x00\x00\x00'
self.put('SET/world', gzipped_data)
f = self.query('GET/world.gzip?type=text/plain')
self.assertTrue(f.getheader('Content-Type') == 'text/plain')
self.assertTrue(f.getheader('Content-Encoding') == 'gzip')
self.assertTrue(f.getheader('ETag') == '"8c50e25769b3ee8892d466d536a6ce2f"')
self.assertTrue(gzip.decompress(f.read()) == b'{"user_id": 1234}')

def test_error(self):
"error return type"
f = self.query('UNKNOWN/COMMAND')
Expand Down