kellnr_settings/
origin.rs1use provcfg::{ClapArgs, Configurable};
2use serde::{Deserialize, Serialize};
3
4use crate::protocol::Protocol;
5
6fn default_hostname() -> String {
7 "127.0.0.1".to_string()
8}
9
10fn default_origin_port() -> u16 {
11 std::env::var("KELLNR_ORIGIN__PORT")
12 .ok()
13 .and_then(|v| v.parse::<u16>().ok())
14 .unwrap_or(8000)
15}
16
17#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
18#[serde(default)]
19#[configurable(clap_prefix = "origin")]
20pub struct Origin {
21 pub hostname: String,
23
24 pub port: u16,
26
27 #[arg(skip)]
29 pub protocol: Protocol,
30
31 pub path: String,
33
34 pub cookie_secure: Option<bool>,
39}
40
41impl Default for Origin {
42 fn default() -> Self {
43 Self {
44 hostname: default_hostname(),
45 port: default_origin_port(),
46 protocol: Protocol::Http,
47 path: String::new(),
48 cookie_secure: None,
49 }
50 }
51}
52
53impl Origin {
54 pub fn secure_cookies(&self) -> bool {
56 self.cookie_secure
57 .unwrap_or(self.protocol == Protocol::Https)
58 }
59
60 pub fn base_url(&self) -> String {
61 let path_prefix = self.path.trim().trim_end_matches('/');
62 if self.port == 443 || self.port == 80 {
63 format!("{}://{}{}", self.protocol, self.hostname, path_prefix)
64 } else {
65 format!(
66 "{}://{}:{}{}",
67 self.protocol, self.hostname, self.port, path_prefix
68 )
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn origin(port: u16, path: &str) -> Origin {
78 Origin {
79 hostname: "kellnr.example.com".to_string(),
80 port,
81 protocol: Protocol::Https,
82 path: path.to_string(),
83 cookie_secure: None,
84 }
85 }
86
87 #[test]
88 fn secure_cookies_follow_protocol_by_default() {
89 let mut o = origin(443, "");
90 assert!(o.secure_cookies());
91
92 o.protocol = Protocol::Http;
93 assert!(!o.secure_cookies());
94 }
95
96 #[test]
99 fn secure_cookies_can_be_overridden() {
100 let mut o = origin(80, "");
101 o.protocol = Protocol::Http;
102 o.cookie_secure = Some(true);
103 assert!(o.secure_cookies());
104
105 let mut o = origin(443, "");
106 o.cookie_secure = Some(false);
107 assert!(!o.secure_cookies());
108 }
109
110 #[test]
111 fn base_url_omits_default_ports() {
112 assert_eq!(
113 origin(443, "").base_url(),
114 "https://kellnr.example.com",
115 "443 is implied by https and must not be spelled out"
116 );
117
118 let mut http = origin(80, "");
119 http.protocol = Protocol::Http;
120 assert_eq!(http.base_url(), "http://kellnr.example.com");
121 }
122
123 #[test]
124 fn base_url_includes_non_default_port() {
125 assert_eq!(
126 origin(8000, "").base_url(),
127 "https://kellnr.example.com:8000"
128 );
129 }
130
131 #[test]
132 fn base_url_appends_path_prefix() {
133 assert_eq!(
134 origin(8000, "/registry").base_url(),
135 "https://kellnr.example.com:8000/registry"
136 );
137 assert_eq!(
138 origin(443, "/registry").base_url(),
139 "https://kellnr.example.com/registry"
140 );
141 }
142
143 #[test]
146 fn base_url_normalizes_slashes_in_path_prefix() {
147 for path in ["", "/", " / ", "/registry/", "/registry//"] {
148 let url = origin(8000, path).base_url();
149 assert!(
150 !url.ends_with('/'),
151 "base_url for path {path:?} must not end in a slash, got {url}"
152 );
153 }
154
155 assert_eq!(
156 origin(8000, "/registry/").base_url(),
157 "https://kellnr.example.com:8000/registry"
158 );
159 }
160
161 #[test]
164 fn base_url_composes_into_callback_url() {
165 for path in ["", "/", "/registry", "/registry/"] {
166 let url = format!("{}/api/v1/oauth2/callback", origin(443, path).base_url());
167 assert!(
168 !url.contains("com//") && !url.contains("registry//"),
169 "callback URL for path {path:?} has a double slash: {url}"
170 );
171 }
172 }
173}