Skip to main content

webdriver/
server.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::Parameters;
6use crate::command::{WebDriverCommand, WebDriverMessage};
7use crate::error::{ErrorStatus, WebDriverError, WebDriverResult};
8use crate::httpapi::{
9    Route, VoidWebDriverExtensionRoute, WebDriverExtensionRoute, standard_routes,
10};
11use crate::response::{CloseWindowResponse, WebDriverResponse};
12use bytes::Bytes;
13use http::{Method, StatusCode};
14use std::marker::PhantomData;
15use std::net::{SocketAddr, TcpListener as StdTcpListener};
16use std::sync::mpsc::{Receiver, Sender, channel};
17use std::sync::{Arc, Mutex};
18use std::thread;
19use bytes::Buf;
20use tokio::net::TcpListener;
21use url::{Host, Url};
22use warp::{Filter, Rejection};
23
24enum DispatchMessage<U: WebDriverExtensionRoute> {
25    HandleWebDriver(
26        WebDriverMessage<U>,
27        Sender<WebDriverResult<WebDriverResponse>>,
28    ),
29    Quit,
30}
31
32#[derive(Clone, Debug, PartialEq)]
33/// Representation of whether we managed to successfully send a DeleteSession message
34/// and read the response during session teardown.
35pub enum SessionTeardownKind {
36    /// A DeleteSession message has been sent and the response handled.
37    Deleted,
38    /// No DeleteSession message has been sent, or the response was not received.
39    NotDeleted,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct Session {
44    pub id: String,
45}
46
47impl Session {
48    fn new(id: String) -> Session {
49        Session { id }
50    }
51}
52
53pub trait WebDriverHandler<U: WebDriverExtensionRoute = VoidWebDriverExtensionRoute>: Send {
54    fn handle_command(
55        &mut self,
56        session: &Option<Session>,
57        msg: WebDriverMessage<U>,
58    ) -> WebDriverResult<WebDriverResponse>;
59    fn teardown_session(&mut self, kind: SessionTeardownKind);
60}
61
62#[derive(Debug)]
63struct Dispatcher<T: WebDriverHandler<U>, U: WebDriverExtensionRoute> {
64    handler: T,
65    session: Option<Session>,
66    extension_type: PhantomData<U>,
67}
68
69impl<T: WebDriverHandler<U>, U: WebDriverExtensionRoute> Dispatcher<T, U> {
70    fn new(handler: T) -> Dispatcher<T, U> {
71        Dispatcher {
72            handler,
73            session: None,
74            extension_type: PhantomData,
75        }
76    }
77
78    fn run(&mut self, msg_chan: &Receiver<DispatchMessage<U>>) {
79        loop {
80            match msg_chan.recv() {
81                Ok(DispatchMessage::HandleWebDriver(msg, resp_chan)) => {
82                    let resp = match self.check_session(&msg) {
83                        Ok(_) => self.handler.handle_command(&self.session, msg),
84                        Err(e) => Err(e),
85                    };
86
87                    match resp {
88                        Ok(WebDriverResponse::NewSession(ref new_session)) => {
89                            self.session = Some(Session::new(new_session.session_id.clone()));
90                        }
91                        Ok(WebDriverResponse::CloseWindow(CloseWindowResponse(ref handles))) => {
92                            if handles.is_empty() {
93                                debug!("Last window was closed, deleting session");
94                                // The teardown_session implementation is responsible for actually
95                                // sending the DeleteSession message in this case
96                                self.teardown_session(SessionTeardownKind::NotDeleted);
97                            }
98                        }
99                        Ok(WebDriverResponse::DeleteSession) => {
100                            self.teardown_session(SessionTeardownKind::Deleted);
101                        }
102                        Err(ref x) if x.delete_session => {
103                            // This includes the case where we failed during session creation
104                            self.teardown_session(SessionTeardownKind::NotDeleted)
105                        }
106                        _ => {}
107                    }
108
109                    if resp_chan.send(resp).is_err() {
110                        error!("Sending response to the main thread failed");
111                    };
112                }
113                Ok(DispatchMessage::Quit) => {
114                    debug!("Quit signal received, tearing down session");
115                    self.teardown_session(SessionTeardownKind::NotDeleted);
116                    break;
117                }
118                Err(e) => panic!("Error receiving message in handler: {:?}", e),
119            }
120        }
121    }
122
123    fn teardown_session(&mut self, kind: SessionTeardownKind) {
124        debug!("Teardown session");
125        let final_kind = match kind {
126            SessionTeardownKind::NotDeleted if self.session.is_some() => {
127                let delete_session = WebDriverMessage {
128                    session_id: Some(
129                        self.session
130                            .as_ref()
131                            .expect("Failed to get session")
132                            .id
133                            .clone(),
134                    ),
135                    command: WebDriverCommand::DeleteSession,
136                };
137                match self.handler.handle_command(&self.session, delete_session) {
138                    Ok(_) => SessionTeardownKind::Deleted,
139                    Err(_) => SessionTeardownKind::NotDeleted,
140                }
141            }
142            _ => kind,
143        };
144        self.handler.teardown_session(final_kind);
145        self.session = None;
146    }
147
148    fn check_session(&self, msg: &WebDriverMessage<U>) -> WebDriverResult<()> {
149        match msg.session_id {
150            Some(ref msg_session_id) => match self.session {
151                Some(ref existing_session) => {
152                    if existing_session.id != *msg_session_id {
153                        Err(WebDriverError::new(
154                            ErrorStatus::InvalidSessionId,
155                            format!("Got unexpected session id {}", msg_session_id),
156                        ))
157                    } else {
158                        Ok(())
159                    }
160                }
161                None => Ok(()),
162            },
163            None => {
164                match self.session {
165                    Some(_) => {
166                        match msg.command {
167                            WebDriverCommand::Status => Ok(()),
168                            WebDriverCommand::NewSession(_) => Err(WebDriverError::new(
169                                ErrorStatus::SessionNotCreated,
170                                "Session is already started",
171                            )),
172                            _ => {
173                                //This should be impossible
174                                error!("Got a message with no session id");
175                                Err(WebDriverError::new(
176                                    ErrorStatus::UnknownError,
177                                    "Got a command with no session?!",
178                                ))
179                            }
180                        }
181                    }
182                    None => match msg.command {
183                        WebDriverCommand::NewSession(_) => Ok(()),
184                        WebDriverCommand::Status => Ok(()),
185                        _ => Err(WebDriverError::new(
186                            ErrorStatus::InvalidSessionId,
187                            "Tried to run a command before creating a session",
188                        )),
189                    },
190                }
191            }
192        }
193    }
194}
195
196pub struct Listener {
197    server: Option<thread::JoinHandle<()>>,
198    dispatcher: Option<thread::JoinHandle<()>>,
199    pub socket: SocketAddr,
200}
201
202impl Drop for Listener {
203    fn drop(&mut self) {
204        let _ = self.server.take().map(|j| j.join());
205        let _ = self.dispatcher.take().map(|j| j.join());
206    }
207}
208
209#[cfg(unix)]
210struct ShutdownSignal {
211    sigint: Option<tokio::signal::unix::Signal>,
212    sigterm: Option<tokio::signal::unix::Signal>,
213}
214
215#[cfg(unix)]
216impl ShutdownSignal {
217    fn new() -> Self {
218        use tokio::signal::unix::{SignalKind, signal};
219        ShutdownSignal {
220            sigint: signal(SignalKind::interrupt())
221                .map_err(|_| warn!("Failed to register SIGINT handler"))
222                .ok(),
223            sigterm: signal(SignalKind::terminate())
224                .map_err(|_| warn!("Failed to register SIGTERM handler"))
225                .ok(),
226        }
227    }
228
229    async fn recv(&mut self) {
230        tokio::select! {
231            _ = async { self.sigint.as_mut().unwrap().recv().await }, if self.sigint.is_some() => {},
232            _ = async { self.sigterm.as_mut().unwrap().recv().await }, if self.sigterm.is_some() => {},
233            _ = std::future::pending::<()>(), if self.sigint.is_none() && self.sigterm.is_none() => {},
234        }
235    }
236}
237
238#[cfg(windows)]
239struct ShutdownSignal {
240    ctrl_c: Option<tokio::signal::windows::CtrlC>,
241    ctrl_break: Option<tokio::signal::windows::CtrlBreak>,
242}
243
244#[cfg(windows)]
245impl ShutdownSignal {
246    fn new() -> Self {
247        use tokio::signal::windows;
248        ShutdownSignal {
249            ctrl_c: windows::ctrl_c()
250                .map_err(|_| warn!("Failed to register ctrl_c handler"))
251                .ok(),
252            ctrl_break: windows::ctrl_break()
253                .map_err(|_| warn!("Failed to register ctrl_break handler"))
254                .ok(),
255        }
256    }
257
258    async fn recv(&mut self) {
259        tokio::select! {
260            _ = async { self.ctrl_c.as_mut().unwrap().recv().await }, if self.ctrl_c.is_some() => {},
261            _ = async { self.ctrl_break.as_mut().unwrap().recv().await }, if self.ctrl_break.is_some() => {},
262            _ = std::future::pending::<()>(), if self.ctrl_c.is_none() && self.ctrl_break.is_none() => {},
263        }
264    }
265}
266
267pub fn start<T, U>(
268    mut address: SocketAddr,
269    allow_hosts: Vec<Host>,
270    allow_origins: Vec<Url>,
271    handler: T,
272    extension_routes: Vec<(Method, &'static str, U)>,
273) -> ::std::io::Result<Listener>
274where
275    T: 'static + WebDriverHandler<U>,
276    U: 'static + WebDriverExtensionRoute + Send + Sync,
277{
278    let listener = StdTcpListener::bind(address)?;
279    listener.set_nonblocking(true)?;
280    let addr = listener.local_addr()?;
281    if address.port() == 0 {
282        // If we passed in 0 as the port number the OS will assign an unused port;
283        // we want to update the address to the actual used port
284        address.set_port(addr.port())
285    }
286    let (msg_send, msg_recv) = channel();
287    let (dispatcher_done_send, dispatcher_done_recv) = tokio::sync::oneshot::channel();
288
289    let builder = thread::Builder::new().name("webdriver server".to_string());
290    let msg_send_signal = msg_send.clone();
291    let server_handle = builder.spawn(move || {
292        let rt = tokio::runtime::Builder::new_current_thread()
293            .enable_io()
294            .build()
295            .unwrap();
296        let listener = rt.block_on(async { TcpListener::from_std(listener).unwrap() });
297        let wroutes = build_warp_routes(
298            address,
299            allow_hosts,
300            allow_origins,
301            &extension_routes,
302            msg_send.clone(),
303        );
304        let fut = warp::serve(wroutes).incoming(listener).run();
305        rt.block_on(async move {
306            let mut shutdown_signal = ShutdownSignal::new();
307            tokio::select! {
308                _ = fut => {}
309                _ =  shutdown_signal.recv() => {
310                    info!("Shutting down");
311                    let _ = msg_send_signal.send(DispatchMessage::Quit);
312                    tokio::select! {
313                        _ = dispatcher_done_recv => {},
314                        _ = shutdown_signal.recv() => {
315                            std::process::exit(130);
316                        }
317                    }
318                }
319            }
320        });
321    })?;
322
323    let builder = thread::Builder::new().name("webdriver dispatcher".to_string());
324    let dispatcher_handle = builder.spawn(move || {
325        let mut dispatcher = Dispatcher::new(handler);
326        dispatcher.run(&msg_recv);
327        let _ = dispatcher_done_send.send(());
328    })?;
329
330    Ok(Listener {
331        server: Some(server_handle),
332        dispatcher: Some(dispatcher_handle),
333        socket: addr,
334    })
335}
336
337fn build_warp_routes<U: 'static + WebDriverExtensionRoute + Send + Sync>(
338    address: SocketAddr,
339    allow_hosts: Vec<Host>,
340    allow_origins: Vec<Url>,
341    ext_routes: &[(Method, &'static str, U)],
342    chan: Sender<DispatchMessage<U>>,
343) -> impl Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone + 'static {
344    let chan = Arc::new(Mutex::new(chan));
345    let mut std_routes = standard_routes::<U>();
346
347    let (method, path, res) = std_routes.pop().unwrap();
348    trace!("Build standard route for {path}");
349    let mut wroutes = build_route(
350        address,
351        allow_hosts.clone(),
352        allow_origins.clone(),
353        method,
354        path,
355        res,
356        chan.clone(),
357    );
358
359    for (method, path, res) in std_routes {
360        trace!("Build standard route for {path}");
361        wroutes = wroutes
362            .or(build_route(
363                address,
364                allow_hosts.clone(),
365                allow_origins.clone(),
366                method,
367                path,
368                res.clone(),
369                chan.clone(),
370            ))
371            .unify()
372            .boxed()
373    }
374
375    for (method, path, res) in ext_routes {
376        trace!("Build vendor route for {path}");
377        wroutes = wroutes
378            .or(build_route(
379                address,
380                allow_hosts.clone(),
381                allow_origins.clone(),
382                method.clone(),
383                path,
384                Route::Extension(res.clone()),
385                chan.clone(),
386            ))
387            .unify()
388            .boxed()
389    }
390
391    wroutes
392}
393
394fn is_host_allowed(server_address: &SocketAddr, allow_hosts: &[Host], host_header: &str) -> bool {
395    // Validate that the Host header value has a hostname in allow_hosts and
396    // the port matches the server configuration
397    let header_host_url = match Url::parse(&format!("http://{}", &host_header)) {
398        Ok(x) => x,
399        Err(_) => {
400            return false;
401        }
402    };
403
404    let host = match header_host_url.host() {
405        Some(host) => host.to_owned(),
406        None => {
407            // This shouldn't be possible since http URL always have a
408            // host, but conservatively return false here, which will cause
409            // an error response
410            return false;
411        }
412    };
413    let port = match header_host_url.port_or_known_default() {
414        Some(port) => port,
415        None => {
416            // This shouldn't be possible since http URL always have a
417            // default port, but conservatively return false here, which will cause
418            // an error response
419            return false;
420        }
421    };
422
423    let host_matches = match host {
424        Host::Domain(_) => allow_hosts.contains(&host),
425        Host::Ipv4(_) | Host::Ipv6(_) => true,
426    };
427    let port_matches = server_address.port() == port;
428    host_matches && port_matches
429}
430
431fn is_origin_allowed(allow_origins: &[Url], origin_url: Url) -> bool {
432    // Validate that the Origin header value is in allow_origins
433    allow_origins.contains(&origin_url)
434}
435
436fn build_route<U: 'static + WebDriverExtensionRoute + Send + Sync>(
437    server_address: SocketAddr,
438    allow_hosts: Vec<Host>,
439    allow_origins: Vec<Url>,
440    method: Method,
441    path: &'static str,
442    route: Route<U>,
443    chan: Arc<Mutex<Sender<DispatchMessage<U>>>>,
444) -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
445    // Create an empty filter based on the provided method and append an empty hashmap to it. The
446    // hashmap will be used to store path parameters.
447    let mut subroute = match method {
448        Method::GET => warp::get().boxed(),
449        Method::POST => warp::post().boxed(),
450        Method::DELETE => warp::delete().boxed(),
451        Method::OPTIONS => warp::options().boxed(),
452        Method::PUT => warp::put().boxed(),
453        _ => panic!("Unsupported method"),
454    }
455    .or(warp::head())
456    .unify()
457    .map(Parameters::new)
458    .boxed();
459
460    // For each part of the path, if it's a normal part, just append it to the current filter,
461    // otherwise if it's a parameter (a named enclosed in { }), we take that parameter and insert
462    // it into the hashmap created earlier.
463    for part in path.split('/') {
464        if part.is_empty() {
465            continue;
466        } else if part.starts_with('{') {
467            assert!(part.ends_with('}'));
468
469            subroute = subroute
470                .and(warp::path::param())
471                .map(move |mut params: Parameters, param: String| {
472                    let name = &part[1..part.len() - 1];
473                    params.insert(name.to_string(), param);
474                    params
475                })
476                .boxed();
477        } else {
478            subroute = subroute.and(warp::path(part)).boxed();
479        }
480    }
481
482    // Finally, tell warp that the path is complete
483    subroute
484        .and(warp::path::end())
485        .and(warp::path::full())
486        .and(warp::method())
487        .and(warp::header::optional::<String>("origin"))
488        .and(warp::header::optional::<String>("host"))
489        .and(warp::header::optional::<String>("content-type"))
490        .and(warp::body::bytes())
491        .map(
492            move |params,
493                  full_path: warp::path::FullPath,
494                  method,
495                  origin_header: Option<String>,
496                  host_header: Option<String>,
497                  content_type_header: Option<String>,
498                  body: Bytes| {
499                if method == Method::HEAD {
500                    return warp::reply::with_status("".into(), StatusCode::OK);
501                }
502                if let Some(host) = host_header {
503                    if !is_host_allowed(&server_address, &allow_hosts, &host) {
504                        warn!(
505                            "Rejected request with Host header {}, allowed values are [{}]",
506                            host,
507                            allow_hosts
508                                .iter()
509                                .map(|x| format!("{}:{}", x, server_address.port()))
510                                .collect::<Vec<_>>()
511                                .join(",")
512                        );
513                        let err = WebDriverError::new(
514                            ErrorStatus::UnknownError,
515                            format!("Invalid Host header {}", host),
516                        );
517                        return warp::reply::with_status(
518                            serde_json::to_string(&err).unwrap(),
519                            StatusCode::INTERNAL_SERVER_ERROR,
520                        );
521                    };
522                } else {
523                    warn!("Rejected request with missing Host header");
524                    let err = WebDriverError::new(
525                        ErrorStatus::UnknownError,
526                        "Missing Host header".to_string(),
527                    );
528                    return warp::reply::with_status(
529                        serde_json::to_string(&err).unwrap(),
530                        StatusCode::INTERNAL_SERVER_ERROR,
531                    );
532                }
533                if let Some(origin) = origin_header {
534                    let make_err = || {
535                        warn!(
536                            "Rejected request with Origin header {}, allowed values are [{}]",
537                            origin,
538                            allow_origins
539                                .iter()
540                                .map(|x| x.to_string())
541                                .collect::<Vec<_>>()
542                                .join(",")
543                        );
544                        WebDriverError::new(
545                            ErrorStatus::UnknownError,
546                            format!("Invalid Origin header {}", origin),
547                        )
548                    };
549                    let origin_url = match Url::parse(&origin) {
550                        Ok(url) => url,
551                        Err(_) => {
552                            return warp::reply::with_status(
553                                serde_json::to_string(&make_err()).unwrap(),
554                                StatusCode::INTERNAL_SERVER_ERROR,
555                            );
556                        }
557                    };
558                    if !is_origin_allowed(&allow_origins, origin_url) {
559                        return warp::reply::with_status(
560                            serde_json::to_string(&make_err()).unwrap(),
561                            StatusCode::INTERNAL_SERVER_ERROR,
562                        );
563                    }
564                }
565                if method == Method::POST {
566                    // Disallow CORS-safelisted request headers
567                    // c.f. https://fetch.spec.whatwg.org/#cors-safelisted-request-header
568                    let content_type = content_type_header
569                        .as_ref()
570                        .map(|x| x.find(';').and_then(|idx| x.get(0..idx)).unwrap_or(x))
571                        .map(|x| x.trim())
572                        .map(|x| x.to_lowercase());
573                    match content_type.as_ref().map(|x| x.as_ref()) {
574                        Some("application/x-www-form-urlencoded")
575                        | Some("multipart/form-data")
576                        | Some("text/plain") => {
577                            warn!(
578                                "Rejected POST request with disallowed content type {}",
579                                content_type.unwrap_or_else(|| "".into())
580                            );
581                            let err = WebDriverError::new(
582                                ErrorStatus::UnknownError,
583                                "Invalid Content-Type",
584                            );
585                            return warp::reply::with_status(
586                                serde_json::to_string(&err).unwrap(),
587                                StatusCode::INTERNAL_SERVER_ERROR,
588                            );
589                        }
590                        Some(_) | None => {}
591                    }
592                }
593                let body = String::from_utf8(body.chunk().to_vec());
594                if body.is_err() {
595                    let err = WebDriverError::new(
596                        ErrorStatus::UnknownError,
597                        "Request body wasn't valid UTF-8",
598                    );
599                    return warp::reply::with_status(
600                        serde_json::to_string(&err).unwrap(),
601                        StatusCode::INTERNAL_SERVER_ERROR,
602                    );
603                }
604                let body = body.unwrap();
605
606                debug!("-> {} {} {}", method, full_path.as_str(), body);
607                let msg_result = WebDriverMessage::from_http(
608                    route.clone(),
609                    &params,
610                    &body,
611                    method == Method::POST,
612                );
613
614                let (status, resp_body) = match msg_result {
615                    Ok(message) => {
616                        let (send_res, recv_res) = channel();
617                        match chan.lock() {
618                            Ok(ref c) => {
619                                let res =
620                                    c.send(DispatchMessage::HandleWebDriver(message, send_res));
621                                match res {
622                                    Ok(x) => x,
623                                    Err(e) => panic!("Error: {:?}", e),
624                                }
625                            }
626                            Err(e) => panic!("Error reading response: {:?}", e),
627                        }
628
629                        match recv_res.recv() {
630                            Ok(data) => match data {
631                                Ok(response) => {
632                                    (StatusCode::OK, serde_json::to_string(&response).unwrap())
633                                }
634                                Err(e) => (e.http_status(), serde_json::to_string(&e).unwrap()),
635                            },
636                            Err(e) => panic!("Error reading response: {:?}", e),
637                        }
638                    }
639                    Err(e) => (e.http_status(), serde_json::to_string(&e).unwrap()),
640                };
641
642                debug!("<- {} {}", status, resp_body);
643                warp::reply::with_status(resp_body, status)
644            },
645        )
646        .with(warp::reply::with::header(
647            http::header::CONTENT_TYPE,
648            "application/json; charset=utf-8",
649        ))
650        .with(warp::reply::with::header(
651            http::header::CACHE_CONTROL,
652            "no-cache",
653        ))
654        .boxed()
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660    use std::net::IpAddr;
661    use std::str::FromStr;
662
663    #[test]
664    fn test_host_allowed() {
665        let addr_80 = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 80);
666        let addr_8000 = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 8000);
667        let addr_v6_80 = SocketAddr::new(IpAddr::from_str("::1").unwrap(), 80);
668        let addr_v6_8000 = SocketAddr::new(IpAddr::from_str("::1").unwrap(), 8000);
669
670        // We match the host ip address to the server, so we can only use hosts that actually resolve
671        let localhost_host = Host::Domain("localhost".to_string());
672        let test_host = Host::Domain("example.test".to_string());
673        let subdomain_localhost_host = Host::Domain("subdomain.localhost".to_string());
674
675        assert!(is_host_allowed(
676            &addr_80,
677            &[localhost_host.clone()],
678            "localhost:80"
679        ));
680        assert!(is_host_allowed(
681            &addr_80,
682            &[test_host.clone()],
683            "example.test:80"
684        ));
685        assert!(is_host_allowed(
686            &addr_80,
687            &[test_host.clone(), localhost_host.clone()],
688            "example.test"
689        ));
690        assert!(is_host_allowed(
691            &addr_80,
692            &[subdomain_localhost_host.clone()],
693            "subdomain.localhost"
694        ));
695
696        // ip address cases
697        assert!(is_host_allowed(&addr_80, &[], "127.0.0.1:80"));
698        assert!(is_host_allowed(&addr_v6_80, &[], "127.0.0.1"));
699        assert!(is_host_allowed(&addr_80, &[], "[::1]"));
700        assert!(is_host_allowed(&addr_8000, &[], "127.0.0.1:8000"));
701        assert!(is_host_allowed(
702            &addr_80,
703            &[subdomain_localhost_host.clone()],
704            "[::1]"
705        ));
706        assert!(is_host_allowed(
707            &addr_v6_8000,
708            &[subdomain_localhost_host.clone()],
709            "[::1]:8000"
710        ));
711
712        // Mismatch cases
713
714        assert!(!is_host_allowed(&addr_80, &[test_host], "localhost"));
715
716        assert!(!is_host_allowed(&addr_80, &[], "localhost:80"));
717
718        // Port mismatch cases
719
720        assert!(!is_host_allowed(
721            &addr_80,
722            &[localhost_host.clone()],
723            "localhost:8000"
724        ));
725        assert!(!is_host_allowed(
726            &addr_8000,
727            &[localhost_host.clone()],
728            "localhost"
729        ));
730        assert!(!is_host_allowed(
731            &addr_v6_8000,
732            &[localhost_host.clone()],
733            "[::1]"
734        ));
735    }
736
737    #[test]
738    fn test_origin_allowed() {
739        assert!(is_origin_allowed(
740            &[Url::parse("http://localhost").unwrap()],
741            Url::parse("http://localhost").unwrap()
742        ));
743        assert!(is_origin_allowed(
744            &[Url::parse("http://localhost").unwrap()],
745            Url::parse("http://localhost:80").unwrap()
746        ));
747        assert!(is_origin_allowed(
748            &[
749                Url::parse("https://test.example").unwrap(),
750                Url::parse("http://localhost").unwrap()
751            ],
752            Url::parse("http://localhost").unwrap()
753        ));
754        assert!(is_origin_allowed(
755            &[
756                Url::parse("https://test.example").unwrap(),
757                Url::parse("http://localhost").unwrap()
758            ],
759            Url::parse("https://test.example:443").unwrap()
760        ));
761        // Mismatch cases
762        assert!(!is_origin_allowed(
763            &[],
764            Url::parse("http://localhost").unwrap()
765        ));
766        assert!(!is_origin_allowed(
767            &[Url::parse("http://localhost").unwrap()],
768            Url::parse("http://localhost:8000").unwrap()
769        ));
770        assert!(!is_origin_allowed(
771            &[Url::parse("https://localhost").unwrap()],
772            Url::parse("http://localhost").unwrap()
773        ));
774        assert!(!is_origin_allowed(
775            &[Url::parse("https://example.test").unwrap()],
776            Url::parse("http://subdomain.example.test").unwrap()
777        ));
778    }
779}