Skip to content

Commit 022fd3b

Browse files
hultoclaudegithub-actions[bot]
authored
Add YAML config support for transport configuration in build.rs (#1554)
* Add YAML config support for transport configuration in build.rs Implemented parse_yaml_config() to parse IMIX_CONFIG environment variable containing YAML configuration for transports. The YAML specifies a list of transports with URI, type (GRPC, http1, DNS), and extra (JSON) fields. The function: - Validates transport types and JSON in extra fields - Ensures YAML config is not mixed with other config methods - Converts YAML to DSN format with query parameters - Emits IMIX_CALLBACK_URI via cargo:rustc-env Updated validate_dsn_config() to skip when YAML config is used. Added serde, serde_yaml, and urlencoding to build dependencies. * Fix transport type mapping and add interval field - Map transport types to proper URL schemas (GRPC -> grpc://, HTTP1 -> http://, DNS -> dns://) - Add validation to error if URI already contains query parameters - Add optional interval field to TransportConfig - Remove transport as query parameter, use schema instead Co-authored-by: Hulto <[email protected]> * Add server_pubkey field to YAML config - Added optional server_pubkey field at root level of YamlConfig - Field is emitted as IMIX_SERVER_PUBKEY environment variable when present - Maintains backward compatibility with existing configs Co-authored-by: Hulto <[email protected]> * fix * fmt --------- Co-authored-by: Claude <[email protected]> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Hulto <[email protected]>
1 parent 3e58749 commit 022fd3b

File tree

4 files changed

+175
-1
lines changed

4 files changed

+175
-1
lines changed

implants/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ russh-keys = "0.37.1"
114114
rust-embed = "8.5.0"
115115
serde = "1.0"
116116
serde_json = "1.0.87"
117+
serde_yaml = "0.9"
117118
sha1 = "0.10.5"
118119
sha2 = "0.10.7"
119120
sha256 = { version = "1.0.3", default-features = false }
@@ -134,6 +135,7 @@ tonic = { git = "https://github.com/hyperium/tonic.git", rev = "07e4ee1" }
134135
tonic-build = { git = "https://github.com/hyperium/tonic.git", rev = "c783652" } # Needed git for `.codec_path` in build.rs - previous version of codec setting is really gross. https://github.com/hyperium/tonic/blob/ea8cd3f384e953e177f20a62aa156a75676853f4/examples/build.rs#L44
135136
trait-variant = "0.1.1"
136137
uuid = "1.5.0"
138+
urlencoding = "2.1.3"
137139
static_vcruntime = "2.0"
138140
url = "2.5"
139141
which = "4.4.2"

implants/lib/eldritchv2/stdlib/tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ eldritch-core = { workspace = true, default-features = false }
99
anyhow = { workspace = true }
1010
glob = { workspace = true }
1111
serde = { workspace = true, features = ["derive"] }
12-
serde_yaml = "0.9"
12+
serde_yaml = {workspace = true}
1313
spin = { version = "0.10.0", features = ["rwlock"] }
1414
log = { workspace = true }

implants/lib/pb/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ which = { workspace = true }
4040
home = "=0.5.11"
4141
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }
4242
serde_json = { workspace = true }
43+
serde = { workspace = true, features = ["derive"] }
44+
serde_yaml = { workspace = true }
45+
urlencoding = { workspace = true }

implants/lib/pb/build.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,166 @@
1+
use serde::Deserialize;
12
use std::collections::HashMap;
23
use std::env;
34
use std::path::PathBuf;
45
use which::which;
56

7+
#[derive(Debug, Deserialize)]
8+
struct TransportConfig {
9+
#[serde(rename = "URI")]
10+
uri: String,
11+
#[serde(rename = "type")]
12+
transport_type: String,
13+
extra: String,
14+
#[serde(default)]
15+
interval: Option<u64>,
16+
}
17+
18+
#[derive(Debug, Deserialize)]
19+
struct YamlConfig {
20+
transports: Vec<TransportConfig>,
21+
#[serde(default)]
22+
server_pubkey: Option<String>,
23+
}
24+
25+
fn parse_yaml_config() -> Result<bool, Box<dyn std::error::Error>> {
26+
// Check if IMIX_CONFIG is set
27+
let config_yaml = match std::env::var("IMIX_CONFIG") {
28+
Ok(yaml_content) => yaml_content,
29+
Err(_) => return Ok(false), // No config set, return false
30+
};
31+
32+
// Check that other configuration options are not set
33+
let has_callback_uri = std::env::var("IMIX_CALLBACK_URI").is_ok();
34+
let has_callback_interval = std::env::var("IMIX_CALLBACK_INTERVAL").is_ok();
35+
let has_transport_extra = std::env::vars().any(|(k, _)| k.starts_with("IMIX_TRANSPORT_EXTRA_"));
36+
37+
if has_callback_uri || has_callback_interval || has_transport_extra {
38+
let mut error_msg = String::from(
39+
"Configuration error: Cannot use IMIX_CONFIG with other configuration options.\n",
40+
);
41+
error_msg.push_str(
42+
"When IMIX_CONFIG is set, all configuration must be done through the YAML file.\n",
43+
);
44+
error_msg.push_str("Found one or more of:\n");
45+
46+
if has_callback_uri {
47+
error_msg.push_str(" - IMIX_CALLBACK_URI\n");
48+
}
49+
if has_callback_interval {
50+
error_msg.push_str(" - IMIX_CALLBACK_INTERVAL\n");
51+
}
52+
if has_transport_extra {
53+
error_msg.push_str(" - IMIX_TRANSPORT_EXTRA_*\n");
54+
}
55+
56+
error_msg.push_str(
57+
"\nPlease use ONLY the YAML config file OR use environment variables, but not both.",
58+
);
59+
60+
return Err(error_msg.into());
61+
}
62+
63+
// Parse the YAML config
64+
let config: YamlConfig = serde_yaml::from_str(&config_yaml)
65+
.map_err(|e| format!("Failed to parse YAML config: {}", e))?;
66+
67+
// Validate that we have at least one transport
68+
if config.transports.is_empty() {
69+
return Err("YAML config must contain at least one transport".into());
70+
}
71+
72+
// Build DSN string from transports
73+
let mut dsn_parts = Vec::new();
74+
75+
for transport in &config.transports {
76+
// Validate transport type
77+
let transport_type_lower = transport.transport_type.to_lowercase();
78+
if !["grpc", "http1", "dns"].contains(&transport_type_lower.as_str()) {
79+
return Err(format!(
80+
"Invalid transport type '{}'. Must be one of: GRPC, http1, DNS",
81+
transport.transport_type
82+
)
83+
.into());
84+
}
85+
86+
// Validate that extra is valid JSON
87+
if !transport.extra.is_empty() {
88+
serde_json::from_str::<serde_json::Value>(&transport.extra).map_err(|e| {
89+
format!(
90+
"Invalid JSON in 'extra' field for transport '{}': {}",
91+
transport.uri, e
92+
)
93+
})?;
94+
}
95+
96+
// Error if URI already contains query parameters
97+
if transport.uri.contains('?') {
98+
return Err(format!("URI '{}' already contains query parameters. Query parameters should not be present in the URI field.", transport.uri).into());
99+
}
100+
101+
// Map transport type to appropriate schema
102+
let schema = match transport_type_lower.as_str() {
103+
"grpc" => "grpc",
104+
"http1" => "http",
105+
"dns" => "dns",
106+
_ => unreachable!(), // Already validated above
107+
};
108+
109+
// Strip any existing schema from the URI and replace with the correct one
110+
let uri_without_schema = transport
111+
.uri
112+
.split_once("://")
113+
.map(|(_, rest)| rest)
114+
.unwrap_or(&transport.uri);
115+
116+
// Build DSN part with correct schema and query parameters
117+
let mut dsn_part = format!("{}://{}", schema, uri_without_schema);
118+
119+
// Add query parameters
120+
dsn_part.push('?');
121+
let mut params = Vec::new();
122+
123+
// Add interval if present
124+
if let Some(interval) = transport.interval {
125+
params.push(format!("interval={}", interval));
126+
}
127+
128+
// Add extra as query parameter if not empty
129+
if !transport.extra.is_empty() {
130+
let encoded_extra = urlencoding::encode(&transport.extra);
131+
params.push(format!("extra={}", encoded_extra));
132+
}
133+
134+
if !params.is_empty() {
135+
dsn_part.push_str(&params.join("&"));
136+
} else {
137+
// Remove the trailing '?' if no params were added
138+
dsn_part.pop();
139+
}
140+
141+
dsn_parts.push(dsn_part);
142+
}
143+
144+
// Join all DSN parts with semicolons
145+
let dsn = dsn_parts.join(";");
146+
147+
// Emit the DSN configuration
148+
println!("cargo:rustc-env=IMIX_CALLBACK_URI={}", dsn);
149+
150+
// Emit server_pubkey if present
151+
if let Some(ref pubkey) = config.server_pubkey {
152+
println!("cargo:rustc-env=IMIX_SERVER_PUBKEY={}", pubkey);
153+
println!("cargo:warning=Using server_pubkey from YAML config");
154+
}
155+
156+
println!(
157+
"cargo:warning=Successfully parsed YAML config with {} transport(s)",
158+
config.transports.len()
159+
);
160+
161+
Ok(true)
162+
}
163+
6164
fn get_pub_key() {
7165
// Check if IMIX_SERVER_PUBKEY is already set
8166
if std::env::var("IMIX_SERVER_PUBKEY").is_ok() {
@@ -95,6 +253,12 @@ fn build_extra_vars() -> Result<(), Box<dyn std::error::Error>> {
95253
}
96254

97255
fn validate_dsn_config() -> Result<(), Box<dyn std::error::Error>> {
256+
// Skip validation if YAML config is being used
257+
// (parse_yaml_config already handles validation in that case)
258+
if std::env::var("IMIX_CONFIG").is_ok() {
259+
return Ok(());
260+
}
261+
98262
// Check if IMIX_CALLBACK_URI contains query parameters
99263
let callback_uri =
100264
std::env::var("IMIX_CALLBACK_URI").unwrap_or_else(|_| "http://127.0.0.1:8000".to_string());
@@ -126,7 +290,12 @@ fn validate_dsn_config() -> Result<(), Box<dyn std::error::Error>> {
126290
}
127291

128292
fn main() -> Result<(), Box<dyn std::error::Error>> {
293+
// Parse YAML config if present (this will emit IMIX_CALLBACK_URI if successful)
294+
parse_yaml_config()?;
295+
296+
// Validate DSN config (skips if YAML config was used)
129297
validate_dsn_config()?;
298+
130299
get_pub_key();
131300
build_extra_vars()?;
132301

0 commit comments

Comments
 (0)