• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Unleash / unleash-edge / 16370651224

18 Jul 2025 12:33PM UTC coverage: 78.25% (-0.09%) from 78.338%
16370651224

push

github

web-flow
feat: add an option for setting upstream keep alive timeout (#1047)

67 of 77 new or added lines in 6 files covered. (87.01%)

10671 of 13637 relevant lines covered (78.25%)

5437.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.51
/server/src/cli.rs
1
use std::fmt::{Display, Formatter};
2
use std::net::IpAddr;
3
use std::path::PathBuf;
4
use std::str::FromStr;
5
use std::time::Duration;
6

7
use crate::error;
8
use crate::error::EdgeError;
9
use crate::tokens::parse_trusted_token_pair;
10
use crate::types::EdgeToken;
11
use actix_cors::Cors;
12
use actix_http::Method;
13
use cidr::{Ipv4Cidr, Ipv6Cidr};
14
use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum};
15
use ipnet::IpNet;
16

17
#[derive(Subcommand, Debug, Clone)]
18
#[allow(clippy::large_enum_variant)]
19
pub enum EdgeMode {
20
    /// Run in edge mode
21
    Edge(EdgeArgs),
22
    /// Run in offline mode
23
    Offline(OfflineArgs),
24
    /// Perform a health check against a running edge instance
25
    Health(HealthCheckArgs),
26
    /// Perform a ready check against a running edge instance
27
    Ready(ReadyCheckArgs),
28
}
29

30
#[derive(ValueEnum, Debug, Clone)]
31
pub enum RedisScheme {
32
    Tcp,
33
    Tls,
34
    Redis,
35
    Rediss,
36
    RedisUnix,
37
    Unix,
38
}
39

40
impl Display for RedisScheme {
41
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4✔
42
        match self {
4✔
43
            RedisScheme::Redis => write!(f, "redis"),
1✔
44
            RedisScheme::Rediss => write!(f, "rediss"),
1✔
45
            RedisScheme::RedisUnix => write!(f, "redis+unix"),
×
46
            RedisScheme::Unix => write!(f, "unix"),
×
47
            RedisScheme::Tcp => write!(f, "redis"),
1✔
48
            RedisScheme::Tls => write!(f, "rediss"),
1✔
49
        }
50
    }
4✔
51
}
52

53
#[derive(Args, Debug, Clone)]
54
pub struct S3Args {
55
    /// Bucket name to use for storing feature and token data
56
    #[clap(long, env)]
57
    pub s3_bucket_name: Option<String>,
58
}
59

60
#[derive(Copy, Debug, Clone, Eq, PartialEq, PartialOrd, Ord, ValueEnum)]
61
pub enum RedisMode {
62
    Single,
63
    Cluster,
64
}
65

66
#[derive(Args, Debug, Clone)]
67
pub struct RedisArgs {
68
    #[clap(long, env, value_delimiter = ',')]
69
    pub redis_url: Option<Vec<String>>,
3✔
70
    #[clap(long, env, value_enum, default_value_t = RedisMode::Single)]
1✔
71
    pub redis_mode: RedisMode,
×
72
    #[clap(long, env)]
73
    pub redis_password: Option<String>,
74
    #[clap(long, env)]
75
    pub redis_username: Option<String>,
76
    #[clap(long, env)]
77
    pub redis_port: Option<u16>,
78
    #[clap(long, env)]
79
    pub redis_host: Option<String>,
80
    #[clap(long, env, default_value_t = false)]
1✔
81
    pub redis_secure: bool,
×
82
    #[clap(long, env, default_value_t = RedisScheme::Redis, value_enum)]
1✔
83
    pub redis_scheme: RedisScheme,
×
84
    /// Timeout (in milliseconds) for waiting for a successful connection to redis, when restoring
85
    #[clap(long, env, default_value_t = 2000)]
1✔
86
    pub redis_read_connection_timeout_milliseconds: u64,
×
87
    /// Timeout (in milliseconds) for waiting for a successful connection to redis when persisting
88
    #[clap(long, env, default_value_t = 2000)]
1✔
89
    pub redis_write_connection_timeout_milliseconds: u64,
×
90
}
91

92
impl RedisArgs {
93
    pub fn to_url(&self) -> Option<String> {
7✔
94
        self.redis_url
7✔
95
            .clone()
7✔
96
            .map(|url| {
7✔
97
                reqwest::Url::parse(&url[0]).unwrap_or_else(|_| panic!("Failed to create url from REDIS_URL: {:?}, REDIS_USERNAME: {} and REDIS_PASSWORD: {}", self.redis_url.clone().unwrap_or(vec!["NO_URL".into()]), self.redis_username.clone().unwrap_or("NO_USERNAME_SET".into()), self.redis_password.is_some()))
3✔
98
            })
7✔
99
            .or_else(|| self.redis_host.clone().map(|host| {
7✔
100
                reqwest::Url::parse(format!("{}://{}", self.redis_scheme, &host).as_str()).expect("Failed to parse hostname from REDIS_HOSTNAME or --redis-hostname parameters")
4✔
101
            }))
7✔
102
            .map(|base| {
7✔
103
                let mut base_url = base;
7✔
104
                if self.redis_password.is_some() {
7✔
105
                    base_url.set_password(Some(&self.redis_password.clone().unwrap())).expect("Failed to set password");
6✔
106
                }
6✔
107
                if self.redis_username.is_some() {
7✔
108
                    base_url.set_username(&self.redis_username.clone().unwrap()).expect("Failed to set username");
6✔
109
                }
6✔
110
                base_url.set_port(self.redis_port).expect("Failed to set port");
7✔
111
                base_url
7✔
112
            }).map(|almost_finished_url| {
7✔
113
                let mut base_url = almost_finished_url;
7✔
114
                if self.redis_secure {
7✔
115
                    base_url.set_scheme("rediss").expect("Failed to set redis scheme");
2✔
116
                }
5✔
117
                base_url
7✔
118
        }).map(|f| f.to_string())
7✔
119
    }
7✔
120
    pub fn read_timeout(&self) -> std::time::Duration {
×
121
        Duration::from_millis(self.redis_read_connection_timeout_milliseconds)
×
122
    }
×
123
    pub fn write_timeout(&self) -> std::time::Duration {
×
124
        Duration::from_millis(self.redis_write_connection_timeout_milliseconds)
×
125
    }
×
126
}
127
#[derive(Args, Debug, Clone)]
128
pub struct ClientIdentity {
129
    /// Client certificate chain in PEM encoded X509 format with the leaf certificate first.
130
    /// The certificate chain should contain any intermediate certificates that should be sent to clients to allow them to build a chain to a trusted root
131
    #[clap(long, env)]
132
    pub pkcs8_client_certificate_file: Option<PathBuf>,
133
    /// Client key is a PEM encoded PKCS#8 formatted private key for the leaf certificate
134
    #[clap(long, env)]
135
    pub pkcs8_client_key_file: Option<PathBuf>,
136
    /// Identity file in pkcs12 format. Typically this file has a pfx extension
137
    #[clap(long, env)]
138
    pub pkcs12_identity_file: Option<PathBuf>,
139
    #[clap(long, env)]
140
    /// Passphrase used to unlock the pkcs12 file
141
    pub pkcs12_passphrase: Option<String>,
142

143
    pub pem_cert_file: Option<PathBuf>,
144
}
145

146
pub enum PromAuth {
147
    None,
148
    Basic(String, String),
149
}
150

151
#[derive(Args, Debug, Clone, Default)]
152
#[command(group(
153
    ArgGroup::new("data-provider")
154
        .args(["redis_url", "backup_folder", "s3_bucket_name"]),
155
))]
156
pub struct EdgeArgs {
157
    /// Where is your upstream URL. Remember, this is the URL to your instance, without any trailing /api suffix
158
    #[clap(short, long, env)]
159
    pub upstream_url: String,
×
160

161
    /// A path to a local folder. Edge will write feature and token data to disk in this folder and read this back after restart. Mutually exclusive with the --redis-url option
162
    #[clap(short, long, env)]
163
    pub backup_folder: Option<PathBuf>,
164
    /// How often should we post metrics upstream?
165
    #[clap(short, long, env, default_value_t = 60)]
1✔
166
    pub metrics_interval_seconds: u64,
×
167
    /// How long between each refresh for a token
168
    #[clap(short, long, env, default_value_t = 15)]
1✔
169
    pub features_refresh_interval_seconds: u64,
×
170

171
    /// How long between each revalidation of a token
172
    #[clap(long, env, default_value_t = 3600)]
1✔
173
    pub token_revalidation_interval_seconds: u64,
×
174

175
    /// Get data for these client tokens at startup. Accepts comma-separated list of tokens. Hot starts your feature cache
176
    #[clap(short, long, env, value_delimiter = ',')]
177
    pub tokens: Vec<String>,
×
178

179
    /// Set a list of frontend tokens that Edge will always trust. These need to either match the Unleash token format, or they're an arbitrary string followed by an @ and then an environment, e.g. secret-123@development
180
    #[clap(short, long, env, value_delimiter = ',', value_parser = parse_trusted_token_pair)]
181
    pub pretrusted_tokens: Option<Vec<(String, EdgeToken)>>,
×
182

183
    /// Expects curl header format (`-H <HEADERNAME>: <HEADERVALUE>`)
184
    /// for instance `-H X-Api-Key: mysecretapikey`
185
    #[clap(short = 'H', long, env, value_delimiter = ',', value_parser = string_to_header_tuple)]
186
    pub custom_client_headers: Vec<(String, String)>,
3✔
187

188
    /// If set to true, we will skip SSL verification when connecting to the upstream Unleash server
189
    #[clap(short, long, env, default_value_t = false)]
1✔
190
    pub skip_ssl_verification: bool,
×
191

192
    #[clap(flatten)]
193
    pub client_identity: Option<ClientIdentity>,
194

195
    /// Extra certificate passed to the client for building its trust chain. Needs to be in PEM format (crt or pem extensions usually are)
196
    #[clap(long, env)]
197
    pub upstream_certificate_file: Option<PathBuf>,
198

199
    /// Timeout for requests to the upstream server
200
    #[clap(long, env, default_value_t = 5)]
1✔
201
    pub upstream_request_timeout: i64,
×
202

203
    /// Socket timeout for requests to upstream
204
    #[clap(long, env, default_value_t = 5)]
1✔
205
    pub upstream_socket_timeout: i64,
×
206

207
    /// A URL pointing to a running Redis instance. Edge will use this instance to persist feature and token data and read this back after restart. Mutually exclusive with the --backup-folder and --s3-bucket options
208
    #[clap(flatten)]
209
    pub redis: Option<RedisArgs>,
210

211
    /// Configuration for S3 storage. Edge will use this instance to persist feature and token data and read this back after restart. Mutually exclusive with the --redis-url and --backup-folder options
212
    #[clap(flatten)]
213
    pub s3: Option<S3Args>,
214

215
    /// If set to true, Edge starts with strict behavior. Strict behavior means that Edge will refuse tokens outside the scope of the startup tokens
216
    #[clap(long, env, default_value_t = false)]
1✔
217
    pub strict: bool,
×
218

219
    /// If set to true, Edge starts with dynamic behavior. Dynamic behavior means that Edge will accept tokens outside the scope of the startup tokens
220
    #[clap(long, env, default_value_t = false, conflicts_with = "strict")]
1✔
221
    pub dynamic: bool,
×
222

223
    /// If set to true, Edge connects to upstream using streaming instead of polling. This is an experimental feature and may change. Changes to this feature may not follow semantic versioning. Requires strict mode
224
    #[clap(long, env, default_value_t = false, requires = "strict", hide = true)]
1✔
225
    pub streaming: bool,
×
226

227
    /// If set to true, Edge connects to upstream using delta polling instead of normal polling. This is an experimental feature and may change. Changes to this feature may not follow semantic versioning. Requires strict mode
228
    #[clap(long, env, default_value_t = false, requires = "strict", hide = true)]
1✔
229
    pub delta: bool,
×
230

231
    /// If set to true, Edge will track and report consumption metrics. This is an experimental feature and may change. Changes to this feature may not follow semantic versioning. Requires strict mode
232
    #[clap(long, env, default_value_t = false, requires = "strict", hide = true)]
1✔
233
    pub consumption: bool,
×
234

235
    /// Sets the keep-alive timeout for connections from Edge to upstream
236
    #[clap(long, env, default_value_t = 15)]
1✔
NEW
237
    pub client_keepalive_timeout: i64,
×
238

239
    /// If set to true, it compares features payload with delta payload and logs diff. This flag is for internal testing only. Do not turn this on for production configurations
240
    #[clap(
241
        long,
242
        env,
243
        default_value_t = false,
1✔
244
        conflicts_with = "delta",
245
        hide = true
246
    )]
247
    pub delta_diff: bool,
×
248

249
    /// Sets a remote write url for prometheus metrics, if this is set, prometheus metrics will be written upstream
250
    #[clap(long, env)]
251
    pub prometheus_remote_write_url: Option<String>,
252

253
    /// Sets the interval for prometheus push metrics, only relevant if `prometheus_remote_write_url` is set. Defaults to 60 seconds
254
    #[clap(long, env, default_value_t = 60)]
1✔
255
    pub prometheus_push_interval: u64,
×
256

257
    #[clap(long, env)]
258
    pub prometheus_username: Option<String>,
259

260
    #[clap(long, env)]
261
    pub prometheus_password: Option<String>,
262

263
    #[clap(long, env)]
264
    pub prometheus_user_id: Option<String>,
265
}
266

267
pub fn string_to_header_tuple(s: &str) -> Result<(String, String), String> {
5✔
268
    let format_message = "Please pass headers in the format <headername>:<headervalue>".to_string();
5✔
269
    if s.contains(':') {
5✔
270
        if let Some((header_name, header_value)) = s.split_once(':') {
5✔
271
            Ok((
5✔
272
                header_name.trim().to_string(),
5✔
273
                header_value.trim().to_string(),
5✔
274
            ))
5✔
275
        } else {
276
            Err(format_message)
×
277
        }
278
    } else {
279
        Err(format_message)
×
280
    }
281
}
5✔
282

283
#[derive(Args, Debug, Clone)]
284
pub struct OfflineArgs {
285
    /// The file to load our features from. This data will be loaded at startup
286
    #[clap(short, long, env)]
287
    pub bootstrap_file: Option<PathBuf>,
288
    /// Tokens that should be allowed to connect to Edge. Supports a comma separated list or multiple instances of the `--tokens` argument
289
    /// (v19.4.0) deprecated "Please use --client-tokens | CLIENT_TOKENS instead"
290
    #[clap(short, long, env, value_delimiter = ',')]
291
    pub tokens: Vec<String>,
×
292
    /// Client tokens that should be allowed to connect to Edge. Supports a comma separated list or multiple instances of the `--client-tokens` argument
293
    #[clap(short, long, env, value_delimiter = ',')]
294
    pub client_tokens: Vec<String>,
×
295
    /// Frontend tokens that should be allowed to connect to Edge. Supports a comma separated list or multiple instances of the `--frontend-tokens` argument
296
    #[clap(short, long, env, value_delimiter = ',')]
297
    pub frontend_tokens: Vec<String>,
×
298
    /// The interval in seconds between reloading the bootstrap file. Disabled if unset or 0
299
    #[clap(short, long, env, default_value_t = 0)]
1✔
300
    pub reload_interval: u64,
×
301
}
302

303
#[derive(Args, Debug, Clone)]
304
pub struct HealthCheckArgs {
305
    /// Where the instance you want to health check is running
306
    #[clap(short, long, env, default_value = "http://localhost:3063")]
307
    pub edge_url: String,
×
308

309
    /// If you're hosting Edge using a self-signed TLS certificate use this to tell healthcheck about your CA
310
    #[clap(short, long, env)]
311
    pub ca_certificate_file: Option<PathBuf>,
312
}
313

314
#[derive(Args, Debug, Clone)]
315
pub struct InternalBackstageArgs {
316
    /// Disables /internal-backstage/metricsbatch endpoint
317
    ///
318
    /// This endpoint shows the current cached client metrics
319
    #[clap(long, env, global = true)]
320
    pub disable_metrics_batch_endpoint: bool,
×
321
    /// Disables /internal-backstage/metrics endpoint
322
    ///
323
    /// Typically used for prometheus scraping metrics.
324
    #[clap(long, env, global = true)]
325
    pub disable_metrics_endpoint: bool,
×
326
    /// Disables /internal-backstage/features endpoint
327
    ///
328
    /// Used to show current cached features across environments
329
    #[clap(long, env, global = true)]
330
    pub disable_features_endpoint: bool,
×
331
    /// Disables /internal-backstage/tokens endpoint
332
    ///
333
    /// Used to show tokens used to refresh feature caches, but also tokens already validated/invalidated against upstream
334
    #[clap(long, env, global = true)]
335
    pub disable_tokens_endpoint: bool,
×
336

337
    /// Disables /internal-backstage/instancedata endpoint
338
    ///
339
    /// Used to show instance data for the edge instance.
340
    #[clap(long, env, global = true)]
341
    pub disable_instance_data_endpoint: bool,
×
342
}
343

344
#[derive(Debug, Clone, Args)]
345
pub struct AuthHeaders {
346
    /// Header to use for edge authorization
347
    #[clap(long, env, global = true, conflicts_with = "token_header")]
348
    pub edge_auth_header: Option<String>,
349
    /// Header to use for upstream authorization
350
    #[clap(long, env, global = true, conflicts_with = "token_header")]
351
    pub upstream_auth_header: Option<String>,
352
}
353

354
impl Default for AuthHeaders {
355
    fn default() -> Self {
1✔
356
        Self {
1✔
357
            edge_auth_header: Some("Authorization".to_string()),
1✔
358
            upstream_auth_header: Some("Authorization".to_string()),
1✔
359
        }
1✔
360
    }
1✔
361
}
362

363
impl FromStr for AuthHeaders {
364
    type Err = EdgeError;
365

366
    fn from_str(s: &str) -> Result<Self, Self::Err> {
1✔
367
        Ok(Self::from_token_header(s))
1✔
368
    }
1✔
369
}
370

371
impl From<&CliArgs> for AuthHeaders {
372
    fn from(value: &CliArgs) -> Self {
×
373
        match value.token_header.clone() {
×
374
            Some(header) => AuthHeaders::from_token_header(&header),
×
375
            None => value.auth_headers.clone(),
×
376
        }
377
    }
×
378
}
379

380
impl AuthHeaders {
381
    pub fn from_token_header(header: &str) -> Self {
1✔
382
        Self {
1✔
383
            edge_auth_header: Some(header.to_string()),
1✔
384
            upstream_auth_header: Some(header.to_string()),
1✔
385
        }
1✔
386
    }
1✔
387

388
    pub fn custom_upstream_header(header: &str) -> Self {
×
389
        Self {
×
390
            upstream_auth_header: Some(header.to_string()),
×
391
            ..Default::default()
×
392
        }
×
393
    }
×
394

395
    pub fn custom_edge_authorization_header(header: &str) -> Self {
×
396
        Self {
×
397
            edge_auth_header: Some(header.to_string()),
×
398
            ..Default::default()
×
399
        }
×
400
    }
×
401
}
402

403
#[derive(Args, Debug, Clone)]
404
pub struct ReadyCheckArgs {
405
    /// Where the instance you want to health check is running
406
    #[clap(short, long, env, default_value = "http://localhost:3063")]
407
    pub edge_url: String,
×
408

409
    /// If you're hosting Edge using a self-signed TLS certificate use this to tell the readychecker about your CA
410
    #[clap(short, long, env)]
411
    pub ca_certificate_file: Option<PathBuf>,
412
}
413

414
#[derive(Debug, Clone, ValueEnum)]
415
pub enum LogFormat {
416
    Plain,
417
    Json,
418
    Pretty,
419
}
420

421
#[derive(Parser, Debug, Clone)]
422
pub struct CliArgs {
423
    #[clap(flatten)]
424
    pub http: HttpServerArgs,
425

426
    #[command(subcommand)]
427
    pub mode: EdgeMode,
428

429
    /// Instance id. Used for metrics reporting.
430
    #[clap(long, env, global = true, default_value_t = format!("unleash-edge@{}", ulid::Ulid::new()))]
1✔
431
    pub instance_id: String,
×
432

433
    /// App name. Used for metrics reporting.
434
    #[clap(short, long, env, global = true, default_value = "unleash-edge")]
435
    pub app_name: String,
×
436

437
    #[arg(long, hide = true, global = true)]
438
    pub markdown_help: bool,
×
439

440
    #[clap(flatten)]
441
    pub trust_proxy: TrustProxy,
442

443
    /// Set this flag to true if you want to disable /api/proxy/all and /api/frontend/all
444
    /// Because returning all toggles regardless of their state is a potential security vulnerability, these endpoints can be disabled
445
    #[clap(long, env, default_value_t = false, global = true)]
1✔
446
    pub disable_all_endpoint: bool,
×
447

448
    /// Timeout for requests to Edge
449
    #[clap(long, env, default_value_t = 5)]
1✔
450
    pub edge_request_timeout: u64,
×
451

452
    /// Keepalive timeout for requests to Edge
453
    #[clap(long, env, default_value_t = 5)]
1✔
454
    pub edge_keepalive_timeout: u64,
×
455

456
    /// Which log format should Edge use
457
    #[clap(short, long, env, global = true, value_enum, default_value_t = LogFormat::Plain)]
1✔
458
    pub log_format: LogFormat,
×
459

460
    #[clap(flatten)]
461
    pub auth_headers: AuthHeaders,
462

463
    /// token header to use for edge authorization.
464
    #[clap(long, env, global = true)]
465
    pub token_header: Option<String>,
466

467
    #[clap(flatten)]
468
    pub internal_backstage: InternalBackstageArgs,
469
}
470

471
#[derive(Args, Debug, Clone)]
472
pub struct TlsOptions {
473
    /// Should we bind TLS
474
    #[clap(env, long, default_value_t = false)]
1✔
475
    pub tls_enable: bool,
×
476
    /// Server key to use for TLS - Needs to be a path to a file
477
    #[clap(env, long)]
478
    pub tls_server_key: Option<PathBuf>,
479
    #[clap(env, long)]
480
    /// Server Cert to use for TLS - Needs to be a path to a file
481
    pub tls_server_cert: Option<PathBuf>,
482
    /// Port to listen for https connection on (will use the interfaces already defined)
483
    #[clap(env, long, default_value_t = 3043)]
1✔
484
    pub tls_server_port: u16,
×
485
}
486

487
pub fn parse_http_method(value: &str) -> Result<actix_http::Method, String> {
2✔
488
    Method::from_bytes(value.as_bytes()).map_err(|f| format!("Failed to format method: {f:?}"))
2✔
489
}
2✔
490

491
#[derive(Args, Debug, Clone)]
492
pub struct CorsOptions {
493
    /// Sets the [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header to this value
494
    #[clap(env, long, value_delimiter = ',', global = true)]
495
    pub cors_origin: Option<Vec<String>>,
1✔
496
    /// Sets the [Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) header to this value
497
    #[clap(env, long, value_delimiter = ',', global = true)]
498
    pub cors_allowed_headers: Option<Vec<String>>,
×
499
    /// Sets the [Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) header to this value
500
    #[clap(env, long, default_value_t = 172800, global = true)]
1✔
501
    pub cors_max_age: usize,
×
502
    /// Sets the [Access-Control-Expose-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) header to this value
503
    #[clap(env, long, value_delimiter = ',', global = true)]
504
    pub cors_exposed_headers: Option<Vec<String>>,
×
505
    /// Sets the [Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) header to this value
506
    #[clap(env, long, value_delimiter = ',', value_parser = parse_http_method, global = true)]
507
    pub cors_methods: Option<Vec<actix_http::Method>>,
1✔
508
}
509

510
impl CorsOptions {
511
    pub fn middleware(&self) -> Cors {
1✔
512
        let mut cors_middleware = Cors::default()
1✔
513
            .max_age(self.cors_max_age)
1✔
514
            .allow_any_method()
1✔
515
            .allow_any_header();
1✔
516
        if let Some(origins) = self.cors_origin.clone() {
1✔
517
            for origin in origins {
5✔
518
                cors_middleware = cors_middleware.allowed_origin(&origin);
4✔
519
            }
4✔
520
            cors_middleware = cors_middleware.supports_credentials();
1✔
521
        } else {
×
522
            cors_middleware = cors_middleware.allow_any_origin().send_wildcard();
×
523
        }
×
524
        if let Some(allowed_headers) = self.cors_allowed_headers.clone() {
1✔
525
            for header in allowed_headers {
×
526
                cors_middleware = cors_middleware.allowed_header(header);
×
527
            }
×
528
        }
1✔
529
        if let Some(allowed_methods) = self.cors_methods.clone() {
1✔
530
            cors_middleware = cors_middleware.allowed_methods(allowed_methods);
×
531
        }
1✔
532
        if let Some(exposed_headers) = self.cors_exposed_headers.clone() {
1✔
533
            cors_middleware = cors_middleware.expose_headers(exposed_headers);
×
534
        }
1✔
535
        cors_middleware
1✔
536
    }
1✔
537
}
538

539
#[derive(Args, Debug, Clone)]
540
pub struct HttpServerArgs {
541
    /// Which port should this server listen for HTTP traffic on
542
    #[clap(short, long, env, default_value_t = 3063)]
1✔
543
    pub port: u16,
×
544
    /// Which interfaces should this server listen for HTTP traffic on
545
    #[clap(short, long, env, default_value = "0.0.0.0")]
546
    pub interface: String,
×
547
    /// Which base path should this server listen for HTTP traffic on
548
    #[clap(short, long, env, default_value = "")]
549
    pub base_path: String,
×
550

551
    /// How many workers should be started to handle requests.
552
    /// Defaults to number of physical cpus
553
    #[clap(short, long, env, global=true, default_value_t = num_cpus::get_physical())]
1✔
554
    pub workers: usize,
×
555

556
    #[clap(flatten)]
557
    pub tls: TlsOptions,
558

559
    #[clap(flatten)]
560
    pub cors: CorsOptions,
561

562
    /// Configures the AllowList middleware to only accept requests from IPs that belong to the CIDRs configured here. Defaults to 0.0.0.0/0, ::/0 (ALL Ips v4 and v6)
563
    #[clap(long, env, global=true, value_delimiter = ',', value_parser = ip_net_parser)]
564
    pub allow_list: Option<Vec<IpNet>>,
1✔
565

566
    /// Configures the DenyList middleware to deny requests from IPs that belong to the CIDRs configured here. Defaults to denying no IPs.
567
    #[clap(long, env, global=true, value_parser = ip_net_parser, value_delimiter = ',')]
568
    pub deny_list: Option<Vec<IpNet>>,
1✔
569
}
570

571
fn ip_net_parser(arg: &str) -> Result<IpNet, String> {
3✔
572
    IpNet::from_str(arg).map_err(|e| format!("{e}"))
3✔
573
}
3✔
574

575
#[derive(Debug, Clone)]
576
pub enum NetworkAddr {
577
    Ip(IpAddr),
578
    CidrIpv4(Ipv4Cidr),
579
    CidrIpv6(Ipv6Cidr),
580
}
581

582
#[derive(Args, Debug, Clone)]
583
pub struct TrustProxy {
584
    /// By enabling the trust proxy option. Unleash Edge will have knowledge that it's sitting behind a proxy and that the X-Forward-\* header fields may be trusted, which otherwise may be easily spoofed.
585
    /// Edge will use this to populate its context's  remoteAddress field
586
    /// If you need to only trust specific ips or CIDR, enable this flag and then set `--proxy-trusted-servers`
587
    #[clap(long, env, global = true)]
588
    pub trust_proxy: bool,
×
589

590
    /// Tells Unleash Edge which servers to trust the X-Forwarded-For. Accepts explicit Ip addresses or Cidrs (127.0.0.1/16). Accepts a comma separated list or multiple instances of the flag.
591
    /// E.g `--proxy-trusted-servers "127.0.0.1,192.168.0.1"` and `--proxy-trusted-servers 127.0.0.1 --proxy-trusted-servers 192.168.0.1` are equivalent
592
    #[clap(long, env, value_delimiter = ',', global = true, value_parser = ip_or_cidr)]
593
    pub proxy_trusted_servers: Vec<NetworkAddr>,
2✔
594
}
595

596
pub fn ip_or_cidr(s: &str) -> Result<NetworkAddr, String> {
5✔
597
    match IpAddr::from_str(s) {
5✔
598
        Ok(ipaddr) => Ok(NetworkAddr::Ip(ipaddr)),
2✔
599
        Err(_e) => match Ipv4Cidr::from_str(s) {
3✔
600
            Ok(ipv4cidr) => Ok(NetworkAddr::CidrIpv4(ipv4cidr)),
1✔
601
            Err(_e) => match Ipv6Cidr::from_str(s) {
2✔
602
                Ok(ipv6cidr) => Ok(NetworkAddr::CidrIpv6(ipv6cidr)),
1✔
603
                Err(_e) => Err(error::TRUST_PROXY_PARSE_ERROR.into()),
1✔
604
            },
605
        },
606
    }
607
}
5✔
608

609
impl HttpServerArgs {
610
    pub fn http_server_tuple(&self) -> (String, u16) {
×
611
        (self.interface.clone(), self.port)
×
612
    }
×
613

614
    pub fn https_server_tuple(&self) -> (String, u16) {
×
615
        (self.interface.clone(), self.tls.tls_server_port)
×
616
    }
×
617
}
618

619
#[cfg(test)]
620
mod tests {
621
    use actix_web::http;
622
    use clap::Parser;
623
    use ipnet::IpNet;
624
    use std::str::FromStr;
625
    use tracing::info;
626
    use tracing_test::traced_test;
627

628
    use crate::cli::{CliArgs, EdgeMode, NetworkAddr};
629
    use crate::error;
630

631
    #[test]
632
    pub fn can_parse_multiple_client_headers() {
1✔
633
        let args = vec![
1✔
634
            "unleash-edge",
1✔
635
            "edge",
1✔
636
            "-u http://localhost:4242",
1✔
637
            r#"-H Authorization: abc123"#,
1✔
638
            r#"-H X-Api-Key: mysecret"#,
1✔
639
        ];
1✔
640
        let args = CliArgs::parse_from(args);
1✔
641
        match args.mode {
1✔
642
            EdgeMode::Edge(args) => {
1✔
643
                let client_headers = args.custom_client_headers;
1✔
644
                assert_eq!(client_headers.len(), 2);
1✔
645
                let auth = client_headers.first().unwrap();
1✔
646
                assert_eq!(auth.0, "Authorization");
1✔
647
                assert_eq!(auth.1, "abc123");
1✔
648
                let api_key = client_headers.get(1).unwrap();
1✔
649
                assert_eq!(api_key.0, "X-Api-Key");
1✔
650
                assert_eq!(api_key.1, "mysecret")
1✔
651
            }
652
            _ => unreachable!(),
×
653
        }
654
    }
1✔
655

656
    #[test]
657
    pub fn can_parse_comma_separated_client_headers() {
1✔
658
        let args = vec![
1✔
659
            "unleash-edge",
1✔
660
            "edge",
1✔
661
            "-u http://localhost:4242",
1✔
662
            r#"-H Authorization: abc123,X-Api-Key: mysecret"#,
1✔
663
        ];
1✔
664
        let args = CliArgs::parse_from(args);
1✔
665
        match args.mode {
1✔
666
            EdgeMode::Edge(args) => {
1✔
667
                let client_headers = args.custom_client_headers;
1✔
668
                assert_eq!(client_headers.len(), 2);
1✔
669
                let auth = client_headers.first().unwrap();
1✔
670
                assert_eq!(auth.0, "Authorization");
1✔
671
                assert_eq!(auth.1, "abc123");
1✔
672
                let api_key = client_headers.get(1).unwrap();
1✔
673
                assert_eq!(api_key.0, "X-Api-Key");
1✔
674
                assert_eq!(api_key.1, "mysecret")
1✔
675
            }
676
            _ => unreachable!(),
×
677
        }
678
    }
1✔
679

680
    #[test]
681
    pub fn can_handle_colons_in_header_value() {
1✔
682
        let args = vec![
1✔
683
            "unleash-edge",
1✔
684
            "edge",
1✔
685
            "-u http://localhost:4242",
1✔
686
            r#"-H Authorization: test:test.secret"#,
1✔
687
        ];
1✔
688
        let args = CliArgs::parse_from(args);
1✔
689
        match args.mode {
1✔
690
            EdgeMode::Edge(args) => {
1✔
691
                let client_headers = args.custom_client_headers;
1✔
692
                assert_eq!(client_headers.len(), 1);
1✔
693
                let auth = client_headers.first().unwrap();
1✔
694
                assert_eq!(auth.0, "Authorization");
1✔
695
                assert_eq!(auth.1, "test:test.secret");
1✔
696
            }
697
            _ => unreachable!(),
×
698
        }
699
    }
1✔
700

701
    #[test]
702
    pub fn can_create_redis_url_from_redis_url_argument() {
1✔
703
        let args = vec![
1✔
704
            "unleash-edge",
1✔
705
            "edge",
1✔
706
            "-u http://localhost:4242",
1✔
707
            "--redis-url",
1✔
708
            "redis://localhost/redis",
1✔
709
        ];
1✔
710
        let args = CliArgs::parse_from(args);
1✔
711
        match args.mode {
1✔
712
            EdgeMode::Edge(args) => {
1✔
713
                let redis_url = args.redis.unwrap().to_url();
1✔
714
                assert!(redis_url.is_some());
1✔
715
                assert_eq!(redis_url.unwrap(), "redis://localhost/redis");
1✔
716
            }
717
            _ => unreachable!(),
×
718
        }
719
    }
1✔
720

721
    #[test]
722
    pub fn can_create_redis_url_from_more_specific_redis_arguments() {
1✔
723
        let args = vec![
1✔
724
            "unleash-edge",
1✔
725
            "edge",
1✔
726
            "-u http://localhost:4242",
1✔
727
            "--redis-host",
1✔
728
            "localhost",
1✔
729
            "--redis-username",
1✔
730
            "redis",
1✔
731
            "--redis-password",
1✔
732
            "password",
1✔
733
            "--redis-port",
1✔
734
            "6389",
1✔
735
            "--redis-scheme",
1✔
736
            "rediss",
1✔
737
        ];
1✔
738
        let args = CliArgs::parse_from(args);
1✔
739
        match args.mode {
1✔
740
            EdgeMode::Edge(args) => {
1✔
741
                let redis_url = args.redis.unwrap().to_url();
1✔
742
                assert!(redis_url.is_some());
1✔
743
                assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost:6389");
1✔
744
            }
745
            _ => unreachable!(),
×
746
        }
747
    }
1✔
748

749
    #[test]
750
    pub fn can_combine_redis_url_with_username_and_password() {
1✔
751
        let args = vec![
1✔
752
            "unleash-edge",
1✔
753
            "edge",
1✔
754
            "-u http://localhost:4242",
1✔
755
            "--redis-url",
1✔
756
            "redis://localhost",
1✔
757
            "--redis-username",
1✔
758
            "redis",
1✔
759
            "--redis-password",
1✔
760
            "password",
1✔
761
        ];
1✔
762
        let args = CliArgs::parse_from(args);
1✔
763
        match args.mode {
1✔
764
            EdgeMode::Edge(args) => {
1✔
765
                let redis_url = args.redis.unwrap().to_url();
1✔
766
                assert!(redis_url.is_some());
1✔
767
                assert_eq!(redis_url.unwrap(), "redis://redis:password@localhost");
1✔
768
            }
769
            _ => unreachable!(),
×
770
        }
771
    }
1✔
772

773
    #[test]
774
    pub fn setting_redis_secure_to_true_overrides_set_scheme() {
1✔
775
        let args = vec![
1✔
776
            "unleash-edge",
1✔
777
            "edge",
1✔
778
            "-u http://localhost:4242",
1✔
779
            "--redis-url",
1✔
780
            "redis://localhost",
1✔
781
            "--redis-username",
1✔
782
            "redis",
1✔
783
            "--redis-password",
1✔
784
            "password",
1✔
785
            "--redis-secure",
1✔
786
        ];
1✔
787
        let args = CliArgs::parse_from(args);
1✔
788
        match args.mode {
1✔
789
            EdgeMode::Edge(args) => {
1✔
790
                let redis_url = args.redis.unwrap().to_url();
1✔
791
                assert!(redis_url.is_some());
1✔
792
                assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost");
1✔
793
            }
794
            _ => unreachable!(),
×
795
        }
796
    }
1✔
797

798
    #[test]
799
    pub fn setting_secure_to_true_overrides_the_scheme_for_detailed_arguments_as_well() {
1✔
800
        let args = vec![
1✔
801
            "unleash-edge",
1✔
802
            "edge",
1✔
803
            "-u http://localhost:4242",
1✔
804
            "--redis-host",
1✔
805
            "localhost",
1✔
806
            "--redis-username",
1✔
807
            "redis",
1✔
808
            "--redis-password",
1✔
809
            "password",
1✔
810
            "--redis-port",
1✔
811
            "6389",
1✔
812
            "--redis-secure",
1✔
813
        ];
1✔
814
        let args = CliArgs::parse_from(args);
1✔
815
        match args.mode {
1✔
816
            EdgeMode::Edge(args) => {
1✔
817
                let redis_url = args.redis.unwrap().to_url();
1✔
818
                assert!(redis_url.is_some());
1✔
819
                assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost:6389");
1✔
820
            }
821
            _ => unreachable!(),
×
822
        }
823
    }
1✔
824

825
    #[test]
826
    pub fn setting_scheme_to_tls_uses_the_rediss_protocol() {
1✔
827
        let args = vec![
1✔
828
            "unleash-edge",
1✔
829
            "edge",
1✔
830
            "-u http://localhost:4242",
1✔
831
            "--redis-host",
1✔
832
            "localhost",
1✔
833
            "--redis-username",
1✔
834
            "redis",
1✔
835
            "--redis-password",
1✔
836
            "password",
1✔
837
            "--redis-port",
1✔
838
            "6389",
1✔
839
            "--redis-scheme",
1✔
840
            "tls",
1✔
841
        ];
1✔
842
        let args = CliArgs::parse_from(args);
1✔
843
        match args.mode {
1✔
844
            EdgeMode::Edge(args) => {
1✔
845
                let redis_url = args.redis.unwrap().to_url();
1✔
846
                assert!(redis_url.is_some());
1✔
847
                assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost:6389");
1✔
848
            }
849
            _ => unreachable!(),
×
850
        }
851
    }
1✔
852

853
    #[test]
854
    pub fn setting_scheme_to_tcp_uses_the_redis_protocol() {
1✔
855
        let args = vec![
1✔
856
            "unleash-edge",
1✔
857
            "edge",
1✔
858
            "-u http://localhost:4242",
1✔
859
            "--redis-host",
1✔
860
            "localhost",
1✔
861
            "--redis-username",
1✔
862
            "redis",
1✔
863
            "--redis-password",
1✔
864
            "password",
1✔
865
            "--redis-port",
1✔
866
            "6389",
1✔
867
            "--redis-scheme",
1✔
868
            "tcp",
1✔
869
        ];
1✔
870
        let args = CliArgs::parse_from(args);
1✔
871
        match args.mode {
1✔
872
            EdgeMode::Edge(args) => {
1✔
873
                let redis_url = args.redis.unwrap().to_url();
1✔
874
                assert!(redis_url.is_some());
1✔
875
                assert_eq!(redis_url.unwrap(), "redis://redis:password@localhost:6389");
1✔
876
            }
877
            _ => unreachable!(),
×
878
        }
879
    }
1✔
880

881
    #[test]
882
    #[traced_test]
×
883
    pub fn proxy_trusted_servers_accept_both_ipv4_and_ipv6() {
1✔
884
        let args = vec![
1✔
885
            "unleash-edge",
1✔
886
            "edge",
1✔
887
            "-u http://localhost:4242",
1✔
888
            "--trust-proxy",
1✔
889
            "--proxy-trusted-servers",
1✔
890
            "192.168.0.1",
1✔
891
            "--proxy-trusted-servers",
1✔
892
            "::1",
1✔
893
        ];
1✔
894
        let args = CliArgs::parse_from(args);
1✔
895
        assert!(args.trust_proxy.trust_proxy);
1✔
896
        info!("{:?}", args.trust_proxy.proxy_trusted_servers);
1✔
897
        assert_eq!(args.trust_proxy.proxy_trusted_servers.len(), 2);
1✔
898
        let first = args.trust_proxy.proxy_trusted_servers.first().unwrap();
1✔
899
        if let NetworkAddr::Ip(ip_addr) = first {
1✔
900
            assert!(ip_addr.is_ipv4());
1✔
901
        } else {
902
            unreachable!()
×
903
        }
904
        let second = args.trust_proxy.proxy_trusted_servers.get(1).unwrap();
1✔
905
        if let NetworkAddr::Ip(ip_addr) = second {
1✔
906
            assert!(ip_addr.is_ipv6());
1✔
907
        } else {
908
            unreachable!()
×
909
        }
910
    }
1✔
911

912
    #[test]
913
    pub fn cors_origin_can_be_set_via_cli() {
1✔
914
        let args = vec![
1✔
915
            "unleash-edge",
1✔
916
            "--cors-origin",
1✔
917
            "example.com",
1✔
918
            "--cors-origin",
1✔
919
            "otherexample.com",
1✔
920
            "--cors-origin",
1✔
921
            "one.com,two.com",
1✔
922
            "edge",
1✔
923
            "-u http://localhost:4242",
1✔
924
        ];
1✔
925
        let args = CliArgs::parse_from(args);
1✔
926
        assert_eq!(args.http.cors.cors_origin.clone().unwrap().len(), 4);
1✔
927
        let _middleware = args.http.cors.middleware();
1✔
928
    }
1✔
929

930
    #[test]
931
    pub fn can_set_custom_cors_method() {
1✔
932
        let args = vec![
1✔
933
            "unleash-edge",
1✔
934
            "--cors-methods",
1✔
935
            "GET",
1✔
936
            "--cors-methods",
1✔
937
            "PATCH",
1✔
938
            "edge",
1✔
939
            "-u http://localhost:4242",
1✔
940
        ];
1✔
941
        let cli = CliArgs::parse_from(args);
1✔
942
        assert_eq!(
1✔
943
            cli.http.cors.cors_methods,
1✔
944
            Some(vec![http::Method::GET, http::Method::PATCH])
1✔
945
        );
1✔
946
    }
1✔
947

948
    #[test]
949
    pub fn proxy_trusted_servers_accept_both_ipv4_and_ipv6_cidr_addresses() {
1✔
950
        let args = vec![
1✔
951
            "unleash-edge",
1✔
952
            "edge",
1✔
953
            "-u http://localhost:4242",
1✔
954
            "--trust-proxy",
1✔
955
            "--proxy-trusted-servers",
1✔
956
            "192.168.0.0/16",
1✔
957
            "--proxy-trusted-servers",
1✔
958
            "2001:db8:1234::/48",
1✔
959
        ];
1✔
960
        let args = CliArgs::parse_from(args);
1✔
961
        info!("{:?}", args.trust_proxy.proxy_trusted_servers);
1✔
962
        assert_eq!(args.trust_proxy.proxy_trusted_servers.len(), 2);
1✔
963
        let first = args.trust_proxy.proxy_trusted_servers.first().unwrap();
1✔
964
        if let NetworkAddr::CidrIpv4(cidr) = first {
1✔
965
            assert_eq!(cidr.network_length(), 16);
1✔
966
        } else {
967
            unreachable!()
×
968
        }
969
        let second = args.trust_proxy.proxy_trusted_servers.get(1).unwrap();
1✔
970
        if let NetworkAddr::CidrIpv6(ip_addr) = second {
1✔
971
            assert_eq!(ip_addr.network_length(), 48);
1✔
972
        } else {
973
            unreachable!()
×
974
        }
975
    }
1✔
976

977
    #[test]
978
    pub fn incorrect_trusted_servers_format_yields_error_message() {
1✔
979
        let args = vec![
1✔
980
            "unleash-edge",
1✔
981
            "edge",
1✔
982
            "-u http://localhost:4242",
1✔
983
            "--trust-proxy",
1✔
984
            "--proxy-trusted-servers",
1✔
985
            "192.168.0.0/125",
1✔
986
        ];
1✔
987
        let args = CliArgs::try_parse_from(args);
1✔
988
        assert!(args.is_err());
1✔
989
        assert!(
1✔
990
            args.err()
1✔
991
                .unwrap()
1✔
992
                .to_string()
1✔
993
                .contains(error::TRUST_PROXY_PARSE_ERROR)
1✔
994
        );
1✔
995
    }
1✔
996

997
    #[test]
998
    pub fn can_parse_allow_list_cidrs() {
1✔
999
        let args = vec![
1✔
1000
            "unleash-edge",
1✔
1001
            "--allow-list",
1✔
1002
            "192.168.0.0/16",
1✔
1003
            "edge",
1✔
1004
            "-u http://localhost:4242",
1✔
1005
        ];
1✔
1006
        let args = CliArgs::try_parse_from(args);
1✔
1007
        assert!(args.is_ok());
1✔
1008
        assert_eq!(
1✔
1009
            args.unwrap().http.allow_list.unwrap().first(),
1✔
1010
            IpNet::from_str("192.168.0.0/16").ok().as_ref()
1✔
1011
        );
1✔
1012
    }
1✔
1013
    #[test]
1014
    pub fn default_allow_list_is_empty() {
1✔
1015
        let args = vec!["unleash-edge", "edge", "-u http://localhost:4242"];
1✔
1016
        let args = CliArgs::try_parse_from(args);
1✔
1017
        assert!(args.is_ok());
1✔
1018
        assert!(args.unwrap().http.allow_list.is_none());
1✔
1019
    }
1✔
1020

1021
    #[test]
1022
    pub fn errors_if_allow_list_is_not_a_valid_cidr() {
1✔
1023
        let args = vec![
1✔
1024
            "unleash-edge",
1✔
1025
            "--allow-list",
1✔
1026
            "192.168.0.1",
1✔
1027
            "edge",
1✔
1028
            "-u http://localhost:4242",
1✔
1029
        ];
1✔
1030
        let args = CliArgs::try_parse_from(args);
1✔
1031
        assert!(args.is_err());
1✔
1032
        if let Err(e) = args {
1✔
1033
            assert!(e.to_string().contains("invalid IP address syntax"));
1✔
1034
        }
×
1035
    }
1✔
1036

1037
    #[test]
1038
    pub fn no_default_deny_list() {
1✔
1039
        let args = vec!["unleash-edge", "edge", "-u http://localhost:4242"];
1✔
1040
        let args = CliArgs::try_parse_from(args);
1✔
1041
        assert!(args.is_ok());
1✔
1042
        assert!(args.unwrap().http.deny_list.is_none())
1✔
1043
    }
1✔
1044

1045
    #[test]
1046
    pub fn can_parse_deny_list_cidrs() {
1✔
1047
        let args = vec![
1✔
1048
            "unleash-edge",
1✔
1049
            "--deny-list",
1✔
1050
            "192.168.0.0/16",
1✔
1051
            "edge",
1✔
1052
            "-u http://localhost:4242",
1✔
1053
        ];
1✔
1054
        let args = CliArgs::try_parse_from(args);
1✔
1055
        assert!(args.is_ok());
1✔
1056
        assert_eq!(
1✔
1057
            args.unwrap().http.deny_list.unwrap().first(),
1✔
1058
            IpNet::from_str("192.168.0.0/16").ok().as_ref()
1✔
1059
        );
1✔
1060
    }
1✔
1061

1062
    #[test]
1063
    pub fn token_header_is_mutually_exclusive_with_edge_auth_header() {
1✔
1064
        let args = vec![
1✔
1065
            "unleash-edge",
1✔
1066
            "--token-header",
1✔
1067
            "My-Auth",
1✔
1068
            "--edge-auth-header",
1✔
1069
            "X-Edge-Auth",
1✔
1070
            "edge",
1✔
1071
            "-u",
1✔
1072
            "http://localhost:4242",
1✔
1073
        ];
1✔
1074
        let args = CliArgs::try_parse_from(args);
1✔
1075
        assert!(args.is_err());
1✔
1076
    }
1✔
1077

1078
    #[test]
1079
    pub fn token_header_is_mutually_exclusive_with_upstream_auth_header() {
1✔
1080
        let args = vec![
1✔
1081
            "unleash-edge",
1✔
1082
            "--token-header",
1✔
1083
            "My-Auth",
1✔
1084
            "--upstream-auth-header",
1✔
1085
            "X-Edge-Auth",
1✔
1086
            "edge",
1✔
1087
            "-u",
1✔
1088
            "http://localhost:4242",
1✔
1089
        ];
1✔
1090
        let args = CliArgs::try_parse_from(args);
1✔
1091
        assert!(args.is_err());
1✔
1092
    }
1✔
1093

1094
    #[test]
1095
    pub fn can_pass_edge_auth_header_and_upstream_auth_header() {
1✔
1096
        let args = vec![
1✔
1097
            "unleash-edge",
1✔
1098
            "--upstream-auth-header",
1✔
1099
            "My-Auth",
1✔
1100
            "--edge-auth-header",
1✔
1101
            "X-Edge-Auth",
1✔
1102
            "edge",
1✔
1103
            "-u",
1✔
1104
            "http://localhost:4242",
1✔
1105
        ];
1✔
1106
        let args = CliArgs::try_parse_from(args);
1✔
1107
        assert!(args.is_ok());
1✔
1108
    }
1✔
1109

1110
    #[test]
1111
    pub fn can_pass_edge_auth_header() {
1✔
1112
        let args = vec![
1✔
1113
            "unleash-edge",
1✔
1114
            "--edge-auth-header",
1✔
1115
            "X-Edge-Auth",
1✔
1116
            "edge",
1✔
1117
            "-u",
1✔
1118
            "http://localhost:4242",
1✔
1119
        ];
1✔
1120
        let args = CliArgs::try_parse_from(args);
1✔
1121
        assert!(args.is_ok());
1✔
1122
    }
1✔
1123

1124
    #[test]
1125
    pub fn can_pass_upstream_auth_header() {
1✔
1126
        let args = vec![
1✔
1127
            "unleash-edge",
1✔
1128
            "--upstream-auth-header",
1✔
1129
            "My-Auth",
1✔
1130
            "edge",
1✔
1131
            "-u",
1✔
1132
            "http://localhost:4242",
1✔
1133
        ];
1✔
1134
        let args = CliArgs::try_parse_from(args);
1✔
1135
        assert!(args.is_ok());
1✔
1136
    }
1✔
1137
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc