junobuild_storage/
routing.rs

1use crate::constants::{
2    RAW_DOMAINS, RESPONSE_STATUS_CODE_200, RESPONSE_STATUS_CODE_404, ROOT_404_HTML,
3    ROOT_INDEX_HTML, ROOT_PATH,
4};
5use crate::http::types::HeaderField;
6use crate::rewrites::{is_root_path, redirect_url, rewrite_url};
7use crate::strategies::StorageStateStrategy;
8use crate::types::config::StorageConfigRawAccess;
9use crate::types::http_request::{
10    MapUrl, Routing, RoutingDefault, RoutingRedirect, RoutingRedirectRaw, RoutingRewrite,
11};
12use crate::types::state::FullPath;
13use crate::types::store::Asset;
14use crate::url::{map_alternative_paths, map_url};
15use junobuild_collections::types::rules::Memory;
16use junobuild_shared::ic::api::id;
17
18pub fn get_routing(
19    url: String,
20    req_headers: &[HeaderField],
21    include_alternative_routing: bool,
22    storage_state: &impl StorageStateStrategy,
23) -> Result<Routing, &'static str> {
24    if url.is_empty() {
25        return Err("No url provided.");
26    }
27
28    // .raw. is not allowed per default for security reason.
29    let redirect_raw = get_routing_redirect_raw(&url, req_headers, storage_state);
30
31    match redirect_raw {
32        None => (),
33        Some(redirect_raw) => {
34            return Ok(redirect_raw);
35        }
36    }
37
38    // The certification considers, and should only, the path of the URL. If query parameters, these should be omitted in the certificate.
39    // Likewise the memory contains only assets indexed with their respective path.
40    // e.g.
41    // url: /hello/something?param=123
42    // path: /hello/something
43
44    let MapUrl { path, token } = map_url(&url)?;
45
46    // We return the asset that matches the effective path
47    let asset: Option<(Asset, Memory)> =
48        storage_state.get_public_asset(path.clone(), token.clone());
49
50    match asset {
51        None => (),
52        Some(_) => {
53            return Ok(Routing::Default(RoutingDefault { url: path, asset }));
54        }
55    }
56
57    // ⚠️ Limitation: requesting an url without extension try to resolve first a corresponding asset
58    // e.g. /.well-known/hello -> try to find /.well-known/hello.html
59    // Therefore if a file without extension is uploaded to the storage, it is important to not upload an .html file with the same name next to it or a folder/index.html
60    let alternative_asset = get_alternative_asset(&path, &token, storage_state);
61    match alternative_asset {
62        None => (),
63        Some(alternative_asset) => {
64            return Ok(Routing::Default(RoutingDefault {
65                url: path.clone(),
66                asset: Some(alternative_asset),
67            }));
68        }
69    }
70
71    if include_alternative_routing {
72        // Search for potential redirect
73        let redirect = get_routing_redirect(&path, storage_state);
74
75        match redirect {
76            None => (),
77            Some(redirect) => {
78                return Ok(redirect);
79            }
80        }
81
82        // Search for potential rewrite
83        let rewrite = get_routing_rewrite(&path, &token, storage_state);
84
85        match rewrite {
86            None => (),
87            Some(rewrite) => {
88                return Ok(rewrite);
89            }
90        }
91
92        // Search for potential default rewrite for HTML pages
93        let root_rewrite = get_routing_root_rewrite(&path, storage_state);
94
95        match root_rewrite {
96            None => (),
97            Some(root_rewrite) => {
98                return Ok(root_rewrite);
99            }
100        }
101    }
102
103    Ok(Routing::Default(RoutingDefault {
104        url: path,
105        asset: None,
106    }))
107}
108
109fn get_alternative_asset(
110    path: &String,
111    token: &Option<String>,
112    storage_state: &impl StorageStateStrategy,
113) -> Option<(Asset, Memory)> {
114    let alternative_paths = map_alternative_paths(path);
115
116    for alternative_path in alternative_paths {
117        let asset: Option<(Asset, Memory)> =
118            storage_state.get_public_asset(alternative_path, token.clone());
119
120        // We return the first match
121        match asset {
122            None => (),
123            Some(_) => {
124                return asset;
125            }
126        }
127    }
128
129    None
130}
131
132fn get_routing_rewrite(
133    path: &FullPath,
134    token: &Option<String>,
135    storage_state: &impl StorageStateStrategy,
136) -> Option<Routing> {
137    // If we have found no asset, we try a rewrite rule
138    // This is for example useful for single-page app to redirect all urls to /index.html
139    let rewrite = rewrite_url(path, &storage_state.get_config());
140
141    match rewrite {
142        None => (),
143        Some(rewrite) => {
144            let (source, destination) = rewrite;
145
146            // Search for rewrite configured as an alternative path
147            // e.g. rewrite /demo/* to /sample
148            let rewrite_asset = get_alternative_asset(&destination, token, storage_state);
149
150            match rewrite_asset {
151                None => (),
152                Some(_) => {
153                    return Some(Routing::Rewrite(RoutingRewrite {
154                        url: path.clone(),
155                        asset: rewrite_asset,
156                        source,
157                        status_code: RESPONSE_STATUS_CODE_200,
158                    }));
159                }
160            }
161
162            // Rewrite is maybe configured as an absolute path
163            // e.g. write /demo/* to /sample.html
164            let rewrite_absolute_asset: Option<(Asset, Memory)> =
165                storage_state.get_public_asset(destination.clone(), token.clone());
166
167            match rewrite_absolute_asset {
168                None => (),
169                Some(_) => {
170                    return Some(Routing::Rewrite(RoutingRewrite {
171                        url: path.clone(),
172                        asset: rewrite_absolute_asset,
173                        source,
174                        status_code: RESPONSE_STATUS_CODE_200,
175                    }));
176                }
177            }
178        }
179    }
180
181    None
182}
183
184fn get_routing_root_rewrite(
185    path: &FullPath,
186    storage_state: &impl StorageStateStrategy,
187) -> Option<Routing> {
188    if !is_root_path(path) {
189        // Search for potential /404.html to rewrite to
190        let asset_404: Option<(Asset, Memory)> =
191            storage_state.get_public_asset(ROOT_404_HTML.to_string(), None);
192
193        match asset_404 {
194            None => (),
195            Some(_) => {
196                return Some(Routing::Rewrite(RoutingRewrite {
197                    url: path.clone(),
198                    asset: asset_404,
199                    source: ROOT_PATH.to_string(),
200                    status_code: RESPONSE_STATUS_CODE_404,
201                }));
202            }
203        }
204
205        // Search for potential /index.html to rewrite to
206        let asset_index: Option<(Asset, Memory)> =
207            storage_state.get_public_asset(ROOT_INDEX_HTML.to_string(), None);
208
209        match asset_index {
210            None => (),
211            Some(_) => {
212                return Some(Routing::Rewrite(RoutingRewrite {
213                    url: path.clone(),
214                    asset: asset_index,
215                    source: ROOT_PATH.to_string(),
216                    status_code: RESPONSE_STATUS_CODE_200,
217                }));
218            }
219        }
220    }
221
222    None
223}
224
225fn get_routing_redirect(
226    path: &FullPath,
227    storage_state: &impl StorageStateStrategy,
228) -> Option<Routing> {
229    let config = storage_state.get_config();
230    let redirect = redirect_url(path, &config);
231
232    match redirect {
233        None => (),
234        Some(redirect) => {
235            return Some(Routing::Redirect(RoutingRedirect {
236                url: path.clone(),
237                redirect,
238                iframe: config.unwrap_iframe(),
239            }));
240        }
241    }
242
243    None
244}
245
246fn get_routing_redirect_raw(
247    url: &String,
248    req_headers: &[HeaderField],
249    storage_state: &impl StorageStateStrategy,
250) -> Option<Routing> {
251    let raw = req_headers.iter().any(|HeaderField(key, value)| {
252        key.eq_ignore_ascii_case("Host") && RAW_DOMAINS.iter().any(|domain| value.contains(domain))
253    });
254
255    let config = storage_state.get_config();
256
257    if raw {
258        let allow_raw_access = config.unwrap_raw_access();
259
260        match allow_raw_access {
261            StorageConfigRawAccess::Deny => {
262                return Some(Routing::RedirectRaw(RoutingRedirectRaw {
263                    redirect_url: format!("https://{}.icp0.io{}", id().to_text(), url),
264                    iframe: config.unwrap_iframe(),
265                }));
266            }
267            StorageConfigRawAccess::Allow => (),
268        }
269    }
270
271    None
272}