diff --git a/src/etag.c b/src/etag.c index e7e9e3f..9189f19 100644 --- a/src/etag.c +++ b/src/etag.c @@ -8,10 +8,104 @@ #endif #include +#include -int etag_is_equal(buffer *etag, const char *matches) { - if (etag && !buffer_is_empty(etag) && 0 == strcmp(etag->ptr, matches)) return 1; - return 0; +#define BEGIN tok = etag->ptr; matches = 1; state = TOKEN +#define TEST matches = matches && (*tok == *current) + +int etag_is_equal(buffer *etag, const char *line, char weak_ok) { + const int MALFORMED_VALUE = 0, ACCEPTED_VALUE = 1; + enum { FOUND_TOKEN = 0, START, TOKEN, MAYBE_WEAK, QUOTING, TAIL } states; + const char *current = line; + const char *tok = etag->ptr; + + if (line[0] == '*' && line[1] == '\0') + return ACCEPTED_VALUE; + + int state = START; + int matches = 0; + + while (*current && state) { + switch (state) { + case START: + switch (*current) { + case 'W': + state = MAYBE_WEAK; + break; + case '"': + BEGIN; + TEST; + break; + case ' ': + case '\t': + break; + default: + return MALFORMED_VALUE; + } + break; + case MAYBE_WEAK: + if (*current == '/') { + ++current; + BEGIN; + matches = matches && weak_ok; + TEST; + } + else + return MALFORMED_VALUE; + break; + case QUOTING: + state = TOKEN; + TEST; + break; + case TOKEN: + switch (*current) { + case '"': + TEST; + if (matches) + state = FOUND_TOKEN; + else + state = TAIL; + break; + case '\\': + TEST; + if (state != QUOTING) + state = QUOTING; + else + state = TOKEN; + break; + case '\0': + break; + default: + TEST; + break; + } + break; + case TAIL: + switch (*current) { + case ',': + case ' ': + case '\t': + /* skip LWS */ + break; + default: + state = START; + continue; + } + break; + default: + return(0); + } + + ++current; + if (matches) { + if (*tok) + ++tok; + else + matches = 0; + } + } + + return !state; /* FOUND_TOKEN = 0 */ } int etag_create(buffer *etag, struct stat *st,etag_flags_t flags) { diff --git a/src/etag.h b/src/etag.h index 97cb063..c83996d 100644 --- a/src/etag.h +++ b/src/etag.h @@ -9,7 +9,7 @@ typedef enum { ETAG_USE_INODE = 1, ETAG_USE_MTIME = 2, ETAG_USE_SIZE = 4 } etag_flags_t; -int etag_is_equal(buffer *etag, const char *matches); +int etag_is_equal(buffer *etag, const char *matches, char weak_ok); int etag_create(buffer *etag, struct stat *st, etag_flags_t flags); int etag_mutate(buffer *mut, buffer *etag); diff --git a/src/http-header-glue.c b/src/http-header-glue.c index fe2f4fb..122dff4 100644 --- a/src/http-header-glue.c +++ b/src/http-header-glue.c @@ -259,11 +259,11 @@ int http_response_handle_cachable(server *srv, connection *con, buffer *mtime) { */ /* last-modified handling */ + char head_or_get = (con->request.http_method == HTTP_METHOD_GET || + con->request.http_method == HTTP_METHOD_HEAD); if (con->request.http_if_none_match) { - if (etag_is_equal(con->physical.etag, con->request.http_if_none_match)) { - if (con->request.http_method == HTTP_METHOD_GET || - con->request.http_method == HTTP_METHOD_HEAD) { - + if (etag_is_equal(con->physical.etag, con->request.http_if_none_match, head_or_get)) { + if (head_or_get) { con->http_status = 304; return HANDLER_FINISHED; } else { @@ -272,9 +272,7 @@ int http_response_handle_cachable(server *srv, connection *con, buffer *mtime) { return HANDLER_FINISHED; } } - } else if (con->request.http_if_modified_since && - (con->request.http_method == HTTP_METHOD_GET || - con->request.http_method == HTTP_METHOD_HEAD)) { + } else if (con->request.http_if_modified_since && head_or_get) { size_t used_len; char *semicolon; diff --git a/tests/cachable.t b/tests/cachable.t index 381f44e..61a2469 100755 --- a/tests/cachable.t +++ b/tests/cachable.t @@ -8,7 +8,7 @@ BEGIN { use strict; use IO::Socket; -use Test::More tests => 13; +use Test::More tests => 24; use LightyTest; my $tf = LightyTest->new(); @@ -117,5 +117,90 @@ EOF $t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; ok($tf->handle_http($t) == 0, 'Conditional GET - ETag + disabled etags on server side'); +############### + +ok($etag =~ /^\"(.*)\"$/, "The server must quote ETags"); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'The client must send a quoted ETag'); + +$etag =~ /^(\".*)\"$/; +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'The ETag must be surrounded by quotes'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ]; +ok($tf->handle_http($t) == 0, 'An unquoted star matches any ETag'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'A quoted star is just a regular ETag'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ]; +ok($tf->handle_http($t) == 0, 'A weak etag matches like a regular ETag for HEAD and GET'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'However, a weak ETag is not *'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ]; +ok($tf->handle_http($t) == 0, 'Client sent a list of ETags, the second matches'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ]; +ok($tf->handle_http($t) == 0, 'The second provided ETag matches weakly'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ]; +ok($tf->handle_http($t) == 0, 'Broken client did get around to sending good data'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ]; +ok($tf->handle_http($t) == 0, 'Bad syntax *after* a matching ETag doesn\'t matter'); + ok($tf->stop_proc == 0, "Stopping lighttpd");