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

Unleash / unleash-edge / #2042

04 Jun 2025 09:18AM UTC coverage: 67.995% (+63.5%) from 4.469%
#2042

push

chriswk
chore(deps): Update testcontainers to 0.24

1980 of 2912 relevant lines covered (67.99%)

1.82 hits per line

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

65.91
/server/src/http/unleash_client.rs
1
use std::collections::HashMap;
2
use std::fs;
3
use std::fs::File;
4
use std::io::BufReader;
5
use std::io::Read;
6
use std::path::PathBuf;
7
use std::str::FromStr;
8

9
use actix_web::http::header::EntityTag;
10
use chrono::Duration;
11
use chrono::Utc;
12
use lazy_static::lazy_static;
13
use prometheus::{HistogramVec, IntGaugeVec, Opts, register_histogram_vec, register_int_gauge_vec};
14
use reqwest::header::{HeaderMap, HeaderName};
15
use reqwest::{Client, header};
16
use reqwest::{ClientBuilder, Identity, RequestBuilder, StatusCode, Url};
17
use serde::{Deserialize, Serialize};
18
use tracing::{debug, error};
19
use tracing::{info, trace, warn};
20
use unleash_types::client_features::{ClientFeatures, ClientFeaturesDelta};
21
use unleash_types::client_metrics::ClientApplication;
22

23
use crate::cli::ClientIdentity;
24
use crate::error::EdgeError::EdgeMetricsRequestError;
25
use crate::error::{CertificateError, FeatureError};
26
use crate::http::headers::{
27
    UNLEASH_APPNAME_HEADER, UNLEASH_CLIENT_SPEC_HEADER, UNLEASH_CONNECTION_ID_HEADER,
28
    UNLEASH_INSTANCE_ID_HEADER, UNLEASH_INTERVAL,
29
};
30
use crate::metrics::client_metrics::MetricsBatch;
31
use crate::metrics::edge_metrics::EdgeInstanceData;
32
use crate::tls::build_upstream_certificate;
33
use crate::types::{
34
    ClientFeaturesDeltaResponse, ClientFeaturesResponse, EdgeResult, EdgeToken,
35
    TokenValidationStatus, ValidateTokensRequest,
36
};
37
use crate::urls::UnleashUrls;
38
use crate::{error::EdgeError, types::ClientFeaturesRequest};
39

40
lazy_static! {
41
    pub static ref CLIENT_REGISTER_FAILURES: IntGaugeVec = register_int_gauge_vec!(
4✔
42
        Opts::new(
1✔
43
            "client_register_failures",
44
            "Why we failed to register upstream"
45
        ),
46
        &["status_code", "app_name", "instance_id"]
47
    )
48
    .unwrap();
49
    pub static ref CLIENT_FEATURE_FETCH: HistogramVec = register_histogram_vec!(
4✔
50
        "client_feature_fetch",
51
        "Timings for fetching features in milliseconds",
52
        &["status_code", "app_name", "instance_id"],
53
        vec![
2✔
54
            1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 5000.0
55
        ]
56
    )
57
    .unwrap();
58
    pub static ref CLIENT_FEATURE_DELTA_FETCH: HistogramVec = register_histogram_vec!(
4✔
59
        "client_feature_delta_fetch",
60
        "Timings for fetching feature deltas in milliseconds",
61
        &["status_code", "app_name", "instance_id"],
62
        vec![
2✔
63
            1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 5000.0
64
        ]
65
    )
66
    .unwrap();
67
    pub static ref METRICS_UPLOAD: HistogramVec = register_histogram_vec!(
4✔
68
        "client_metrics_upload",
69
        "Timings for uploading client metrics in milliseconds",
70
        &["status_code", "app_name", "instance_id"],
71
        vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0]
2✔
72
    )
73
    .unwrap();
74
    pub static ref INSTANCE_DATA_UPLOAD: HistogramVec = register_histogram_vec!(
×
75
        "instance_data_upload",
76
        "Timings for uploading Edge instance data in milliseconds",
77
        &["status_code", "app_name", "instance_id"],
78
        vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0]
×
79
    )
80
    .unwrap();
81
    pub static ref CLIENT_FEATURE_FETCH_FAILURES: IntGaugeVec = register_int_gauge_vec!(
4✔
82
        Opts::new(
1✔
83
            "client_feature_fetch_failures",
84
            "Why we failed to fetch features"
85
        ),
86
        &["status_code", "app_name", "instance_id"]
87
    )
88
    .unwrap();
89
    pub static ref TOKEN_VALIDATION_FAILURES: IntGaugeVec = register_int_gauge_vec!(
×
90
        Opts::new(
×
91
            "token_validation_failures",
92
            "Why we failed to validate tokens"
93
        ),
94
        &["status_code", "app_name", "instance_id"]
95
    )
96
    .unwrap();
97
    pub static ref UPSTREAM_VERSION: IntGaugeVec = register_int_gauge_vec!(
×
98
        Opts::new(
×
99
            "upstream_version",
100
            "The server type (Unleash or Edge) and version of the upstream we're connected to"
101
        ),
102
        &["server", "version", "app_name", "instance_id"]
103
    )
104
    .unwrap();
105
}
106

107
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108
pub struct ClientMetaInformation {
109
    pub app_name: String,
110
    pub instance_id: String,
111
    pub connection_id: String,
112
}
113

114
impl Default for ClientMetaInformation {
115
    fn default() -> Self {
4✔
116
        let connection_id = ulid::Ulid::new().to_string();
4✔
117
        Self {
118
            app_name: "unleash-edge".into(),
4✔
119
            instance_id: format!("unleash-edge@{}", connection_id.clone()),
8✔
120
            connection_id,
121
        }
122
    }
123
}
124

125
impl ClientMetaInformation {
126
    pub fn test_config() -> Self {
1✔
127
        Self {
128
            app_name: "test-app-name".into(),
1✔
129
            instance_id: "test-instance-id".into(),
1✔
130
            connection_id: "test-connection-id".into(),
1✔
131
        }
132
    }
133
}
134

135
#[derive(Clone, Debug, Default)]
136
pub struct UnleashClient {
137
    pub urls: UnleashUrls,
138
    backing_client: Client,
139
    custom_headers: HashMap<String, String>,
140
    token_header: String,
141
    meta_info: ClientMetaInformation,
142
}
143

144
fn load_pkcs12(id: &ClientIdentity) -> EdgeResult<Identity> {
1✔
145
    let p12_file = fs::read(id.pkcs12_identity_file.clone().unwrap()).map_err(|e| {
2✔
146
        EdgeError::ClientCertificateError(CertificateError::Pkcs12ArchiveNotFound(format!("{e:?}")))
×
147
    })?;
148
    let p12_keystore =
5✔
149
        p12_keystore::KeyStore::from_pkcs12(&p12_file, &id.pkcs12_passphrase.clone().unwrap())
150
            .map_err(|e| {
1✔
151
                EdgeError::ClientCertificateError(CertificateError::Pkcs12ParseError(format!(
3✔
152
                    "{e:?}"
153
                )))
154
            })?;
155
    let mut pem = vec![];
1✔
156
    for (alias, entry) in p12_keystore.entries() {
3✔
157
        debug!("P12 entry: {alias}");
5✔
158
        match entry {
1✔
159
            p12_keystore::KeyStoreEntry::Certificate(_) => {
160
                info!(
×
161
                    "Direct Certificate, skipping. We want chain because client identity needs the private key"
162
                );
163
            }
164
            p12_keystore::KeyStoreEntry::PrivateKeyChain(chain) => {
1✔
165
                let key_pem = pkix::pem::der_to_pem(chain.key(), pkix::pem::PEM_PRIVATE_KEY);
2✔
166
                pem.extend(key_pem.as_bytes());
2✔
167
                pem.push(0x0a); // Added new line
1✔
168
                for cert in chain.chain() {
2✔
169
                    let cert_pem = pkix::pem::der_to_pem(cert.as_der(), pkix::pem::PEM_CERTIFICATE);
1✔
170
                    pem.extend(cert_pem.as_bytes());
2✔
171
                    pem.push(0x0a); // Added new line
1✔
172
                }
173
            }
174
        }
175
    }
176

177
    Identity::from_pem(&pem).map_err(|e| {
2✔
178
        EdgeError::ClientCertificateError(CertificateError::Pkcs12X509Error(format!("{e:?}")))
×
179
    })
180
}
181

182
fn load_pkcs8_identity(id: &ClientIdentity) -> EdgeResult<Vec<u8>> {
1✔
183
    let cert = File::open(id.pkcs8_client_certificate_file.clone().unwrap()).map_err(|e| {
2✔
184
        EdgeError::ClientCertificateError(CertificateError::Pem8ClientCertNotFound(format!("{e:}")))
×
185
    })?;
186
    let key = File::open(id.pkcs8_client_key_file.clone().unwrap()).map_err(|e| {
2✔
187
        EdgeError::ClientCertificateError(CertificateError::Pem8ClientKeyNotFound(format!("{e:?}")))
×
188
    })?;
189
    let mut cert_reader = BufReader::new(cert);
1✔
190
    let mut key_reader = BufReader::new(key);
1✔
191
    let mut pem = vec![];
1✔
192
    let _ = key_reader.read_to_end(&mut pem);
2✔
193
    pem.push(0x0a);
1✔
194
    let _ = cert_reader.read_to_end(&mut pem);
1✔
195
    Ok(pem)
1✔
196
}
197

198
fn load_pkcs8(id: &ClientIdentity) -> EdgeResult<Identity> {
1✔
199
    Identity::from_pem(&load_pkcs8_identity(id)?).map_err(|e| {
2✔
200
        EdgeError::ClientCertificateError(CertificateError::Pem8IdentityGeneration(format!(
×
201
            "{e:?}"
202
        )))
203
    })
204
}
205

206
fn load_pem_identity(pem_file: PathBuf) -> EdgeResult<Vec<u8>> {
1✔
207
    let mut pem = vec![];
1✔
208
    let mut pem_reader = BufReader::new(File::open(pem_file).expect("No such file"));
2✔
209
    let _ = pem_reader.read_to_end(&mut pem);
2✔
210
    Ok(pem)
1✔
211
}
212

213
fn load_pem(id: &ClientIdentity) -> EdgeResult<Identity> {
1✔
214
    Identity::from_pem(&load_pem_identity(id.pem_cert_file.clone().unwrap())?).map_err(|e| {
3✔
215
        EdgeError::ClientCertificateError(CertificateError::Pem8IdentityGeneration(format!(
×
216
            "{e:?}"
217
        )))
218
    })
219
}
220

221
fn build_identity(tls: Option<ClientIdentity>) -> EdgeResult<ClientBuilder> {
4✔
222
    tls.map_or_else(
4✔
223
        || Ok(ClientBuilder::new().use_rustls_tls()),
8✔
224
        |tls| {
1✔
225
            let req_identity = if tls.pkcs12_identity_file.is_some() {
2✔
226
                // We're going to assume that we're using pkcs#12
227
                load_pkcs12(&tls)
2✔
228
            } else if tls.pkcs8_client_certificate_file.is_some() {
2✔
229
                load_pkcs8(&tls)
2✔
230
            } else if tls.pem_cert_file.is_some() {
2✔
231
                load_pem(&tls)
2✔
232
            } else {
233
                Err(EdgeError::ClientCertificateError(
×
234
                    CertificateError::NoCertificateFiles,
×
235
                ))
236
            };
237
            req_identity.map(|id| ClientBuilder::new().use_rustls_tls().identity(id))
3✔
238
        },
239
    )
240
}
241

242
pub fn new_reqwest_client(
4✔
243
    skip_ssl_verification: bool,
244
    client_identity: Option<ClientIdentity>,
245
    upstream_certificate_file: Option<PathBuf>,
246
    connect_timeout: Duration,
247
    socket_timeout: Duration,
248
    client_meta_information: ClientMetaInformation,
249
) -> EdgeResult<Client> {
250
    build_identity(client_identity)
12✔
251
        .and_then(|builder| {
8✔
252
            build_upstream_certificate(upstream_certificate_file).map(|cert| match cert {
16✔
253
                Some(c) => builder.add_root_certificate(c),
×
254
                None => builder,
4✔
255
            })
256
        })
257
        .and_then(|client| {
8✔
258
            let mut header_map = HeaderMap::new();
4✔
259
            header_map.insert(
8✔
260
                UNLEASH_APPNAME_HEADER,
261
                header::HeaderValue::from_str(&client_meta_information.app_name)
8✔
262
                    .expect("Could not add app name as a header"),
263
            );
264
            header_map.insert(
4✔
265
                UNLEASH_INSTANCE_ID_HEADER,
266
                header::HeaderValue::from_str(&client_meta_information.instance_id).unwrap(),
4✔
267
            );
268
            header_map.insert(
4✔
269
                UNLEASH_CONNECTION_ID_HEADER,
270
                header::HeaderValue::from_str(&client_meta_information.connection_id).unwrap(),
4✔
271
            );
272
            header_map.insert(
4✔
273
                UNLEASH_CLIENT_SPEC_HEADER,
274
                header::HeaderValue::from_static(unleash_yggdrasil::SUPPORTED_SPEC_VERSION),
4✔
275
            );
276

277
            client
28✔
278
                .user_agent(format!("unleash-edge-{}", crate::types::build::PKG_VERSION))
8✔
279
                .default_headers(header_map)
4✔
280
                .danger_accept_invalid_certs(skip_ssl_verification)
8✔
281
                .timeout(socket_timeout.to_std().unwrap())
8✔
282
                .connect_timeout(connect_timeout.to_std().unwrap())
8✔
283
                .build()
284
                .map_err(|e| EdgeError::ClientBuildError(format!("Failed to build client {e:?}")))
×
285
        })
286
}
287

288
#[derive(Clone, Debug, Serialize, Deserialize)]
289
pub struct EdgeTokens {
290
    pub tokens: Vec<EdgeToken>,
291
}
292

293
impl UnleashClient {
294
    pub fn from_url(
1✔
295
        server_url: Url,
296
        token_header: String,
297
        backing_client: Client,
298
        client_meta_information: ClientMetaInformation,
299
    ) -> Self {
300
        Self {
301
            urls: UnleashUrls::from_base_url(server_url),
1✔
302
            backing_client,
303
            custom_headers: Default::default(),
1✔
304
            token_header,
305
            meta_info: client_meta_information,
306
        }
307
    }
308

309
    pub fn new(server_url: &str, instance_id_opt: Option<String>) -> Result<Self, EdgeError> {
2✔
310
        use ulid::Ulid;
311

312
        let connection_id = Ulid::new().to_string();
4✔
313
        let instance_id = instance_id_opt.unwrap_or_else(|| connection_id.clone());
6✔
314
        let client_meta_info = ClientMetaInformation {
315
            instance_id,
316
            connection_id,
317
            app_name: "test-client".into(),
2✔
318
        };
319
        Ok(Self {
1✔
320
            urls: UnleashUrls::from_str(server_url)?,
6✔
321
            backing_client: new_reqwest_client(
8✔
322
                false,
323
                None,
4✔
324
                None,
4✔
325
                Duration::seconds(5),
4✔
326
                Duration::seconds(5),
4✔
327
                client_meta_info.clone(),
4✔
328
            )
329
            .unwrap(),
330
            custom_headers: Default::default(),
3✔
331
            token_header: "Authorization".to_string(),
4✔
332
            meta_info: client_meta_info.clone(),
3✔
333
        })
334
    }
335

336
    #[cfg(test)]
337
    pub fn new_insecure(server_url: &str) -> Result<Self, EdgeError> {
1✔
338
        Ok(Self {
2✔
339
            urls: UnleashUrls::from_str(server_url)?,
1✔
340
            backing_client: new_reqwest_client(
2✔
341
                true,
342
                None,
1✔
343
                None,
1✔
344
                Duration::seconds(5),
1✔
345
                Duration::seconds(5),
1✔
346
                ClientMetaInformation::test_config(),
1✔
347
            )
348
            .unwrap(),
349
            custom_headers: Default::default(),
1✔
350
            token_header: "Authorization".to_string(),
1✔
351
            meta_info: ClientMetaInformation::test_config(),
1✔
352
        })
353
    }
354

355
    fn client_features_req(&self, req: ClientFeaturesRequest) -> RequestBuilder {
1✔
356
        let mut client_req = self
5✔
357
            .backing_client
358
            .get(self.urls.client_features_url.to_string())
2✔
359
            .headers(self.header_map(Some(req.api_key)));
4✔
360

361
        if let Some(tag) = req.etag {
2✔
362
            client_req = client_req.header(header::IF_NONE_MATCH, tag.to_string());
1✔
363
        }
364

365
        if let Some(interval) = req.interval {
6✔
366
            client_req = client_req.header(UNLEASH_INTERVAL, interval.to_string());
2✔
367
        }
368

369
        client_req
2✔
370
    }
371

372
    fn client_features_delta_req(&self, req: ClientFeaturesRequest) -> RequestBuilder {
1✔
373
        let client_req = self
3✔
374
            .backing_client
375
            .get(self.urls.client_features_delta_url.to_string())
2✔
376
            .headers(self.header_map(Some(req.api_key)));
2✔
377
        if let Some(tag) = req.etag {
4✔
378
            client_req.header(header::IF_NONE_MATCH, tag.to_string())
1✔
379
        } else {
380
            client_req
2✔
381
        }
382
    }
383

384
    fn header_map(&self, api_key: Option<String>) -> HeaderMap {
1✔
385
        let mut header_map = HeaderMap::new();
1✔
386
        let token_header: HeaderName = HeaderName::from_str(self.token_header.as_str()).unwrap();
3✔
387
        if let Some(key) = api_key {
1✔
388
            header_map.insert(token_header, key.parse().unwrap());
2✔
389
        }
390
        for (header_name, header_value) in self.custom_headers.iter() {
3✔
391
            let key = HeaderName::from_str(header_name.as_str()).unwrap();
2✔
392
            header_map.insert(key, header_value.parse().unwrap());
2✔
393
        }
394
        header_map
1✔
395
    }
396

397
    pub fn with_custom_client_headers(self, custom_headers: Vec<(String, String)>) -> Self {
1✔
398
        Self {
399
            custom_headers: custom_headers.iter().cloned().collect(),
2✔
400
            ..self
401
        }
402
    }
403

404
    pub async fn register_as_client(
1✔
405
        &self,
406
        api_key: String,
407
        application: ClientApplication,
408
    ) -> EdgeResult<()> {
409
        self.backing_client
9✔
410
            .post(self.urls.client_register_app_url.to_string())
2✔
411
            .headers(self.header_map(Some(api_key)))
2✔
412
            .json(&application)
1✔
413
            .send()
414
            .await
4✔
415
            .map_err(|e| {
1✔
416
                warn!("Failed to register client: {e:?}");
5✔
417
                EdgeError::ClientRegisterError
1✔
418
            })
419
            .map(|r| {
2✔
420
                if !r.status().is_success() {
2✔
421
                    CLIENT_REGISTER_FAILURES
2✔
422
                        .with_label_values(&[
1✔
423
                            r.status().as_str(),
1✔
424
                            &self.meta_info.app_name,
1✔
425
                            &self.meta_info.instance_id,
1✔
426
                        ])
427
                        .inc();
×
428
                    warn!(
6✔
429
                        "Failed to register client upstream with status code {}",
430
                        r.status()
2✔
431
                    );
432
                }
433
            })
434
    }
435

436
    pub async fn get_client_features(
1✔
437
        &self,
438
        request: ClientFeaturesRequest,
439
    ) -> EdgeResult<ClientFeaturesResponse> {
440
        let start_time = Utc::now();
1✔
441
        let response = self
9✔
442
            .client_features_req(request.clone())
1✔
443
            .send()
444
            .await
5✔
445
            .map_err(|e| {
1✔
446
                warn!("Failed to fetch. Due to [{e:?}] - Will retry");
5✔
447
                match e.status() {
2✔
448
                    Some(s) => EdgeError::ClientFeaturesFetchError(FeatureError::Retriable(s)),
×
449
                    None => EdgeError::ClientFeaturesFetchError(FeatureError::NotFound),
1✔
450
                }
451
            })?;
452
        let stop_time = Utc::now();
1✔
453
        CLIENT_FEATURE_FETCH
7✔
454
            .with_label_values(&[
2✔
455
                &response.status().as_u16().to_string(),
2✔
456
                &self.meta_info.app_name,
2✔
457
                &self.meta_info.instance_id,
2✔
458
            ])
459
            .observe(
460
                stop_time
4✔
461
                    .signed_duration_since(start_time)
2✔
462
                    .num_milliseconds() as f64,
463
            );
464
        if response.status() == StatusCode::NOT_MODIFIED {
3✔
465
            Ok(ClientFeaturesResponse::NoUpdate(
1✔
466
                request.etag.expect("Got NOT_MODIFIED without an ETag"),
1✔
467
            ))
468
        } else if response.status().is_success() {
6✔
469
            let etag = response
6✔
470
                .headers()
471
                .get("ETag")
472
                .or_else(|| response.headers().get("etag"))
6✔
473
                .and_then(|etag| EntityTag::from_str(etag.to_str().unwrap()).ok());
3✔
474
            let features = response.json::<ClientFeatures>().await.map_err(|e| {
5✔
475
                warn!("Could not parse features response to internal representation");
×
476
                EdgeError::ClientFeaturesParseError(e.to_string())
×
477
            })?;
478
            Ok(ClientFeaturesResponse::Updated(features, etag))
2✔
479
        } else if response.status() == StatusCode::FORBIDDEN {
3✔
480
            CLIENT_FEATURE_FETCH_FAILURES
2✔
481
                .with_label_values(&[
1✔
482
                    response.status().as_str(),
1✔
483
                    &self.meta_info.app_name,
1✔
484
                    &self.meta_info.instance_id,
1✔
485
                ])
486
                .inc();
487
            Err(EdgeError::ClientFeaturesFetchError(
1✔
488
                FeatureError::AccessDenied,
1✔
489
            ))
490
        } else if response.status() == StatusCode::UNAUTHORIZED {
×
491
            CLIENT_FEATURE_FETCH_FAILURES
×
492
                .with_label_values(&[
×
493
                    response.status().as_str(),
×
494
                    &self.meta_info.app_name,
×
495
                    &self.meta_info.instance_id,
×
496
                ])
497
                .inc();
498
            warn!(
×
499
                "Failed to get features. Url: [{}]. Status code: [401]",
500
                self.urls.client_features_url.to_string()
501
            );
502
            Err(EdgeError::ClientFeaturesFetchError(
×
503
                FeatureError::AccessDenied,
×
504
            ))
505
        } else if response.status() == StatusCode::NOT_FOUND {
×
506
            CLIENT_FEATURE_FETCH_FAILURES
×
507
                .with_label_values(&[
×
508
                    response.status().as_str(),
×
509
                    &self.meta_info.app_name,
×
510
                    &self.meta_info.instance_id,
×
511
                ])
512
                .inc();
513
            warn!(
×
514
                "Failed to get features. Url: [{}]. Status code: [{}]",
515
                self.urls.client_features_url.to_string(),
516
                response.status().as_str()
517
            );
518
            Err(EdgeError::ClientFeaturesFetchError(FeatureError::NotFound))
×
519
        } else {
520
            CLIENT_FEATURE_FETCH_FAILURES
×
521
                .with_label_values(&[
×
522
                    response.status().as_str(),
×
523
                    &self.meta_info.app_name,
×
524
                    &self.meta_info.instance_id,
×
525
                ])
526
                .inc();
527
            Err(EdgeError::ClientFeaturesFetchError(
×
528
                FeatureError::Retriable(response.status()),
×
529
            ))
530
        }
531
    }
532

533
    pub async fn get_client_features_delta(
1✔
534
        &self,
535
        request: ClientFeaturesRequest,
536
    ) -> EdgeResult<ClientFeaturesDeltaResponse> {
537
        let start_time = Utc::now();
1✔
538
        let response = self
12✔
539
            .client_features_delta_req(request.clone())
1✔
540
            .send()
541
            .await
8✔
542
            .map_err(|e| {
2✔
543
                warn!("Failed to fetch. Due to [{e:?}] - Will retry");
10✔
544
                match e.status() {
4✔
545
                    Some(s) => EdgeError::ClientFeaturesFetchError(FeatureError::Retriable(s)),
×
546
                    None => EdgeError::ClientFeaturesFetchError(FeatureError::NotFound),
1✔
547
                }
548
            })?;
549
        let stop_time = Utc::now();
1✔
550
        CLIENT_FEATURE_DELTA_FETCH
4✔
551
            .with_label_values(&[
1✔
552
                &response.status().as_u16().to_string(),
1✔
553
                &self.meta_info.app_name,
1✔
554
                &self.meta_info.instance_id,
1✔
555
            ])
556
            .observe(
557
                stop_time
2✔
558
                    .signed_duration_since(start_time)
1✔
559
                    .num_milliseconds() as f64,
560
            );
561
        if response.status() == StatusCode::NOT_MODIFIED {
1✔
562
            Ok(ClientFeaturesDeltaResponse::NoUpdate(
×
563
                request.etag.expect("Got NOT_MODIFIED without an ETag"),
×
564
            ))
565
        } else if response.status().is_success() {
3✔
566
            let etag = response
3✔
567
                .headers()
568
                .get("ETag")
569
                .or_else(|| response.headers().get("etag"))
1✔
570
                .and_then(|etag| EntityTag::from_str(etag.to_str().unwrap()).ok());
3✔
571
            let features = response.json::<ClientFeaturesDelta>().await.map_err(|e| {
2✔
572
                warn!("Could not parse features response to internal representation");
×
573
                EdgeError::ClientFeaturesParseError(e.to_string())
×
574
            })?;
575
            Ok(ClientFeaturesDeltaResponse::Updated(features, etag))
1✔
576
        } else if response.status() == StatusCode::FORBIDDEN {
×
577
            CLIENT_FEATURE_FETCH_FAILURES
×
578
                .with_label_values(&[response.status().as_str(), &self.meta_info.app_name])
×
579
                .inc();
580
            Err(EdgeError::ClientFeaturesFetchError(
×
581
                FeatureError::AccessDenied,
×
582
            ))
583
        } else if response.status() == StatusCode::UNAUTHORIZED {
×
584
            CLIENT_FEATURE_FETCH_FAILURES
×
585
                .with_label_values(&[response.status().as_str()])
×
586
                .inc();
587
            warn!(
×
588
                "Failed to get features. Url: [{}]. Status code: [401]",
589
                self.urls.client_features_delta_url.to_string()
590
            );
591
            Err(EdgeError::ClientFeaturesFetchError(
×
592
                FeatureError::AccessDenied,
×
593
            ))
594
        } else if response.status() == StatusCode::NOT_FOUND {
×
595
            CLIENT_FEATURE_FETCH_FAILURES
×
596
                .with_label_values(&[response.status().as_str()])
×
597
                .inc();
598
            warn!(
×
599
                "Failed to get features. Url: [{}]. Status code: [{}]",
600
                self.urls.client_features_delta_url.to_string(),
601
                response.status().as_str()
602
            );
603
            Err(EdgeError::ClientFeaturesFetchError(FeatureError::NotFound))
×
604
        } else {
605
            CLIENT_FEATURE_FETCH_FAILURES
×
606
                .with_label_values(&[response.status().as_str()])
×
607
                .inc();
608
            Err(EdgeError::ClientFeaturesFetchError(
×
609
                FeatureError::Retriable(response.status()),
×
610
            ))
611
        }
612
    }
613

614
    pub async fn send_batch_metrics(
×
615
        &self,
616
        request: MetricsBatch,
617
        interval: Option<i64>,
618
    ) -> EdgeResult<()> {
619
        trace!("Sending metrics to old /edge/metrics endpoint");
×
620
        let mut client_req = self
×
621
            .backing_client
622
            .post(self.urls.edge_metrics_url.to_string())
×
623
            .headers(self.header_map(None));
×
624
        if let Some(interval_value) = interval {
×
625
            client_req = client_req.header(UNLEASH_INTERVAL, interval_value.to_string());
×
626
        }
627

628
        let result = client_req.json(&request).send().await.map_err(|e| {
×
629
            info!("Failed to send batch metrics: {e:?}");
×
630
            EdgeError::EdgeMetricsError
×
631
        })?;
632
        if result.status().is_success() {
×
633
            Ok(())
×
634
        } else {
635
            match result.status() {
×
636
                StatusCode::BAD_REQUEST => Err(EdgeError::EdgeMetricsRequestError(
×
637
                    result.status(),
×
638
                    result.json().await.ok(),
×
639
                )),
640
                _ => Err(EdgeMetricsRequestError(result.status(), None)),
×
641
            }
642
        }
643
    }
644

645
    pub async fn send_bulk_metrics_to_client_endpoint(
1✔
646
        &self,
647
        request: MetricsBatch,
648
        token: &str,
649
    ) -> EdgeResult<()> {
650
        trace!("Sending metrics to bulk endpoint");
5✔
651
        let started_at = Utc::now();
1✔
652
        let result = self
8✔
653
            .backing_client
654
            .post(self.urls.client_bulk_metrics_url.to_string())
2✔
655
            .headers(self.header_map(Some(token.to_string())))
2✔
656
            .json(&request)
1✔
657
            .send()
658
            .await
4✔
659
            .map_err(|e| {
×
660
                info!("Failed to send metrics to /api/client/metrics/bulk endpoint {e:?}");
×
661
                EdgeError::EdgeMetricsError
×
662
            })?;
663
        let ended = Utc::now();
1✔
664
        METRICS_UPLOAD
5✔
665
            .with_label_values(&[
2✔
666
                result.status().as_str(),
2✔
667
                &self.meta_info.app_name,
2✔
668
                &self.meta_info.instance_id,
2✔
669
            ])
670
            .observe(ended.signed_duration_since(started_at).num_milliseconds() as f64);
4✔
671
        if result.status().is_success() {
3✔
672
            Ok(())
1✔
673
        } else {
674
            match result.status() {
2✔
675
                StatusCode::BAD_REQUEST => Err(EdgeMetricsRequestError(
×
676
                    result.status(),
×
677
                    result.json().await.ok(),
×
678
                )),
679
                _ => Err(EdgeMetricsRequestError(result.status(), None)),
2✔
680
            }
681
        }
682
    }
683

684
    #[tracing::instrument(skip(self, instance_data, token))]
685
    pub async fn post_edge_observability_data(
686
        &self,
687
        instance_data: EdgeInstanceData,
688
        token: &str,
689
    ) -> EdgeResult<()> {
690
        let started_at = Utc::now();
×
691
        let result = self
×
692
            .backing_client
693
            .post(self.urls.edge_instance_data_url.to_string())
×
694
            .headers(self.header_map(Some(token.into())))
×
695
            .timeout(Duration::seconds(3).to_std().unwrap())
×
696
            .json(&instance_data)
×
697
            .send()
698
            .await
×
699
            .map_err(|e| {
×
700
                info!("Failed to send instance data: {e:?}");
×
701
                EdgeError::EdgeMetricsError
×
702
            })?;
703
        let ended_at = Utc::now();
×
704
        INSTANCE_DATA_UPLOAD
×
705
            .with_label_values(&[
×
706
                result.status().as_str(),
×
707
                &self.meta_info.app_name,
×
708
                &self.meta_info.instance_id,
×
709
            ])
710
            .observe(
711
                ended_at
×
712
                    .signed_duration_since(started_at)
×
713
                    .num_milliseconds() as f64,
714
            );
715
        let r = if result.status().is_success() {
×
716
            Ok(())
×
717
        } else {
718
            match result.status() {
×
719
                StatusCode::BAD_REQUEST => Err(EdgeMetricsRequestError(
×
720
                    result.status(),
×
721
                    result.json().await.ok(),
×
722
                )),
723
                _ => Err(EdgeMetricsRequestError(result.status(), None)),
×
724
            }
725
        };
726
        debug!("Sent instance data to upstream server");
×
727
        r
×
728
    }
729

730
    pub async fn validate_tokens(
1✔
731
        &self,
732
        request: ValidateTokensRequest,
733
    ) -> EdgeResult<Vec<EdgeToken>> {
734
        let check_api_suffix = || {
1✔
735
            let base_url = self.urls.base_url.to_string();
×
736
            if base_url.ends_with("/api") || base_url.ends_with("/api/") {
×
737
                error!("Try passing the instance URL without '/api'.");
×
738
            }
739
        };
740

741
        let result = self
13✔
742
            .backing_client
743
            .post(self.urls.edge_validate_url.to_string())
2✔
744
            .headers(self.header_map(None))
3✔
745
            .json(&request)
1✔
746
            .send()
747
            .await
6✔
748
            .map_err(|e| {
1✔
749
                info!("Failed to validate tokens: [{e:?}]");
5✔
750
                EdgeError::EdgeTokenError
1✔
751
            })?;
752
        match result.status() {
2✔
753
            StatusCode::OK => {
754
                let token_response = result.json::<EdgeTokens>().await.map_err(|e| {
2✔
755
                    warn!("Failed to parse validation response with error: {e:?}");
×
756
                    EdgeError::EdgeTokenParseError
×
757
                })?;
758
                Ok(token_response
2✔
759
                    .tokens
760
                    .into_iter()
761
                    .map(|t| {
1✔
762
                        let remaining_info =
1✔
763
                            EdgeToken::try_from(t.token.clone()).unwrap_or_else(|_| t.clone());
2✔
764
                        EdgeToken {
1✔
765
                            token: t.token.clone(),
1✔
766
                            token_type: t.token_type,
1✔
767
                            environment: t.environment.or(remaining_info.environment),
1✔
768
                            projects: t.projects,
1✔
769
                            status: TokenValidationStatus::Validated,
1✔
770
                        }
771
                    })
772
                    .collect())
773
            }
774
            s => {
×
775
                TOKEN_VALIDATION_FAILURES
×
776
                    .with_label_values(&[
×
777
                        result.status().as_str(),
×
778
                        &self.meta_info.app_name,
×
779
                        &self.meta_info.instance_id,
×
780
                    ])
781
                    .inc();
782
                error!(
×
783
                    "Failed to validate tokens. Requested url: [{}]. Got status: {:?}",
784
                    self.urls.edge_validate_url.to_string(),
785
                    s
786
                );
787
                check_api_suffix();
×
788
                Err(EdgeError::TokenValidationError(
×
789
                    reqwest::StatusCode::from_u16(s.as_u16()).unwrap(),
×
790
                ))
791
            }
792
        }
793
    }
794
}
795

796
#[cfg(test)]
797
mod tests {
798
    use std::path::PathBuf;
799
    use std::str::FromStr;
800

801
    use crate::cli::ClientIdentity;
802
    use crate::http::unleash_client::new_reqwest_client;
803
    use crate::{
804
        cli::TlsOptions,
805
        middleware::as_async_middleware::as_async_middleware,
806
        tls,
807
        types::{
808
            ClientFeaturesRequest, ClientFeaturesResponse, EdgeToken, TokenValidationStatus,
809
            ValidateTokensRequest,
810
        },
811
    };
812
    use actix_http::{HttpService, TlsAcceptorConfig, body::MessageBody};
813
    use actix_http_test::{TestServer, test_server};
814
    use actix_middleware_etag::Etag;
815
    use actix_service::map_config;
816
    use actix_web::{
817
        App, HttpResponse,
818
        dev::{AppConfig, ServiceRequest, ServiceResponse},
819
        http::header::EntityTag,
820
        web,
821
    };
822
    use chrono::Duration;
823
    use ulid::Ulid;
824
    use unleash_types::client_features::{ClientFeature, ClientFeatures};
825

826
    use super::{ClientMetaInformation, EdgeTokens, UnleashClient};
827

828
    impl ClientFeaturesRequest {
829
        pub(crate) fn new(api_key: String, etag: Option<String>) -> Self {
830
            Self {
831
                api_key,
832
                etag: etag.map(EntityTag::new_weak),
833
                interval: Some(15),
834
            }
835
        }
836
    }
837

838
    const TEST_TOKEN: &str = "[]:development.08bce4267a3b1aa";
839
    fn two_client_features() -> ClientFeatures {
840
        ClientFeatures {
841
            version: 2,
842
            features: vec![
843
                ClientFeature {
844
                    name: "test1".into(),
845
                    feature_type: Some("release".into()),
846
                    ..Default::default()
847
                },
848
                ClientFeature {
849
                    name: "test2".into(),
850
                    feature_type: Some("release".into()),
851
                    ..Default::default()
852
                },
853
            ],
854
            segments: None,
855
            query: None,
856
            meta: None,
857
        }
858
    }
859

860
    async fn return_client_features() -> HttpResponse {
861
        HttpResponse::Ok().json(two_client_features())
862
    }
863

864
    async fn return_validate_tokens() -> HttpResponse {
865
        HttpResponse::Ok().json(EdgeTokens {
866
            tokens: vec![EdgeToken {
867
                token: TEST_TOKEN.into(),
868
                ..Default::default()
869
            }],
870
        })
871
    }
872

873
    async fn test_features_server() -> TestServer {
874
        test_server(move || {
875
            HttpService::new(map_config(
876
                App::new()
877
                    .wrap(Etag)
878
                    .service(
879
                        web::resource("/api/client/features")
880
                            .route(web::get().to(return_client_features)),
881
                    )
882
                    .service(
883
                        web::resource("/edge/validate")
884
                            .route(web::post().to(return_validate_tokens)),
885
                    )
886
                    .service(
887
                        web::resource("/api/edge/validate")
888
                            .route(web::post().to(HttpResponse::Forbidden)),
889
                    ),
890
                |_| AppConfig::default(),
891
            ))
892
            .tcp()
893
        })
894
        .await
895
    }
896

897
    async fn test_features_server_with_untrusted_ssl() -> TestServer {
898
        test_server(move || {
899
            let tls_options = TlsOptions {
900
                tls_server_cert: Some("../examples/server.crt".into()),
901
                tls_enable: true,
902
                tls_server_key: Some("../examples/server.key".into()),
903
                tls_server_port: 443,
904
            };
905
            let server_config = tls::config(tls_options).expect("Failed to load TLS configuration");
906
            let tls_acceptor_config =
907
                TlsAcceptorConfig::default().handshake_timeout(std::time::Duration::from_secs(5));
908
            HttpService::new(map_config(
909
                App::new()
910
                    .wrap(Etag)
911
                    .service(
912
                        web::resource("/api/client/features")
913
                            .route(web::get().to(return_client_features)),
914
                    )
915
                    .service(
916
                        web::resource("/edge/validate")
917
                            .route(web::post().to(return_validate_tokens)),
918
                    ),
919
                |_| AppConfig::default(),
920
            ))
921
            .rustls_0_23_with_config(server_config, tls_acceptor_config)
922
        })
923
        .await
924
    }
925

926
    async fn validate_headers_middleware(
927
        req: ServiceRequest,
928
        srv: crate::middleware::as_async_middleware::Next<impl MessageBody + 'static>,
929
    ) -> Result<ServiceResponse<impl MessageBody>, actix_web::Error> {
930
        let custom_header_valid = req
931
            .headers()
932
            .get("X-Api-Key")
933
            .map(|key| key.to_str().unwrap() == "MyMagicKey")
934
            .unwrap_or(false);
935

936
        let unleash_interval_valid = req
937
            .headers()
938
            .get("Unleash-Interval")
939
            .map(|value| value.to_str().unwrap() == "15")
940
            .unwrap_or(false);
941

942
        let unleash_connection_id_valid = req
943
            .headers()
944
            .get("Unleash-Connection-ID")
945
            .and_then(|value| value.to_str().ok())
946
            .and_then(|value| Ulid::from_str(value).ok())
947
            .is_some();
948

949
        let res = if custom_header_valid && unleash_interval_valid && unleash_connection_id_valid {
950
            srv.call(req).await?.map_into_left_body()
951
        } else {
952
            req.into_response(HttpResponse::Forbidden().finish())
953
                .map_into_right_body()
954
        };
955
        Ok(res)
956
    }
957

958
    async fn test_features_server_with_required_headers() -> TestServer {
959
        test_server(move || {
960
            HttpService::new(map_config(
961
                App::new()
962
                    .wrap(Etag)
963
                    .wrap(as_async_middleware(validate_headers_middleware))
964
                    .service(
965
                        web::resource("/api/client/features")
966
                            .route(web::get().to(return_client_features)),
967
                    )
968
                    .service(
969
                        web::resource("/edge/validate")
970
                            .route(web::post().to(return_validate_tokens)),
971
                    ),
972
                |_| AppConfig::default(),
973
            ))
974
            .tcp()
975
        })
976
        .await
977
    }
978

979
    fn expected_etag(features: ClientFeatures) -> String {
980
        let hash = features.xx3_hash().unwrap();
981
        let len = serde_json::to_string(&features)
982
            .map(|string| string.len())
983
            .unwrap();
984
        format!("{len:x}-{hash}")
985
    }
986

987
    #[actix_web::test]
988
    async fn client_can_get_features() {
989
        let srv = test_features_server().await;
990
        let tag = EntityTag::new_weak(expected_etag(two_client_features()));
991
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
992
        let client_features_result = client
993
            .get_client_features(ClientFeaturesRequest::new("somekey".to_string(), None))
994
            .await;
995
        assert!(client_features_result.is_ok());
996
        let client_features_response = client_features_result.unwrap();
997
        match client_features_response {
998
            ClientFeaturesResponse::Updated(f, e) => {
999
                assert!(e.is_some());
1000
                assert_eq!(e.unwrap(), tag);
1001
                assert!(!f.features.is_empty());
1002
            }
1003
            _ => panic!("Got no update when expecting an update"),
1004
        }
1005
    }
1006

1007
    #[actix_web::test]
1008
    async fn client_handles_304() {
1009
        let srv = test_features_server().await;
1010
        let tag = expected_etag(two_client_features());
1011
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
1012
        let client_features_result = client
1013
            .get_client_features(ClientFeaturesRequest::new(
1014
                TEST_TOKEN.to_string(),
1015
                Some(tag.clone()),
1016
            ))
1017
            .await;
1018
        assert!(client_features_result.is_ok());
1019
        let client_features_response = client_features_result.unwrap();
1020
        match client_features_response {
1021
            ClientFeaturesResponse::NoUpdate(t) => {
1022
                assert_eq!(t, EntityTag::new_weak(tag));
1023
            }
1024
            _ => panic!("Got an update when no update was expected"),
1025
        }
1026
    }
1027

1028
    #[actix_web::test]
1029
    async fn can_validate_token() {
1030
        let srv = test_features_server().await;
1031
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
1032
        let validate_result = client
1033
            .validate_tokens(ValidateTokensRequest {
1034
                tokens: vec![TEST_TOKEN.to_string()],
1035
            })
1036
            .await;
1037
        match validate_result {
1038
            Ok(token_status) => {
1039
                assert_eq!(token_status.len(), 1);
1040
                let validated_token = token_status.first().unwrap();
1041
                assert_eq!(validated_token.status, TokenValidationStatus::Validated);
1042
                assert_eq!(validated_token.environment, Some("development".into()))
1043
            }
1044
            Err(e) => {
1045
                panic!("Error validating token: {e}");
1046
            }
1047
        }
1048
    }
1049

1050
    #[test]
1051
    pub fn can_parse_entity_tag() {
1052
        let etag = EntityTag::from_str("W/\"b5e6-DPC/1RShRw1J/jtxvRtTo1jf4+o\"").unwrap();
1053
        assert!(etag.weak);
1054
    }
1055

1056
    #[test]
1057
    pub fn parse_entity_tag() {
1058
        let optimal_304_tag = EntityTag::from_str("\"76d8bb0e:2841\"");
1059
        assert!(optimal_304_tag.is_ok());
1060
    }
1061

1062
    #[actix_web::test]
1063
    pub async fn custom_and_identification_headers_are_sent_along() {
1064
        let custom_headers = vec![("X-Api-Key".to_string(), "MyMagicKey".to_string())];
1065
        let srv = test_features_server_with_required_headers().await;
1066
        let client_without_extra_headers = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
1067
        let client_with_headers = client_without_extra_headers
1068
            .clone()
1069
            .with_custom_client_headers(custom_headers);
1070
        let res = client_without_extra_headers
1071
            .get_client_features(ClientFeaturesRequest {
1072
                api_key: "notneeded".into(),
1073
                etag: None,
1074
                interval: Some(15),
1075
            })
1076
            .await;
1077
        assert!(res.is_err());
1078
        let authed_res = client_with_headers
1079
            .get_client_features(ClientFeaturesRequest {
1080
                api_key: "notneeded".into(),
1081
                etag: None,
1082
                interval: Some(15),
1083
            })
1084
            .await;
1085
        assert!(authed_res.is_ok());
1086
    }
1087

1088
    #[actix_web::test]
1089
    pub async fn disabling_ssl_verification_allows_communicating_with_upstream_unleash_with_self_signed_cert()
1090
     {
1091
        let srv = test_features_server_with_untrusted_ssl().await;
1092
        let client = UnleashClient::new_insecure(srv.surl("/").as_str()).unwrap();
1093

1094
        let validate_result = client
1095
            .validate_tokens(ValidateTokensRequest {
1096
                tokens: vec![TEST_TOKEN.to_string()],
1097
            })
1098
            .await;
1099

1100
        assert!(validate_result.is_ok());
1101
    }
1102

1103
    #[actix_web::test]
1104
    pub async fn not_disabling_ssl_verification_fails_communicating_with_upstream_unleash_with_self_signed_cert()
1105
     {
1106
        let srv = test_features_server_with_untrusted_ssl().await;
1107
        let client = UnleashClient::new(srv.surl("/").as_str(), None).unwrap();
1108

1109
        let validate_result = client
1110
            .validate_tokens(ValidateTokensRequest {
1111
                tokens: vec![TEST_TOKEN.to_string()],
1112
            })
1113
            .await;
1114

1115
        assert!(validate_result.is_err());
1116
    }
1117

1118
    #[cfg(target_os = "linux")]
1119
    #[test]
1120
    pub fn can_instantiate_pkcs_12_client() {
1121
        let pfx = "./testdata/pkcs12/snakeoil.pfx";
1122
        let passphrase = "password";
1123
        let identity = ClientIdentity {
1124
            pkcs8_client_certificate_file: None,
1125
            pkcs8_client_key_file: None,
1126
            pkcs12_identity_file: Some(PathBuf::from(pfx)),
1127
            pkcs12_passphrase: Some(passphrase.into()),
1128
            pem_cert_file: None,
1129
        };
1130
        let client = new_reqwest_client(
1131
            false,
1132
            Some(identity),
1133
            None,
1134
            Duration::seconds(5),
1135
            Duration::seconds(5),
1136
            ClientMetaInformation {
1137
                app_name: "test-client".into(),
1138
                instance_id: "test-pkcs12".into(),
1139
                connection_id: "test-pkcs12".into(),
1140
            },
1141
        );
1142
        assert!(client.is_ok());
1143
    }
1144

1145
    #[test]
1146
    pub fn should_throw_error_if_wrong_passphrase_to_pfx_file() {
1147
        let pfx = "./testdata/pkcs12/snakeoil.pfx";
1148
        let passphrase = "wrongpassword";
1149
        let identity = ClientIdentity {
1150
            pkcs8_client_certificate_file: None,
1151
            pkcs8_client_key_file: None,
1152
            pkcs12_identity_file: Some(PathBuf::from(pfx)),
1153
            pkcs12_passphrase: Some(passphrase.into()),
1154
            pem_cert_file: None,
1155
        };
1156
        let client = new_reqwest_client(
1157
            false,
1158
            Some(identity),
1159
            None,
1160
            Duration::seconds(5),
1161
            Duration::seconds(5),
1162
            ClientMetaInformation {
1163
                app_name: "test-client".into(),
1164
                instance_id: "test-pkcs12".into(),
1165
                connection_id: "test-pkcs12".into(),
1166
            },
1167
        );
1168
        assert!(client.is_err());
1169
    }
1170

1171
    #[test]
1172
    pub fn can_instantiate_pkcs_8_client() {
1173
        let key = "./testdata/pkcs8/snakeoil.key.pem";
1174
        let cert = "./testdata/pkcs8/snakeoil.crt";
1175
        let identity = ClientIdentity {
1176
            pkcs8_client_certificate_file: Some(cert.into()),
1177
            pkcs8_client_key_file: Some(key.into()),
1178
            pkcs12_identity_file: None,
1179
            pkcs12_passphrase: None,
1180
            pem_cert_file: None,
1181
        };
1182
        let client = new_reqwest_client(
1183
            false,
1184
            Some(identity),
1185
            None,
1186
            Duration::seconds(5),
1187
            Duration::seconds(5),
1188
            ClientMetaInformation {
1189
                app_name: "test-client".into(),
1190
                instance_id: "test-pkcs8".into(),
1191
                connection_id: "test-pkcs8".into(),
1192
            },
1193
        );
1194
        assert!(client.is_ok());
1195
    }
1196

1197
    #[test]
1198
    pub fn can_instantiate_pem_client() {
1199
        let cert = "./testdata/pem/keyStore.pem";
1200
        let identity = ClientIdentity {
1201
            pkcs8_client_certificate_file: None,
1202
            pkcs8_client_key_file: None,
1203
            pkcs12_identity_file: None,
1204
            pkcs12_passphrase: None,
1205
            pem_cert_file: Some(cert.into()),
1206
        };
1207
        let client = new_reqwest_client(
1208
            false,
1209
            Some(identity),
1210
            None,
1211
            Duration::seconds(5),
1212
            Duration::seconds(5),
1213
            ClientMetaInformation {
1214
                app_name: "test-client".into(),
1215
                instance_id: "test-pkcs8".into(),
1216
                connection_id: "test-pkcs8".into(),
1217
            },
1218
        );
1219
        assert!(client.is_ok());
1220
    }
1221
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc