1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! Broker transport security policy for adapter connect paths.
use crate::{PhotonError, Result};
/// Environment opt-in for plaintext broker endpoints (development/CI only).
pub const ALLOW_INSECURE_BROKER_ENV: &str = "PHOTON_ALLOW_INSECURE_BROKER";
/// How Photon may connect to an external broker.
///
/// Production hosts should use [`Self::RequireTls`]. Plaintext endpoints require an explicit
/// [`Self::AllowInsecurePlaintext`] opt-in (builder method or [`ALLOW_INSECURE_BROKER_ENV`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BrokerTransportSecurity {
/// Reject plaintext broker URLs/endpoints; prefer TLS (`tls://`, SDK TLS options).
#[default]
RequireTls,
/// Development/CI only: allow `nats://` and other plaintext broker endpoints.
AllowInsecurePlaintext,
}
impl BrokerTransportSecurity {
/// Load from [`ALLOW_INSECURE_BROKER_ENV`] (`1`/`true` → insecure; otherwise require TLS).
#[must_use]
pub fn from_env() -> Self {
match std::env::var(ALLOW_INSECURE_BROKER_ENV).as_deref() {
Ok("1" | "true" | "TRUE" | "yes" | "YES") => Self::AllowInsecurePlaintext,
_ => Self::RequireTls,
}
}
/// Returns true when plaintext broker endpoints are permitted.
#[must_use]
pub const fn allows_plaintext(self) -> bool {
matches!(self, Self::AllowInsecurePlaintext)
}
/// Fail closed when `endpoint` looks like plaintext and insecure opt-in is absent.
///
/// Recognizes common plaintext schemes: `nats://`, `kafka://`, `ftp://`, bare `host:port`
/// without a `tls` / `ssl` / `https` marker. `tls://`, `nats+tls://`, and URLs containing
/// `ssl`/`tls` as the scheme are treated as TLS-oriented.
///
/// # Errors
///
/// Returns [`PhotonError::Internal`] when plaintext would be used without opt-in.
pub fn check_endpoint(self, endpoint: &str) -> Result<()> {
if self.allows_plaintext() || endpoint_looks_tls(endpoint) {
return Ok(());
}
Err(PhotonError::Internal(format!(
"plaintext broker endpoint rejected under BrokerTransportSecurity::RequireTls \
(endpoint looks non-TLS). Opt in explicitly with .allow_insecure_plaintext() \
or {ALLOW_INSECURE_BROKER_ENV}=1 for development/CI only"
)))
}
}
fn endpoint_looks_tls(endpoint: &str) -> bool {
let trimmed = endpoint.trim();
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("tls://")
|| lower.starts_with("ssl://")
|| lower.starts_with("https://")
|| lower.starts_with("nats+tls://")
|| lower.starts_with("rediss://")
{
return true;
}
// Multi-URL lists: every entry must look TLS-capable when requiring TLS.
if trimmed.contains(',') {
return trimmed
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.all(endpoint_looks_tls);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn require_tls_rejects_nats_plaintext() {
let err = BrokerTransportSecurity::RequireTls
.check_endpoint("nats://127.0.0.1:4222")
.expect_err("plaintext");
assert!(err.to_string().contains("plaintext"));
}
#[test]
fn require_tls_accepts_tls_scheme() {
BrokerTransportSecurity::RequireTls
.check_endpoint("tls://broker.example:4222")
.expect("tls ok");
}
#[test]
fn insecure_allows_nats_plaintext() {
BrokerTransportSecurity::AllowInsecurePlaintext
.check_endpoint("nats://127.0.0.1:4222")
.expect("insecure ok");
}
}