Skip to main content

soma_provider_adapters/python/
host.rs

1//! Capability broker for persistent Python workers.
2
3use std::{
4    collections::VecDeque,
5    net::{IpAddr, SocketAddr},
6    sync::{
7        Arc, Mutex,
8        atomic::{AtomicBool, Ordering},
9    },
10    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
11};
12
13use serde::Serialize;
14use serde_json::{Value, json};
15use soma_provider_core::{BrokerCapability, HostCapabilities, NetworkCapability};
16use url::Url;
17
18use crate::{
19    broker_state::BrokerStateStore,
20    python_protocol::{
21        PythonActorContext, PythonRunnerError, PythonRunnerErrorCode, PythonRunnerErrorPhase,
22        PythonRunnerHostCall,
23    },
24};
25
26const MAX_AUDIT_EVENTS: usize = 256;
27type HostResult<T> = Result<T, Box<PythonRunnerError>>;
28
29/// Ambient-authority posture for Python execution.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum PythonExecutionProfile {
33    Disabled,
34    #[default]
35    Trusted,
36    Brokered,
37}
38
39/// Bounded, secret-free record of one broker decision.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
41pub struct PythonHostAuditEvent {
42    pub unix_ms: u64,
43    pub invocation_id: String,
44    pub operation: &'static str,
45    pub allowed: bool,
46    pub detail: String,
47}
48
49/// Host-side services and authority declarations for one provider worker.
50pub struct PythonHostBroker {
51    profile: PythonExecutionProfile,
52    network: Option<NetworkCapability>,
53    broker: Option<BrokerCapability>,
54    max_http_response_bytes: usize,
55    state: Result<Arc<BrokerStateStore>, String>,
56    audit: Mutex<VecDeque<PythonHostAuditEvent>>,
57    cancelled: Arc<AtomicBool>,
58}
59
60impl std::fmt::Debug for PythonHostBroker {
61    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        formatter
63            .debug_struct("PythonHostBroker")
64            .field("profile", &self.profile)
65            .field("network", &self.network)
66            .field("broker", &self.broker)
67            .finish_non_exhaustive()
68    }
69}
70
71impl PythonHostBroker {
72    #[must_use]
73    pub fn new(
74        profile: PythonExecutionProfile,
75        capabilities: &HostCapabilities,
76        cancelled: Arc<AtomicBool>,
77    ) -> Arc<Self> {
78        Arc::new(Self {
79            profile,
80            network: capabilities.network.clone(),
81            broker: capabilities.broker.clone(),
82            max_http_response_bytes: 256 * 1024,
83            state: BrokerStateStore::configured(),
84            audit: Mutex::new(VecDeque::new()),
85            cancelled,
86        })
87    }
88
89    #[must_use]
90    pub fn profile(&self) -> PythonExecutionProfile {
91        self.profile
92    }
93
94    #[must_use]
95    pub fn audit_events(&self) -> Vec<PythonHostAuditEvent> {
96        self.audit
97            .lock()
98            .expect("Python host audit lock should not be poisoned")
99            .iter()
100            .cloned()
101            .collect()
102    }
103
104    pub(crate) fn begin_invocation(&self) {
105        self.cancelled.store(false, Ordering::Release);
106    }
107
108    pub(crate) fn cancel_invocation(&self) {
109        self.cancelled.store(true, Ordering::Release);
110    }
111
112    pub async fn execute(
113        &self,
114        call: &PythonRunnerHostCall,
115        actor: Option<&PythonActorContext>,
116    ) -> HostResult<Value> {
117        if self.profile == PythonExecutionProfile::Disabled {
118            return Err(self.denied(call, "Python execution is disabled"));
119        }
120        let result = match call {
121            PythonRunnerHostCall::Http {
122                invocation_id,
123                request,
124                ..
125            } => {
126                self.require_scope(actor, false, call)?;
127                self.http(invocation_id, request).await
128            }
129            PythonRunnerHostCall::Secret {
130                invocation_id,
131                name,
132                ..
133            } => {
134                self.require_scope(actor, false, call)?;
135                self.secret(invocation_id, name)
136            }
137            PythonRunnerHostCall::StateGet {
138                invocation_id, key, ..
139            } => {
140                self.require_scope(actor, false, call)?;
141                self.state_get(invocation_id, key).await
142            }
143            PythonRunnerHostCall::StatePut {
144                invocation_id,
145                key,
146                value,
147                ..
148            } => {
149                self.require_scope(actor, true, call)?;
150                self.state_put(invocation_id, key, value).await
151            }
152            PythonRunnerHostCall::Log {
153                invocation_id,
154                level,
155                message,
156                fields,
157                ..
158            } => {
159                self.require_scope(actor, false, call)?;
160                self.log(invocation_id, level, message, fields)
161            }
162            PythonRunnerHostCall::Metric {
163                invocation_id,
164                name,
165                value,
166                attributes,
167                ..
168            } => {
169                self.require_scope(actor, false, call)?;
170                self.metric(invocation_id, name, value, attributes)
171            }
172            PythonRunnerHostCall::Progress {
173                invocation_id,
174                current,
175                total,
176                message,
177                ..
178            } => {
179                self.require_scope(actor, false, call)?;
180                self.progress(invocation_id, *current, *total, message.as_deref())
181            }
182            PythonRunnerHostCall::Cancelled { invocation_id, .. } => {
183                let value = self.cancelled.load(Ordering::Acquire);
184                self.record(invocation_id, "cancelled", true, "queried".to_owned());
185                Ok(Value::Bool(value))
186            }
187        };
188        if let Err(error) = &result {
189            self.record(
190                invocation_id(call),
191                operation(call),
192                false,
193                error.public_message.clone(),
194            );
195        }
196        result
197    }
198
199    fn require_scope(
200        &self,
201        actor: Option<&PythonActorContext>,
202        write: bool,
203        call: &PythonRunnerHostCall,
204    ) -> HostResult<()> {
205        let actor =
206            actor.ok_or_else(|| self.denied(call, "authenticated actor context is required"))?;
207        let allowed = actor
208            .scopes
209            .iter()
210            .any(|scope| scope == "soma:write" || (!write && scope == "soma:read"));
211        if allowed {
212            Ok(())
213        } else {
214            Err(self.denied(call, "actor scopes do not authorize this host operation"))
215        }
216    }
217
218    async fn http(&self, invocation_id: &str, request: &Value) -> HostResult<Value> {
219        let capability = self
220            .network
221            .as_ref()
222            .filter(|capability| capability.enabled)
223            .ok_or_else(|| self.policy_error("provider did not declare network capability"))?;
224        let method = request
225            .get("method")
226            .and_then(Value::as_str)
227            .unwrap_or("GET");
228        let raw_url = request
229            .get("url")
230            .and_then(Value::as_str)
231            .ok_or_else(|| self.policy_error("HTTP request URL is required"))?;
232        let url =
233            Url::parse(raw_url).map_err(|_| self.policy_error("HTTP request URL is invalid"))?;
234        if url.scheme() != "https" || !url.username().is_empty() || url.password().is_some() {
235            return Err(self.policy_error("brokered HTTP requires HTTPS without URL credentials"));
236        }
237        let host = url
238            .host_str()
239            .ok_or_else(|| self.policy_error("HTTP request host is required"))?
240            .to_owned();
241        if !capability
242            .allowed_hosts
243            .iter()
244            .any(|allowed| allowed == &host)
245        {
246            return Err(self.policy_error("HTTP request host is not declared"));
247        }
248        let port = url.port_or_known_default().unwrap_or(443);
249        let addresses = tokio::net::lookup_host((host.as_str(), port))
250            .await
251            .map_err(|_| self.policy_error("HTTP host resolution failed"))?
252            .collect::<Vec<_>>();
253        if addresses.is_empty() || addresses.iter().any(|address| !public_ip(address.ip())) {
254            return Err(self.policy_error("HTTP host resolved to a non-public address"));
255        }
256
257        let mut builder = reqwest::Client::builder()
258            .no_proxy()
259            .redirect(reqwest::redirect::Policy::none())
260            .timeout(Duration::from_secs(10))
261            .https_only(true);
262        for address in addresses {
263            builder = builder.resolve(&host, address);
264        }
265        let client = builder
266            .build()
267            .map_err(|_| self.policy_error("HTTP client initialization failed"))?;
268        let method = reqwest::Method::from_bytes(method.as_bytes())
269            .map_err(|_| self.policy_error("HTTP method is invalid"))?;
270        let mut outbound = client.request(method, url);
271        if let Some(headers) = request.get("headers").and_then(Value::as_object) {
272            for (name, value) in headers {
273                if forbidden_forwarded_header(name) {
274                    return Err(self.policy_error("HTTP header is controlled by the broker"));
275                }
276                let value = value
277                    .as_str()
278                    .ok_or_else(|| self.policy_error("HTTP header values must be strings"))?;
279                outbound = outbound.header(name, value);
280            }
281        }
282        if let Some(body) = request.get("body_base64").and_then(Value::as_str) {
283            use base64::Engine as _;
284
285            let body = base64::engine::general_purpose::STANDARD
286                .decode(body)
287                .map_err(|_| self.policy_error("HTTP request body_base64 is invalid"))?;
288            outbound = outbound.body(body);
289        } else if let Some(body) = request.get("body").and_then(Value::as_str) {
290            // Protocol 1.0 compatibility for older SDKs.
291            outbound = outbound.body(body.to_owned());
292        }
293        let mut response = outbound
294            .send()
295            .await
296            .map_err(|_| self.policy_error("HTTP request failed"))?;
297        if response.status().is_redirection() {
298            return Err(
299                self.policy_error("HTTP redirects are not followed by the capability broker")
300            );
301        }
302        let status = response.status().as_u16();
303        if response
304            .content_length()
305            .is_some_and(|length| length > self.max_http_response_bytes as u64)
306        {
307            return Err(self.policy_error("HTTP response exceeds broker limit"));
308        }
309        let mut bytes = Vec::new();
310        while let Some(chunk) = response
311            .chunk()
312            .await
313            .map_err(|_| self.policy_error("HTTP response body failed"))?
314        {
315            if bytes.len().saturating_add(chunk.len()) > self.max_http_response_bytes {
316                return Err(self.policy_error("HTTP response exceeds broker limit"));
317            }
318            bytes.extend_from_slice(&chunk);
319        }
320        self.record(
321            invocation_id,
322            "http",
323            true,
324            format!(
325                "https://{host}:{port} status={status} bytes={}",
326                bytes.len()
327            ),
328        );
329        use base64::Engine as _;
330
331        let mut result = json!({
332            "status": status,
333            "body_base64": base64::engine::general_purpose::STANDARD.encode(&bytes),
334        });
335        if let Ok(body) = String::from_utf8(bytes)
336            && let Some(object) = result.as_object_mut()
337        {
338            object.insert("body".to_owned(), Value::String(body));
339        }
340        Ok(result)
341    }
342
343    fn secret(&self, invocation_id: &str, name: &str) -> HostResult<Value> {
344        let capability = self.broker_capability()?;
345        if !capability
346            .secret_names
347            .iter()
348            .any(|allowed| allowed == name)
349        {
350            return Err(self.policy_error("secret name is not declared"));
351        }
352        let variable = crate::secret_name::environment_name(name)
353            .map_err(|message| self.policy_error(&message))?;
354        let secret = std::env::var(variable)
355            .map_err(|_| self.policy_error("declared secret is unavailable"))?;
356        self.record(invocation_id, "secret", true, name.to_owned());
357        Ok(Value::String(secret))
358    }
359
360    async fn state_get(&self, invocation_id: &str, key: &str) -> HostResult<Value> {
361        let namespace = self.state_namespace()?.to_owned();
362        let key = key.to_owned();
363        let task_key = key.clone();
364        let state = self.state_store()?;
365        let cancelled = self.cancelled.clone();
366        let deadline = Instant::now() + Duration::from_secs(10);
367        let result = tokio::task::spawn_blocking(move || {
368            state.get(&namespace, &task_key, deadline, Some(&cancelled))
369        })
370        .await
371        .map_err(|_| self.policy_error("provider state task failed"))?
372        .map_err(|message| self.policy_error(&message))?;
373        self.record(invocation_id, "state.get", true, key);
374        Ok(result)
375    }
376
377    async fn state_put(&self, invocation_id: &str, key: &str, value: &Value) -> HostResult<Value> {
378        let capability = self.broker_capability()?;
379        if !capability.state_write {
380            return Err(self.policy_error("provider did not declare state write access"));
381        }
382        let namespace = self.state_namespace()?.to_owned();
383        let state = self.state_store()?;
384        let key_owned = key.to_owned();
385        let value = value.clone();
386        let cancelled = self.cancelled.clone();
387        let deadline = Instant::now() + Duration::from_secs(10);
388        tokio::task::spawn_blocking(move || {
389            state.put(&namespace, &key_owned, &value, deadline, Some(&cancelled))
390        })
391        .await
392        .map_err(|_| self.policy_error("provider state task failed"))?
393        .map_err(|message| self.policy_error(&message))?;
394        self.record(invocation_id, "state.put", true, key.to_owned());
395        Ok(Value::Null)
396    }
397
398    fn log(
399        &self,
400        invocation_id: &str,
401        level: &str,
402        message: &str,
403        fields: &Value,
404    ) -> HostResult<Value> {
405        if !self.broker_capability()?.logging {
406            return Err(self.policy_error("provider did not declare structured logging"));
407        }
408        let message = self.public_diagnostic(message);
409        tracing::info!(
410            provider_invocation = invocation_id,
411            provider_level = level,
412            message,
413            fields = %self.public_diagnostic(&fields.to_string()),
414            "Python provider structured log"
415        );
416        self.record(invocation_id, "log", true, level.to_owned());
417        Ok(Value::Null)
418    }
419
420    fn metric(
421        &self,
422        invocation_id: &str,
423        name: &str,
424        value: &serde_json::Number,
425        attributes: &Value,
426    ) -> HostResult<Value> {
427        if !self.broker_capability()?.metrics {
428            return Err(self.policy_error("provider did not declare metrics"));
429        }
430        tracing::info!(
431            provider_invocation = invocation_id,
432            metric = name,
433            value = %value,
434            attributes = %self.public_diagnostic(&attributes.to_string()),
435            "Python provider metric"
436        );
437        self.record(invocation_id, "metric", true, name.to_owned());
438        Ok(Value::Null)
439    }
440
441    fn progress(
442        &self,
443        invocation_id: &str,
444        current: u64,
445        total: Option<u64>,
446        message: Option<&str>,
447    ) -> HostResult<Value> {
448        if !self.broker_capability()?.progress {
449            return Err(self.policy_error("provider did not declare progress"));
450        }
451        tracing::info!(
452            provider_invocation = invocation_id,
453            current,
454            ?total,
455            message = %self.public_diagnostic(message.unwrap_or_default()),
456            "Python provider progress"
457        );
458        self.record(
459            invocation_id,
460            "progress",
461            true,
462            format!("{current}/{total:?}"),
463        );
464        Ok(Value::Null)
465    }
466
467    fn broker_capability(&self) -> HostResult<&BrokerCapability> {
468        self.broker
469            .as_ref()
470            .filter(|capability| capability.enabled)
471            .ok_or_else(|| self.policy_error("provider did not declare broker capabilities"))
472    }
473
474    fn state_namespace(&self) -> HostResult<&str> {
475        self.broker_capability()?
476            .state_namespace
477            .as_deref()
478            .ok_or_else(|| self.policy_error("provider did not declare a state namespace"))
479    }
480
481    fn state_store(&self) -> HostResult<Arc<BrokerStateStore>> {
482        self.state
483            .as_ref()
484            .map(Arc::clone)
485            .map_err(|message| self.policy_error(message))
486    }
487
488    fn denied(&self, call: &PythonRunnerHostCall, message: &str) -> Box<PythonRunnerError> {
489        self.record(
490            invocation_id(call),
491            operation(call),
492            false,
493            message.to_owned(),
494        );
495        self.policy_error(message)
496    }
497
498    fn policy_error(&self, message: &str) -> Box<PythonRunnerError> {
499        Box::new(PythonRunnerError {
500            code: PythonRunnerErrorCode::PythonPolicyDenied,
501            phase: PythonRunnerErrorPhase::Policy,
502            provider: None,
503            source: None,
504            generation_id: None,
505            action: None,
506            retryable: false,
507            public_message: message.to_owned(),
508        })
509    }
510
511    fn record(&self, invocation_id: &str, operation: &'static str, allowed: bool, detail: String) {
512        let mut audit = self
513            .audit
514            .lock()
515            .expect("Python host audit lock should not be poisoned");
516        if audit.len() == MAX_AUDIT_EVENTS {
517            audit.pop_front();
518        }
519        audit.push_back(PythonHostAuditEvent {
520            unix_ms: SystemTime::now()
521                .duration_since(UNIX_EPOCH)
522                .unwrap_or_default()
523                .as_millis()
524                .min(u128::from(u64::MAX)) as u64,
525            invocation_id: invocation_id.to_owned(),
526            operation,
527            allowed,
528            detail: self.public_diagnostic(&detail),
529        });
530    }
531
532    fn public_diagnostic(&self, message: &str) -> String {
533        let names = self
534            .broker
535            .as_ref()
536            .map(|broker| broker.secret_names.as_slice())
537            .unwrap_or_default();
538        crate::secret_name::redact(message, names)
539    }
540}
541
542fn invocation_id(call: &PythonRunnerHostCall) -> &str {
543    match call {
544        PythonRunnerHostCall::Http { invocation_id, .. }
545        | PythonRunnerHostCall::Secret { invocation_id, .. }
546        | PythonRunnerHostCall::StateGet { invocation_id, .. }
547        | PythonRunnerHostCall::StatePut { invocation_id, .. }
548        | PythonRunnerHostCall::Log { invocation_id, .. }
549        | PythonRunnerHostCall::Metric { invocation_id, .. }
550        | PythonRunnerHostCall::Progress { invocation_id, .. }
551        | PythonRunnerHostCall::Cancelled { invocation_id, .. } => invocation_id,
552    }
553}
554
555fn operation(call: &PythonRunnerHostCall) -> &'static str {
556    match call {
557        PythonRunnerHostCall::Http { .. } => "http",
558        PythonRunnerHostCall::Secret { .. } => "secret",
559        PythonRunnerHostCall::StateGet { .. } => "state.get",
560        PythonRunnerHostCall::StatePut { .. } => "state.put",
561        PythonRunnerHostCall::Log { .. } => "log",
562        PythonRunnerHostCall::Metric { .. } => "metric",
563        PythonRunnerHostCall::Progress { .. } => "progress",
564        PythonRunnerHostCall::Cancelled { .. } => "cancelled",
565    }
566}
567
568fn forbidden_forwarded_header(name: &str) -> bool {
569    matches!(
570        name.to_ascii_lowercase().as_str(),
571        "host"
572            | "connection"
573            | "content-length"
574            | "proxy-authenticate"
575            | "proxy-authorization"
576            | "te"
577            | "trailer"
578            | "transfer-encoding"
579            | "upgrade"
580    )
581}
582
583fn public_ip(ip: IpAddr) -> bool {
584    match ip {
585        IpAddr::V4(ip) => {
586            let [first, second, third, _] = ip.octets();
587            !(ip.is_private()
588                || ip.is_loopback()
589                || ip.is_link_local()
590                || ip.is_broadcast()
591                || ip.is_documentation()
592                || ip.is_unspecified()
593                || ip.is_multicast()
594                || first == 0
595                || (first == 100 && (64..=127).contains(&second))
596                || (first == 192 && second == 0 && third == 0)
597                || (first == 192 && second == 88 && third == 99)
598                || (first == 198 && (18..=19).contains(&second))
599                || first >= 240)
600        }
601        IpAddr::V6(ip) => {
602            let segments = ip.segments();
603            (0x2000..=0x3fff).contains(&segments[0])
604                && !(segments[0] == 0x2001 && segments[1] == 0x0db8)
605                && !ip.is_multicast()
606        }
607    }
608}
609
610#[allow(dead_code)]
611fn _socket_address_is_public(address: SocketAddr) -> bool {
612    public_ip(address.ip())
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use crate::python_protocol::{PythonActorContext, PythonRunnerHostCall};
619
620    fn capabilities(namespace: &str) -> HostCapabilities {
621        HostCapabilities {
622            broker: Some(BrokerCapability {
623                enabled: true,
624                state_namespace: Some(namespace.to_owned()),
625                state_write: true,
626                logging: true,
627                metrics: true,
628                progress: true,
629                ..BrokerCapability::default()
630            }),
631            ..HostCapabilities::default()
632        }
633    }
634
635    fn actor(scopes: &[&str]) -> PythonActorContext {
636        PythonActorContext {
637            actor_id: "actor".to_owned(),
638            scopes: scopes.iter().map(|scope| (*scope).to_owned()).collect(),
639        }
640    }
641
642    #[tokio::test]
643    async fn state_is_namespaced_and_actor_write_scope_is_required() {
644        let mut broker = PythonHostBroker::new(
645            PythonExecutionProfile::Brokered,
646            &capabilities("provider-a"),
647            Arc::new(AtomicBool::new(false)),
648        );
649        Arc::get_mut(&mut broker).unwrap().state = Ok(BrokerStateStore::in_memory_for_test());
650        let denied = broker
651            .execute(
652                &PythonRunnerHostCall::StatePut {
653                    request_id: 1,
654                    invocation_id: "invocation".to_owned(),
655                    key: "count".to_owned(),
656                    value: json!(1),
657                },
658                Some(&actor(&["soma:read"])),
659            )
660            .await
661            .expect_err("read scope must not grant state writes");
662        assert_eq!(denied.code, PythonRunnerErrorCode::PythonPolicyDenied);
663
664        broker
665            .execute(
666                &PythonRunnerHostCall::StatePut {
667                    request_id: 2,
668                    invocation_id: "invocation".to_owned(),
669                    key: "count".to_owned(),
670                    value: json!(2),
671                },
672                Some(&actor(&["soma:write"])),
673            )
674            .await
675            .expect("write scope and provider declaration intersect");
676        let value = broker
677            .execute(
678                &PythonRunnerHostCall::StateGet {
679                    request_id: 3,
680                    invocation_id: "invocation".to_owned(),
681                    key: "count".to_owned(),
682                },
683                Some(&actor(&["soma:read"])),
684            )
685            .await
686            .expect("read scope can access declared state");
687        assert_eq!(value, json!(2));
688    }
689
690    #[tokio::test]
691    async fn disabled_profile_and_undeclared_services_fail_closed() {
692        let disabled = PythonHostBroker::new(
693            PythonExecutionProfile::Disabled,
694            &HostCapabilities::default(),
695            Arc::new(AtomicBool::new(false)),
696        );
697        let error = disabled
698            .execute(
699                &PythonRunnerHostCall::Cancelled {
700                    request_id: 1,
701                    invocation_id: "invocation".to_owned(),
702                },
703                None,
704            )
705            .await
706            .expect_err("disabled profile rejects host calls");
707        assert_eq!(error.code, PythonRunnerErrorCode::PythonPolicyDenied);
708    }
709
710    #[tokio::test]
711    async fn missing_actor_context_fails_closed() {
712        let broker = PythonHostBroker::new(
713            PythonExecutionProfile::Brokered,
714            &capabilities("provider-a"),
715            Arc::new(AtomicBool::new(false)),
716        );
717        let error = broker
718            .execute(
719                &PythonRunnerHostCall::StateGet {
720                    request_id: 1,
721                    invocation_id: "invocation".to_owned(),
722                    key: "count".to_owned(),
723                },
724                None,
725            )
726            .await
727            .expect_err("brokered host services require an authenticated actor");
728        assert_eq!(error.code, PythonRunnerErrorCode::PythonPolicyDenied);
729    }
730
731    #[test]
732    fn diagnostics_and_network_targets_are_conservative() {
733        let broker = PythonHostBroker::new(
734            PythonExecutionProfile::Trusted,
735            &HostCapabilities::default(),
736            Arc::new(AtomicBool::new(false)),
737        );
738        assert_eq!(
739            broker.public_diagnostic("Authorization: bearer value"),
740            "[redacted]"
741        );
742        assert!(!public_ip("127.0.0.1".parse().unwrap()));
743        assert!(!public_ip("10.0.0.1".parse().unwrap()));
744        assert!(!public_ip("100.64.0.1".parse().unwrap()));
745        assert!(!public_ip("224.0.0.1".parse().unwrap()));
746        assert!(public_ip("1.1.1.1".parse().unwrap()));
747        assert!(forbidden_forwarded_header("Host"));
748        assert!(forbidden_forwarded_header("transfer-encoding"));
749        assert!(!forbidden_forwarded_header("authorization"));
750    }
751}