parse If-None-Match for ETag validation (fixes #2578)

From: Stefan Bühler <stbuehler@web.de>

git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2994 152afb58-edef-0310-8abb-c4023f1b3aa9
This commit is contained in:
Stefan Bühler 2015-07-05 16:59:01 +00:00
parent 71b5c53a0a
commit 9f05b61ab4
5 changed files with 247 additions and 12 deletions

1
NEWS
View File

@ -22,6 +22,7 @@ NEWS
* escape all strings for logging (fixes #2646 log file injection, reported by Jaanus Kääp)
* fix hex escape in accesslog (fixes #2559)
* show extforward re-run warning only with debug.log-request-handling (fixes #2561)
* parse If-None-Match for ETag validation (fixes #2578)
- 1.4.35 - 2014-03-12
* [network/ssl] fix build error if TLSEXT is disabled

View File

@ -9,8 +9,139 @@
#include <string.h>
int etag_is_equal(buffer *etag, const char *matches) {
if (etag && !buffer_string_is_empty(etag) && 0 == strcmp(etag->ptr, matches)) return 1;
int etag_is_equal(buffer *etag, const char *line, int weak_ok) {
enum {
START = 0,
CHECK,
CHECK_QUOTED,
SKIP,
SKIP_QUOTED,
TAIL
} state = START;
const char *current;
const char *tok_start = etag->ptr;
const char *tok = NULL;
int matched;
if ('*' == line[0] && '\0' == line[1]) {
return 1;
}
if (!etag || buffer_string_is_empty(etag)) return 0;
if ('W' == tok_start[0]) {
if (!weak_ok || '/' != tok_start[1]) return 0; /* bad etag */
tok_start = tok_start + 2;
}
if ('"' != tok_start[0]) return 0; /* bad etag */
/* we start comparing after the first '"' */
++tok_start;
for (current = line; *current; ++current) {
switch (state) {
case START:
/* wait for etag to start; ignore whitespace and ',' */
switch (*current) {
case 'W':
/* weak etag always starts with 'W/"' */
if ('/' != *++current) return 0; /* bad etag list */
if ('"' != *++current) return 0; /* bad etag list */
if (!weak_ok) {
state = SKIP;
} else {
state = CHECK;
tok = tok_start;
}
break;
case '"':
/* strong etag starts with '"' */
state = CHECK;
tok = tok_start;
break;
case ' ':
case ',':
case '\t':
case '\r':
case '\n':
break;
default:
return 0; /* bad etag list */
}
break;
case CHECK:
/* compare etags (after the beginning '"')
* quoted-pairs must match too (i.e. quoted in both strings):
* > (RFC 2616:) both validators MUST be identical in every way
*/
matched = *tok && *tok == *current;
++tok;
switch (*current) {
case '\\':
state = matched ? CHECK_QUOTED : SKIP_QUOTED;
break;
case '"':
if (*tok) {
/* bad etag - string should end after '"' */
return 0;
}
if (matched) {
/* matching etag: strings were equal */
return 1;
}
state = TAIL;
break;
default:
if (!matched) {
/* strings not matching, skip remainder of etag */
state = SKIP;
}
break;
}
break;
case CHECK_QUOTED:
if (!*tok || *tok != *current) {
/* strings not matching, skip remainder of etag */
state = SKIP;
break;
}
++tok;
state = CHECK;
break;
case SKIP:
/* wait for final (not quoted) '"' */
switch (*current) {
case '\\':
state = SKIP_QUOTED;
break;
case '"':
state = TAIL;
break;
}
break;
case SKIP_QUOTED:
state = SKIP;
break;
case TAIL:
/* search for ',', ignore white space */
switch (*current) {
case ',':
state = START;
break;
case ' ':
case '\t':
case '\r':
case '\n':
break;
default:
return 0; /* bad etag list */
}
break;
}
}
/* no matching etag found */
return 0;
}

View File

@ -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, int weak_ok);
int etag_create(buffer *etag, struct stat *st, etag_flags_t flags);
int etag_mutate(buffer *mut, buffer *etag);

View File

@ -244,7 +244,11 @@ buffer * strftime_cache_get(server *srv, time_t last_mod) {
int http_response_handle_cachable(server *srv, connection *con, buffer *mtime) {
int head_or_get =
( HTTP_METHOD_GET == con->request.http_method
|| HTTP_METHOD_HEAD == con->request.http_method);
UNUSED(srv);
/*
* 14.26 If-None-Match
* [...]
@ -255,12 +259,12 @@ int http_response_handle_cachable(server *srv, connection *con, buffer *mtime) {
* return a 304 (Not Modified) response.
*/
/* last-modified handling */
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) {
/* use strong etag checking for now: weak comparison must not be used
* for ranged requests
*/
if (etag_is_equal(con->physical.etag, con->request.http_if_none_match, 0)) {
if (head_or_get) {
con->http_status = 304;
return HANDLER_FINISHED;
} else {
@ -269,9 +273,8 @@ 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) {
/* last-modified handling */
size_t used_len;
char *semicolon;

View File

@ -8,7 +8,7 @@ BEGIN {
use strict;
use IO::Socket;
use Test::More tests => 13;
use Test::More tests => 25;
use LightyTest;
my $tf = LightyTest->new();
@ -117,5 +117,105 @@ 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} = ( <<EOF
GET / HTTP/1.0
If-None-Match: $1
EOF
);
$t->{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} = ( <<EOF
GET / HTTP/1.0
If-None-Match: $1
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ];
ok($tf->handle_http($t) == 0, 'The ETag must be surrounded by quotes');
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: *
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ];
ok($tf->handle_http($t) == 0, 'An unquoted star matches any ETag');
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: "*"
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ];
ok($tf->handle_http($t) == 0, 'A quoted star is just a regular ETag');
TODO: {
local $TODO = "weak etags not allowed yet";
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: W/$etag
EOF
);
$t->{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} = ( <<EOF
GET / HTTP/1.0
If-None-Match: W/$etag
Range: bytes=0-0
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206, 'HTTP-Content' => '<' } ];
ok($tf->handle_http($t) == 0, 'A weak etag does not match for ranged requests');
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: W/"12345"
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ];
ok($tf->handle_http($t) == 0, 'However, a weak ETag is not *');
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: "12345", $etag
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ];
ok($tf->handle_http($t) == 0, 'Client sent a list of ETags, the second matches');
TODO: {
local $TODO = "weak etags not allowed yet";
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: "12345", W/$etag
EOF
);
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 304 } ];
ok($tf->handle_http($t) == 0, 'The second provided ETag matches weakly');
}
$t->{REQUEST} = ( <<EOF
GET / HTTP/1.0
If-None-Match: "12345",, ,, , $etag
EOF
);
$t->{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} = ( <<EOF
GET / HTTP/1.0
If-None-Match: "1234", $etag, "brokentrailing
EOF
);
$t->{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");