Skip to content

Commit 3662aff

Browse files
committed
refactor: [#272] Replace tls with domain+use_tls_proxy for HTTP API
Step 7.5.2: Apply the same TLS configuration pattern to the Tracker REST API (HttpApiSection/HttpApiConfig) as was done for HTTP trackers in Step 7.5.1. Changes: - Replace `tls: Option<TlsSection>` with `domain: Option<String>` and `use_tls_proxy: Option<bool>` in HttpApiSection DTO - Update HttpApiConfig domain type with `domain: Option<DomainName>` and `use_tls_proxy: bool` fields - Add `uses_tls_proxy()` and `tls_domain()` helper methods to HttpApiConfig - Update TrackerConfig::check_localhost_with_tls() validation - Update show command to use new fields for API endpoint display - Update all test code and doc comments to use new structure - Update envs/manual-https-test.json with new HTTP API configuration This is part of the incremental migration to remove the TlsSection type and use explicit domain + use_tls_proxy fields for clearer semantics.
1 parent 80b4b32 commit 3662aff

File tree

12 files changed

+275
-97
lines changed

12 files changed

+275
-97
lines changed

docs/issues/272-add-https-support-with-caddy.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,15 +1443,15 @@ The implementation is split into incremental steps, one service type at a time,
14431443

14441444
##### Step 7.5.2: Tracker REST API
14451445

1446-
- [ ] Add `domain: Option<String>` and `use_tls_proxy: Option<bool>` to `HttpApiSection` DTO
1447-
- [ ] Update `HttpApiConfig` domain type
1448-
- [ ] Add validation rules (same as HTTP trackers)
1449-
- [ ] Update Caddy template for API
1450-
- [ ] Update show command `ServiceInfo` for API
1451-
- [ ] Update `envs/manual-https-test.json` for API
1452-
- [ ] Remove `TlsSection` from API
1453-
- [ ] Add unit tests for API validation
1454-
- [ ] Run E2E tests
1446+
- [x] Add `domain: Option<String>` and `use_tls_proxy: Option<bool>` to `HttpApiSection` DTO
1447+
- [x] Update `HttpApiConfig` domain type
1448+
- [x] Add validation rules (same as HTTP trackers)
1449+
- [x] Update Caddy template for API
1450+
- [x] Update show command `ServiceInfo` for API
1451+
- [x] Update `envs/manual-https-test.json` for API
1452+
- [x] Remove `TlsSection` from API
1453+
- [x] Add unit tests for API validation
1454+
- [x] Run E2E tests
14551455

14561456
##### Step 7.5.3: Tracker Health Check API
14571457

src/application/command_handlers/create/config/environment_config.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ impl EnvironmentCreationConfig {
397397
#[must_use]
398398
pub fn has_any_tls_configured(&self) -> bool {
399399
// Check HTTP API
400-
if self.tracker.http_api.tls.is_some() {
400+
if self.tracker.http_api.use_tls_proxy == Some(true) {
401401
return true;
402402
}
403403

@@ -513,7 +513,8 @@ impl EnvironmentCreationConfig {
513513
http_api: super::tracker::HttpApiSection {
514514
bind_address: "0.0.0.0:1212".to_string(),
515515
admin_token: "MyAccessToken".to_string(),
516-
tls: None,
516+
domain: None,
517+
use_tls_proxy: None,
517518
},
518519
health_check_api: super::tracker::HealthCheckApiSection::default(),
519520
},
@@ -1392,7 +1393,6 @@ mod tests {
13921393

13931394
#[test]
13941395
fn it_should_return_true_for_has_any_tls_configured_when_http_api_has_tls() {
1395-
use crate::application::command_handlers::create::config::https::TlsSection;
13961396
use crate::application::command_handlers::create::config::tracker::{
13971397
DatabaseSection, HealthCheckApiSection, HttpApiSection, HttpTrackerSection,
13981398
TrackerCoreSection, TrackerSection, UdpTrackerSection,
@@ -1416,9 +1416,8 @@ mod tests {
14161416
http_api: HttpApiSection {
14171417
bind_address: "0.0.0.0:1212".to_string(),
14181418
admin_token: "MyAccessToken".to_string(),
1419-
tls: Some(TlsSection {
1420-
domain: "api.tracker.example.com".to_string(),
1421-
}),
1419+
domain: Some("api.tracker.example.com".to_string()),
1420+
use_tls_proxy: Some(true),
14221421
},
14231422
health_check_api: HealthCheckApiSection::default(),
14241423
};
@@ -1492,7 +1491,8 @@ mod tests {
14921491
http_api: HttpApiSection {
14931492
bind_address: "0.0.0.0:1212".to_string(),
14941493
admin_token: "MyAccessToken".to_string(),
1495-
tls: None,
1494+
domain: None,
1495+
use_tls_proxy: None,
14961496
},
14971497
health_check_api: HealthCheckApiSection::default(),
14981498
};
@@ -1582,7 +1582,8 @@ mod tests {
15821582
http_api: HttpApiSection {
15831583
bind_address: "0.0.0.0:1212".to_string(),
15841584
admin_token: "MyAccessToken".to_string(),
1585-
tls: None,
1585+
domain: None,
1586+
use_tls_proxy: None,
15861587
},
15871588
health_check_api: HealthCheckApiSection::default(),
15881589
};

src/application/command_handlers/create/config/tracker/http_api_section.rs

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ use schemars::JsonSchema;
44
use serde::{Deserialize, Serialize};
55

66
use crate::application::command_handlers::create::config::errors::CreateConfigError;
7-
use crate::application::command_handlers::create::config::https::TlsSection;
8-
use crate::domain::tls::TlsConfig;
97
use crate::domain::tracker::HttpApiConfig;
108
use crate::shared::secrets::PlainApiToken;
119
use crate::shared::DomainName;
@@ -15,12 +13,26 @@ pub struct HttpApiSection {
1513
pub bind_address: String,
1614
pub admin_token: PlainApiToken,
1715

18-
/// Optional TLS configuration for HTTPS
16+
/// Domain name for HTTPS certificate acquisition
1917
///
20-
/// When present, this service will be proxied through Caddy with HTTPS enabled.
21-
/// The domain specified will be used for Let's Encrypt certificate acquisition.
18+
/// When present along with `use_tls_proxy: true`, this service will be
19+
/// accessible via HTTPS through the Caddy reverse proxy using this domain.
20+
/// The domain is used for Let's Encrypt certificate acquisition.
2221
#[serde(default, skip_serializing_if = "Option::is_none")]
23-
pub tls: Option<TlsSection>,
22+
pub domain: Option<String>,
23+
24+
/// Whether to proxy this service through Caddy with TLS termination
25+
///
26+
/// When `true`:
27+
/// - The service is proxied through Caddy with HTTPS enabled
28+
/// - `domain` field is required
29+
/// - Cannot be used with localhost bind addresses (`127.0.0.1`, `::1`)
30+
///
31+
/// When `false` or omitted:
32+
/// - The service is accessed directly without TLS termination
33+
/// - `domain` field is optional (ignored if present)
34+
#[serde(default, skip_serializing_if = "Option::is_none")]
35+
pub use_tls_proxy: Option<bool>,
2436
}
2537

2638
impl HttpApiSection {
@@ -30,7 +42,8 @@ impl HttpApiSection {
3042
///
3143
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
3244
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
33-
/// Returns `CreateConfigError::InvalidDomain` if the TLS domain is invalid.
45+
/// Returns `CreateConfigError::InvalidDomain` if the domain is invalid.
46+
/// Returns `CreateConfigError::TlsProxyWithoutDomain` if `use_tls_proxy` is true but domain is missing.
3447
///
3548
/// Note: Localhost + TLS validation is performed at the domain layer
3649
/// (see `TrackerConfig::validate()`) to avoid duplicating business rules.
@@ -50,25 +63,34 @@ impl HttpApiSection {
5063
});
5164
}
5265

53-
// Convert TLS section to domain type with validation
54-
let tls = match &self.tls {
55-
Some(tls_section) => {
56-
tls_section.validate()?;
57-
let domain = DomainName::new(&tls_section.domain).map_err(|e| {
58-
CreateConfigError::InvalidDomain {
59-
domain: tls_section.domain.clone(),
66+
let use_tls_proxy = self.use_tls_proxy.unwrap_or(false);
67+
68+
// Validate: use_tls_proxy: true requires domain
69+
if use_tls_proxy && self.domain.is_none() {
70+
return Err(CreateConfigError::TlsProxyWithoutDomain {
71+
service_type: "HTTP API".to_string(),
72+
bind_address: self.bind_address.clone(),
73+
});
74+
}
75+
76+
// Convert domain to domain type with validation (if present)
77+
let domain = match &self.domain {
78+
Some(domain_str) => {
79+
let domain =
80+
DomainName::new(domain_str).map_err(|e| CreateConfigError::InvalidDomain {
81+
domain: domain_str.clone(),
6082
reason: e.to_string(),
61-
}
62-
})?;
63-
Some(TlsConfig::new(domain))
83+
})?;
84+
Some(domain)
6485
}
6586
None => None,
6687
};
6788

6889
Ok(HttpApiConfig {
6990
bind_address,
7091
admin_token: self.admin_token.clone().into(),
71-
tls,
92+
domain,
93+
use_tls_proxy,
7294
})
7395
}
7496
}
@@ -82,7 +104,8 @@ mod tests {
82104
let section = HttpApiSection {
83105
bind_address: "0.0.0.0:1212".to_string(),
84106
admin_token: "MyAccessToken".to_string(),
85-
tls: None,
107+
domain: None,
108+
use_tls_proxy: None,
86109
};
87110

88111
let result = section.to_http_api_config();
@@ -94,14 +117,16 @@ mod tests {
94117
"0.0.0.0:1212".parse::<SocketAddr>().unwrap()
95118
);
96119
assert_eq!(config.admin_token.expose_secret(), "MyAccessToken");
120+
assert!(!config.use_tls_proxy);
97121
}
98122

99123
#[test]
100124
fn it_should_fail_for_invalid_bind_address() {
101125
let section = HttpApiSection {
102126
bind_address: "invalid-address".to_string(),
103127
admin_token: "token".to_string(),
104-
tls: None,
128+
domain: None,
129+
use_tls_proxy: None,
105130
};
106131

107132
let result = section.to_http_api_config();
@@ -119,7 +144,8 @@ mod tests {
119144
let section = HttpApiSection {
120145
bind_address: "0.0.0.0:0".to_string(),
121146
admin_token: "token".to_string(),
122-
tls: None,
147+
domain: None,
148+
use_tls_proxy: None,
123149
};
124150

125151
let result = section.to_http_api_config();
@@ -137,7 +163,8 @@ mod tests {
137163
let section = HttpApiSection {
138164
bind_address: "0.0.0.0:1212".to_string(),
139165
admin_token: "MyAccessToken".to_string(),
140-
tls: None,
166+
domain: None,
167+
use_tls_proxy: None,
141168
};
142169

143170
let json = serde_json::to_string(&section).unwrap();
@@ -153,22 +180,76 @@ mod tests {
153180
let section: HttpApiSection = serde_json::from_str(json).unwrap();
154181
assert_eq!(section.bind_address, "0.0.0.0:1212");
155182
assert_eq!(section.admin_token, "MyAccessToken");
183+
assert!(section.domain.is_none());
184+
assert!(section.use_tls_proxy.is_none());
185+
}
186+
187+
#[test]
188+
fn it_should_allow_non_localhost_with_tls_proxy() {
189+
let section = HttpApiSection {
190+
bind_address: "0.0.0.0:1212".to_string(),
191+
admin_token: "token".to_string(),
192+
domain: Some("api.tracker.local".to_string()),
193+
use_tls_proxy: Some(true),
194+
};
195+
196+
let result = section.to_http_api_config();
197+
198+
assert!(result.is_ok());
199+
let config = result.unwrap();
200+
assert!(config.use_tls_proxy);
201+
assert!(config.domain.is_some());
156202
}
157203

158204
#[test]
159-
fn it_should_allow_non_localhost_with_tls() {
205+
fn it_should_reject_tls_proxy_without_domain() {
160206
let section = HttpApiSection {
161207
bind_address: "0.0.0.0:1212".to_string(),
162208
admin_token: "token".to_string(),
163-
tls: Some(TlsSection {
164-
domain: "api.tracker.local".to_string(),
165-
}),
209+
domain: None,
210+
use_tls_proxy: Some(true),
166211
};
167212

168213
let result = section.to_http_api_config();
214+
assert!(result.is_err());
215+
216+
if let Err(CreateConfigError::TlsProxyWithoutDomain {
217+
service_type,
218+
bind_address,
219+
}) = result
220+
{
221+
assert_eq!(service_type, "HTTP API");
222+
assert_eq!(bind_address, "0.0.0.0:1212");
223+
} else {
224+
panic!("Expected TlsProxyWithoutDomain error");
225+
}
226+
}
227+
228+
#[test]
229+
fn it_should_accept_domain_without_tls_proxy() {
230+
// Domain provided but use_tls_proxy is false - domain is ignored
231+
let section = HttpApiSection {
232+
bind_address: "0.0.0.0:1212".to_string(),
233+
admin_token: "token".to_string(),
234+
domain: Some("api.tracker.local".to_string()),
235+
use_tls_proxy: Some(false),
236+
};
169237

238+
let result = section.to_http_api_config();
170239
assert!(result.is_ok());
240+
171241
let config = result.unwrap();
172-
assert!(config.tls.is_some());
242+
assert!(!config.use_tls_proxy);
243+
// Domain is still stored but won't be used for TLS
244+
assert!(config.domain.is_some());
245+
}
246+
247+
#[test]
248+
fn it_should_deserialize_with_new_fields() {
249+
let json = r#"{"bind_address":"0.0.0.0:1212","admin_token":"token","domain":"api.example.com","use_tls_proxy":true}"#;
250+
let section: HttpApiSection = serde_json::from_str(json).unwrap();
251+
assert_eq!(section.bind_address, "0.0.0.0:1212");
252+
assert_eq!(section.domain, Some("api.example.com".to_string()));
253+
assert_eq!(section.use_tls_proxy, Some(true));
173254
}
174255
}

src/application/command_handlers/create/config/tracker/tracker_section.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ impl Default for TrackerSection {
134134
http_api: HttpApiSection {
135135
bind_address: "0.0.0.0:1212".to_string(),
136136
admin_token: "MyAccessToken".to_string(),
137-
tls: None,
137+
domain: None,
138+
use_tls_proxy: None,
138139
},
139140
health_check_api: HealthCheckApiSection::default(),
140141
}
@@ -169,7 +170,8 @@ mod tests {
169170
http_api: HttpApiSection {
170171
bind_address: "0.0.0.0:1212".to_string(),
171172
admin_token: "MyAccessToken".to_string(),
172-
tls: None,
173+
domain: None,
174+
use_tls_proxy: None,
173175
},
174176
health_check_api: HealthCheckApiSection::default(),
175177
};
@@ -223,7 +225,8 @@ mod tests {
223225
http_api: HttpApiSection {
224226
bind_address: "0.0.0.0:1212".to_string(),
225227
admin_token: "MyAccessToken".to_string(),
226-
tls: None,
228+
domain: None,
229+
use_tls_proxy: None,
227230
},
228231
health_check_api: HealthCheckApiSection::default(),
229232
};
@@ -250,7 +253,8 @@ mod tests {
250253
http_api: HttpApiSection {
251254
bind_address: "0.0.0.0:1212".to_string(),
252255
admin_token: "MyAccessToken".to_string(),
253-
tls: None,
256+
domain: None,
257+
use_tls_proxy: None,
254258
},
255259
health_check_api: HealthCheckApiSection::default(),
256260
};
@@ -284,7 +288,8 @@ mod tests {
284288
http_api: HttpApiSection {
285289
bind_address: "0.0.0.0:1212".to_string(),
286290
admin_token: "MyAccessToken".to_string(),
287-
tls: None,
291+
domain: None,
292+
use_tls_proxy: None,
288293
},
289294
health_check_api: HealthCheckApiSection::default(),
290295
};
@@ -347,7 +352,8 @@ mod tests {
347352
http_api: HttpApiSection {
348353
bind_address: "0.0.0.0:7070".to_string(),
349354
admin_token: "token".to_string(),
350-
tls: None,
355+
domain: None,
356+
use_tls_proxy: None,
351357
},
352358
health_check_api: HealthCheckApiSection::default(),
353359
};
@@ -381,7 +387,8 @@ mod tests {
381387
http_api: HttpApiSection {
382388
bind_address: "0.0.0.0:1212".to_string(),
383389
admin_token: "token".to_string(),
384-
tls: None,
390+
domain: None,
391+
use_tls_proxy: None,
385392
},
386393
health_check_api: HealthCheckApiSection::default(),
387394
};

0 commit comments

Comments
 (0)