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

Unleash / unleash-edge / 15549746691

10 Jun 2025 03:19AM UTC coverage: 78.199% (-0.06%) from 78.263%
15549746691

Pull #979

github

web-flow
Merge c5d3b00f3 into abe23e880
Pull Request #979: chore: release v19.11.1

10151 of 12981 relevant lines covered (78.2%)

158.04 hits per line

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

80.55
/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!(
42
        Opts::new(
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!(
50
        "client_feature_fetch",
51
        "Timings for fetching features in milliseconds",
52
        &["status_code", "app_name", "instance_id"],
53
        vec![
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!(
59
        "client_feature_delta_fetch",
60
        "Timings for fetching feature deltas in milliseconds",
61
        &["status_code", "app_name", "instance_id"],
62
        vec![
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!(
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]
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!(
82
        Opts::new(
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 {
63✔
116
        let connection_id = ulid::Ulid::new().to_string();
63✔
117
        Self {
63✔
118
            app_name: "unleash-edge".into(),
63✔
119
            instance_id: format!("unleash-edge@{}", connection_id.clone()),
63✔
120
            connection_id,
63✔
121
        }
63✔
122
    }
63✔
123
}
124

125
impl ClientMetaInformation {
126
    pub fn test_config() -> Self {
23✔
127
        Self {
23✔
128
            app_name: "test-app-name".into(),
23✔
129
            instance_id: "test-instance-id".into(),
23✔
130
            connection_id: "test-connection-id".into(),
23✔
131
        }
23✔
132
    }
23✔
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> {
2✔
145
    let p12_file = fs::read(id.pkcs12_identity_file.clone().unwrap()).map_err(|e| {
2✔
146
        EdgeError::ClientCertificateError(CertificateError::Pkcs12ArchiveNotFound(format!("{e:?}")))
×
147
    })?;
2✔
148
    let p12_keystore =
1✔
149
        p12_keystore::KeyStore::from_pkcs12(&p12_file, &id.pkcs12_passphrase.clone().unwrap())
2✔
150
            .map_err(|e| {
2✔
151
                EdgeError::ClientCertificateError(CertificateError::Pkcs12ParseError(format!(
1✔
152
                    "{e:?}"
1✔
153
                )))
1✔
154
            })?;
2✔
155
    let mut pem = vec![];
1✔
156
    for (alias, entry) in p12_keystore.entries() {
1✔
157
        debug!("P12 entry: {alias}");
1✔
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);
1✔
166
                pem.extend(key_pem.as_bytes());
1✔
167
                pem.push(0x0a); // Added new line
1✔
168
                for cert in chain.chain() {
1✔
169
                    let cert_pem = pkix::pem::der_to_pem(cert.as_der(), pkix::pem::PEM_CERTIFICATE);
1✔
170
                    pem.extend(cert_pem.as_bytes());
1✔
171
                    pem.push(0x0a); // Added new line
1✔
172
                }
1✔
173
            }
174
        }
175
    }
176

177
    Identity::from_pem(&pem).map_err(|e| {
1✔
178
        EdgeError::ClientCertificateError(CertificateError::Pkcs12X509Error(format!("{e:?}")))
×
179
    })
1✔
180
}
2✔
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| {
1✔
184
        EdgeError::ClientCertificateError(CertificateError::Pem8ClientCertNotFound(format!("{e:}")))
×
185
    })?;
1✔
186
    let key = File::open(id.pkcs8_client_key_file.clone().unwrap()).map_err(|e| {
1✔
187
        EdgeError::ClientCertificateError(CertificateError::Pem8ClientKeyNotFound(format!("{e:?}")))
×
188
    })?;
1✔
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);
1✔
193
    pem.push(0x0a);
1✔
194
    let _ = cert_reader.read_to_end(&mut pem);
1✔
195
    Ok(pem)
1✔
196
}
1✔
197

198
fn load_pkcs8(id: &ClientIdentity) -> EdgeResult<Identity> {
1✔
199
    Identity::from_pem(&load_pkcs8_identity(id)?).map_err(|e| {
1✔
200
        EdgeError::ClientCertificateError(CertificateError::Pem8IdentityGeneration(format!(
×
201
            "{e:?}"
×
202
        )))
×
203
    })
1✔
204
}
1✔
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"));
1✔
209
    let _ = pem_reader.read_to_end(&mut pem);
1✔
210
    Ok(pem)
1✔
211
}
1✔
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| {
1✔
215
        EdgeError::ClientCertificateError(CertificateError::Pem8IdentityGeneration(format!(
×
216
            "{e:?}"
×
217
        )))
×
218
    })
1✔
219
}
1✔
220

221
fn build_identity(tls: Option<ClientIdentity>) -> EdgeResult<ClientBuilder> {
46✔
222
    tls.map_or_else(
46✔
223
        || Ok(ClientBuilder::new().use_rustls_tls()),
46✔
224
        |tls| {
46✔
225
            let req_identity = if tls.pkcs12_identity_file.is_some() {
4✔
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)
1✔
230
            } else if tls.pem_cert_file.is_some() {
1✔
231
                load_pem(&tls)
1✔
232
            } else {
233
                Err(EdgeError::ClientCertificateError(
×
234
                    CertificateError::NoCertificateFiles,
×
235
                ))
×
236
            };
237
            req_identity.map(|id| ClientBuilder::new().use_rustls_tls().identity(id))
4✔
238
        },
46✔
239
    )
46✔
240
}
46✔
241

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

45✔
277
            client
45✔
278
                .user_agent(format!("unleash-edge-{}", crate::types::build::PKG_VERSION))
45✔
279
                .default_headers(header_map)
45✔
280
                .danger_accept_invalid_certs(skip_ssl_verification)
45✔
281
                .timeout(socket_timeout.to_std().unwrap())
45✔
282
                .connect_timeout(connect_timeout.to_std().unwrap())
45✔
283
                .build()
45✔
284
                .map_err(|e| EdgeError::ClientBuildError(format!("Failed to build client {e:?}")))
45✔
285
        })
46✔
286
}
46✔
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(
9✔
295
        server_url: Url,
9✔
296
        token_header: String,
9✔
297
        backing_client: Client,
9✔
298
        client_meta_information: ClientMetaInformation,
9✔
299
    ) -> Self {
9✔
300
        Self {
9✔
301
            urls: UnleashUrls::from_base_url(server_url),
9✔
302
            backing_client,
9✔
303
            custom_headers: Default::default(),
9✔
304
            token_header,
9✔
305
            meta_info: client_meta_information,
9✔
306
        }
9✔
307
    }
9✔
308

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

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

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

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

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

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

369
        client_req
23✔
370
    }
23✔
371

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

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

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

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

436
    pub async fn get_client_features(
23✔
437
        &self,
23✔
438
        request: ClientFeaturesRequest,
23✔
439
    ) -> EdgeResult<ClientFeaturesResponse> {
23✔
440
        let start_time = Utc::now();
23✔
441
        let response = self
23✔
442
            .client_features_req(request.clone())
23✔
443
            .send()
23✔
444
            .await
23✔
445
            .map_err(|e| {
23✔
446
                warn!("Failed to fetch. Due to [{e:?}] - Will retry");
1✔
447
                match e.status() {
1✔
448
                    Some(s) => EdgeError::ClientFeaturesFetchError(FeatureError::Retriable(s)),
×
449
                    None => EdgeError::ClientFeaturesFetchError(FeatureError::NotFound),
1✔
450
                }
451
            })?;
23✔
452
        let stop_time = Utc::now();
22✔
453
        CLIENT_FEATURE_FETCH
22✔
454
            .with_label_values(&[
22✔
455
                &response.status().as_u16().to_string(),
22✔
456
                &self.meta_info.app_name,
22✔
457
                &self.meta_info.instance_id,
22✔
458
            ])
22✔
459
            .observe(
22✔
460
                stop_time
22✔
461
                    .signed_duration_since(start_time)
22✔
462
                    .num_milliseconds() as f64,
22✔
463
            );
22✔
464
        if response.status() == StatusCode::NOT_MODIFIED {
22✔
465
            Ok(ClientFeaturesResponse::NoUpdate(
1✔
466
                request.etag.expect("Got NOT_MODIFIED without an ETag"),
1✔
467
            ))
1✔
468
        } else if response.status().is_success() {
21✔
469
            let etag = response
17✔
470
                .headers()
17✔
471
                .get("ETag")
17✔
472
                .or_else(|| response.headers().get("etag"))
17✔
473
                .and_then(|etag| EntityTag::from_str(etag.to_str().unwrap()).ok());
17✔
474
            let features = response.json::<ClientFeatures>().await.map_err(|e| {
17✔
475
                warn!("Could not parse features response to internal representation");
×
476
                EdgeError::ClientFeaturesParseError(e.to_string())
×
477
            })?;
17✔
478
            Ok(ClientFeaturesResponse::Updated(features, etag))
17✔
479
        } else if response.status() == StatusCode::FORBIDDEN {
4✔
480
            CLIENT_FEATURE_FETCH_FAILURES
4✔
481
                .with_label_values(&[
4✔
482
                    response.status().as_str(),
4✔
483
                    &self.meta_info.app_name,
4✔
484
                    &self.meta_info.instance_id,
4✔
485
                ])
4✔
486
                .inc();
4✔
487
            Err(EdgeError::ClientFeaturesFetchError(
4✔
488
                FeatureError::AccessDenied,
4✔
489
            ))
4✔
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
    }
23✔
532

533
    pub async fn get_client_features_delta(
7✔
534
        &self,
7✔
535
        request: ClientFeaturesRequest,
7✔
536
    ) -> EdgeResult<ClientFeaturesDeltaResponse> {
7✔
537
        let start_time = Utc::now();
7✔
538
        let response = self
7✔
539
            .client_features_delta_req(request.clone())
7✔
540
            .send()
7✔
541
            .await
7✔
542
            .map_err(|e| {
7✔
543
                warn!("Failed to fetch. Due to [{e:?}] - Will retry");
5✔
544
                match e.status() {
5✔
545
                    Some(s) => EdgeError::ClientFeaturesFetchError(FeatureError::Retriable(s)),
×
546
                    None => EdgeError::ClientFeaturesFetchError(FeatureError::NotFound),
5✔
547
                }
548
            })?;
7✔
549
        let stop_time = Utc::now();
2✔
550
        CLIENT_FEATURE_DELTA_FETCH
2✔
551
            .with_label_values(&[
2✔
552
                &response.status().as_u16().to_string(),
2✔
553
                &self.meta_info.app_name,
2✔
554
                &self.meta_info.instance_id,
2✔
555
            ])
2✔
556
            .observe(
2✔
557
                stop_time
2✔
558
                    .signed_duration_since(start_time)
2✔
559
                    .num_milliseconds() as f64,
2✔
560
            );
2✔
561
        if response.status() == StatusCode::NOT_MODIFIED {
2✔
562
            Ok(ClientFeaturesDeltaResponse::NoUpdate(
×
563
                request.etag.expect("Got NOT_MODIFIED without an ETag"),
×
564
            ))
×
565
        } else if response.status().is_success() {
2✔
566
            let etag = response
2✔
567
                .headers()
2✔
568
                .get("ETag")
2✔
569
                .or_else(|| response.headers().get("etag"))
2✔
570
                .and_then(|etag| EntityTag::from_str(etag.to_str().unwrap()).ok());
2✔
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
            })?;
2✔
575
            Ok(ClientFeaturesDeltaResponse::Updated(features, etag))
2✔
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
    }
7✔
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(
2✔
646
        &self,
2✔
647
        request: MetricsBatch,
2✔
648
        token: &str,
2✔
649
    ) -> EdgeResult<()> {
2✔
650
        trace!("Sending metrics to bulk endpoint");
2✔
651
        let started_at = Utc::now();
2✔
652
        let result = self
2✔
653
            .backing_client
2✔
654
            .post(self.urls.client_bulk_metrics_url.to_string())
2✔
655
            .headers(self.header_map(Some(token.to_string())))
2✔
656
            .json(&request)
2✔
657
            .send()
2✔
658
            .await
2✔
659
            .map_err(|e| {
2✔
660
                info!("Failed to send metrics to /api/client/metrics/bulk endpoint {e:?}");
×
661
                EdgeError::EdgeMetricsError
×
662
            })?;
2✔
663
        let ended = Utc::now();
2✔
664
        METRICS_UPLOAD
2✔
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
            ])
2✔
670
            .observe(ended.signed_duration_since(started_at).num_milliseconds() as f64);
2✔
671
        if result.status().is_success() {
2✔
672
            Ok(())
1✔
673
        } else {
674
            match result.status() {
1✔
675
                StatusCode::BAD_REQUEST => Err(EdgeMetricsRequestError(
676
                    result.status(),
×
677
                    result.json().await.ok(),
×
678
                )),
679
                _ => Err(EdgeMetricsRequestError(result.status(), None)),
1✔
680
            }
681
        }
682
    }
2✔
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(
13✔
731
        &self,
13✔
732
        request: ValidateTokensRequest,
13✔
733
    ) -> EdgeResult<Vec<EdgeToken>> {
13✔
734
        let check_api_suffix = || {
13✔
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
13✔
743
            .post(self.urls.edge_validate_url.to_string())
13✔
744
            .headers(self.header_map(None))
13✔
745
            .json(&request)
13✔
746
            .send()
13✔
747
            .await
13✔
748
            .map_err(|e| {
13✔
749
                info!("Failed to validate tokens: [{e:?}]");
1✔
750
                EdgeError::EdgeTokenError
1✔
751
            })?;
13✔
752
        match result.status() {
12✔
753
            StatusCode::OK => {
754
                let token_response = result.json::<EdgeTokens>().await.map_err(|e| {
12✔
755
                    warn!("Failed to parse validation response with error: {e:?}");
×
756
                    EdgeError::EdgeTokenParseError
×
757
                })?;
12✔
758
                Ok(token_response
12✔
759
                    .tokens
12✔
760
                    .into_iter()
12✔
761
                    .map(|t| {
12✔
762
                        let remaining_info =
12✔
763
                            EdgeToken::try_from(t.token.clone()).unwrap_or_else(|_| t.clone());
12✔
764
                        EdgeToken {
12✔
765
                            token: t.token.clone(),
12✔
766
                            token_type: t.token_type,
12✔
767
                            environment: t.environment.or(remaining_info.environment),
12✔
768
                            projects: t.projects,
12✔
769
                            status: TokenValidationStatus::Validated,
12✔
770
                        }
12✔
771
                    })
12✔
772
                    .collect())
12✔
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
    }
13✔
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 {
2✔
830
            Self {
2✔
831
                api_key,
2✔
832
                etag: etag.map(EntityTag::new_weak),
2✔
833
                interval: Some(15),
2✔
834
            }
2✔
835
        }
2✔
836
    }
837

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

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

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

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

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

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

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

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

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

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

979
    fn expected_etag(features: ClientFeatures) -> String {
2✔
980
        let hash = features.xx3_hash().unwrap();
2✔
981
        let len = serde_json::to_string(&features)
2✔
982
            .map(|string| string.len())
2✔
983
            .unwrap();
2✔
984
        format!("{len:x}-{hash}")
2✔
985
    }
2✔
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() {
1✔
1052
        let etag = EntityTag::from_str("W/\"b5e6-DPC/1RShRw1J/jtxvRtTo1jf4+o\"").unwrap();
1✔
1053
        assert!(etag.weak);
1✔
1054
    }
1✔
1055

1056
    #[test]
1057
    pub fn parse_entity_tag() {
1✔
1058
        let optimal_304_tag = EntityTag::from_str("\"76d8bb0e:2841\"");
1✔
1059
        assert!(optimal_304_tag.is_ok());
1✔
1060
    }
1✔
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() {
1✔
1121
        let pfx = "./testdata/pkcs12/snakeoil.pfx";
1✔
1122
        let passphrase = "password";
1✔
1123
        let identity = ClientIdentity {
1✔
1124
            pkcs8_client_certificate_file: None,
1✔
1125
            pkcs8_client_key_file: None,
1✔
1126
            pkcs12_identity_file: Some(PathBuf::from(pfx)),
1✔
1127
            pkcs12_passphrase: Some(passphrase.into()),
1✔
1128
            pem_cert_file: None,
1✔
1129
        };
1✔
1130
        let client = new_reqwest_client(
1✔
1131
            false,
1✔
1132
            Some(identity),
1✔
1133
            None,
1✔
1134
            Duration::seconds(5),
1✔
1135
            Duration::seconds(5),
1✔
1136
            ClientMetaInformation {
1✔
1137
                app_name: "test-client".into(),
1✔
1138
                instance_id: "test-pkcs12".into(),
1✔
1139
                connection_id: "test-pkcs12".into(),
1✔
1140
            },
1✔
1141
        );
1✔
1142
        assert!(client.is_ok());
1✔
1143
    }
1✔
1144

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

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

1197
    #[test]
1198
    pub fn can_instantiate_pem_client() {
1✔
1199
        let cert = "./testdata/pem/keyStore.pem";
1✔
1200
        let identity = ClientIdentity {
1✔
1201
            pkcs8_client_certificate_file: None,
1✔
1202
            pkcs8_client_key_file: None,
1✔
1203
            pkcs12_identity_file: None,
1✔
1204
            pkcs12_passphrase: None,
1✔
1205
            pem_cert_file: Some(cert.into()),
1✔
1206
        };
1✔
1207
        let client = new_reqwest_client(
1✔
1208
            false,
1✔
1209
            Some(identity),
1✔
1210
            None,
1✔
1211
            Duration::seconds(5),
1✔
1212
            Duration::seconds(5),
1✔
1213
            ClientMetaInformation {
1✔
1214
                app_name: "test-client".into(),
1✔
1215
                instance_id: "test-pkcs8".into(),
1✔
1216
                connection_id: "test-pkcs8".into(),
1✔
1217
            },
1✔
1218
        );
1✔
1219
        assert!(client.is_ok());
1✔
1220
    }
1✔
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

© 2025 Coveralls, Inc