Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ssl capture switch and ssl capture rule #18

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions crates/lynx-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -9,10 +9,7 @@ use lynx_core::server::{Server, ServerConfig};
use lynx_core::server_context::set_up_context;
use tracing::{info, Level};
use tracing_subscriber::filter;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{
filter::FilterFn, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
2 changes: 1 addition & 1 deletion crates/lynx-core/examples/localhost_ip.rs
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ use local_ip_address::list_afinet_netifas;
fn main() {
let network_interfaces = list_afinet_netifas().unwrap();

let result: Vec<IpAddr> = network_interfaces
let _result: Vec<IpAddr> = network_interfaces
.into_iter()
.filter(|x| x.1.is_ipv4())
.map(|x| x.1)
6 changes: 4 additions & 2 deletions crates/lynx-core/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use std::{default, fs, path::PathBuf};
use std::{fs, path::PathBuf};

use derive_builder::Builder;
use tracing::debug;

use include_dir::{include_dir, Dir};

#[derive(Builder, Debug, Default, Clone)]
pub struct AppConfig {
@@ -68,3 +67,6 @@ pub fn create_dir_if_not_exists(dir: &PathBuf) {
}
debug!("dir {} exists", &dir.to_string_lossy());
}

pub const REQ_DIR: &str = "req";
pub const RES_DIR: &str = "res";
38 changes: 37 additions & 1 deletion crates/lynx-core/src/entities/app_config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4

use anyhow::{anyhow, Result};
use schemars::JsonSchema;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -12,8 +13,10 @@ use crate::server_context::DB;
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub capture_https: bool,
pub recording_status: RecordingStatus,
#[serde(rename = "captureSSL")]
pub capture_ssl: bool,
pub ssl_config: Option<Json>,
}

#[derive(
@@ -41,3 +44,36 @@ pub async fn get_app_config() -> Model {
.expect("app config not found")
.expect("app config not found")
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SSLConfigRule {
pub switch: bool,
pub host: String,
pub port: Option<u16>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SSLConfig {
pub include_domains: Vec<SSLConfigRule>,
pub exclude_domains: Vec<SSLConfigRule>,
}

pub async fn get_enabled_ssl_config() -> Result<(Vec<SSLConfigRule>, Vec<SSLConfigRule>)> {
let app_config = get_app_config().await;
match app_config.ssl_config {
None => Ok((vec![], vec![])),
Some(ssl_config) => {
let ssl_config: SSLConfig = serde_json::from_value(ssl_config)
.map_err(|e| anyhow!(e).context("parse ssl config error"))?;
let SSLConfig {
include_domains,
exclude_domains,
} = ssl_config;
let include = include_domains.into_iter().filter(|x| x.switch).collect();
let exclude = exclude_domains.into_iter().filter(|x| x.switch).collect();
Ok((include, exclude))
}
}
}
6 changes: 4 additions & 2 deletions crates/lynx-core/src/entities/request.rs
Original file line number Diff line number Diff line change
@@ -16,10 +16,12 @@ pub struct Model {
pub schema: String,
pub version: String,
pub status_code: u16,
pub header: Json,
pub header_size: u32,
pub header: Option<Json>,
pub header_size: Option<u32>,
}



#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::response::Entity")]
1 change: 0 additions & 1 deletion crates/lynx-core/src/entities/rule_content.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4
use anyhow::anyhow;
use async_trait::async_trait;
use glob_match::glob_match_with_captures;
use schemars::{schema_for, JsonSchema};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
Original file line number Diff line number Diff line change
@@ -54,14 +54,30 @@ impl MigrationTrait for Migration {
.table(AppConfig::Table)
.if_not_exists()
.col(pk_auto(AppConfig::Id))
.col(boolean(AppConfig::CaptureHttps))
.col(boolean(AppConfig::CaptureSSL))
.col(string(AppConfig::RecordingStatus))
.col(json_null(AppConfig::SSLConfig))
.to_owned(),
)
.await?;

let _ = manager
.create_table(
Table::create()
.table(SSLConfig::Table)
.if_not_exists()
.col(pk_auto(SSLConfig::Id))
.col(string(SSLConfig::Kind))
.col(boolean(SSLConfig::Switch))
.col(string(SSLConfig::Domain))
.col(integer_null(SSLConfig::Port))
.to_owned(),
)
.await?;

let insert = Query::insert()
.into_table(AppConfig::Table)
.columns([AppConfig::CaptureHttps, AppConfig::RecordingStatus])
.columns([AppConfig::CaptureSSL, AppConfig::RecordingStatus])
.values_panic([true.into(), RecordingStatus::StartRecording.into()])
.to_owned();
let _ = manager.exec_stmt(insert).await?;
@@ -78,8 +94,8 @@ impl MigrationTrait for Migration {
.col(string(Request::Schema))
.col(string(Request::Version))
.col(integer_null(Request::StatusCode))
.col(json(Request::Header))
.col(integer(Request::HeaderSize))
.col(json_null(Request::Header))
.col(integer_null(Request::HeaderSize))
.to_owned(),
)
.await?;
@@ -124,7 +140,7 @@ impl MigrationTrait for Migration {
.insert(connect)
.await?;

let rule_content = rule_content::ActiveModel {
let _ = rule_content::ActiveModel {
rule_id: ActiveValue::set(rule.id),
content: ActiveValue::set(json!({
"demo": "demo"
@@ -161,8 +177,19 @@ impl MigrationTrait for Migration {
enum AppConfig {
Table,
Id,
CaptureHttps,
RecordingStatus,
CaptureSSL,
SSLConfig,
}

#[derive(DeriveIden)]
enum SSLConfig {
Table,
Id,
Kind,
Switch,
Domain,
Port,
}

#[derive(DeriveIden)]
@@ -176,6 +203,7 @@ enum RuleGroup {
}

#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Rule {
Table,
Id,
3 changes: 1 addition & 2 deletions crates/lynx-core/src/plugins/http_request_plugin.rs
Original file line number Diff line number Diff line change
@@ -26,7 +26,6 @@ use tracing::{error, trace, warn};

use crate::entities::rule_content::{self, parse_rule_content};
use crate::proxy_log::body_write_to_file::{req_body_file, res_body_file};
use crate::proxy_log::has_receiver;
use crate::schedular::get_req_trace_id;
use crate::server_context::DB;

@@ -63,7 +62,7 @@ pub async fn build_proxy_request(
}
});

let req_url = url::Url::parse(&parts.uri.to_string().as_str()).unwrap();
let req_url = url::Url::parse(parts.uri.to_string().as_str()).unwrap();
let mut builder = hyper::Request::builder().method(parts.method);

let db = DB.get().unwrap();
6 changes: 3 additions & 3 deletions crates/lynx-core/src/proxy/http_proxy.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ use http_body_util::combinators::BoxBody;
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
use sea_orm::{ActiveModelTrait, Set};
use tracing::{debug, info, trace};
use tracing::{info, trace};

use crate::entities::app_config::{get_app_config, RecordingStatus};
use crate::entities::request::{self};
@@ -35,8 +35,8 @@ pub async fn proxy_http_request(req: Request<Incoming>) -> Result<Response<BoxBo
method: Set(req.method().to_string()),
schema: Set(req.uri().scheme_str().unwrap_or("").to_string()),
version: Set(format!("{:?}", req.version())),
header: Set(headers),
header_size: Set(header_size as u32),
header: Set(Some(headers)),
header_size: Set(Some(header_size as u32)),
..Default::default()
};

4 changes: 2 additions & 2 deletions crates/lynx-core/src/proxy/https_proxy.rs
Original file line number Diff line number Diff line change
@@ -54,13 +54,13 @@ pub async fn https_proxy(
info!("proxying http request {:?}", req);
req.extensions_mut().insert(Arc::new(nanoid!()));
let res = proxy_http_request(req).await;
return match res {
match res {
Ok(res) => Ok::<_, hyper::Error>(res),
Err(err) => {
error!("proxy http request error: {}", err.to_string());
Ok(Response::new(empty()))
}
};
}
});
if let Err(err) =
hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
2 changes: 1 addition & 1 deletion crates/lynx-core/src/proxy_log/body_write_to_file.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use tokio::fs::File;
use tracing::{info, trace};
use tracing::trace;

use std::fmt;

54 changes: 47 additions & 7 deletions crates/lynx-core/src/schedular.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
use std::sync::Arc;

use anyhow::{Error, Result};
use glob_match::glob_match;
use http::header::CONTENT_TYPE;
use http::status;
use http_body_util::combinators::BoxBody;
use hyper::body::Bytes;
use hyper::body::{Bytes, Incoming};
use hyper::{Method, Request, Response};
use nanoid::nanoid;
use sea_orm::EntityTrait;
use tracing::{debug, info, trace};

use crate::entities::app_config;
use crate::entities::app_config::{get_app_config, get_enabled_ssl_config, SSLConfigRule};
use crate::proxy::http_proxy::proxy_http_request;
use crate::proxy::https_proxy::https_proxy;
use crate::self_service::{handle_self_service, match_self_service};
use crate::server_context::DB;
use crate::tunnel_proxy::tunnel_proxy;
use crate::utils::{full, is_http};

@@ -25,6 +24,46 @@ pub fn get_req_trace_id(req: &Request<hyper::body::Incoming>) -> Arc<String> {
.expect("trace id not found")
}

pub async fn capture_ssl(req: &Request<Incoming>) -> Result<bool> {
let app_config = get_app_config().await;
if !app_config.capture_ssl {
return Ok(false);
}
let (include, exclude) = get_enabled_ssl_config().await?;

let uri = req.uri();

let host = uri.host();
let port = uri.port_u16();

let match_host = |config: &SSLConfigRule, host: &str, port: u16| -> bool {
let glob_match_host = glob_match(&config.host, host);
trace!(
"matching host: {:?} {:?} {:?}",
config.host,
host,
glob_match_host
);
if !glob_match_host {
return false;
}
if matches!(config.port, Some(p) if p != port) {
return false;
}
true
};

match (host, port) {
(Some(host), Some(port)) => {
let include = include.iter().any(|config| match_host(config, host, port));
let exclude = exclude.iter().any(|config| match_host(config, host, port));
trace!("capture ssl: {:?} {:?} {:?}", include, exclude, uri);
Ok(include && !exclude)
}
_ => Ok(false),
}
}

pub async fn dispatch(
mut req: Request<hyper::body::Incoming>,
) -> Result<Response<BoxBody<Bytes, Error>>> {
@@ -42,9 +81,7 @@ pub async fn dispatch(
return proxy_http_request(req).await;
}

let config = app_config::Entity::find().one(DB.get().unwrap()).await?;
trace!("app config {:?}", req);
if matches!(config.map(|c| c.capture_https), Some(true)) {
if capture_ssl(&req).await? {
// TODO: support websocket
// let is_websocket = hyper_tungstenite::is_upgrade_request(&req);
// if is_websocket {
@@ -67,3 +104,6 @@ pub async fn dispatch(
)))
.unwrap())
}

#[test]
fn global_() {}
1 change: 0 additions & 1 deletion crates/lynx-core/src/self_service/api/app_config_api.rs
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ use schemars::{schema_for, JsonSchema};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, Set};
use serde::{Deserialize, Serialize};


#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct ChangeRecordingStatusParams {
status: RecordingStatus,
Loading