mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-22 15:42:20 +03:00
Add certificate in HTTP response
This commit is contained in:
parent
381cc5f142
commit
b6b27c9562
@ -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)]
|
||||
pub struct Certificate {
|
||||
@ -26,3 +28,160 @@ pub struct Certificate {
|
||||
pub expire_date: DateTime<Utc>,
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ use super::request::*;
|
||||
use super::request_spec::*;
|
||||
use super::response::*;
|
||||
use super::{Header, HttpError, Verbosity};
|
||||
use crate::http::certificate::Certificate;
|
||||
use crate::http::{easy_ext, ContextDir};
|
||||
use crate::util::logger::Logger;
|
||||
use base64::engine::general_purpose;
|
||||
@ -314,8 +315,17 @@ impl Client {
|
||||
let headers = self.parse_response_headers(&response_headers);
|
||||
let duration = start.elapsed();
|
||||
let length = response_body.len();
|
||||
let certificate = None;
|
||||
let _certinfo = easy_ext::get_certinfo(&self.handle)?;
|
||||
let certificate = if let Some(cert_info) = 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();
|
||||
|
||||
let request = Request {
|
||||
|
@ -23,6 +23,7 @@ use std::ptr;
|
||||
|
||||
/// Represents certificate information.
|
||||
/// `data` has format "name:content";
|
||||
#[derive(Clone)]
|
||||
pub struct CertInfo {
|
||||
pub data: Vec<String>,
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
pub use self::certificate::Certificate;
|
||||
pub use self::client::Client;
|
||||
pub use self::context_dir::ContextDir;
|
||||
pub use self::cookie::{CookieAttribute, ResponseCookie};
|
||||
|
@ -16,10 +16,11 @@
|
||||
*
|
||||
*/
|
||||
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::util::logger;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
impl HurlResult {
|
||||
/// Serializes an [`HurlResult`] to a JSON representation.
|
||||
@ -125,6 +126,9 @@ impl Response {
|
||||
map.insert("headers".to_string(), headers);
|
||||
let cookies = self.cookies().iter().map(|e| e.to_json()).collect();
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
fn to_json(&self) -> serde_json::Value {
|
||||
let mut map = serde_json::Map::new();
|
||||
@ -303,3 +328,7 @@ impl Cookie {
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
fn json_date(value: DateTime<Utc>) -> serde_json::Value {
|
||||
serde_json::Value::String(value.to_string())
|
||||
}
|
||||
|
@ -613,6 +613,45 @@ fn test_cacert() {
|
||||
let request_spec = default_get_request("https://localhost:8001/hello");
|
||||
let (_, response) = client.execute(&request_spec, &options, &logger).unwrap();
|
||||
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]
|
||||
|
Loading…
Reference in New Issue
Block a user