fix(spammer): validate WebSocket targets - #348
Conversation
osr21
left a comment
There was a problem hiding this comment.
Disclosure: I'm not affiliated with Circle — an external community contributor, not a maintainer, with no write access. This review is advisory only and carries no merge authority; please defer to Circle maintainers.
The bug is real, and your reported output is exact. I can't run Rust here, so I verified the normalization against the WHATWG URL Standard — which the url crate implements — using another conforming implementation:
"ws://wss://rpc.example:8546" -> ws://wss//rpc.example:8546
"wss://rpc.example:8546" -> wss://rpc.example:8546/
"WSS://rpc.example:8546" -> wss://rpc.example:8546/
"ws://<ZWJ>.example:8546" -> THROWS (invalid URL)
ws://wss//rpc.example:8546 matches the string in your issue character-for-character, including the collapsed colon. The double-prefix corruption and the IDNA failure both reproduce at spec level.
The scheme heuristic holds up
contains("://") is a heuristic rather than a real scheme parse, so I probed where it misfires. Every path I could find degrades safely:
localhost:8546/path://x→ treated as schemed → parses with schemelocalhost→ caught by yourmatches!check → contextual error.http:rpc.example(scheme, no//) → treated as schemeless →ws://http:rpc.example→ parse error → contextual error.""→ws://→ invalid empty host → contextual error.[::1]:8546→ no://→ws://[::1]:8546/→ correct.
So the loose check never produces a panic or a silently wrong endpoint — the two properties the PR exists to guarantee. Worth stating explicitly, because contains("://") is the line a reviewer is most likely to squint at, and it's fine as written. Notably it also avoids the trap a Url::parse-based scheme probe would hit, where localhost:8546 parses as scheme localhost and a bare host:port gets misclassified.
This coverage is genuinely CI-enforced
Worth confirming, since it isn't true everywhere in this repo. spammer is a workspace member via crates/*, and the unit test job runs:
cargo nextest run --locked --workspace --exclude arc-test-integrationso all five tests execute on every PR. cargo clippy --all-targets --all-features -- -D warnings covers the test code too.
Pre-empting one objection: the workspace sets unwrap_used = "deny" and your new tests call .unwrap(). That's fine — clippy.toml sets allow-unwrap-in-tests = true. Not a problem, but a reviewer skimming the diff may well flag it.
Your toolchain caveat is also well-founded: rust-toolchain.toml pins 1.93.0 and the workspace declares rust-version = "1.93", and setup-rust-toolchain honours that file, so CI will be authoritative as you say. Nothing in the diff needs anything newer — ?, matches! and wrap_err_with are all long-stable — so 1.93 shouldn't surprise you. Context is already imported at the top of the file, so wrap_err_with resolves.
The change I'd actually suggest — make the fix self-enforcing
This is the one worth taking, and it's a single line.
main.rs opens with:
#![allow(
clippy::arithmetic_side_effects,
clippy::cast_possible_truncation,
clippy::unwrap_used
)]That blanket allow is why Url::parse(&url_str).unwrap() survived in production code despite the workspace denying unwrap_used. I checked what remains after your change — scanning everything above mod tests:
pr348 production unwrap/expect/panic before tests: (NONE remaining)
main production unwrap/expect/panic before tests: 246: Url::parse(&url_str).unwrap()
Yours was the last one. So dropping just clippy::unwrap_used from that allow list would let the workspace lint enforce the invariant you've established, and the tests keep passing untouched because allow-unwrap-in-tests already covers them. Leave the other two allows alone.
Without it, the fix is a point repair and the next unwrap() added to this file lands silently. With it, the file can't regress. Same PR, same file, one line.
Two minor notes
- The
wssubcommand'safter_long_helpstill advertises onlyws://:
spammer ws --targets ws://127.0.0.1:8546,ws://127.0.0.1:9546
Adding a wss:// example would make the newly-supported form discoverable — otherwise the capability exists but nothing points at it.
ws_urls_from_stringscollects intoResult<Vec<_>>, so it short-circuits on the first bad entry. With a comma-separated list, a user fixes one target and immediately rediscovers the next. Accumulating and reporting all invalid entries at once would be friendlier. Entirely optional, and arguably out of scope.
Careful, well-evidenced work — the RED-before-GREEN verification, the explicit statement that the nodes-metadata path is untouched, and the scope note all made this quick to check. Approving on the strength of source review and the spec-level reproduction; I could not compile or run the crate here, so CI remains authoritative on the pinned toolchain.
Summary
Fixes #347
wss://targets in the spammer's direct WebSocket target path.ws://to barehost:porttargets.unwrap().Problem
ws_url_from_str()previously recognized only the literalws://prefix:This caused two related failures.
First, an explicit secure WebSocket target such as:
was prefixed again and parsed as:
which normalized to the wrong endpoint:
Second, invalid target values reached
Url::parse(...).unwrap()and terminated the process with a panic. An invalid IDNA hostname produced:Changes
The direct-target parsing path now:
ws://only to schemeless targets such as127.0.0.1:8546;unwrap();wsandwssschemes;--targetsentry in parsing and scheme errors;ws_urls_from_strings()andmain().The nodes-metadata path is unchanged because it already deserializes
execution.ws_urldirectly asUrland does not callws_url_from_str().Regression Coverage
Added tests verifying that:
host:porttargets still receivews://;ws://targets remain valid;wss://targets are preserved;http://are rejected.Existing direct-target list and nodes-metadata tests remain in place.
RED Verification
Before the production fix, the focused regression tests failed on current
main:The real CLI entry point also panicked with exit code 101 before attempting a network connection.
How to Test
Focused target parsing tests:
cargo +1.94.0 test \ -p spammer \ --bin spammer \ ws_url_from_str_ \ -- --nocaptureResult:
Complete spammer package:
cargo +1.94.0 test -p spammerResults:
Additional checks:
All checks passed.
The fixed binary was also invoked with the same invalid IDNA target. It now exits normally with code 1 and reports:
No panic text is emitted.
Local verification used Rust 1.94.0 because the local pinned 1.93.0 installation has a
cargo-clippycomponent conflict. CI should provide the authoritative pinned-toolchain result.Scope and Risk
Risk is low. The change is limited to direct spammer WebSocket target parsing in:
It does not change:
The existing bare
host:portand explicitws://forms remain supported.Duplicate Check
Open and closed issues and pull requests were searched using the issue number, helper name, affected schemes, panic text, and target-parsing terms. No duplicate PR or existing implementation was found.
Checklist
mainwss://targets are preservedImpact
Type: 🐛 Bug fix
Fixes: #347