diff --git a/doc/ref/status.xml b/doc/ref/status.xml index 86de706f..85400284 100644 --- a/doc/ref/status.xml +++ b/doc/ref/status.xml @@ -15,9 +15,11 @@ #include <ne_utils.h> typedef struct { - int major_version, minor_version; - int code, klass; - const char *reason_phrase; + int major_version; + int minor_version; + int code; + int klass; + char *reason_phrase; } ne_status; @@ -25,19 +27,27 @@ typedef struct { Description - An ne_status type represents an HTTP -response status; used in response messages giving a result of request. -The major_version and -minor_version fields give the HTTP version -supported by the server issuing the response. The -code field gives the status code of the -result (lying between 100 and 999 inclusive), and the -klass field gives the -class, which is equal to the most significant digit -of the status. + The ne_status type represents an HTTP + response status; used in response messages giving a result of + request. The major_version and + minor_version fields give the + protocol version supported by the server issuing the response. + The code field gives the status + code of the result (lying between 100 and + 599 inclusive), and the + klass field gives the class, which + is equal to the most significant digit of the status. The + reason_phrase field gives a + human-readable string describing the status, which is purely + informational (and optional) - any interpretation of the + response status must be done using the + klass and/or + code fields. - There are five classes of HTTP status code defined by - RFC2616: + There are five classes of response status code defined + for HTTP/1.1 (see RFC + 9110 ẞ15): diff --git a/src/mktable.c b/src/mktable.c index d194f1df..796f5a2a 100644 --- a/src/mktable.c +++ b/src/mktable.c @@ -92,6 +92,28 @@ static unsigned char gen_extparam(unsigned char ch) } } +/* + * Map: '0'-'9' => 0-9 + * reason-phrase characters => 0-10 + * bad things => 99 + * + * RFC 9112: reason-phrase = 1*( HTAB / SP / VCHAR / obs-text ) + * RFC 5234: VCHAR = %x21-7E + * RFC 9110: obs-text = %x80-FF + */ +static unsigned char gen_status_line(unsigned char ch) +{ + if (ch >= '0' && ch <= '9') + return ch - '0'; + + if (ch == '\t' || ch == ' ' + || (ch >= 0x21 && ch != 0x7F)) { + return 10; + } + + return 99; +} + #define FLAG_DECIMAL (0x01) #define FLAG_SHORT (0x02) @@ -105,6 +127,7 @@ static const struct { { "validb64", valid_b64, FLAG_DECIMAL | FLAG_SHORT }, { "decodeb64", decode_b64, 0 }, { "quote", gen_quote, FLAG_DECIMAL | FLAG_SHORT }, + { "status_line", gen_status_line, FLAG_DECIMAL | FLAG_SHORT }, { "extparam", gen_extparam, FLAG_DECIMAL | FLAG_SHORT }, { "safe_username", safe_username, FLAG_DECIMAL | FLAG_SHORT }, }; diff --git a/src/ne_207.c b/src/ne_207.c index 391a6b47..8bc804ae 100644 --- a/src/ne_207.c +++ b/src/ne_207.c @@ -209,7 +209,7 @@ end_element(void *userdata, int state, const char *nspace, const char *name) if (p->status.reason_phrase) ne_free(p->status.reason_phrase); if (ne_parse_statusline(cdata, &p->status)) { char buf[500]; - NE_DEBUG(NE_DBG_HTTP, "Status line: %s\n", cdata); + NE_DEBUG(NE_DBG_HTTP, "[207] Invalid status-line: [%s]\n", cdata); ne_snprintf(buf, 500, _("Invalid HTTP status line in status element " "at line %d of response:\nStatus line was: %s"), @@ -217,7 +217,7 @@ end_element(void *userdata, int state, const char *nspace, const char *name) ne_xml_set_error(p->parser, buf); return -1; } else { - NE_DEBUG(NE_DBG_XML, "Decoded status line: %s\n", cdata); + NE_DEBUG(NE_DBG_XML, "[207] valid status-line: %s\n", cdata); } } break; diff --git a/src/ne_utils.c b/src/ne_utils.c index 6d5382d0..b5fb15ae 100644 --- a/src/ne_utils.c +++ b/src/ne_utils.c @@ -1,6 +1,6 @@ /* HTTP utility functions - Copyright (C) 1999-2021, Joe Orton + Copyright (C) 1999-2024, Joe Orton This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -187,47 +187,74 @@ int ne_has_support(int feature) } } +/* Lookup table - digit values=0-9, reason_phrase=0-10. */ + +/* Generated with 'mktable status_line', do not alter here -- */ +static const unsigned char table_status_line[256] = { +/* x00 */ 99, 99, 99, 99, 99, 99, 99, 99, 99, 10, 99, 99, 99, 99, 99, 99, +/* x10 */ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, +/* x20 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* x30 */ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 10, 10, +/* x40 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* x50 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* x60 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* x70 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 99, +/* x80 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* x90 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* xA0 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* xB0 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* xC0 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* xD0 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* xE0 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +/* xF0 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 +}; /* -- Generated code from 'mktable status_line' ends. */ + +/* Strict parser per RFC9112ẞ4: + * + * status-line = HTTP-version SP status-code SP [ reason-phrase ] + * HTTP-version = HTTP-name "/" DIGIT "." DIGIT + * reason-phrase = 1*( HTAB / SP / VCHAR / obs-text ) + */ int ne_parse_statusline(const char *status_line, ne_status *st) { - const char *part; - int major, minor, status_code, klass; + const unsigned char *p = (const unsigned char *)status_line, *rp; + unsigned int major, minor, status_code, klass; - /* skip leading garbage if any. */ - part = strstr(status_line, "HTTP/"); - if (part == NULL) return -1; + /* p => status-line */ + if (strncmp((const char *)p, "HTTP/", 5) != 0) + return -1; minor = major = 0; + p += 5; - /* Parse version string, skipping leading zeroes. */ - for (part += 5; *part != '\0' && isdigit(*part); part++) - major = major*10 + (*part-'0'); + /* X.Y */ + if ((major = table_status_line[*p++]) > 9) + return -1; + if (*p++ != '.') + return -1; + if ((minor = table_status_line[*p++]) > 9) + return -1; - if (*part++ != '.') return -1; + if (*p++ != ' ') return -1; - for (;*part != '\0' && isdigit(*part); part++) - minor = minor*10 + (*part-'0'); + if ((klass = table_status_line[p[0]]) > 5 /* note 5xx maximum */ + || table_status_line[p[1]] > 9 || table_status_line[p[2]] > 9 + || p[3] != ' ') + return -1; - if (*part != ' ') return -1; + status_code = klass * 100 + table_status_line[p[1]] * 10 + + table_status_line[p[2]]; - /* Skip any spaces */ - for (; *part == ' '; part++) /* noop */; - - /* Parse the Status-Code; part now points at the first Y in - * "HTTP/x.x YYY". */ - if (!isdigit(part[0]) || !isdigit(part[1]) || !isdigit(part[2]) || - (part[3] != '\0' && part[3] != ' ')) return -1; - status_code = 100*(part[0]-'0') + 10*(part[1]-'0') + (part[2]-'0'); - klass = part[0]-'0'; - - /* Skip whitespace between status-code and reason-phrase */ - for (part+=3; *part == ' ' || *part == '\t'; part++) /* noop */; - - /* part now may be pointing to \0 if reason phrase is blank */ + rp = p += 4; /* p => [ reason-phrase ] */ + while (table_status_line[*p] < 11) /* note this terminates for *p == '\0' */ + p++; /* Fill in the results */ st->major_version = major; st->minor_version = minor; - st->reason_phrase = ne_strclean(ne_strdup(part)); + st->reason_phrase = ne_malloc(p - rp + 1); + ne_strnzcpy(st->reason_phrase, (const char *)rp, p - rp + 1); + ne_strclean(st->reason_phrase); st->code = status_code; st->klass = klass; return 0; diff --git a/src/ne_utils.h b/src/ne_utils.h index 59fc4e22..50ac737f 100644 --- a/src/ne_utils.h +++ b/src/ne_utils.h @@ -96,7 +96,7 @@ void ne_debug(int ch, const char *, ...) ne_attribute((format(printf, 2, 3))); typedef struct { int major_version; int minor_version; - int code; /* Status-Code value */ + int code; /* Status-Code value (100..599 inclusive) */ int klass; /* Class of Status-Code (1-5) */ char *reason_phrase; } ne_status; diff --git a/test/props.c b/test/props.c index 753ed447..dc210fce 100644 --- a/test/props.c +++ b/test/props.c @@ -264,7 +264,6 @@ static int run_207_response(char *resp, const char *expected) ctx.p207 = p207; ne_xml_push_handler(p, tos_startprop, tos_cdata, tos_endprop, &ctx); - ONREQ(ne_request_dispatch(req)); CALL(await_server()); @@ -318,10 +317,10 @@ static int two_oh_seven(void) /* test multiple responses */ { MULTI_207(RESP_207("/hello/world", STAT_207("200 OK")) - RESP_207("/foo/bar", STAT_207("999 French Fries"))), + RESP_207("/foo/bar", STAT_207("599 French Fries"))), "start-resp[/hello/world];end-resp[/hello/world]-status={200 OK};" "start-resp[/foo/bar];end-resp[/foo/bar]" - "-status={999 French Fries};" + "-status={599 French Fries};" }, /* test multiple propstats in multiple responses */ @@ -330,16 +329,16 @@ static int two_oh_seven(void) PSTAT_207(STAT_207("432 Deux")) PSTAT_207(STAT_207("543 Trois"))) RESP_207("/be/ta", - PSTAT_207(STAT_207("787 Quatre")) - PSTAT_207(STAT_207("878 Cinq")))), + PSTAT_207(STAT_207("587 Quatre")) + PSTAT_207(STAT_207("578 Cinq")))), "start-resp[/al/pha];" "start-pstat[/al/pha-1];end-pstat[/al/pha-1]-status={321 Une};" "start-pstat[/al/pha-2];end-pstat[/al/pha-2]-status={432 Deux};" "start-pstat[/al/pha-3];end-pstat[/al/pha-3]-status={543 Trois};" "end-resp[/al/pha];" "start-resp[/be/ta];" - "start-pstat[/be/ta-1];end-pstat[/be/ta-1]-status={787 Quatre};" - "start-pstat[/be/ta-2];end-pstat[/be/ta-2]-status={878 Cinq};" + "start-pstat[/be/ta-1];end-pstat[/be/ta-1]-status={587 Quatre};" + "start-pstat[/be/ta-2];end-pstat[/be/ta-2]-status={578 Cinq};" "end-resp[/be/ta];" }, @@ -355,9 +354,9 @@ static int two_oh_seven(void) /* tests for propstat status */ { MULTI_207(RESP_207("/pstat", - PSTAT_207("" STAT_207("666 Doomed")))), + PSTAT_207("" STAT_207("466 Doomed")))), "start-resp[/pstat];start-pstat[/pstat-1];" - "end-pstat[/pstat-1]-status={666 Doomed};end-resp[/pstat];" }, + "end-pstat[/pstat-1]-status={466 Doomed};end-resp[/pstat];" }, { MULTI_207(RESP_207("/pstat", PSTAT_207(""))), "start-resp[/pstat];start-pstat[/pstat-1];" @@ -382,7 +381,7 @@ static int two_oh_seven(void) { MULTI_207("blargl" RESP_207("/bar", "" PSTAT_207("blergl") - STAT_207("200 OK") "foop" + STAT_207("200 OK") "foop" DESCR_207(DESCR_REM) "carroon") "carapi"), "start-resp[/bar];start-pstat[/bar-1];end-pstat[/bar-1];" diff --git a/test/util-tests.c b/test/util-tests.c index fd7cc193..8aa82fad 100644 --- a/test/util-tests.c +++ b/test/util-tests.c @@ -45,19 +45,13 @@ static const struct { } accept_sl[] = { /* These are really valid. */ { "HTTP/1.1 200 OK", 1, 1, 200, "OK" }, - { "HTTP/1.1000 200 OK", 1, 1000, 200, "OK" }, - { "HTTP/1000.1000 200 OK", 1000, 1000, 200, "OK" }, - { "HTTP/00001.1 200 OK", 1, 1, 200, "OK" }, - { "HTTP/1.00001 200 OK", 1, 1, 200, "OK" }, - { "HTTP/99.99 999 99999", 99, 99, 999, "99999" }, - { "HTTP/1.1 100 ", 1, 1, 100, "" }, + { "HTTP/9.9 599 OK", 9, 9, 599, "OK" }, + { "HTTP/1.0 123 OK-is-OK1234", 1, 0, 123, "OK-is-OK1234" }, + { "HTTP/1.1 100 Alpha\tBeta", 1, 1, 100, "Alpha Beta" }, /* should be cleaned. */ + { "HTTP/1.1 100 Alpha Beta", 1, 1, 100, "Alpha Beta" }, + { "HTTP/1.1 100 fØØbÆr", 1, 1, 100, "f b r" }, /* UTF-8 should be cleaned */ /* these aren't really valid but we should be able to parse them. */ - { "HTTP/1.1 100", 1, 1, 100, "" }, - { "HTTP/1.1 200 OK", 1, 1, 200, "OK" }, - { "HTTP/1.1 200 \t OK", 1, 1, 200, "OK" }, - { " HTTP/1.1 200 OK", 1, 1, 200, "OK" }, - { "Norman is a dog HTTP/1.1 200 OK", 1, 1, 200, "OK" }, { NULL } }; @@ -76,6 +70,19 @@ static const char *const bad_sl[] = { "HTTP/1.1 10", "HTTP", "H\0TP/1.1 100 OK", + + /* Previously allowed, now disallowed. */ + "HTTP/1.1 200 OK", + "HTTP/1.1 200 \t OK", + " HTTP/1.1 200 OK", + "Norman is a dog HTTP/1.1 200 OK", + "HTTP/1.1000 100 OK", + "HTTP/1000.1000 100 OK", + "HTTP/00001.1 100 OK", + "HTTP/1.00001 100 OK", + "HTTP/99.99 100 OK", + "HTTP/1.1 600 OK", + NULL }; @@ -86,18 +93,19 @@ static int status_lines(void) for (n = 0; accept_sl[n].status != NULL; n++) { ONV(ne_parse_statusline(accept_sl[n].status, &s), - ("valid #%d: parse", n)); + ("valid #%d: parsing '%s' failed", n, accept_sl[n].status)); ONV(accept_sl[n].major != s.major_version, ("valid #%d: major", n)); ONV(accept_sl[n].minor != s.minor_version, ("valid #%d: minor", n)); - ONV(accept_sl[n].code != s.code, ("valid #%d: code", n)); + ONV(accept_sl[n].code != s.code, ("valid #%d: code %d not %d", n, s.code, accept_sl[n].code)); ONV(strcmp(accept_sl[n].rp, s.reason_phrase), - ("valid #%d: reason phrase", n)); + ("valid #%d: reason phrase [%s] not [%s]", n, s.reason_phrase, accept_sl[n].rp)); ne_free(s.reason_phrase); + memset(&s, 0, sizeof s); } for (n = 0; bad_sl[n] != NULL; n++) { ONV(ne_parse_statusline(bad_sl[n], &s) == 0, - ("invalid #%d", n)); + ("invalid #%d parsed OK - [%s]", n, bad_sl[n])); } return OK;