fix(openwrt): harden TollGate PR integration
This commit is contained in:
@@ -270,6 +270,10 @@ impl RpcHandler {
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string);
|
||||
if let Some(address) = payout_address.as_deref() {
|
||||
tollgate::config::validate_payout_address(address)
|
||||
.context("invalid TollGate payout address")?;
|
||||
}
|
||||
|
||||
let config = TollGateConfig {
|
||||
ssid: "archipelago".to_string(),
|
||||
|
||||
@@ -136,11 +136,44 @@ pub fn apply_payout_identity(router: &Router, payout_address: Option<&str>) -> R
|
||||
let Some(address) = payout_address else {
|
||||
return Ok(());
|
||||
};
|
||||
validate_payout_address(address)?;
|
||||
|
||||
let existing = router.run_ok("cat /etc/tollgate/identities.json 2>/dev/null || echo '{}'")?;
|
||||
let mut doc: serde_json::Value =
|
||||
serde_json::from_str(existing.trim()).unwrap_or_else(|_| serde_json::json!({}));
|
||||
let mut doc = parse_identities(&existing)?;
|
||||
|
||||
merge_payout_identity(&mut doc, address)?;
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&doc).context("serialize identities.json")?;
|
||||
router
|
||||
.upload_file("/etc/tollgate/identities.json", json_str.as_bytes())
|
||||
.context("upload /etc/tollgate/identities.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_identities(existing: &str) -> Result<serde_json::Value> {
|
||||
serde_json::from_str(existing.trim()).context(
|
||||
"parse existing /etc/tollgate/identities.json; refusing to overwrite malformed identity data",
|
||||
)
|
||||
}
|
||||
|
||||
/// Reject malformed values before provisioning changes anything on the
|
||||
/// router. A payout typo otherwise remains dormant until the threshold is
|
||||
/// reached, when the operator discovers that settlement cannot resolve.
|
||||
pub fn validate_payout_address(address: &str) -> Result<()> {
|
||||
let (name, domain) = address
|
||||
.split_once('@')
|
||||
.context("Lightning address must look like name@example.com")?;
|
||||
if name.is_empty()
|
||||
|| domain.is_empty()
|
||||
|| domain.contains('@')
|
||||
|| address.chars().any(char::is_whitespace)
|
||||
{
|
||||
anyhow::bail!("Lightning address must look like name@example.com");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_payout_identity(doc: &mut serde_json::Value, address: &str) -> Result<()> {
|
||||
let identities = doc
|
||||
.as_object_mut()
|
||||
.context("identities.json root is not a JSON object")?
|
||||
@@ -166,9 +199,70 @@ pub fn apply_payout_identity(router: &Router, payout_address: Option<&str>) -> R
|
||||
}
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&doc).context("serialize identities.json")?;
|
||||
router
|
||||
.upload_file("/etc/tollgate/identities.json", json_str.as_bytes())
|
||||
.context("upload /etc/tollgate/identities.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{merge_payout_identity, parse_identities, validate_payout_address};
|
||||
|
||||
#[test]
|
||||
fn payout_merge_changes_only_owner_address() {
|
||||
let mut doc = serde_json::json!({
|
||||
"config_version": "v0.0.1",
|
||||
"owned_identities": [{ "name": "merchant", "privatekey": "keep-secret" }],
|
||||
"public_identities": [
|
||||
{ "name": "owner", "pubkey": "not currently used", "lightning_address": "old@example.com" },
|
||||
{ "name": "upstream", "lightning_address": "keep@example.com" }
|
||||
]
|
||||
});
|
||||
let before_owned = doc["owned_identities"].clone();
|
||||
let before_other = doc["public_identities"][1].clone();
|
||||
|
||||
merge_payout_identity(&mut doc, "operator@example.com").unwrap();
|
||||
|
||||
assert_eq!(doc["owned_identities"], before_owned);
|
||||
assert_eq!(doc["public_identities"][1], before_other);
|
||||
assert_eq!(
|
||||
doc["public_identities"][0]["lightning_address"],
|
||||
"operator@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payout_merge_can_create_missing_owner() {
|
||||
let mut doc = serde_json::json!({ "public_identities": [] });
|
||||
merge_payout_identity(&mut doc, "operator@example.com").unwrap();
|
||||
assert_eq!(doc["public_identities"][0]["name"], "owner");
|
||||
assert_eq!(
|
||||
doc["public_identities"][0]["lightning_address"],
|
||||
"operator@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payout_address_validation_rejects_typographical_failures() {
|
||||
assert!(validate_payout_address("operator@example.com").is_ok());
|
||||
for invalid in [
|
||||
"",
|
||||
"operator",
|
||||
"@example.com",
|
||||
"operator@",
|
||||
"a@b@c",
|
||||
"a b@example.com",
|
||||
] {
|
||||
assert!(
|
||||
validate_payout_address(invalid).is_err(),
|
||||
"accepted {invalid:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_identity_data_is_never_replaced() {
|
||||
let err = parse_identities("{ truncated").unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("refusing to overwrite malformed identity data"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +149,18 @@ pub fn regate_upstream_default_aps(router: &Router) -> Result<()> {
|
||||
for section in sections.lines().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let network_key = format!("{}.network", section);
|
||||
let current = router.uci_get(&network_key).unwrap_or_default();
|
||||
if current == "lan" {
|
||||
let ssid = router
|
||||
.uci_get(&format!("{}.ssid", section))
|
||||
.unwrap_or_default();
|
||||
// A failed/changed upstream first-boot script can leave a stock
|
||||
// default_radioN section in place. Moving that interface merely
|
||||
// because it is on LAN can seize the router's existing management AP.
|
||||
// Only the public APs the TollGate installer demonstrably rebranded
|
||||
// belong on the paid network.
|
||||
if should_regate_upstream_ap(¤t, &ssid) {
|
||||
info!(
|
||||
"[{}] Re-gating upstream default AP {} (was network=lan) onto the tollgate network",
|
||||
router.host, section
|
||||
"[{}] Re-gating upstream default AP {} ({}) onto the tollgate network",
|
||||
router.host, section, ssid
|
||||
);
|
||||
router.uci_set(&network_key, "tollgate")?;
|
||||
}
|
||||
@@ -161,6 +169,10 @@ pub fn regate_upstream_default_aps(router: &Router) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_regate_upstream_ap(network: &str, ssid: &str) -> bool {
|
||||
network.trim() == "lan" && ssid.trim().starts_with("TollGate-")
|
||||
}
|
||||
|
||||
/// Return the first available wireless radio device name (e.g. "radio0").
|
||||
fn detect_radio(router: &Router) -> Result<String> {
|
||||
let out =
|
||||
@@ -169,3 +181,19 @@ fn detect_radio(router: &Router) -> Result<String> {
|
||||
let radio = out.trim().split('.').nth(1).unwrap_or("radio0").to_string();
|
||||
Ok(radio)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_regate_upstream_ap;
|
||||
|
||||
#[test]
|
||||
fn regates_only_confirmed_upstream_tollgate_aps() {
|
||||
assert!(should_regate_upstream_ap("lan", "TollGate-3458"));
|
||||
assert!(should_regate_upstream_ap(" lan\n", " TollGate-A1B2 "));
|
||||
|
||||
assert!(!should_regate_upstream_ap("lan", "OpenWrt"));
|
||||
assert!(!should_regate_upstream_ap("lan", "Archipelago Admin"));
|
||||
assert!(!should_regate_upstream_ap("tollgate", "TollGate-3458"));
|
||||
assert!(!should_regate_upstream_ap("lan", "tollgate-3458"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user