Skip to main content

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    body: Vec<u8>,
26    method: String,
27}
28
29impl Request {
30    pub fn url(&self) -> &str {
31        &self.url
32    }
33
34    pub fn body(&self) -> &[u8] {
35        &self.body
36    }
37
38    pub fn method(&self) -> &str {
39        &self.method
40    }
41}
42
43pub struct Response {
44    headers: HashMap<String, String>,
45    status_code: u16,
46    body: Vec<u8>,
47    handled: bool,
48}
49
50impl Response {
51    pub fn add_header<H: Into<String>, V: Into<String>>(&mut self, header: H, value: V) {
52        self.headers.insert(header.into(), value.into());
53    }
54
55    pub fn set_status(&mut self, code: u16) {
56        self.status_code = code;
57    }
58
59    pub fn set_body(&mut self, body: Vec<u8>) {
60        self.body = body;
61    }
62
63    /// Mark as handled — the plugin will send this response instead of
64    /// looking up a static asset. Use for proxy routes, health checks, etc.
65    pub fn set_handled(&mut self, handled: bool) {
66        self.handled = handled;
67    }
68}
69
70type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
71
72pub struct Builder {
73    port: u16,
74    host: Option<String>,
75    on_request: OnRequest,
76}
77
78impl Builder {
79    pub fn new(port: u16) -> Self {
80        Self {
81            port,
82            host: None,
83            on_request: None,
84        }
85    }
86
87    // Change the host the plugin binds to. Defaults to `localhost`.
88    pub fn host<H: Into<String>>(mut self, host: H) -> Self {
89        self.host = Some(host.into());
90        self
91    }
92
93    pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
94        mut self,
95        f: F,
96    ) -> Self {
97        self.on_request.replace(Box::new(f));
98        self
99    }
100
101    pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
102        let port = self.port;
103        let host = self.host.unwrap_or("localhost".to_string());
104        let on_request = self.on_request.take();
105
106        PluginBuilder::new("localhost")
107            .setup(move |app, _api| {
108                let asset_resolver = app.asset_resolver();
109                std::thread::spawn(move || {
110                    let server =
111                        Server::http(format!("{host}:{port}")).expect("Unable to spawn server");
112                    for mut req in server.incoming_requests() {
113                        let path: String = req
114                            .url()
115                            .parse::<Uri>()
116                            .map(|uri| uri.path().into())
117                            .unwrap_or_else(|_| req.url().into());
118
119                        // Read request body (for proxy routes, webhooks, etc.)
120                        let mut body_bytes = Vec::new();
121                        let _ = std::io::Read::read_to_end(req.as_reader(), &mut body_bytes);
122
123                        let request = Request {
124                            url: req.url().into(),
125                            body: body_bytes,
126                            method: req.method().to_string(),
127                        };
128                        let mut response = Response {
129                            headers: Default::default(),
130                            status_code: 200,
131                            body: Vec::new(),
132                            handled: false,
133                        };
134
135                        // Call on_request first — it may handle proxy routes
136                        if let Some(on_request) = &on_request {
137                            on_request(&request, &mut response);
138                        }
139
140                        // If on_request marked as handled, send custom response
141                        if response.handled {
142                            let mut resp = HttpResponse::from_data(response.body)
143                                .with_status_code(response.status_code);
144                            for (header, value) in response.headers {
145                                if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
146                                    resp.add_header(h);
147                                }
148                            }
149                            let _ = req.respond(resp);
150                            continue;
151                        }
152
153                        // Standard asset serving
154                        #[allow(unused_mut)]
155                        if let Some(mut asset) = asset_resolver.get(path) {
156                            response.add_header("Content-Type", asset.mime_type);
157                            if let Some(csp) = asset.csp_header {
158                                response
159                                    .headers
160                                    .insert("Content-Security-Policy".into(), csp);
161                            }
162
163                            response
164                                .headers
165                                .insert("Cache-Control".into(), "no-cache".into());
166
167                            let mut resp = HttpResponse::from_data(asset.bytes);
168                            for (header, value) in response.headers {
169                                if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
170                                    resp.add_header(h);
171                                }
172                            }
173                            req.respond(resp).expect("unable to setup response");
174                        }
175                    }
176                });
177                Ok(())
178            })
179            .build()
180    }
181}