Add certificate in HTTP response

This commit is contained in:
Fabrice Reix 2023-02-24 22:47:17 +01:00 committed by jcamiel
parent 381cc5f142
commit b6b27c9562
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
6 changed files with 243 additions and 4 deletions

View File

@ -16,7 +16,9 @@
* *
*/ */
use chrono::{DateTime, Utc}; use crate::http::easy_ext::CertInfo;
use chrono::{DateTime, NaiveDateTime, Utc};
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Certificate { pub struct Certificate {
@ -26,3 +28,160 @@ pub struct Certificate {
pub expire_date: DateTime<Utc>, pub expire_date: DateTime<Utc>,
pub serial_number: String, pub serial_number: String,
} }
impl TryFrom<CertInfo> for Certificate {
type Error = String;
/// parse `cert_info`
/// support different "formats" in cert info
/// - attribute name: "Start date" vs "Start Date"
/// - date format: "Jan 10 08:29:52 2023 GMT" vs "2023-01-10 08:29:52 GMT"
fn try_from(cert_info: CertInfo) -> Result<Self, Self::Error> {
let attributes = parse_attributes(&cert_info.data);
let subject = parse_subject(&attributes)?;
let issuer = parse_issuer(&attributes)?;
let start_date = parse_start_date(&attributes)?;
let expire_date = parse_expire_date(&attributes)?;
let serial_number = parse_serial_number(&attributes)?;
Ok(Certificate {
subject,
issuer,
start_date,
expire_date,
serial_number,
})
}
}
fn parse_subject(attributes: &HashMap<String, String>) -> Result<String, String> {
attributes
.get("subject")
.cloned()
.ok_or(format!("missing Subject attribute in {attributes:?}"))
}
fn parse_issuer(attributes: &HashMap<String, String>) -> Result<String, String> {
attributes
.get("issuer")
.cloned()
.ok_or(format!("missing issuer attribute in {attributes:?}"))
}
fn parse_start_date(attributes: &HashMap<String, String>) -> Result<DateTime<Utc>, String> {
match attributes.get("start date") {
None => Err(format!("missing start date attribute in {attributes:?}")),
Some(value) => Ok(parse_date(value)?),
}
}
fn parse_expire_date(attributes: &HashMap<String, String>) -> Result<DateTime<Utc>, String> {
match attributes.get("expire date") {
None => Err("missing expire date attribute".to_string()),
Some(value) => Ok(parse_date(value)?),
}
}
fn parse_date(value: &str) -> Result<DateTime<Utc>, String> {
let naive_date_time = match NaiveDateTime::parse_from_str(value, "%b %d %H:%M:%S %Y GMT") {
Ok(d) => d,
Err(_) => NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S GMT")
.map_err(|_| format!("can not parse date <{value}>"))?,
};
Ok(naive_date_time.and_local_timezone(Utc).unwrap())
}
fn parse_serial_number(attributes: &HashMap<String, String>) -> Result<String, String> {
attributes
.get("serial number")
.cloned()
.ok_or(format!("Missing serial number attribute in {attributes:?}"))
}
fn parse_attributes(data: &Vec<String>) -> HashMap<String, String> {
let mut map = HashMap::new();
for s in data {
if let Some((name, value)) = parse_attribute(s) {
map.insert(name.to_lowercase(), value);
}
}
map
}
fn parse_attribute(s: &str) -> Option<(String, String)> {
if let Some(index) = s.find(':') {
let (name, value) = s.split_at(index);
Some((name.to_string(), value[1..].to_string()))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::certificate::Certificate;
use crate::http::easy_ext::CertInfo;
#[test]
fn test_parse_start_date() {
let mut attributes = HashMap::new();
attributes.insert(
"start date".to_string(),
"Jan 10 08:29:52 2023 GMT".to_string(),
);
assert_eq!(
parse_start_date(&attributes).unwrap(),
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&chrono::Utc)
);
let mut attributes = HashMap::new();
attributes.insert(
"start date".to_string(),
"2023-01-10 08:29:52 GMT".to_string(),
);
assert_eq!(
parse_start_date(&attributes).unwrap(),
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&chrono::Utc)
)
}
#[test]
fn test_try_from() {
assert_eq!(
Certificate::try_from(CertInfo {
data: vec![
"Subject:C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost"
.to_string(),
"Issuer:C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost"
.to_string(),
"Serial Number:1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string(),
"Start date:Jan 10 08:29:52 2023 GMT".to_string(),
"Expire date:Oct 30 08:29:52 2025 GMT".to_string(),
]
})
.unwrap(),
Certificate {
subject: "C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost"
.to_string(),
issuer: "C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost".to_string(),
start_date: chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&chrono::Utc),
expire_date: chrono::DateTime::parse_from_rfc2822("Thu, 30 Oct 2025 08:29:52 GMT")
.unwrap()
.with_timezone(&chrono::Utc),
serial_number: "1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string()
}
);
assert_eq!(
Certificate::try_from(CertInfo { data: vec![] })
.err()
.unwrap(),
"missing Subject attribute in {}".to_string()
);
}
}

View File

@ -29,6 +29,7 @@ use super::request::*;
use super::request_spec::*; use super::request_spec::*;
use super::response::*; use super::response::*;
use super::{Header, HttpError, Verbosity}; use super::{Header, HttpError, Verbosity};
use crate::http::certificate::Certificate;
use crate::http::{easy_ext, ContextDir}; use crate::http::{easy_ext, ContextDir};
use crate::util::logger::Logger; use crate::util::logger::Logger;
use base64::engine::general_purpose; use base64::engine::general_purpose;
@ -314,8 +315,17 @@ impl Client {
let headers = self.parse_response_headers(&response_headers); let headers = self.parse_response_headers(&response_headers);
let duration = start.elapsed(); let duration = start.elapsed();
let length = response_body.len(); let length = response_body.len();
let certificate = None; let certificate = if let Some(cert_info) = easy_ext::get_certinfo(&self.handle)? {
let _certinfo = easy_ext::get_certinfo(&self.handle)?; match Certificate::try_from(cert_info) {
Ok(value) => Some(value),
Err(message) => {
logger.error(format!("can not parse certificate - {message}").as_str());
None
}
}
} else {
None
};
self.handle.reset(); self.handle.reset();
let request = Request { let request = Request {

View File

@ -23,6 +23,7 @@ use std::ptr;
/// Represents certificate information. /// Represents certificate information.
/// `data` has format "name:content"; /// `data` has format "name:content";
#[derive(Clone)]
pub struct CertInfo { pub struct CertInfo {
pub data: Vec<String>, pub data: Vec<String>,
} }

View File

@ -16,6 +16,7 @@
* *
*/ */
pub use self::certificate::Certificate;
pub use self::client::Client; pub use self::client::Client;
pub use self::context_dir::ContextDir; pub use self::context_dir::ContextDir;
pub use self::cookie::{CookieAttribute, ResponseCookie}; pub use self::cookie::{CookieAttribute, ResponseCookie};

View File

@ -16,10 +16,11 @@
* *
*/ */
use crate::http::{ use crate::http::{
Cookie, Header, Param, Request, RequestCookie, Response, ResponseCookie, Version, Certificate, Cookie, Header, Param, Request, RequestCookie, Response, ResponseCookie, Version,
}; };
use crate::runner::{AssertResult, Call, CaptureResult, EntryResult, HurlResult}; use crate::runner::{AssertResult, Call, CaptureResult, EntryResult, HurlResult};
use crate::util::logger; use crate::util::logger;
use chrono::{DateTime, Utc};
impl HurlResult { impl HurlResult {
/// Serializes an [`HurlResult`] to a JSON representation. /// Serializes an [`HurlResult`] to a JSON representation.
@ -125,6 +126,9 @@ impl Response {
map.insert("headers".to_string(), headers); map.insert("headers".to_string(), headers);
let cookies = self.cookies().iter().map(|e| e.to_json()).collect(); let cookies = self.cookies().iter().map(|e| e.to_json()).collect();
map.insert("cookies".to_string(), serde_json::Value::Array(cookies)); map.insert("cookies".to_string(), serde_json::Value::Array(cookies));
if let Some(certificate) = &self.certificate {
map.insert("certificate".to_string(), certificate.to_json());
}
serde_json::Value::Object(map) serde_json::Value::Object(map)
} }
} }
@ -237,6 +241,27 @@ impl ResponseCookie {
} }
} }
impl Certificate {
fn to_json(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert(
"subject".to_string(),
serde_json::Value::String(self.subject.clone()),
);
map.insert(
"issue".to_string(),
serde_json::Value::String(self.issuer.clone()),
);
map.insert("start_date".to_string(), json_date(self.start_date));
map.insert("expire_date".to_string(), json_date(self.expire_date));
map.insert(
"serial_number".to_string(),
serde_json::Value::String(self.serial_number.clone()),
);
serde_json::Value::Object(map)
}
}
impl CaptureResult { impl CaptureResult {
fn to_json(&self) -> serde_json::Value { fn to_json(&self) -> serde_json::Value {
let mut map = serde_json::Map::new(); let mut map = serde_json::Map::new();
@ -303,3 +328,7 @@ impl Cookie {
serde_json::Value::Object(map) serde_json::Value::Object(map)
} }
} }
fn json_date(value: DateTime<Utc>) -> serde_json::Value {
serde_json::Value::String(value.to_string())
}

View File

@ -613,6 +613,45 @@ fn test_cacert() {
let request_spec = default_get_request("https://localhost:8001/hello"); let request_spec = default_get_request("https://localhost:8001/hello");
let (_, response) = client.execute(&request_spec, &options, &logger).unwrap(); let (_, response) = client.execute(&request_spec, &options, &logger).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
let certificate = response.certificate.unwrap();
let issuer = certificate.issuer;
let issuers = [
"C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost".to_string(),
"C=US, ST=Denial, L=Springfield, O=Dis, CN=localhost".to_string(),
];
assert!(issuers.contains(&issuer), "actual issuer is {issuer}");
let subject = certificate.subject;
let subjects = [
"C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost".to_string(),
"C=US, ST=Denial, L=Springfield, O=Dis, CN=localhost".to_string(),
];
assert!(subjects.contains(&subject), "actual subject is {subject}");
assert_eq!(
certificate.start_date,
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&chrono::Utc)
);
assert_eq!(
certificate.expire_date,
chrono::DateTime::parse_from_rfc2822("Thu, 30 Oct 2025 08:29:52 GMT")
.unwrap()
.with_timezone(&chrono::Utc)
);
let serial_number = certificate.serial_number;
let serial_numbers = [
"1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string(),
"1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0:".to_string(),
];
assert!(
serial_numbers.contains(&serial_number),
"actual serial_number is {serial_number}"
);
} }
#[test] #[test]