Skip to content

Commit 27304af

Browse files
committed
feat: subscription audit logging, Hysteria2 TLS, and cleanup fixes
- Add subscription_audit module with trace_id support via X-Trace-Id header - Log all subscription days changes to separate subscription_audit.log - Log subscription expiration and standalone connection expiration - Log referral bonuses with paid_days, bonus_days, and both sub_ids - Support negative days logging (add/subtract/noop direction) - Add Hysteria2 manual TLS certificate support (tls.cert / tls.key) - Fix cleanup_expired_connections to skip subscription-owned connections - Translate Russian comments to English - Fix peer_online_by_handshake dead code warning
1 parent e774bbf commit 27304af

15 files changed

Lines changed: 530 additions & 50 deletions

File tree

config-api-example.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ subscription_expire_interval = 600
4747
traffic_persist_interval_sec = 3600
4848
connection_expire_interval = 600
4949

50+
[subscription_audit]
51+
enabled = true
52+
directory = "logs/"
53+
file = "subscription_audit.log"
54+
rotation = "daily"
55+
level = "info"
56+
5057
[pg]
5158
host = "localhost"
5259
port = 5432

dev/h2.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
listen: :443
22

3+
# Option 1: automatic certificates via ACME
34
acme:
45
domains:
56
- api.example.org
67
email: admin@example.com
78

9+
# Option 2: provide your own certificate
10+
# tls:
11+
# cert: /path/to/cert.pem
12+
# key: /path/to/key.pem
13+
# host: api.example.org # required when ACME is not used
14+
815
auth:
916
type: http
1017
http:

src/bin/api/bootstrap.rs

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use openssl::pkey::PKey;
22
use std::sync::Arc;
33
use tokio::sync::RwLock;
4-
use tracing_subscriber::Layer;
54

65
use fcore::{
76
utils::level_from_settings, utils::measure_time, Connection, ConnectionApiOperations,
@@ -137,16 +136,23 @@ fn parse_level(level: &str) -> Option<tracing::Level> {
137136
}
138137

139138
pub fn init_tracing(settings: ServiceSettings) {
139+
use tracing_subscriber::Layer;
140+
140141
let level = level_from_settings(&settings.service.log_level);
141142

142-
let stdout_layer = fmt::layer()
143-
.with_target(true)
144-
.with_filter(level)
145-
.with_filter(filter_fn(|metadata| {
146-
!metadata.target().starts_with("metrics") && !metadata.target().starts_with("sqlx")
147-
}));
143+
let stdout_layer: Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync> = Box::new(
144+
fmt::layer()
145+
.with_target(true)
146+
.with_filter(level)
147+
.with_filter(filter_fn(|metadata| {
148+
!metadata.target().starts_with("metrics")
149+
&& !metadata.target().starts_with("subscription.audit")
150+
&& !metadata.target().starts_with("sqlx")
151+
})),
152+
);
148153

149-
let registry = tracing_subscriber::registry().with(stdout_layer);
154+
let mut layers: Vec<Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync>> =
155+
vec![stdout_layer];
150156

151157
if settings.metrics.log.enabled {
152158
let log_directory = settings.metrics.log.directory;
@@ -157,20 +163,42 @@ pub fn init_tracing(settings: ServiceSettings) {
157163

158164
let metrics_file = RollingFileAppender::new(rotation, log_directory, log_file);
159165

160-
let metrics_layer = fmt::layer()
161-
.with_ansi(false)
162-
.with_target(true)
163-
.with_writer(metrics_file)
164-
.with_filter(
165-
Targets::new()
166-
.with_target("metrics", metrics_level)
167-
.with_target("metrics.ingest", metrics_level)
168-
.with_target("metrics.gc", metrics_level)
169-
.with_target("metrics.heartbeat", metrics_level),
170-
);
171-
172-
registry.with(metrics_layer).init();
173-
} else {
174-
registry.init();
166+
let metrics_layer: Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync> = Box::new(
167+
fmt::layer()
168+
.with_ansi(false)
169+
.with_target(true)
170+
.with_writer(metrics_file)
171+
.with_filter(
172+
Targets::new()
173+
.with_target("metrics", metrics_level)
174+
.with_target("metrics.ingest", metrics_level)
175+
.with_target("metrics.gc", metrics_level)
176+
.with_target("metrics.heartbeat", metrics_level),
177+
),
178+
);
179+
180+
layers.push(metrics_layer);
175181
}
182+
183+
if settings.subscription_audit.enabled {
184+
let log_directory = settings.subscription_audit.directory;
185+
let log_file = settings.subscription_audit.file;
186+
let rotation = parse_rotation(&settings.subscription_audit.rotation);
187+
let audit_level = parse_level(&settings.subscription_audit.level)
188+
.unwrap_or(tracing::Level::INFO);
189+
190+
let audit_file = RollingFileAppender::new(rotation, log_directory, log_file);
191+
192+
let audit_layer: Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync> = Box::new(
193+
fmt::layer()
194+
.with_ansi(false)
195+
.with_target(true)
196+
.with_writer(audit_file)
197+
.with_filter(Targets::new().with_target("subscription.audit", audit_level)),
198+
);
199+
200+
layers.push(audit_layer);
201+
}
202+
203+
tracing_subscriber::registry().with(layers).init();
176204
}

src/bin/api/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct ServiceSettings {
1111
pub metrics: MetricsRxConfig,
1212
pub tasks: TasksConfig,
1313
pub smtp: SmtpConfig,
14+
pub subscription_audit: SubscriptionAuditConfig,
1415
}
1516

1617
impl Settings for ServiceSettings {
@@ -127,6 +128,28 @@ impl Default for MetricsLogConfig {
127128
}
128129
}
129130

131+
#[derive(Clone, Debug, Deserialize)]
132+
#[serde(default)]
133+
pub struct SubscriptionAuditConfig {
134+
pub enabled: bool,
135+
pub directory: String,
136+
pub file: String,
137+
pub rotation: String,
138+
pub level: String,
139+
}
140+
141+
impl Default for SubscriptionAuditConfig {
142+
fn default() -> Self {
143+
Self {
144+
enabled: true,
145+
directory: "logs".to_string(),
146+
file: "subscription_audit.log".to_string(),
147+
rotation: "daily".to_string(),
148+
level: "info".to_string(),
149+
}
150+
}
151+
}
152+
130153
#[derive(Clone, Default, Debug, Deserialize)]
131154
pub struct MetricsRxConfig {
132155
pub reciever: String,

src/bin/api/http/handlers/key.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use tracing::error;
1+
use tracing::{error, Instrument};
22

33
use fcore::{
44
http::{helpers as http, response::Instance},
@@ -7,6 +7,7 @@ use fcore::{
77
};
88

99
use super::super::{
10+
super::subscription_audit,
1011
super::sync::{tasks::SyncOp, MemSync},
1112
param::KeyQueryParams,
1213
request::{ActivateKeyReq, KeyReq},
@@ -105,6 +106,7 @@ where
105106
/// Post activate key
106107
pub async fn post_activate_key_handler<N, C, S>(
107108
req: ActivateKeyReq,
109+
trace_id_header: Option<String>,
108110
memory: MemSync<N, C, S>,
109111
) -> Result<impl warp::Reply, warp::Rejection>
110112
where
@@ -120,6 +122,7 @@ where
120122
S: SubscriptionOperations + Send + Sync + Clone + 'static + From<Subscription> + PartialEq,
121123
Connection: From<C>,
122124
{
125+
let trace_id = subscription_audit::trace_id_from_header(trace_id_header);
123126
let key_db = memory.db.key();
124127

125128
let mut key = match key_db.get(&req.code).await {
@@ -131,7 +134,20 @@ where
131134
return Ok(http::bad_request("Key already activated"));
132135
}
133136

134-
match SyncOp::add_days(&memory, &req.subscription_id, key.days as i64).await {
137+
subscription_audit::log_transaction_start(req.subscription_id, Some(key.days as i64));
138+
139+
match SyncOp::add_days(
140+
&memory,
141+
&req.subscription_id,
142+
key.days as i64,
143+
)
144+
.instrument(subscription_audit::transaction_span(
145+
"key_activate_handler",
146+
req.subscription_id,
147+
Some(trace_id),
148+
))
149+
.await
150+
{
135151
Ok(Status::Updated(_)) => {
136152
key.activate(&req.subscription_id);
137153
if let Err(err) = key_db.activate(&key).await {

src/bin/api/http/handlers/premium.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
use chrono::{DateTime, Utc};
2+
use tracing::Instrument;
23
use serde::{Deserialize, Serialize};
34
use std::net::{IpAddr, Ipv4Addr};
45
use std::sync::Arc;
56
use uuid::Uuid;
67
use warp::{http::StatusCode, Rejection, Reply};
78

8-
use super::super::super::sync::{tasks::SyncOp, MemSync};
9+
use super::super::super::{
10+
subscription_audit,
11+
sync::{tasks::SyncOp, MemSync},
12+
};
913
use super::admin::AdminSubscriptionTraffic;
1014
use super::subscription::build_subscription_traffic;
1115

@@ -169,6 +173,7 @@ where
169173
pub async fn premium_create_child_handler<N, C, S>(
170174
parent: S,
171175
req: PremiumChildCreateRequest,
176+
trace_id_header: Option<String>,
172177
memory: MemSync<N, C, S>,
173178
) -> Result<impl Reply, Rejection>
174179
where
@@ -184,14 +189,24 @@ where
184189
Connection: From<C>,
185190
S: SubscriptionOperations + Send + Sync + Clone + 'static + PartialEq + From<Subscription>,
186191
{
192+
let trace_id = subscription_audit::trace_id_from_header(trace_id_header);
187193
let sub_id = Uuid::new_v4();
188194
let ref_code = fcore::utils::get_uuid_last_octet_simple(&sub_id);
189195
let expires_at = req.days.map(|d| Utc::now() + chrono::Duration::days(d));
190196

191197
let mut sub = Subscription::new(sub_id, None, ref_code, expires_at, req.limit_bytes);
192198
sub.set_parent_id(parent.id());
193199

194-
match SyncOp::add_sub(&memory, sub).await {
200+
subscription_audit::log_transaction_start(sub_id, req.days);
201+
202+
match SyncOp::add_sub(&memory, sub)
203+
.instrument(subscription_audit::transaction_span(
204+
"premium_create_child_handler",
205+
sub_id,
206+
Some(trace_id),
207+
))
208+
.await
209+
{
195210
Ok(_) => Ok(warp::reply::with_status(
196211
warp::reply::json(&serde_json::json!({ "id": sub_id })),
197212
StatusCode::CREATED,
@@ -207,6 +222,7 @@ pub async fn premium_update_child_handler<N, C, S>(
207222
parent: S,
208223
child_id: Uuid,
209224
req: PremiumChildUpdateRequest,
225+
trace_id_header: Option<String>,
210226
memory: MemSync<N, C, S>,
211227
) -> Result<impl Reply, Rejection>
212228
where
@@ -222,6 +238,7 @@ where
222238
Connection: From<C>,
223239
S: SubscriptionOperations + Send + Sync + Clone + 'static + PartialEq + From<Subscription>,
224240
{
241+
let trace_id = subscription_audit::trace_id_from_header(trace_id_header);
225242
{
226243
let mem = memory.memory.read().await;
227244
let child = mem.subscriptions.find_by_id(&child_id).ok_or_else(|| {
@@ -237,7 +254,20 @@ where
237254
limit_bytes: req.limit_bytes,
238255
};
239256

240-
match SyncOp::update_sub(&memory, &child_id, update_req).await {
257+
subscription_audit::log_transaction_start(child_id, req.days);
258+
259+
match SyncOp::update_sub(
260+
&memory,
261+
&child_id,
262+
update_req,
263+
)
264+
.instrument(subscription_audit::transaction_span(
265+
"premium_update_child_handler",
266+
child_id,
267+
Some(trace_id),
268+
))
269+
.await
270+
{
241271
Ok(_) => Ok(warp::reply::with_status(
242272
warp::reply::json(&serde_json::json!({ "id": child_id })),
243273
StatusCode::OK,

src/bin/api/http/handlers/subscription.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use base64::Engine;
22
use chrono::{DateTime, Utc};
3+
use tracing::Instrument;
34
use serde::Deserialize;
45
use std::collections::{HashMap, HashSet};
56
use std::sync::Arc;
@@ -21,7 +22,10 @@ use fcore::{
2122
SubscriptionOperations, SubscriptionStorageOperations, Tag,
2223
};
2324

24-
use super::super::super::sync::{tasks::SyncOp, MemSync};
25+
use super::super::super::{
26+
subscription_audit,
27+
sync::{tasks::SyncOp, MemSync},
28+
};
2529
use super::super::{
2630
param::SubIdQueryParam,
2731
request::EnvFilter,
@@ -165,6 +169,7 @@ pub(crate) async fn build_subscription_traffic(
165169
// POST /subscription
166170
pub async fn post_subscription_handler<N, C, S>(
167171
req: SubReq,
172+
trace_id_header: Option<String>,
168173
memory: MemSync<N, C, S>,
169174
system_refer_codes: Vec<String>,
170175
) -> Result<impl warp::Reply, warp::Rejection>
@@ -181,6 +186,7 @@ where
181186
Connection: From<C>,
182187
S: SubscriptionOperations + Send + Sync + Clone + 'static + PartialEq + From<Subscription>,
183188
{
189+
let trace_id = subscription_audit::trace_id_from_header(trace_id_header);
184190
let sub_id = uuid::Uuid::new_v4();
185191

186192
let ref_code = req
@@ -209,7 +215,16 @@ where
209215
req.limit_bytes,
210216
);
211217

212-
match SyncOp::add_sub(&memory, sub.clone()).await {
218+
subscription_audit::log_transaction_start(sub_id, req.days);
219+
220+
match SyncOp::add_sub(&memory, sub.clone())
221+
.instrument(subscription_audit::transaction_span(
222+
"create_subscription_handler",
223+
sub_id,
224+
Some(trace_id),
225+
))
226+
.await
227+
{
213228
Ok(Status::Ok(id)) => Ok(http::success_response(
214229
format!("Subscription {} has been created", id),
215230
Some(sub_id),
@@ -236,6 +251,7 @@ where
236251
pub async fn put_subscription_handler<N, C, S>(
237252
sub_param: SubIdQueryParam,
238253
req: SubReq,
254+
trace_id_header: Option<String>,
239255
memory: MemSync<N, C, S>,
240256
) -> Result<impl warp::Reply, warp::Rejection>
241257
where
@@ -251,9 +267,23 @@ where
251267
Connection: From<C>,
252268
S: SubscriptionOperations + Send + Sync + Clone + 'static + PartialEq + From<Subscription>,
253269
{
270+
let trace_id = subscription_audit::trace_id_from_header(trace_id_header);
254271
let sub_id = sub_param.id;
255272

256-
match SyncOp::update_sub(&memory, &sub_id, req).await {
273+
subscription_audit::log_transaction_start(sub_id, req.days);
274+
275+
match SyncOp::update_sub(
276+
&memory,
277+
&sub_id,
278+
req,
279+
)
280+
.instrument(subscription_audit::transaction_span(
281+
"put_subscription_handler",
282+
sub_id,
283+
Some(trace_id),
284+
))
285+
.await
286+
{
257287
Ok(Status::Updated(id)) => Ok(http::success_response(
258288
format!("Subscription {} has been updated", id),
259289
Some(sub_id),

0 commit comments

Comments
 (0)