mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-26 11:43:08 +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)]
|
#[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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
@ -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};
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
@ -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]
|
||||||
|
Loading…
Reference in New Issue
Block a user