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