1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use glob::Pattern;
use tauri_utils::config::HttpAllowlistScope;

/// Scope for filesystem access.
#[derive(Debug, Clone)]
pub struct Scope {
  allowed_urls: Vec<Pattern>,
}

impl Scope {
  /// Creates a new scope from the allowlist's `http` scope configuration.
  #[allow(dead_code)]
  pub(crate) fn for_http_api(scope: &HttpAllowlistScope) -> Self {
    Self {
      allowed_urls: scope
        .0
        .iter()
        .flat_map(|url| {
          [
            glob::Pattern::new(url.as_str())
              .unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{url}`")),
            glob::Pattern::new(
              url
                .as_str()
                .strip_suffix('/')
                .unwrap_or_else(|| url.as_str()),
            )
            .unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{url}`")),
          ]
        })
        .collect(),
    }
  }

  /// Determines if the given URL is allowed on this scope.
  pub fn is_allowed(&self, url: &url::Url) -> bool {
    self
      .allowed_urls
      .iter()
      .any(|allowed| allowed.matches(url.as_str()))
  }
}

#[cfg(test)]
mod tests {
  use tauri_utils::config::HttpAllowlistScope;

  #[test]
  fn is_allowed() {
    // plain URL
    let scope = super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://localhost:8080"
      .parse()
      .unwrap()]));
    assert!(scope.is_allowed(&"http://localhost:8080".parse().unwrap()));
    assert!(scope.is_allowed(&"http://localhost:8080/".parse().unwrap()));

    assert!(!scope.is_allowed(&"http://localhost:8080/file".parse().unwrap()));
    assert!(!scope.is_allowed(&"http://localhost:8080/path/to/asset.png".parse().unwrap()));
    assert!(!scope.is_allowed(&"https://localhost:8080".parse().unwrap()));
    assert!(!scope.is_allowed(&"http://localhost:8081".parse().unwrap()));
    assert!(!scope.is_allowed(&"http://local:8080".parse().unwrap()));

    // URL with fixed path
    let scope =
      super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://localhost:8080/file.png"
        .parse()
        .unwrap()]));

    assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap()));

    assert!(!scope.is_allowed(&"http://localhost:8080".parse().unwrap()));
    assert!(!scope.is_allowed(&"http://localhost:8080/file".parse().unwrap()));
    assert!(!scope.is_allowed(&"http://localhost:8080/file.png/other.jpg".parse().unwrap()));

    // URL with glob pattern
    let scope =
      super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://localhost:8080/*.png"
        .parse()
        .unwrap()]));

    assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap()));
    assert!(scope.is_allowed(&"http://localhost:8080/assets/file.png".parse().unwrap()));

    assert!(!scope.is_allowed(&"http://localhost:8080/file.jpeg".parse().unwrap()));

    let scope = super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://*".parse().unwrap()]));

    assert!(scope.is_allowed(&"http://something.else".parse().unwrap()));
    assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
    assert!(!scope.is_allowed(&"https://something.else".parse().unwrap()));

    let scope = super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://**".parse().unwrap()]));

    assert!(scope.is_allowed(&"http://something.else".parse().unwrap()));
    assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
  }
}