tauri_plugin_localhost/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Expose your apps assets through a localhost server instead of the default custom protocol.
6//!
7//! **Note: This plugins brings considerable security risks and you should only use it if you know what your are doing. If in doubt, use the default custom protocol implementation.**
8
9#![doc(
10    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
11    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
12)]
13
14use std::collections::HashMap;
15
16use http::Uri;
17use tauri::{
18    plugin::{Builder as PluginBuilder, TauriPlugin},
19    Runtime,
20};
21use tiny_http::{Header, Response as HttpResponse, Server};
22
23pub struct Request {
24    url: String,
25}
26
27impl Request {
28    pub fn url(&self) -> &str {
29        &self.url
30    }
31}
32
33pub struct Response {
34    headers: HashMap<String, String>,
35}
36
37impl Response {
38    pub fn add_header<H: Into<String>, V: Into<String>>(&mut self, header: H, value: V) {
39        self.headers.insert(header.into(), value.into());
40    }
41}
42
43type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
44
45pub struct Builder {
46    port: u16,
47    host: Option<String>,
48    on_request: OnRequest,
49}
50
51impl Builder {
52    pub fn new(port: u16) -> Self {
53        Self {
54            port,
55            host: None,
56            on_request: None,
57        }
58    }
59
60    // Change the host the plugin binds to. Defaults to `localhost`.
61    pub fn host<H: Into<String>>(mut self, host: H) -> Self {
62        self.host = Some(host.into());
63        self
64    }
65
66    pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
67        mut self,
68        f: F,
69    ) -> Self {
70        self.on_request.replace(Box::new(f));
71        self
72    }
73
74    pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
75        let port = self.port;
76        let host = self.host.unwrap_or("localhost".to_string());
77        let on_request = self.on_request.take();
78
79        PluginBuilder::new("localhost")
80            .setup(move |app, _api| {
81                let asset_resolver = app.asset_resolver();
82                std::thread::spawn(move || {
83                    let server =
84                        Server::http(format!("{host}:{port}")).expect("Unable to spawn server");
85                    for req in server.incoming_requests() {
86                        let path = req
87                            .url()
88                            .parse::<Uri>()
89                            .map(|uri| uri.path().into())
90                            .unwrap_or_else(|_| req.url().into());
91
92                        #[allow(unused_mut)]
93                        if let Some(mut asset) = asset_resolver.get(path) {
94                            let request = Request {
95                                url: req.url().into(),
96                            };
97                            let mut response = Response {
98                                headers: Default::default(),
99                            };
100
101                            response.add_header("Content-Type", asset.mime_type);
102                            if let Some(csp) = asset.csp_header {
103                                response
104                                    .headers
105                                    .insert("Content-Security-Policy".into(), csp);
106                            }
107
108                            if let Some(on_request) = &on_request {
109                                on_request(&request, &mut response);
110                            }
111
112                            let mut resp = HttpResponse::from_data(asset.bytes);
113                            for (header, value) in response.headers {
114                                if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
115                                    resp.add_header(h);
116                                }
117                            }
118                            req.respond(resp).expect("unable to setup response");
119                        }
120                    }
121                });
122                Ok(())
123            })
124            .build()
125    }
126}