fix(client-info): address review edge cases

Co-authored-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@buzz.block.builderlab.xyz>
Signed-off-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@buzz.block.builderlab.xyz>
This commit is contained in:
npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e
2026-07-23 15:01:02 -07:00
parent b5d067036e
commit 94db169622
4 changed files with 85 additions and 3 deletions
+49 -2
View File
@@ -35,8 +35,18 @@ impl ClientInfo {
/// counter and also returns `None`, so it can never reject a request.
#[must_use]
pub fn from_headers(headers: &HeaderMap) -> Option<Self> {
let value = headers.get("buzz-client")?;
let parsed = value.to_str().ok().and_then(|raw| Self::parse(raw).ok());
let all_values = headers.get_all("buzz-client");
let mut values = all_values.iter();
let first = values.next()?;
let parsed = (|| {
let mut raw = first.to_str().map_err(|_| ())?.to_owned();
for value in values {
raw.push_str(", ");
raw.push_str(value.to_str().map_err(|_| ())?);
}
Self::parse(&raw)
})()
.ok();
if parsed.is_none() {
metrics::counter!("buzz_client_header_parse_failures_total").increment(1);
}
@@ -265,6 +275,43 @@ mod tests {
assert_eq!(parse_failures(&recorder), 0);
}
#[test]
fn joins_multiple_header_lines_before_parsing() {
let mut headers = HeaderMap::new();
headers.append(
"buzz-client",
HeaderValue::from_static(r#"v=1, app=buzz-mobile, platform=ios, app-version="0.4.5""#),
);
headers.append(
"buzz-client",
HeaderValue::from_static(r#"app-build="6", os-version="18.5""#),
);
let client = ClientInfo::from_headers(&headers).expect("valid combined header");
assert_eq!(client.app_version, "0.4.5");
assert_eq!(client.app_build, "6");
assert_eq!(client.os_version, "18.5");
}
#[test]
fn rejects_non_utf8_in_any_header_line() {
let recorder = DebuggingRecorder::new();
let mut headers = HeaderMap::new();
headers.append(
"buzz-client",
HeaderValue::from_static(r#"v=1, app=buzz-mobile, platform=ios, app-version="0.4.5""#),
);
headers.append(
"buzz-client",
HeaderValue::from_bytes(&[0xff]).expect("opaque test header value"),
);
metrics::with_local_recorder(&recorder, || {
assert_eq!(ClientInfo::from_headers(&headers), None);
});
assert_eq!(parse_failures(&recorder), 1);
}
#[test]
fn malformed_or_semantically_invalid_header_is_absent_and_counted() {
for raw in [
@@ -131,6 +131,8 @@ class InviteJoinNotifier extends Notifier<InviteJoinState> {
...clientHeadersForUrl(
headers: ref.read(clientHeadersProvider),
targetUrl: url,
// Explicit confirmation makes the invite relay the user's
// intended first-party destination for this claim.
relayUrl: invite.relayUrl,
),
'Authorization': buildNip98AuthHeader(
+2 -1
View File
@@ -57,7 +57,8 @@ class MediaGetAuthService {
);
final uri = Uri.tryParse(url);
final relayUri = Uri.tryParse(_baseUrl);
if (uri == null || relayUri == null) return const {};
if (uri == null) return const {};
if (relayUri == null) return identificationHeaders;
if (!_isRelayMediaUrl(uri, relayUri)) return identificationHeaders;
final nsec = _nsec;
@@ -102,6 +102,38 @@ void main() {
expect(headers, isNot(contains('Authorization')));
});
test('sends only User-Agent when the relay base URL is malformed', () {
const clientHeaders = ClientHeaders(
appVersion: '1.0',
buzzClient: 'test-client',
userAgent: 'test-agent',
);
final auth = MediaGetAuthService(
baseUrl: '://',
nsec: nostr.Keys.generate().nsec,
clientHeaders: clientHeaders,
);
expect(
auth.headersFor('https://cdn.cloudflare.example/attachments/abc.png'),
{'User-Agent': 'test-agent'},
);
});
test('malformed target URL gets no headers', () {
const clientHeaders = ClientHeaders(
appVersion: '1.0',
buzzClient: 'test-client',
userAgent: 'test-agent',
);
final auth = _auth(
nsec: nostr.Keys.generate().nsec,
clientHeaders: clientHeaders,
);
expect(auth.headersFor('://'), isEmpty);
});
test('sends identification without a signing key', () {
const clientHeaders = ClientHeaders(
appVersion: '1.0',