1use super::{
16 Error, HttpDetectorModel, Model, ModelListParams, ResultValue, Schema, SchemaAllowCreate,
17 SchemaAllowEdit, SchemaOption, SchemaOptionValue, SchemaType, SchemaView, SqlxSnafu,
18 format_datetime,
19};
20use serde::{Deserialize, Serialize};
21use snafu::ResultExt;
22use sqlx::FromRow;
23use sqlx::{Pool, Postgres, QueryBuilder};
24use std::collections::HashMap;
25use time::PrimitiveDateTime;
26
27type Result<T> = std::result::Result<T, Error>;
28
29#[derive(FromRow)]
30struct HttpStatSchema {
31 id: i64,
32 target_id: i64,
33 target_name: String,
34 url: String,
35 dns_lookup: i32,
36 quic_connect: i32,
37 tcp_connect: i32,
38 tls_handshake: i32,
39 server_processing: i32,
40 content_transfer: i32,
41 total: i32,
42 addr: String,
43 status_code: i32,
44 tls: String,
45 alpn: String,
46 subject: String,
47 issuer: String,
48 cert_not_before: String,
49 cert_not_after: String,
50 cert_cipher: String,
51 cert_domains: String,
52 body_size: i32,
53 error: String,
54 result: i16,
55 remark: String,
56 region: String,
57 created: PrimitiveDateTime,
58 modified: PrimitiveDateTime,
59}
60
61#[derive(Default, Deserialize, Serialize, Debug, Clone)]
62pub struct HttpStat {
63 pub id: i64,
64 pub target_id: i64,
65 pub target_name: String,
66 pub url: String,
67 pub dns_lookup: i32,
68 pub quic_connect: i32,
69 pub tcp_connect: i32,
70 pub tls_handshake: i32,
71 pub server_processing: i32,
72 pub content_transfer: i32,
73 pub total: i32,
74 pub addr: String,
75 pub status_code: i32,
76 pub tls: String,
77 pub alpn: String,
78 pub subject: String,
79 pub issuer: String,
80 pub cert_not_before: String,
81 pub cert_not_after: String,
82 pub cert_cipher: String,
83 pub cert_domains: Vec<String>,
84 pub body_size: i32,
85 pub region: String,
86 pub error: String,
87 pub result: i16,
88 pub remark: String,
89 pub created: String,
90 pub modified: String,
91}
92
93impl From<HttpStatSchema> for HttpStat {
94 fn from(schema: HttpStatSchema) -> Self {
95 Self {
96 id: schema.id,
97 target_id: schema.target_id,
98 target_name: schema.target_name,
99 url: schema.url,
100 dns_lookup: schema.dns_lookup,
101 quic_connect: schema.quic_connect,
102 tcp_connect: schema.tcp_connect,
103 tls_handshake: schema.tls_handshake,
104 server_processing: schema.server_processing,
105 content_transfer: schema.content_transfer,
106 total: schema.total,
107 addr: schema.addr,
108 status_code: schema.status_code,
109 tls: schema.tls,
110 alpn: schema.alpn,
111 subject: schema.subject,
112 issuer: schema.issuer,
113 cert_not_before: schema.cert_not_before,
114 cert_not_after: schema.cert_not_after,
115 cert_cipher: schema.cert_cipher,
116 cert_domains: schema
117 .cert_domains
118 .split(',')
119 .map(|s| s.to_string())
120 .collect(),
121 body_size: schema.body_size,
122 region: schema.region,
123 error: schema.error,
124 result: schema.result,
125 remark: schema.remark,
126 created: format_datetime(schema.created),
127 modified: format_datetime(schema.modified),
128 }
129 }
130}
131
132#[derive(Debug, Deserialize, Serialize, Default)]
133
134pub struct HttpStatInsertParams {
135 pub target_id: i64,
136 pub target_name: String,
137 pub url: String,
138 pub dns_lookup: Option<i32>,
139 pub quic_connect: Option<i32>,
140 pub tcp_connect: Option<i32>,
141 pub tls_handshake: Option<i32>,
142 pub server_processing: Option<i32>,
143 pub content_transfer: Option<i32>,
144 pub total: Option<i32>,
145 pub addr: String,
146 pub status_code: Option<i16>,
147 pub tls: Option<String>,
148 pub alpn: Option<String>,
149 pub subject: Option<String>,
150 pub issuer: Option<String>,
151 pub cert_not_before: Option<String>,
152 pub cert_not_after: Option<String>,
153 pub cert_cipher: Option<String>,
154 pub cert_domains: Option<String>,
155 pub body_size: Option<i32>,
156 pub region: String,
157 pub error: Option<String>,
158 pub result: i16,
159 pub remark: String,
160}
161
162pub struct HttpStatModel {}
163
164impl HttpStatModel {
165 pub async fn add_stat(
166 &self,
167 pool: &Pool<Postgres>,
168 params: HttpStatInsertParams,
169 ) -> Result<u64> {
170 let row: (i64,) = sqlx::query_as(
171 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"#,
172 )
173 .bind(params.target_id)
174 .bind(params.target_name)
175 .bind(params.url)
176 .bind(params.dns_lookup.unwrap_or(-1))
177 .bind(params.quic_connect.unwrap_or(-1))
178 .bind(params.tcp_connect.unwrap_or(-1))
179 .bind(params.tls_handshake.unwrap_or(-1))
180 .bind(params.server_processing.unwrap_or(-1))
181 .bind(params.content_transfer.unwrap_or(-1))
182 .bind(params.total.unwrap_or(-1))
183 .bind(params.addr)
184 .bind(params.status_code.unwrap_or(0))
185 .bind(params.tls.unwrap_or_default())
186 .bind(params.alpn.unwrap_or_default())
187 .bind(params.subject.unwrap_or_default())
188 .bind(params.issuer.unwrap_or_default())
189 .bind(params.cert_not_before.unwrap_or_default())
190 .bind(params.cert_not_after.unwrap_or_default())
191 .bind(params.cert_cipher.unwrap_or_default())
192 .bind(params.cert_domains.unwrap_or_default())
193 .bind(params.body_size.unwrap_or(-1))
194 .bind(params.region)
195 .bind(params.error.unwrap_or_default())
196 .bind(params.result)
197 .bind(params.remark)
198 .fetch_one(pool)
199 .await
200 .context(SqlxSnafu)?;
201
202 Ok(row.0 as u64)
203 }
204 pub async fn list_by_created(
205 &self,
206 pool: &Pool<Postgres>,
207 created_range: (&str, &str),
208 ) -> Result<Vec<HttpStat>> {
209 let detectors = sqlx::query_as::<_, HttpStatSchema>(
210 r#"SELECT * FROM http_stats WHERE created >= $1 AND created <= $2"#,
211 )
212 .bind(created_range.0)
213 .bind(created_range.1)
214 .fetch_all(pool)
215 .await
216 .context(SqlxSnafu)?;
217 Ok(detectors.into_iter().map(|schema| schema.into()).collect())
218 }
219}
220
221impl Model for HttpStatModel {
222 type Output = HttpStat;
223 fn new() -> Self {
224 Self {}
225 }
226 fn keyword(&self) -> String {
227 "target_name".to_string()
228 }
229 async fn schema_view(&self, pool: &Pool<Postgres>) -> SchemaView {
230 let mut detector_options = vec![];
231 let detector_model = HttpDetectorModel {};
232 if let Ok(detectors) = detector_model.list_enabled(pool).await {
233 for detector in detectors {
234 detector_options.push(SchemaOption {
235 label: detector.name,
236 value: SchemaOptionValue::String(detector.id.to_string()),
237 });
238 }
239 detector_options.sort_by_key(|option| option.label.clone());
240 }
241 SchemaView {
242 schemas: vec![
243 Schema {
244 name: "target_id".to_string(),
245 category: SchemaType::String,
246 hidden: true,
247 filterable: !detector_options.is_empty(),
248 options: Some(detector_options),
249 ..Default::default()
250 },
251 Schema {
252 name: "target_name".to_string(),
253 label: Some("name".to_string()),
254 category: SchemaType::String,
255 fixed: true,
256 ..Default::default()
257 },
258 Schema {
259 name: "url".to_string(),
260 category: SchemaType::String,
261 max_width: Some(200),
262 ..Default::default()
263 },
264 Schema {
265 name: "result".to_string(),
266 category: SchemaType::Result,
267 filterable: true,
268 options: Some(vec![
269 SchemaOption {
270 label: "Success".to_string(),
271 value: SchemaOptionValue::String(
272 (ResultValue::Success as u8).to_string(),
273 ),
274 },
275 SchemaOption {
276 label: "Failed".to_string(),
277 value: SchemaOptionValue::String(
278 (ResultValue::Failed as u8).to_string(),
279 ),
280 },
281 ]),
282 ..Default::default()
283 },
284 Schema {
285 name: "total".to_string(),
286 category: SchemaType::Number,
287 hidden_values: vec!["-1".to_string()],
288 filterable: true,
289 options: Some(vec![
290 SchemaOption {
291 label: ">= 1s".to_string(),
292 value: SchemaOptionValue::String("1000".to_string()),
293 },
294 SchemaOption {
295 label: ">= 2s".to_string(),
296 value: SchemaOptionValue::String("2000".to_string()),
297 },
298 SchemaOption {
299 label: ">= 3s".to_string(),
300 value: SchemaOptionValue::String("3000".to_string()),
301 },
302 ]),
303 ..Default::default()
304 },
305 Schema {
306 name: "dns_lookup".to_string(),
307 category: SchemaType::Number,
308 hidden_values: vec!["-1".to_string()],
309 ..Default::default()
310 },
311 Schema {
312 name: "quic_connect".to_string(),
313 category: SchemaType::Number,
314 hidden_values: vec!["-1".to_string()],
315 ..Default::default()
316 },
317 Schema {
318 name: "tcp_connect".to_string(),
319 category: SchemaType::Number,
320 hidden_values: vec!["-1".to_string()],
321 ..Default::default()
322 },
323 Schema {
324 name: "tls_handshake".to_string(),
325 category: SchemaType::Number,
326 hidden_values: vec!["-1".to_string()],
327 ..Default::default()
328 },
329 Schema {
330 name: "server_processing".to_string(),
331 category: SchemaType::Number,
332 hidden_values: vec!["-1".to_string()],
333 ..Default::default()
334 },
335 Schema {
336 name: "content_transfer".to_string(),
337 category: SchemaType::Number,
338 hidden_values: vec!["-1".to_string()],
339 ..Default::default()
340 },
341 Schema {
342 name: "timing".to_string(),
343 category: SchemaType::PopoverCard,
344 combinations: Some(vec![
345 "dns_lookup".to_string(),
346 "quic_connect".to_string(),
347 "tcp_connect".to_string(),
348 "tls_handshake".to_string(),
349 "server_processing".to_string(),
350 "content_transfer".to_string(),
351 "total".to_string(),
352 ]),
353 ..Default::default()
354 },
355 Schema {
356 name: "addr".to_string(),
357 category: SchemaType::String,
358 ..Default::default()
359 },
360 Schema {
361 name: "status_code".to_string(),
362 category: SchemaType::Number,
363 hidden_values: vec!["0".to_string()],
364 ..Default::default()
365 },
366 Schema {
367 name: "tls".to_string(),
368 category: SchemaType::String,
369 ..Default::default()
370 },
371 Schema {
372 name: "alpn".to_string(),
373 category: SchemaType::String,
374 ..Default::default()
375 },
376 Schema {
377 name: "subject".to_string(),
378 category: SchemaType::String,
379 ..Default::default()
380 },
381 Schema {
382 name: "issuer".to_string(),
383 category: SchemaType::String,
384 ..Default::default()
385 },
386 Schema {
387 name: "cert_not_before".to_string(),
388 category: SchemaType::Date,
389 ..Default::default()
390 },
391 Schema {
392 name: "cert_not_after".to_string(),
393 category: SchemaType::Date,
394 ..Default::default()
395 },
396 Schema {
397 name: "cert_cipher".to_string(),
398 category: SchemaType::String,
399 ..Default::default()
400 },
401 Schema {
402 name: "cert_domains".to_string(),
403 category: SchemaType::Strings,
404 ..Default::default()
405 },
406 Schema {
407 name: "body_size".to_string(),
408 category: SchemaType::ByteSize,
409 ..Default::default()
410 },
411 Schema {
412 name: "error".to_string(),
413 category: SchemaType::String,
414 ..Default::default()
415 },
416 Schema {
417 name: "region".to_string(),
418 category: SchemaType::String,
419 ..Default::default()
420 },
421 Schema::new_readonly_remark(),
422 Schema::new_created(),
423 Schema::new_filterable_modified(),
424 ],
425 allow_edit: SchemaAllowEdit {
426 disabled: true,
427 ..Default::default()
428 },
429 allow_create: SchemaAllowCreate {
430 disabled: true,
431 ..Default::default()
432 },
433 }
434 }
435
436 fn push_filter_conditions<'args>(
437 &self,
438 qb: &mut QueryBuilder<'args, Postgres>,
439 filters: &HashMap<String, String>,
440 ) -> Result<()> {
441 if let Some(result) = filters.get("result").and_then(|s| s.parse::<i16>().ok()) {
442 qb.push(" AND result = ");
443 qb.push_bind(result);
444 }
445 if let Some(target_id) = filters.get("target_id").and_then(|s| s.parse::<i64>().ok()) {
446 qb.push(" AND target_id = ");
447 qb.push_bind(target_id);
448 }
449 if let Some(total) = filters.get("total").and_then(|s| s.parse::<i32>().ok()) {
450 qb.push(" AND total >= ");
451 qb.push_bind(total);
452 }
453 Ok(())
454 }
455
456 async fn list(
457 &self,
458 pool: &Pool<Postgres>,
459 params: &ModelListParams,
460 ) -> Result<Vec<Self::Output>> {
461 let mut qb = QueryBuilder::new("SELECT * FROM http_stats");
462 self.push_conditions(&mut qb, params)?;
463 params.push_pagination(&mut qb);
464 let stats = qb
465 .build_query_as::<HttpStatSchema>()
466 .fetch_all(pool)
467 .await
468 .context(SqlxSnafu)?;
469 Ok(stats.into_iter().map(|s| s.into()).collect())
470 }
471 async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
472 let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM http_stats");
473 self.push_conditions(&mut qb, params)?;
474 let count = qb
475 .build_query_scalar::<i64>()
476 .fetch_one(pool)
477 .await
478 .context(SqlxSnafu)?;
479 Ok(count)
480 }
481 async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
482 let stat = sqlx::query_as::<_, HttpStatSchema>(r#"SELECT * FROM http_stats WHERE id = $1"#)
483 .bind(id as i64)
484 .fetch_optional(pool)
485 .await
486 .context(SqlxSnafu)?;
487 Ok(stat.map(|schema| schema.into()))
488 }
489}