Skip to content

feat(plugins): bump servo to 0.3.0 and add auth config for private pages#630

Open
staging-devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1782551390-servo-auth
Open

feat(plugins): bump servo to 0.3.0 and add auth config for private pages#630
staging-devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1782551390-servo-auth

Conversation

@staging-devin-ai-integration

@staging-devin-ai-integration staging-devin-ai-integration Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Upgrade the native Servo web-renderer plugin from servo 0.1 to servo 0.3.0. The 0.1→0.3 jump renames parts of the embedding API and, crucially, adds per-navigation custom HTTP request headers (WebView::load_request(UrlRequest::new(url).headers(...))), which this PR builds on. Step 2 (compile-clean, behavior-unchanged) was kept as a separate concern from the new auth feature.
  • Add an optional auth block to ServoConfig so private pages can be loaded non-interactively:
    • headers (arbitrary request headers, e.g. Authorization, Cookie, custom X-…) and bearer_token (convenience → Authorization: Bearer …) are attached to the initial navigation via the new 0.3.0 load_request path.
    • basic (username/password) answers HTTP Basic/Digest challenges via the WebViewDelegate::request_authentication hook on FrameDelegate.
    • user_agent is set on servo's Preferences.
  • Auth is init-time only — credentials bind to the WebView at creation and are deliberately not hot-swapped on UpdateConfig (documented in merge_update); changing auth requires recreating the node.
  • Secret hygiene: credentials (headers/token/password/UA) are never logged, and redact_url() strips user:password@ userinfo from every logged URL. Conflicts (bearer_token + explicit Authorization header) and malformed header names/values are rejected at config-validation time.

Behavior changes / risks

  • Global User-Agent caveat: servo's Preferences are a process-global singleton, so auth.user_agent is taken from the first registered servo node and applies to all servo nodes in the process. headers/bearer_token/basic remain per-node. Documented in the README and param schema; the auth/credential params are marked tunable: false.
  • Plugin version bumped 0.1.0 → 0.2.0 (Cargo.toml, plugin.yml) with a matching docs/public/registry/index.json entry.
  • The jemalloc disable_initial_exec_tls dlopen-TLS workaround is retained.

Review & Validation

  • cargo build/clippy -p servo-plugin-native clean under the crate's strict pedantic/nursery + unwrap_used/expect_used lints (verified locally; CI re-runs).
  • cargo test -p servo-plugin-native — new config.rs tests cover auth deserialization, bearer↔Authorization conflict, invalid header name/value rejection, merge_update leaving auth untouched, and redact_url userinfo stripping (45 tests pass).
  • Confirm no credentials appear in logs (header/token/password values are never logged; URLs go through redact_url).
  • Sanity-check the global-UA tradeoff is acceptable vs. a future per-WebView UA.

Notes

  • Per-node User-Agent is not currently exposed by servo 0.3.0; left as a follow-up.
  • Credentials flow through pipeline YAML/params — README recommends sourcing them from host secrets rather than inlining.
  • Manual end-to-end auth rendering (a page behind HTTP Basic and one requiring a bearer header) was not run in CI since it needs a live private endpoint; the auth deserialization/validation paths are unit-tested.

Link to Devin session: https://staging.itsdev.in/sessions/ca387cc24bc3497e8cd0b8e1f2680030
Requested by: @streamer45


Devin Review

Status Commit
🟢 Reviewed b42a05f
Open in Devin Review (Staging)

Upgrade the native Servo web-renderer plugin from servo 0.1 to 0.3.0 and wire a new optional `auth` config through ServoConfig to WebView creation.

- Custom request headers / bearer token attached to the initial navigation via the 0.3.0 UrlRequest/load_request API.
- HTTP Basic/Digest answered non-interactively via the WebViewDelegate request_authentication hook.
- Custom User-Agent applied to the process-global servo Preferences (affects all servo nodes).

Auth is init-time only (not hot-swapped on UpdateConfig). Credentials are never logged and userinfo is stripped from logged URLs.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
@streamer45 streamer45 self-assigned this Jun 27, 2026
@streamer45 streamer45 self-requested a review June 27, 2026 09:10
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment thread plugins/native/servo/src/servo_thread.rs
Comment on lines +425 to +429
// `Preferences` is a process-global singleton, so the User-Agent of
// the first registered node applies to every servo node thereafter.
if let Some(user_agent) = config.auth.as_ref().and_then(|a| a.user_agent.as_ref()) {
prefs.user_agent.clone_from(user_agent);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Per-node user_agent silently ignored after the first servo node registers

handle_register applies auth.user_agent to the process-global servo::Preferences only inside get_or_insert_with, which runs exactly once for the first registered node (plugins/native/servo/src/servo_thread.rs:419-430). Any later node's auth.user_agent is parsed and validated but has no effect. This is explicitly documented in the README (plugins/native/servo/README.md:151-155) and the schema description (plugins/native/servo/src/servo_node.rs:130), so it is an intentional, documented limitation rather than a bug — noting here for reviewer awareness since it is a non-obvious global-state coupling.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +695 to +700
let request_headers = config
.auth
.as_ref()
.map(ServoAuth::build_request_headers)
.transpose()
.map_err(|e| format!("invalid auth config: {e}"))?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Auth header validation is performed redundantly

build_request_headers is invoked once during ServoConfig::validate (plugins/native/servo/src/config.rs:288-290) and again in create_webview (plugins/native/servo/src/servo_thread.rs:695-700). The duplication is harmless (cheap, runs only at init) and provides defense-in-depth, but the second call's error mapping (invalid auth config: {e}) is effectively unreachable for configs that passed validate. Not a bug; flagged only so a reviewer knows the second error path is essentially dead for the normal init flow.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +89 to +104
if header_name == AUTHORIZATION {
has_authorization = true;
}
map.append(header_name, header_value);
}
}

if let Some(ref token) = self.bearer_token {
if has_authorization {
return Err("auth.bearer_token conflicts with an explicit 'Authorization' header"
.to_string());
}
let value = HeaderValue::try_from(format!("Bearer {token}"))
.map_err(|e| format!("invalid bearer_token: {e}"))?;
map.insert(AUTHORIZATION, value);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Bearer-token / Authorization-header conflict check relies on case-insensitive HeaderName comparison

The conflict detection in build_request_headers compares the parsed HeaderName against AUTHORIZATION (plugins/native/servo/src/config.rs:89). Because HeaderName normalizes to lowercase and compares case-insensitively, a user-supplied authorization, Authorization, or AUTHORIZATION entry will all correctly set has_authorization and trigger the conflict error when bearer_token is also set. Verified this works as intended — no bug.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Signed-off-by: streamkit-devin <devin@streamkit.dev>
handle_update_config navigated via webview.load(), which dropped the configured custom/bearer request headers when the tunable url parameter changed at runtime, silently breaking header auth on subsequent private pages (basic auth, bound to the delegate, kept working). Route both initial and runtime navigations through a shared navigate_with_auth helper so header/bearer auth stays consistent with basic auth.

Signed-off-by: streamkit-devin <devin@streamkit.dev>

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 new potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment thread plugins/native/servo/src/config.rs Outdated
Comment on lines 592 to 597
if url_changed {
if let Ok(parsed) = url::Url::parse(&new_config.url) {
state.webview.load(parsed);
navigate_with_auth(&state.webview, state.config.auth.as_ref(), parsed);
state.delegate.loaded.set(false);
// Reset the one-shot post-load gate so the new page gets
// its custom-CSS injection (if any) once it finishes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Runtime URL changes correctly re-apply init-time auth headers

I verified the docs claim that custom headers / bearer token are re-applied on runtime url changes. In handle_update_config (servo_thread.rs:594), navigation uses state.config.auth.as_ref(), and since merge_update deliberately never mutates auth, this is always the original init-time auth — consistent on both initial nav (create_webview at servo_thread.rs:706) and runtime nav. Basic-auth challenges are answered by the delegate bound at WebView creation (servo_thread.rs:689), which also persists across runtime URL changes. Behavior matches the README description.

(Refers to lines 592-602)

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +515 to 520
let rect = DeviceIntRect::new(
DeviceIntPoint::new(0, 0),
DeviceIntPoint::new(
i32::try_from(state.rc_width).unwrap_or(i32::MAX),
i32::try_from(state.rc_height).unwrap_or(i32::MAX),
),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: DeviceIntRect vs Box2D semantics are equivalent for the origin-anchored read rect

The pixel readback rect changed from Box2D::new(Point2D(0,0), Point2D(w,h)) to DeviceIntRect::new(DeviceIntPoint(0,0), DeviceIntPoint(w,h)) (servo_thread.rs:515-521). DeviceIntRect in webrender is a Box2D (min/max), so this is a min/max construction. Even if a reader assumed origin/size semantics, the region is identical because the origin is (0,0) and the second point equals (w,h). No behavioral change to the read region.

(Refers to lines 515-521)

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment thread plugins/native/servo/src/servo_thread.rs Outdated
Comment thread plugins/native/servo/src/config.rs Outdated
Signed-off-by: streamkit-devin <devin@streamkit.dev>

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review (Staging)
Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: update_params does not re-validate auth, but auth is immutable so this is safe

update_params (plugins/native/servo/src/servo_node.rs:240-252) deserializes the update and calls merge_update without calling validate(). Since merge_update never copies auth from the update, no unvalidated auth can reach the WebView at runtime. Malformed runtime URLs are silently dropped by handle_update_config's url::Url::parse guard rather than producing an error to the caller — this is pre-existing behavior unchanged by this PR.

(Refers to lines 240-252)

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Signed-off-by: streamkit-devin <devin@streamkit.dev>

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review (Staging)
Debug

Playground

Comment on lines +79 to +107
pub fn build_request_headers(&self) -> Result<HeaderMap, String> {
let mut map = HeaderMap::new();
let mut has_authorization = false;

if let Some(ref headers) = self.headers {
for (name, value) in headers {
let header_name = HeaderName::try_from(name)
.map_err(|e| format!("invalid header name '{name}': {e}"))?;
let header_value = HeaderValue::try_from(value)
.map_err(|e| format!("invalid header value for '{name}': {e}"))?;
if header_name == AUTHORIZATION {
has_authorization = true;
}
map.append(header_name, header_value);
}
}

if let Some(ref token) = self.bearer_token {
if has_authorization {
return Err("auth.bearer_token conflicts with an explicit 'Authorization' header"
.to_string());
}
let value = HeaderValue::try_from(format!("Bearer {token}"))
.map_err(|e| format!("invalid bearer_token: {e}"))?;
map.insert(AUTHORIZATION, value);
}

Ok(map)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Credential safety: no Debug-leak of auth and consistent URL redaction

ServoConfig/ServoAuth/ServoBasicAuth all derive Debug and carry credentials, so I checked every logging site. The config is never logged via {:?}; all URL logs route through crate::config::redact_url (e.g. servo_node.rs:189, servo_node.rs:281, servo_thread.rs:439, servo_thread.rs:506, servo_thread.rs:288). Error messages from build_request_headers (config.rs:86-102) deliberately interpolate header names and the http crate's InvalidHeaderValue/InvalidHeaderName errors, which do not echo the offending value/token, so credentials don't leak via validation errors either. No credential-leak issue found.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants