From fda58ec5f6bea43870ba62d516fa58febed4efab Mon Sep 17 00:00:00 2001 From: Tereza Tomcova Date: Sat, 6 Jan 2024 20:39:52 +0100 Subject: [PATCH] nhttp: Support unquoted Authorization header parameters --- lib/WUI/nhttp/req_parser.cpp | 6 +- tests/unit/common/automata/generated.cpp | 16 +++++ utils/gen-automata/http.py | 79 +++++++++++++++++------- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/lib/WUI/nhttp/req_parser.cpp b/lib/WUI/nhttp/req_parser.cpp index b423720d68..f1dc33aa66 100644 --- a/lib/WUI/nhttp/req_parser.cpp +++ b/lib/WUI/nhttp/req_parser.cpp @@ -111,7 +111,8 @@ ExecutionControl RequestParser::event(Event event) { error_code = Status::RequestHeaderFieldsTooLarge; return ExecutionControl::Continue; } - case Names::Nonce: { + case Names::Nonce: + case Names::NonceUnquoted: { if (!holds_alternative(auth_status)) { auth_status = DigestAuthParams {}; } @@ -126,7 +127,8 @@ ExecutionControl RequestParser::event(Event event) { digest_params.recieved_nonce += static_cast(value); return ExecutionControl::Continue; } - case Names::Response: { + case Names::Response: + case Names::ResponseUnquoted: { if (!holds_alternative(auth_status)) { auth_status = DigestAuthParams {}; } diff --git a/tests/unit/common/automata/generated.cpp b/tests/unit/common/automata/generated.cpp index 8b0c2ab4e5..90fedd2861 100644 --- a/tests/unit/common/automata/generated.cpp +++ b/tests/unit/common/automata/generated.cpp @@ -288,3 +288,19 @@ TEST_CASE("Digest auth whole") { REQUIRE(ex.collect_entered(Names::Nonce) == "dcd98b7102dd2f0e"); REQUIRE(ex.collect_entered(Names::Response) == "684d849df474f295771de997e7412ea4"); } + +TEST_CASE("Digest auth with algorithm") { + using test::http::Names; + TestExecution ex(http_request); + ex.consume("GET /api/version HTTP/1.1\r\nAuthorization: Digest username=\"user\", realm=\"Printer API\", nonce=\"dcd98b7102dd2f0e\", uri=\"/api/version\", algorithm=MD5, response=\"684d849df474f295771de997e7412ea4\"\r\n\r\n"); + REQUIRE(ex.collect_entered(Names::Nonce) == "dcd98b7102dd2f0e"); + REQUIRE(ex.collect_entered(Names::Response) == "684d849df474f295771de997e7412ea4"); +} + +TEST_CASE("Digest auth without quotes") { + using test::http::Names; + TestExecution ex(http_request); + ex.consume("GET /api/version HTTP/1.1\r\nAuthorization: Digest username=\"user\", realm=\"Printer API\", nonce=dcd98b7102dd2f0e, uri=\"/api/version\", response=684d849df474f295771de997e7412ea4\r\n\r\n"); + REQUIRE(ex.collect_entered(Names::NonceUnquoted) == "dcd98b7102dd2f0e"); + REQUIRE(ex.collect_entered(Names::ResponseUnquoted) == "684d849df474f295771de997e7412ea4"); +} diff --git a/utils/gen-automata/http.py b/utils/gen-automata/http.py index a106df3cd2..6e0edfb937 100644 --- a/utils/gen-automata/http.py +++ b/utils/gen-automata/http.py @@ -287,7 +287,7 @@ def content_encryption_mode_header(): for t in terminals: terminals[t].mark_enter() terminals[t].set_name(encryption_modes[t]) - # Eat spaces before the content type + # Eat spaces before the encryption mode start = tr.start() start.loop('HorizWhitespace', LabelType.Special) @@ -322,23 +322,45 @@ def create_folder_header(): def auth_value(name): + name_unquoted = f"{name}Unquoted" if name != None else None + auto = Automaton() start = auto.start() - value = auto.add_state() + quote = auto.add_state() + value_quoted = auto.add_state() + value_unquoted = auto.add_state() + end = auto.add_state() start.loop("HorizWhitespace", LabelType.Special) - start.loop("\"", LabelType.Char) - value.set_name(name) - value.mark_enter() - line, end = newline() - end.loop('HorizWhitespace', LabelType.Special) + start.add_transition('\"', LabelType.Char, quote) + start.add_transition('Whitespace', + LabelType.Special, + end, + fallthrough=True) # Vertical whitespace + start.add_transition(',', LabelType.Char, end, fallthrough=True) + start.add_transition('All', LabelType.Special, value_unquoted) + + quote.add_transition('\"', LabelType.Char, end) + quote.add_transition('All', LabelType.Special, value_quoted) + + value_quoted.set_name(name) + value_quoted.mark_enter() + value_quoted.add_transition('\"', LabelType.Char, end) + value_quoted.loop_fallback() + + value_unquoted.set_name(name_unquoted) + value_unquoted.mark_enter() + value_unquoted.add_transition('Whitespace', + LabelType.Special, + end, + fallthrough=True) + value_unquoted.add_transition(',', LabelType.Char, end, fallthrough=True) + value_unquoted.loop_fallback() + + end.loop("HorizWhitespace", LabelType.Special) end.loop(',', LabelType.Char) - auto.join(start, line) - start.add_transition('All', LabelType.Special, value) - value.add_transition('\"', LabelType.Char, end) - value.loop_fallback() - return auto, end, False + return auto, end, True def authorization_header(): @@ -347,39 +369,48 @@ def authorization_header(): Authorization: Digest username="user", realm="Printer API", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/api/version", response="684d849df474f295771de997e7412ea4" """ + # Consume the auth scheme (Digest) + auto, scheme_spaces = read_until("HorizWhitespace", LabelType.Special) + + # Eat spaces before the parameters + scheme_spaces.loop('HorizWhitespace', LabelType.Special) + # We only really read nonce and response, the rest is assumed for # performance reasons. auth_values = { 'nonce': auth_value('Nonce'), 'response': auth_value('Response'), } - auto, terminals, add_unknowns = trie(auth_values) - start = auto.start() + tr, terminals, add_unknowns = trie(auth_values) + tr_start = tr.start() line, end = newline() - auto.join(start, line) + tr.join(tr_start, line) for parameter in auth_values: term = terminals[parameter] term.loop("HorizWhitespace", LabelType.Special) - separator = auto.add_state() + separator = tr.add_state() separator.loop("HorizWhitespace", LabelType.Special) term.add_transition('=', LabelType.Char, separator) read_par, read_par_end, fallthrough = auth_values[parameter] - auto.join_transition(separator, read_par, fallthrough=fallthrough) - read_par_end.add_fallback(start, fallthrough=True) + tr.join_transition(separator, read_par, fallthrough=fallthrough) + read_par_end.add_fallback(tr_start, fallthrough=True) # Handling of unknown/ not parsed values - unknown = auto.add_state() + unknown = tr.add_state() for u in add_unknowns: u.add_fallback(unknown) - unknown.loop("HorizWhitespace", LabelType.Special) - after_unknown = auto.add_state() + after_unknown = tr.add_state() unknown.add_transition('=', LabelType.Char, after_unknown) unknown.loop_fallback() after_unknown.loop("HorizWhitespace", LabelType.Special) - ignore_unknown_header, iuh_end, _ = auth_value(None) - auto.join_transition(after_unknown, ignore_unknown_header) - iuh_end.add_fallback(start, fallthrough=True) + ignore_unknown_header, iuh_end, fallthrough = auth_value(None) + tr.join_transition(after_unknown, + ignore_unknown_header, + fallthrough=fallthrough) + iuh_end.add_fallback(tr_start, fallthrough=True) + + auto.join(scheme_spaces, tr) return auto, end, True