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