From 379cbed76a2e8690b2f82b616122d12ab285b60b Mon Sep 17 00:00:00 2001 From: Ros McMahon Date: Thu, 23 Jul 2026 15:34:17 +0100 Subject: [PATCH 1/2] test: verify blacklist blocks /raw and base32 subdomain routes --- src/preloaded/node/dev_blacklist.erl | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/preloaded/node/dev_blacklist.erl b/src/preloaded/node/dev_blacklist.erl index b4fe7f15a..6d25e3adc 100644 --- a/src/preloaded/node/dev_blacklist.erl +++ b/src/preloaded/node/dev_blacklist.erl @@ -404,6 +404,99 @@ basic_test() -> ), ok. +%% @doc A blacklisted ID must be blocked identically regardless of the route +%% form used to request it. Here we verify that `/raw/ID' is blocked by the +%% same mechanism that blocks `/ID', since the ID appears as a later path +%% segment rather than the base message. +raw_route_blocked_test() -> + {ok, #{ + opts := Opts0, + signed1 := SignedID1, + blacklist := BlacklistID + }} = setup_test_env(), + Opts1 = Opts0#{ <<"blacklist-providers">> => [BlacklistID]}, + Node = hb_http_server:start_node(Opts1), + % Sanity: the bare `/ID' form is blocked. + ?assertMatch( + {error, #{ <<"status">> := 451, <<"reason">> := <<"content-policy">> }}, + hb_http:get(Node, SignedID1, Opts1) + ), + % The `/raw/ID' form must be blocked by the same mechanism. + ?assertMatch( + {error, #{ <<"status">> := 451, <<"reason">> := <<"content-policy">> }}, + hb_http:get(Node, <<"/raw/", SignedID1/binary>>, Opts1) + ), + ok. + +%% @doc A blacklisted ID must also be blocked when requested via its base32 +%% subdomain (`BASE32.localhost'). The subdomain is decoded to the real ID by +%% the `name@1.0' hook, which must run *before* `blacklist@1.0' so that the +%% resolved ID is present in the request when the blacklist check runs. A +%% non-blacklisted ID's subdomain must not be blocked. +subdomain_route_blocked_test() -> + {ok, #{ + opts := Opts0, + signed1 := SignedID1, + unsigned3 := UnsignedID3, + blacklist := BlacklistID + }} = setup_test_env(), + % Wire `name@1.0' (resolving base32 subdomains via `b32-name@1.0') ahead of + % `blacklist@1.0' in the request hook chain. + Opts1 = Opts0#{ + <<"blacklist-providers">> => [BlacklistID], + <<"name-resolvers">> => [#{ <<"device">> => <<"b32-name@1.0">> }], + <<"on">> => #{ + <<"request">> => [ + #{ <<"device">> => <<"name@1.0">> }, + #{ <<"device">> => <<"blacklist@1.0">> } + ] + } + }, + Node = hb_http_server:start_node(Opts1), + % The blacklisted ID, requested via its base32 subdomain, is blocked. + ?assertMatch( + {error, #{ <<"status">> := 451, <<"reason">> := <<"content-policy">> }}, + hb_http:get( + Node, + #{ + <<"path">> => <<"/">>, + <<"host">> => b32_subdomain(SignedID1) + }, + Opts1 + ) + ), + % A non-blacklisted ID's subdomain is not blocked by the content policy + % (it resolves normally rather than returning 451). + ?assertNotMatch( + {error, #{ <<"status">> := 451 }}, + hb_http:get( + Node, + #{ + <<"path">> => <<"/">>, + <<"host">> => b32_subdomain(UnsignedID3) + }, + Opts1 + ) + ), + ok. + +%% @doc Encode an ID as its base32 `.localhost' subdomain, mirroring +%% `dev_b32_name:encode/1'. Inlined here because packaged device modules are +%% renamed, so the `dev_b32_name' atom is not callable from this test. +b32_subdomain(ID) -> + Encoded = + hb_util:bin( + string:replace( + string:to_lower( + hb_util:list(base32:encode(hb_util:native_id(ID))) + ), + "=", + "", + all + ) + ), + <>. + %% @doc Ensure that the default provider does not block any requests. first_request_always_return_503_test() -> {ok, #{ From 1ec4a5426ca39cd28e28056a97b1c901bc1efa62 Mon Sep 17 00:00:00 2001 From: Ros McMahon Date: Thu, 23 Jul 2026 16:04:53 +0100 Subject: [PATCH 2/2] test: rename route block tests to indicate parallel execution --- src/preloaded/node/dev_blacklist.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/preloaded/node/dev_blacklist.erl b/src/preloaded/node/dev_blacklist.erl index 6d25e3adc..1f813b580 100644 --- a/src/preloaded/node/dev_blacklist.erl +++ b/src/preloaded/node/dev_blacklist.erl @@ -408,7 +408,7 @@ basic_test() -> %% form used to request it. Here we verify that `/raw/ID' is blocked by the %% same mechanism that blocks `/ID', since the ID appears as a later path %% segment rather than the base message. -raw_route_blocked_test() -> +raw_route_blocked_test_parallel() -> {ok, #{ opts := Opts0, signed1 := SignedID1, @@ -433,7 +433,7 @@ raw_route_blocked_test() -> %% the `name@1.0' hook, which must run *before* `blacklist@1.0' so that the %% resolved ID is present in the request when the blacklist check runs. A %% non-blacklisted ID's subdomain must not be blocked. -subdomain_route_blocked_test() -> +subdomain_route_blocked_test_parallel() -> {ok, #{ opts := Opts0, signed1 := SignedID1,