systemprompt_models/profile/
server.rs1use std::net::IpAddr;
7
8use ipnet::IpNet;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11pub use systemprompt_extension::FrameOptions;
12
13#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
14#[serde(deny_unknown_fields)]
15pub struct ServerConfig {
16 pub host: String,
17
18 pub port: u16,
19
20 pub api_server_url: String,
21
22 pub api_internal_url: String,
23
24 pub api_external_url: String,
25
26 #[serde(default)]
27 pub use_https: bool,
28
29 #[serde(default)]
30 pub cors_allowed_origins: Vec<String>,
31
32 #[serde(default)]
33 pub content_negotiation: ContentNegotiationConfig,
34
35 #[serde(default)]
36 pub security_headers: SecurityHeadersConfig,
37
38 #[serde(default)]
39 pub instance_id: Option<String>,
40
41 #[serde(default)]
42 pub metrics_port: Option<u16>,
43
44 #[serde(default = "default_max_concurrent_streams")]
45 pub max_concurrent_streams: usize,
46
47 #[serde(
48 default,
49 deserialize_with = "deserialize_trusted_proxies",
50 serialize_with = "serialize_trusted_proxies"
51 )]
52 #[schemars(with = "Vec<String>")]
53 pub trusted_proxies: Vec<IpNet>,
54}
55
56fn parse_trusted_proxy(entry: &str) -> Result<IpNet, String> {
57 let trimmed = entry.trim();
58 if let Ok(net) = trimmed.parse::<IpNet>() {
59 return Ok(net);
60 }
61 match trimmed.parse::<IpAddr>() {
62 Ok(IpAddr::V4(v4)) => Ok(IpNet::from(ipnet::Ipv4Net::from(v4))),
63 Ok(IpAddr::V6(v6)) => Ok(IpNet::from(ipnet::Ipv6Net::from(v6))),
64 Err(_) => Err(format!(
65 "'{trimmed}' is not a valid CIDR range or IP address"
66 )),
67 }
68}
69
70fn deserialize_trusted_proxies<'de, D>(deserializer: D) -> Result<Vec<IpNet>, D::Error>
71where
72 D: Deserializer<'de>,
73{
74 let raw = Vec::<String>::deserialize(deserializer)?;
75 raw.iter()
76 .map(|s| s.trim())
77 .filter(|s| !s.is_empty())
78 .map(|s| parse_trusted_proxy(s).map_err(serde::de::Error::custom))
79 .collect()
80}
81
82fn serialize_trusted_proxies<S>(nets: &[IpNet], serializer: S) -> Result<S::Ok, S::Error>
83where
84 S: Serializer,
85{
86 serializer.collect_seq(nets.iter().map(ToString::to_string))
87}
88
89const fn default_max_concurrent_streams() -> usize {
90 crate::config::DEFAULT_MAX_CONCURRENT_STREAMS
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
94#[serde(deny_unknown_fields)]
95pub struct ContentNegotiationConfig {
96 #[serde(default)]
97 pub enabled: bool,
98
99 #[serde(default = "default_markdown_suffix")]
100 pub markdown_suffix: String,
101}
102
103impl Default for ContentNegotiationConfig {
104 fn default() -> Self {
105 Self {
106 enabled: false,
107 markdown_suffix: default_markdown_suffix(),
108 }
109 }
110}
111
112fn default_markdown_suffix() -> String {
113 ".md".to_owned()
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
117#[serde(deny_unknown_fields)]
118pub struct SecurityHeadersConfig {
119 #[serde(default = "default_enabled")]
120 pub enabled: bool,
121
122 #[serde(default = "default_hsts")]
123 pub hsts: String,
124
125 #[serde(default = "default_frame_options")]
126 #[schemars(with = "String")]
127 pub frame_options: FrameOptions,
128
129 #[serde(default = "default_content_type_options")]
130 pub content_type_options: String,
131
132 #[serde(default)]
133 pub referrer_policy: ReferrerPolicy,
134
135 #[serde(default = "default_permissions_policy")]
136 pub permissions_policy: String,
137
138 #[serde(default)]
139 pub content_security_policy: Option<String>,
140}
141
142impl Default for SecurityHeadersConfig {
143 fn default() -> Self {
144 Self {
145 enabled: true,
146 hsts: default_hsts(),
147 frame_options: default_frame_options(),
148 content_type_options: default_content_type_options(),
149 referrer_policy: ReferrerPolicy::default(),
150 permissions_policy: default_permissions_policy(),
151 content_security_policy: None,
152 }
153 }
154}
155
156const fn default_enabled() -> bool {
157 true
158}
159
160fn default_hsts() -> String {
161 "max-age=63072000; includeSubDomains; preload".to_owned()
162}
163
164const fn default_frame_options() -> FrameOptions {
165 FrameOptions::Deny
166}
167
168fn default_content_type_options() -> String {
169 "nosniff".to_owned()
170}
171
172fn default_permissions_policy() -> String {
173 "camera=(), microphone=(), geolocation=()".to_owned()
174}
175
176#[derive(
179 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
180)]
181pub enum ReferrerPolicy {
182 #[serde(rename = "no-referrer")]
183 NoReferrer,
184 #[serde(rename = "no-referrer-when-downgrade")]
185 NoReferrerWhenDowngrade,
186 #[serde(rename = "origin")]
187 Origin,
188 #[serde(rename = "origin-when-cross-origin")]
189 OriginWhenCrossOrigin,
190 #[serde(rename = "same-origin")]
191 SameOrigin,
192 #[serde(rename = "strict-origin")]
193 StrictOrigin,
194 #[default]
195 #[serde(rename = "strict-origin-when-cross-origin")]
196 StrictOriginWhenCrossOrigin,
197 #[serde(rename = "unsafe-url")]
198 UnsafeUrl,
199}
200
201impl ReferrerPolicy {
202 #[must_use]
203 pub const fn header_value(self) -> &'static str {
204 match self {
205 Self::NoReferrer => "no-referrer",
206 Self::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
207 Self::Origin => "origin",
208 Self::OriginWhenCrossOrigin => "origin-when-cross-origin",
209 Self::SameOrigin => "same-origin",
210 Self::StrictOrigin => "strict-origin",
211 Self::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
212 Self::UnsafeUrl => "unsafe-url",
213 }
214 }
215}