Skip to content

Commit 7b71b47

Browse files
author
rbenzing
committed
updates
1 parent ee75ed6 commit 7b71b47

17 files changed

Lines changed: 1840 additions & 149 deletions

src/config.rs

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::paths::InstallPaths;
1+
use crate::paths::{validate_critical_path, InstallPaths};
22
use crate::state::{ApacheConfig, MysqlConfig, PhpConfig, RampConfig};
33
use serde::{Deserialize, Serialize};
44
use std::io::Write;
@@ -38,6 +38,12 @@ impl Default for TomlPhp {
3838
/// Load and validate ramp.toml from install_dir.
3939
pub fn load_config(install_dir: &Path) -> Result<RampConfig, String> {
4040
let paths = InstallPaths::from_install_dir(install_dir)?;
41+
42+
// Reject ramp.toml if it is a symlink — it could redirect config reads/writes
43+
// to a system file, enabling privilege escalation or config escapes.
44+
validate_critical_path(&paths.config, install_dir, false)
45+
.map_err(|e| format!("ramp.toml path rejected: {e}"))?;
46+
4147
let raw = std::fs::read_to_string(&paths.config)
4248
.map_err(|e| format!("cannot read ramp.toml: {e}"))?;
4349
let doc: TomlRoot = toml::from_str(&raw).map_err(|e| format!("ramp.toml parse error: {e}"))?;
@@ -54,7 +60,7 @@ pub fn write_default_config(install_dir: &Path) -> Result<(), String> {
5460
r#"install_dir = "{}"
5561
5662
[apache]
57-
port = 80
63+
port = 8080
5864
5965
[mysql]
6066
port = 3306
@@ -70,15 +76,20 @@ port = 9000
7076
fn validate_and_build(doc: TomlRoot, install_dir: &Path) -> Result<RampConfig, String> {
7177
let paths = InstallPaths::from_install_dir(install_dir)?;
7278

73-
if doc.apache.port == 0 {
74-
return Err(format!("invalid apache.port: {}", doc.apache.port));
75-
}
76-
if doc.mysql.port == 0 {
77-
return Err(format!("invalid mysql.port: {}", doc.mysql.port));
78-
}
79-
if doc.php.port == 0 {
80-
return Err(format!("invalid php.port: {}", doc.php.port));
79+
// Ports must be unprivileged (>= 1024) and non-zero.
80+
// Privileged ports require admin rights on Windows and would cause silent
81+
// startup failures; port 0 is invalid for any bound service.
82+
fn validate_port(name: &str, port: u16) -> Result<(), String> {
83+
if port < 1024 {
84+
return Err(format!(
85+
"invalid {name}.port {port}: must be >= 1024 (privileged ports are not allowed)"
86+
));
87+
}
88+
Ok(())
8189
}
90+
validate_port("apache", doc.apache.port)?;
91+
validate_port("mysql", doc.mysql.port)?;
92+
validate_port("php", doc.php.port)?;
8293
if doc.apache.port == doc.mysql.port {
8394
return Err("apache.port and mysql.port must be different".into());
8495
}
@@ -191,16 +202,41 @@ port = 3306
191202
&format!(
192203
r#"install_dir = "{}"
193204
[apache]
194-
port = 80
205+
port = 8080
195206
[mysql]
196-
port = 80
207+
port = 8080
197208
"#,
198209
dir.display().to_string().replace('\\', "\\\\")
199210
),
200211
);
201212
assert!(load_config(dir).is_err());
202213
}
203214

215+
#[test]
216+
fn rejects_privileged_port() {
217+
let tmp = TempDir::new().unwrap();
218+
let dir = tmp.path();
219+
write_toml(
220+
dir,
221+
&format!(
222+
r#"install_dir = "{}"
223+
[apache]
224+
port = 80
225+
[mysql]
226+
port = 3306
227+
[php]
228+
port = 9000
229+
"#,
230+
dir.display().to_string().replace('\\', "\\\\")
231+
),
232+
);
233+
let err = load_config(dir).unwrap_err();
234+
assert!(
235+
err.contains("1024"),
236+
"expected port>=1024 message, got: {err}"
237+
);
238+
}
239+
204240
#[test]
205241
fn rejects_apache_php_port_clash() {
206242
let tmp = TempDir::new().unwrap();
@@ -243,4 +279,97 @@ port = 9000
243279
write_default_config(dir).unwrap();
244280
assert_eq!(std::fs::read(dir.join("ramp.toml")).unwrap(), b"original");
245281
}
282+
283+
#[test]
284+
fn rejects_malformed_toml() {
285+
let tmp = TempDir::new().unwrap();
286+
let dir = tmp.path();
287+
std::fs::write(dir.join("ramp.toml"), b"[not valid toml @@@").unwrap();
288+
let err = load_config(dir).unwrap_err();
289+
assert!(
290+
err.contains("parse error") || err.contains("TOML") || err.contains("toml"),
291+
"expected parse error message, got: {err}"
292+
);
293+
}
294+
295+
#[test]
296+
fn rejects_missing_ramp_toml() {
297+
let tmp = TempDir::new().unwrap();
298+
let err = load_config(tmp.path()).unwrap_err();
299+
assert!(
300+
err.contains("cannot read") || err.contains("ramp.toml"),
301+
"expected missing file message, got: {err}"
302+
);
303+
}
304+
305+
#[test]
306+
fn rejects_mysql_php_port_clash() {
307+
let tmp = TempDir::new().unwrap();
308+
let dir = tmp.path();
309+
write_toml(
310+
dir,
311+
&format!(
312+
r#"install_dir = "{}"
313+
[apache]
314+
port = 8080
315+
[mysql]
316+
port = 9000
317+
[php]
318+
port = 9000
319+
"#,
320+
dir.display().to_string().replace('\\', "\\\\")
321+
),
322+
);
323+
let err = load_config(dir).unwrap_err();
324+
assert!(
325+
err.contains("mysql") && err.contains("php"),
326+
"expected mysql/php clash message, got: {err}"
327+
);
328+
}
329+
330+
#[test]
331+
fn rejects_privileged_port_mysql() {
332+
let tmp = TempDir::new().unwrap();
333+
let dir = tmp.path();
334+
write_toml(
335+
dir,
336+
&format!(
337+
r#"install_dir = "{}"
338+
[apache]
339+
port = 8080
340+
[mysql]
341+
port = 1023
342+
[php]
343+
port = 9000
344+
"#,
345+
dir.display().to_string().replace('\\', "\\\\")
346+
),
347+
);
348+
let err = load_config(dir).unwrap_err();
349+
assert!(
350+
err.contains("1024"),
351+
"expected port>=1024 message, got: {err}"
352+
);
353+
}
354+
355+
#[test]
356+
fn atomic_write_no_tmp_left_on_success() {
357+
let tmp = TempDir::new().unwrap();
358+
let path = tmp.path().join("out.toml");
359+
atomic_write(&path, b"data").unwrap();
360+
assert!(path.exists());
361+
assert!(!path.with_extension("tmp").exists());
362+
}
363+
364+
#[test]
365+
fn write_default_config_creates_parseable_toml() {
366+
let tmp = TempDir::new().unwrap();
367+
let dir = tmp.path();
368+
write_default_config(dir).unwrap();
369+
// The generated default must be loadable — no syntax errors
370+
let cfg = load_config(dir).unwrap();
371+
assert!(cfg.apache.port >= 1024);
372+
assert!(cfg.mysql.port >= 1024);
373+
assert!(cfg.php.port >= 1024);
374+
}
246375
}

src/events.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub enum Event {
1414
// OS / process signals
1515
ProcessExit {
1616
service: Service,
17-
exit_code: Option<i32>,
17+
exit_code: Option<u32>,
1818
},
1919
ProcessReady(Service),
2020
ProcessSpawnFailed {
@@ -29,8 +29,8 @@ pub enum Event {
2929
// Port management
3030
PortConflictDetected(Service),
3131

32-
// Config
33-
ConfigReloaded,
32+
// Config — boxed to keep the enum variant size uniform
33+
ConfigReloaded(Box<crate::state::RampConfig>),
3434

3535
// Internal
3636
FatalError {

0 commit comments

Comments
 (0)