use super::{
Error, HttpDetectorModel, Model, ModelListParams, ResultValue, Schema, SchemaAllowCreate,
SchemaAllowEdit, SchemaOption, SchemaOptionValue, SchemaType, SchemaView, SqlxSnafu,
format_datetime,
};
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use sqlx::FromRow;
use sqlx::{Pool, Postgres, QueryBuilder};
use std::collections::HashMap;
use time::PrimitiveDateTime;
type Result<T> = std::result::Result<T, Error>;
#[derive(FromRow)]
struct HttpStatSchema {
id: i64,
target_id: i64,
target_name: String,
url: String,
dns_lookup: i32,
quic_connect: i32,
tcp_connect: i32,
tls_handshake: i32,
server_processing: i32,
content_transfer: i32,
total: i32,
addr: String,
status_code: i32,
tls: String,
alpn: String,
subject: String,
issuer: String,
cert_not_before: String,
cert_not_after: String,
cert_cipher: String,
cert_domains: String,
body_size: i32,
error: String,
result: i16,
remark: String,
region: String,
created: PrimitiveDateTime,
modified: PrimitiveDateTime,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone)]
pub struct HttpStat {
pub id: i64,
pub target_id: i64,
pub target_name: String,
pub url: String,
pub dns_lookup: i32,
pub quic_connect: i32,
pub tcp_connect: i32,
pub tls_handshake: i32,
pub server_processing: i32,
pub content_transfer: i32,
pub total: i32,
pub addr: String,
pub status_code: i32,
pub tls: String,
pub alpn: String,
pub subject: String,
pub issuer: String,
pub cert_not_before: String,
pub cert_not_after: String,
pub cert_cipher: String,
pub cert_domains: Vec<String>,
pub body_size: i32,
pub region: String,
pub error: String,
pub result: i16,
pub remark: String,
pub created: String,
pub modified: String,
}
impl From<HttpStatSchema> for HttpStat {
fn from(schema: HttpStatSchema) -> Self {
Self {
id: schema.id,
target_id: schema.target_id,
target_name: schema.target_name,
url: schema.url,
dns_lookup: schema.dns_lookup,
quic_connect: schema.quic_connect,
tcp_connect: schema.tcp_connect,
tls_handshake: schema.tls_handshake,
server_processing: schema.server_processing,
content_transfer: schema.content_transfer,
total: schema.total,
addr: schema.addr,
status_code: schema.status_code,
tls: schema.tls,
alpn: schema.alpn,
subject: schema.subject,
issuer: schema.issuer,
cert_not_before: schema.cert_not_before,
cert_not_after: schema.cert_not_after,
cert_cipher: schema.cert_cipher,
cert_domains: schema
.cert_domains
.split(',')
.map(|s| s.to_string())
.collect(),
body_size: schema.body_size,
region: schema.region,
error: schema.error,
result: schema.result,
remark: schema.remark,
created: format_datetime(schema.created),
modified: format_datetime(schema.modified),
}
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct HttpStatInsertParams {
pub target_id: i64,
pub target_name: String,
pub url: String,
pub dns_lookup: Option<i32>,
pub quic_connect: Option<i32>,
pub tcp_connect: Option<i32>,
pub tls_handshake: Option<i32>,
pub server_processing: Option<i32>,
pub content_transfer: Option<i32>,
pub total: Option<i32>,
pub addr: String,
pub status_code: Option<i16>,
pub tls: Option<String>,
pub alpn: Option<String>,
pub subject: Option<String>,
pub issuer: Option<String>,
pub cert_not_before: Option<String>,
pub cert_not_after: Option<String>,
pub cert_cipher: Option<String>,
pub cert_domains: Option<String>,
pub body_size: Option<i32>,
pub region: String,
pub error: Option<String>,
pub result: i16,
pub remark: String,
}
pub struct HttpStatModel {}
impl HttpStatModel {
pub async fn add_stat(
&self,
pool: &Pool<Postgres>,
params: HttpStatInsertParams,
) -> Result<u64> {
let row: (i64,) = sqlx::query_as(
r#"INSERT INTO http_stats (target_id, target_name, url, dns_lookup, quic_connect, tcp_connect, tls_handshake, server_processing, content_transfer, total, addr, status_code, tls, alpn, subject, issuer, cert_not_before, cert_not_after, cert_cipher, cert_domains, body_size, region, error, result, remark) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25) RETURNING id"#,
)
.bind(params.target_id)
.bind(params.target_name)
.bind(params.url)
.bind(params.dns_lookup.unwrap_or(-1))
.bind(params.quic_connect.unwrap_or(-1))
.bind(params.tcp_connect.unwrap_or(-1))
.bind(params.tls_handshake.unwrap_or(-1))
.bind(params.server_processing.unwrap_or(-1))
.bind(params.content_transfer.unwrap_or(-1))
.bind(params.total.unwrap_or(-1))
.bind(params.addr)
.bind(params.status_code.unwrap_or(0))
.bind(params.tls.unwrap_or_default())
.bind(params.alpn.unwrap_or_default())
.bind(params.subject.unwrap_or_default())
.bind(params.issuer.unwrap_or_default())
.bind(params.cert_not_before.unwrap_or_default())
.bind(params.cert_not_after.unwrap_or_default())
.bind(params.cert_cipher.unwrap_or_default())
.bind(params.cert_domains.unwrap_or_default())
.bind(params.body_size.unwrap_or(-1))
.bind(params.region)
.bind(params.error.unwrap_or_default())
.bind(params.result)
.bind(params.remark)
.fetch_one(pool)
.await
.context(SqlxSnafu)?;
Ok(row.0 as u64)
}
pub async fn list_by_created(
&self,
pool: &Pool<Postgres>,
created_range: (&str, &str),
) -> Result<Vec<HttpStat>> {
let detectors = sqlx::query_as::<_, HttpStatSchema>(
r#"SELECT * FROM http_stats WHERE created >= $1 AND created <= $2"#,
)
.bind(created_range.0)
.bind(created_range.1)
.fetch_all(pool)
.await
.context(SqlxSnafu)?;
Ok(detectors.into_iter().map(|schema| schema.into()).collect())
}
}
impl Model for HttpStatModel {
type Output = HttpStat;
fn new() -> Self {
Self {}
}
fn keyword(&self) -> String {
"target_name".to_string()
}
async fn schema_view(&self, pool: &Pool<Postgres>) -> SchemaView {
let mut detector_options = vec![];
let detector_model = HttpDetectorModel {};
if let Ok(detectors) = detector_model.list_enabled(pool).await {
for detector in detectors {
detector_options.push(SchemaOption {
label: detector.name,
value: SchemaOptionValue::String(detector.id.to_string()),
});
}
detector_options.sort_by_key(|option| option.label.clone());
}
SchemaView {
schemas: vec![
Schema {
name: "target_id".to_string(),
category: SchemaType::String,
hidden: true,
filterable: !detector_options.is_empty(),
options: Some(detector_options),
..Default::default()
},
Schema {
name: "target_name".to_string(),
label: Some("name".to_string()),
category: SchemaType::String,
fixed: true,
..Default::default()
},
Schema {
name: "url".to_string(),
category: SchemaType::String,
max_width: Some(200),
..Default::default()
},
Schema {
name: "result".to_string(),
category: SchemaType::Result,
filterable: true,
options: Some(vec![
SchemaOption {
label: "Success".to_string(),
value: SchemaOptionValue::String(
(ResultValue::Success as u8).to_string(),
),
},
SchemaOption {
label: "Failed".to_string(),
value: SchemaOptionValue::String(
(ResultValue::Failed as u8).to_string(),
),
},
]),
..Default::default()
},
Schema {
name: "total".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
filterable: true,
options: Some(vec![
SchemaOption {
label: ">= 1s".to_string(),
value: SchemaOptionValue::String("1000".to_string()),
},
SchemaOption {
label: ">= 2s".to_string(),
value: SchemaOptionValue::String("2000".to_string()),
},
SchemaOption {
label: ">= 3s".to_string(),
value: SchemaOptionValue::String("3000".to_string()),
},
]),
..Default::default()
},
Schema {
name: "dns_lookup".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
..Default::default()
},
Schema {
name: "quic_connect".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
..Default::default()
},
Schema {
name: "tcp_connect".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
..Default::default()
},
Schema {
name: "tls_handshake".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
..Default::default()
},
Schema {
name: "server_processing".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
..Default::default()
},
Schema {
name: "content_transfer".to_string(),
category: SchemaType::Number,
hidden_values: vec!["-1".to_string()],
..Default::default()
},
Schema {
name: "timing".to_string(),
category: SchemaType::PopoverCard,
combinations: Some(vec![
"dns_lookup".to_string(),
"quic_connect".to_string(),
"tcp_connect".to_string(),
"tls_handshake".to_string(),
"server_processing".to_string(),
"content_transfer".to_string(),
"total".to_string(),
]),
..Default::default()
},
Schema {
name: "addr".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "status_code".to_string(),
category: SchemaType::Number,
hidden_values: vec!["0".to_string()],
..Default::default()
},
Schema {
name: "tls".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "alpn".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "subject".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "issuer".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "cert_not_before".to_string(),
category: SchemaType::Date,
..Default::default()
},
Schema {
name: "cert_not_after".to_string(),
category: SchemaType::Date,
..Default::default()
},
Schema {
name: "cert_cipher".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "cert_domains".to_string(),
category: SchemaType::Strings,
..Default::default()
},
Schema {
name: "body_size".to_string(),
category: SchemaType::ByteSize,
..Default::default()
},
Schema {
name: "error".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema {
name: "region".to_string(),
category: SchemaType::String,
..Default::default()
},
Schema::new_readonly_remark(),
Schema::new_created(),
Schema::new_filterable_modified(),
],
allow_edit: SchemaAllowEdit {
disabled: true,
..Default::default()
},
allow_create: SchemaAllowCreate {
disabled: true,
..Default::default()
},
}
}
fn push_filter_conditions<'args>(
&self,
qb: &mut QueryBuilder<'args, Postgres>,
filters: &HashMap<String, String>,
) -> Result<()> {
if let Some(result) = filters.get("result").and_then(|s| s.parse::<i16>().ok()) {
qb.push(" AND result = ");
qb.push_bind(result);
}
if let Some(target_id) = filters.get("target_id").and_then(|s| s.parse::<i64>().ok()) {
qb.push(" AND target_id = ");
qb.push_bind(target_id);
}
if let Some(total) = filters.get("total").and_then(|s| s.parse::<i32>().ok()) {
qb.push(" AND total >= ");
qb.push_bind(total);
}
Ok(())
}
async fn list(
&self,
pool: &Pool<Postgres>,
params: &ModelListParams,
) -> Result<Vec<Self::Output>> {
let mut qb = QueryBuilder::new("SELECT * FROM http_stats");
self.push_conditions(&mut qb, params)?;
params.push_pagination(&mut qb);
let stats = qb
.build_query_as::<HttpStatSchema>()
.fetch_all(pool)
.await
.context(SqlxSnafu)?;
Ok(stats.into_iter().map(|s| s.into()).collect())
}
async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM http_stats");
self.push_conditions(&mut qb, params)?;
let count = qb
.build_query_scalar::<i64>()
.fetch_one(pool)
.await
.context(SqlxSnafu)?;
Ok(count)
}
async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
let stat = sqlx::query_as::<_, HttpStatSchema>(r#"SELECT * FROM http_stats WHERE id = $1"#)
.bind(id as i64)
.fetch_optional(pool)
.await
.context(SqlxSnafu)?;
Ok(stat.map(|schema| schema.into()))
}
}