删除冗余代码

This commit is contained in:
imengying committed 2026-08-16 14:01:56 +08:00
1 parent 41d9e343a1
commit a01b54a58f
12 files changed
+210 -166

No files matched your search

+34 -1
View File
@@ -39,6 +39,39 @@ jobs:
version="${RAW_VERSION#v}"
echo "version=$version" >> "$GITHUB_OUTPUT"
quality:
name: Quality checks
needs: metadata
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: clippy, rustfmt
targets: wasm32-unknown-unknown
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check Rust formatting
run: cargo fmt --all -- --check
- name: Check Agent formatting
run: cargo fmt --manifest-path agent/Cargo.toml -- --check
- name: Lint Worker
run: cargo clippy --locked --all-targets -- -D warnings
- name: Lint Agent
run: cargo clippy --locked --manifest-path agent/Cargo.toml --all-targets -- -D warnings
- name: Test Worker
run: cargo test --locked --lib
- name: Test Agent
run: cargo test --locked --manifest-path agent/Cargo.toml
- name: Build frontend and Worker
run: bun run build
agent:
name: Agent (${{ matrix.platform }})
needs: metadata
@@ -187,7 +220,7 @@ jobs:
release:
name: Publish GitHub Release
if: github.event_name == 'push'
needs: [metadata, agent, frontend]
needs: [metadata, quality, agent, frontend]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
+36 -9
View File
@@ -63,6 +63,7 @@ struct RuntimeConfig {
report_interval: u64,
collect_interval: u64,
network_interface: String,
agent_mirror: String,
auto_update: bool,
latency_tasks: Vec<LatencyTask>,
}
@@ -179,6 +180,7 @@ struct RemoteConfig {
report_interval: u64,
collect_interval: u64,
network_interface: String,
agent_mirror: String,
auto_update: bool,
latency_tasks: Vec<LatencyTask>,
}
@@ -1258,6 +1260,7 @@ fn runtime_config(options: &CliOptions) -> Result<RuntimeConfig> {
report_interval: interval,
collect_interval: 5,
network_interface: String::new(),
agent_mirror: String::new(),
auto_update: true,
latency_tasks: Vec::new(),
})
@@ -1561,6 +1564,10 @@ fn apply_remote(config: &mut RuntimeConfig, remote: &RemoteConfig) -> bool {
config
.network_interface
.clone_from(&remote.network_interface);
let mirror = remote.agent_mirror.trim().trim_end_matches('/');
if mirror.is_empty() || valid_endpoint(mirror) {
config.agent_mirror = mirror.to_string();
}
config.auto_update = remote.auto_update;
let tasks = sanitize_latency_tasks(&remote.latency_tasks);
let changed = config.latency_tasks != tasks;
@@ -1761,7 +1768,16 @@ fn download_agent(
Ok(valid)
}
fn update(agent: &ureq::Agent) -> Result<bool> {
fn mirrored_download_url(mirror: &str, url: &str) -> String {
let mirror = mirror.trim().trim_end_matches('/');
if mirror.is_empty() {
url.to_string()
} else {
format!("{mirror}/{url}")
}
}
fn update(agent: &ureq::Agent, mirror: &str) -> Result<bool> {
let mut response = agent
.get(LATEST_RELEASE_API)
.header("Accept", "application/vnd.github+json")
@@ -1800,7 +1816,7 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
};
if !download_agent(
agent,
&release_asset.browser_download_url,
&mirrored_download_url(mirror, &release_asset.browser_download_url),
&temporary,
&release.tag_name,
&expected_sha256,
@@ -1943,7 +1959,7 @@ fn run(options: &CliOptions, once: bool, print_only: bool) -> Result<()> {
}
}
if !once && config.auto_update && Instant::now() >= next_update_check {
match update(&agent) {
match update(&agent, &config.agent_mirror) {
Ok(true) => return Ok(()),
Ok(false) => {
next_update_check = Instant::now() + UPDATE_CHECK_INTERVAL;
@@ -2005,12 +2021,12 @@ mod tests {
use super::{
ack_interval, agent_artifact_name, clock_offset_from_http_date, corrected_timestamp,
executable_format_valid, gpu_name_from_uevent, is_public_probe_ip, live_endpoint,
live_update_payload, median, normalized_version, parse_lspci_gpu_names, parse_probe_target,
parse_system_profiler_gpu_names, ping_latency, prune_report_samples, release_asset_sha256,
report_retry_delay, sanitize_latency_tasks, selected_interface, tcp_latency_probe_address,
valid_endpoint, version_triplet, CliOptions, ClockCalibration, GithubReleaseAsset,
LatencyResult, LatencyTask, LiveAck, Report, CLOCK_CALIBRATION_MAX_AGE, MAX_LATENCY_TASKS,
MAX_PENDING_LATENCY_RESULTS, PROBE_ATTEMPTS,
live_update_payload, median, mirrored_download_url, normalized_version,
parse_lspci_gpu_names, parse_probe_target, parse_system_profiler_gpu_names, ping_latency,
prune_report_samples, release_asset_sha256, report_retry_delay, sanitize_latency_tasks,
selected_interface, tcp_latency_probe_address, valid_endpoint, version_triplet, CliOptions,
ClockCalibration, GithubReleaseAsset, LatencyResult, LatencyTask, LiveAck, Report,
CLOCK_CALIBRATION_MAX_AGE, MAX_LATENCY_TASKS, MAX_PENDING_LATENCY_RESULTS, PROBE_ATTEMPTS,
};
#[cfg(any(target_os = "windows", target_os = "macos"))]
@@ -2103,6 +2119,17 @@ mod tests {
assert!(!valid_endpoint("https://bad host.example"));
}
#[test]
fn builds_mirrored_download_urls() {
let release =
"https://github.com/imengying/NodeFlare/releases/download/v1.2.3/agent-linux-x86_64";
assert_eq!(mirrored_download_url("", release), release);
assert_eq!(
mirrored_download_url("https://mirror.example.com/", release),
format!("https://mirror.example.com/{release}")
);
}
#[test]
fn builds_live_websocket_endpoints() {
assert_eq!(
+1
View File
@@ -28,6 +28,7 @@ CREATE TABLE servers (
collect_interval INTEGER NOT NULL DEFAULT 5,
rx_correction INTEGER NOT NULL DEFAULT 0,
tx_correction INTEGER NOT NULL DEFAULT 0,
agent_mirror TEXT NOT NULL DEFAULT '',
offline_notify_disabled INTEGER NOT NULL DEFAULT 0,
auto_update INTEGER NOT NULL DEFAULT 1,
token TEXT NOT NULL UNIQUE,
-2
View File
@@ -1,2 +0,0 @@
-- Agent 下载加速前缀(拼接在 GitHub Release 下载地址之前),空字符串表示直连。
ALTER TABLE servers ADD COLUMN agent_mirror TEXT NOT NULL DEFAULT '';
+2 -2
View File
@@ -67,7 +67,7 @@ request -H "Authorization: Bearer $admin_token" \
"$MONITOR_BASE_URL/api/admin/themes" | \
jq -e '.themes | any(.builtin == true and .id == "builtin-nodeflare-glass" and .name == "NodeFlare Glass" and .active == true)' >/dev/null
server_input='{"name":"Smoke Test Node","region":"JP","group_name":"Test","tags":"smoke","hidden":false,"expires_at":1893456000,"traffic_limit":107374182400,"traffic_limit_type":"max","price":9.9,"billing_cycle":30,"currency":"USD","auto_renewal":true,"network_interface":"","reset_day":1,"report_interval":60,"collect_interval":5,"rx_correction":0,"tx_correction":0,"offline_notify_disabled":false,"auto_update":true}'
server_input='{"name":"Smoke Test Node","region":"JP","group_name":"Test","tags":"smoke","hidden":false,"expires_at":1893456000,"traffic_limit":107374182400,"traffic_limit_type":"max","price":9.9,"billing_cycle":30,"currency":"USD","auto_renewal":true,"network_interface":"","reset_day":1,"report_interval":60,"collect_interval":5,"rx_correction":0,"tx_correction":0,"agent_mirror":"https://mirror.example.com","offline_notify_disabled":false,"auto_update":true}'
server_id=
latency_task_id=
@@ -130,7 +130,7 @@ report_response=$(request -H "Authorization: Bearer $agent_token" -H 'Content-Ty
--data "$report" "$MONITOR_BASE_URL/api/agent/report")
printf '%s' "$report_response" | jq -e \
--arg task_id "$latency_task_id" \
'.collect_interval == 5 and .auto_update == true and (.latency_tasks | any(.id == $task_id and .task_type == "tcp" and .target == "example.com" and .port == 443))' >/dev/null
'.collect_interval == 5 and .agent_mirror == "https://mirror.example.com" and .auto_update == true and (.latency_tasks | any(.id == $task_id and .task_type == "tcp" and .target == "example.com" and .port == 443))' >/dev/null
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/admin/servers" | \
jq -e --arg id "$server_id" '.servers | any(.id == $id and .last_ip == "8.8.8.8")' >/dev/null
+5 -49
View File
@@ -1,5 +1,4 @@
use std::collections::{BTreeMap, HashMap};
use std::sync::Mutex;
use serde::{Deserialize, Serialize, Serializer};
use worker::{wasm_bindgen::JsValue, D1Database, Date, Result};
@@ -68,6 +67,7 @@ struct AgentConfigRow {
report_interval: i64,
collect_interval: i64,
network_interface: String,
agent_mirror: String,
auto_update: i64,
}
@@ -325,7 +325,7 @@ pub async fn update_last_ip(db: &D1Database, id: &str, ip: &str) -> Result<()> {
pub async fn agent_config(db: &D1Database, id: &str) -> Result<Option<AgentConfigView>> {
let row: Option<AgentConfigRow> = db
.prepare(
"SELECT report_interval, collect_interval, network_interface, auto_update \
"SELECT report_interval, collect_interval, network_interface, agent_mirror, auto_update \
FROM servers WHERE id = ?1",
)
.bind(&[text(id)])?
@@ -338,6 +338,7 @@ pub async fn agent_config(db: &D1Database, id: &str) -> Result<Option<AgentConfi
report_interval: row.report_interval,
collect_interval: row.collect_interval,
network_interface: row.network_interface.trim().to_string(),
agent_mirror: row.agent_mirror.trim().trim_end_matches('/').to_string(),
auto_update: row.auto_update != 0,
latency_tasks: crate::latency::tasks_for_server(db, id).await?,
}))
@@ -383,7 +384,7 @@ pub async fn create_server(
number(input.collect_interval),
number(input.rx_correction),
number(input.tx_correction),
text(input.agent_mirror.trim()),
text(input.agent_mirror.trim().trim_end_matches('/')),
JsValue::from_bool(input.offline_notify_disabled),
JsValue::from_bool(input.auto_update),
text(token),
@@ -427,7 +428,7 @@ pub async fn update_server(db: &D1Database, id: &str, input: &ServerInput) -> Re
number(input.collect_interval),
number(input.rx_correction),
number(input.tx_correction),
text(input.agent_mirror.trim()),
text(input.agent_mirror.trim().trim_end_matches('/')),
JsValue::from_bool(input.offline_notify_disabled),
JsValue::from_bool(input.auto_update),
number(now()),
@@ -1071,48 +1072,6 @@ pub async fn settings(
})
}
const SETTINGS_CACHE_TTL_SECONDS: i64 = 30;
static SETTINGS_CACHE: Mutex<Option<(i64, SettingsView)>> = Mutex::new(None);
/// Settings gate every request (including the static-asset fallback), so keep an
/// isolate-local copy to avoid one D1 read per request. Writes invalidate it;
/// other isolates observe changes after at most the TTL.
pub async fn cached_settings(
db: &D1Database,
default_name: &str,
default_threshold: i64,
default_retention: i64,
default_username: &str,
) -> Result<SettingsView> {
let current = now();
if let Ok(slot) = SETTINGS_CACHE.lock() {
if let Some((loaded_at, cached)) = slot.as_ref() {
if current - *loaded_at < SETTINGS_CACHE_TTL_SECONDS {
return Ok(cached.clone());
}
}
}
let settings = settings(
db,
default_name,
default_threshold,
default_retention,
default_username,
)
.await?;
if let Ok(mut slot) = SETTINGS_CACHE.lock() {
*slot = Some((current, settings.clone()));
}
Ok(settings)
}
pub fn invalidate_settings_cache() {
if let Ok(mut slot) = SETTINGS_CACHE.lock() {
*slot = None;
}
}
pub async fn update_settings(
db: &D1Database,
input: &SettingsInput,
@@ -1268,7 +1227,6 @@ pub async fn update_settings(
}
if !statements.is_empty() {
db.batch(statements).await?;
invalidate_settings_cache();
}
Ok(())
}
@@ -1520,7 +1478,6 @@ pub async fn save_setting(db: &D1Database, key: &str, value: &str) -> Result<()>
.bind(&[text(key), text(value), number(now())])?
.run()
.await?;
invalidate_settings_cache();
Ok(())
}
@@ -1534,7 +1491,6 @@ pub async fn increment_setting(db: &D1Database, key: &str) -> Result<()> {
.bind(&[text(key), number(now())])?
.run()
.await?;
invalidate_settings_cache();
Ok(())
}
+79 -25
View File
@@ -1,5 +1,4 @@
mod auth;
mod routes;
mod cloudflare;
mod db;
mod exchange;
@@ -7,6 +6,7 @@ mod latency;
mod live;
mod models;
mod notify;
mod routes;
mod theme;
mod turnstile;
@@ -113,7 +113,10 @@ fn env_secret_text(env: &Env, key: &str) -> String {
.unwrap_or_default()
}
pub(crate) fn settings_for_admin_response(mut settings: db::SettingsView, env: &Env) -> db::SettingsView {
pub(crate) fn settings_for_admin_response(
mut settings: db::SettingsView,
env: &Env,
) -> db::SettingsView {
for (field, variable) in [
(&mut settings.turnstile_site_key, "TURNSTILE_SITE_KEY"),
(&mut settings.turnstile_secret_key, "TURNSTILE_SECRET_KEY"),
@@ -626,27 +629,46 @@ pub(crate) fn validate_server(input: &ServerInput) -> Option<&'static str> {
if input.rx_correction < 0 || input.tx_correction < 0 {
return Some("流量修正值不能为负数");
}
let mirror = input.agent_mirror.trim();
if !mirror.is_empty() {
if mirror.len() > 2048
|| !mirror
.chars()
.all(|value| value.is_ascii_alphanumeric() || "_./:@-".contains(value))
{
return Some("Agent 下载加速地址格式无效");
}
let local_http = mirror.starts_with("http://localhost")
|| mirror.starts_with("http://127.0.0.1");
if !mirror.starts_with("https://") && !local_http {
return Some("Agent 下载加速地址必须使用 HTTPS");
}
if mirror.contains('@') {
return Some("Agent 下载加速地址不能包含用户信息");
}
if !valid_agent_mirror(&input.agent_mirror) {
return Some("Agent 下载加速地址必须是无凭据、查询参数和片段的 HTTPS URL");
}
None
}
pub(crate) fn valid_agent_mirror(value: &str) -> bool {
let value = value.trim().trim_end_matches('/');
if value.is_empty() {
return true;
}
if value.len() > 2048
|| value.chars().any(char::is_whitespace)
|| !value
.chars()
.all(|character| character.is_ascii_alphanumeric() || "_./:@-".contains(character))
{
return false;
}
let Ok(url) = Url::parse(value) else {
return false;
};
if url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
return false;
}
if url.scheme() == "https" {
return true;
}
url.scheme() == "http"
&& matches!(
url.host_str(),
Some(host) if host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1"
)
}
pub(crate) fn validate_report(report: &AgentReport) -> Option<&'static str> {
let floats = [
report.cpu,
@@ -747,7 +769,10 @@ pub(crate) fn validate_report(report: &AgentReport) -> Option<&'static str> {
None
}
pub(crate) fn public_server(server: ServerView, latency: Vec<latency::LatencySample>) -> PublicServer {
pub(crate) fn public_server(
server: ServerView,
latency: Vec<latency::LatencySample>,
) -> PublicServer {
let disks = server
.disk_info
.as_deref()
@@ -985,7 +1010,7 @@ async fn handle(req: Request, env: Env, ctx: Context) -> Result<Response> {
let default_threshold = env_number(&env, "OFFLINE_THRESHOLD_SECONDS", 180).clamp(30, 3600);
let default_retention = env_number(&env, "HISTORY_RETENTION_DAYS", 30).clamp(1, 365);
let default_username = env_text(&env, "ADMIN_USERNAME", "");
let settings = db::cached_settings(
let settings = db::settings(
&database,
&default_name,
default_threshold,
@@ -1051,7 +1076,7 @@ async fn handle(req: Request, env: Env, ctx: Context) -> Result<Response> {
routes::RouteOutcome::Unmatched(req) => req,
};
let response = env.assets("ASSETS")?.fetch_request(req).await?;
Ok(secure_public_response(mutable_response(response)?)?)
secure_public_response(mutable_response(response)?)
}
#[event(fetch, respond_with_errors)]
async fn fetch(req: Request, env: Env, ctx: Context) -> Result<Response> {
@@ -1147,9 +1172,9 @@ async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
mod tests {
use super::{
allow_on_rate_limit_failure, history_cache_key, rate_limit_binding, same_origin,
submitted_secret, valid_cloudflare_account_id, valid_cloudflare_api_token,
valid_password_derived, valid_ping_target, valid_server_price, validate_latency_task,
ADMIN_HTML, ADMIN_SCRIPT, ADMIN_STYLE,
submitted_secret, valid_agent_mirror, valid_cloudflare_account_id,
valid_cloudflare_api_token, valid_password_derived, valid_ping_target, valid_server_price,
validate_latency_task, ADMIN_HTML, ADMIN_SCRIPT, ADMIN_STYLE,
};
use crate::{db::SECRET_MASK, models::LatencyTaskInput};
use worker::{Method, Url};
@@ -1198,6 +1223,35 @@ mod tests {
assert!(!valid_server_price(f64::NAN));
}
#[test]
fn validates_agent_mirrors() {
for mirror in [
"",
"https://mirror.example.com",
"https://mirror.example.com/github",
"http://127.0.0.1:8080",
"http://localhost:8080",
] {
assert!(
valid_agent_mirror(mirror),
"expected valid mirror: {mirror}"
);
}
for mirror in [
"https://",
"http://mirror.example.com",
"https://user@mirror.example.com",
"https://mirror.example.com/?token=secret",
"https://mirror.example.com/#fragment",
"https://bad host.example.com",
] {
assert!(
!valid_agent_mirror(mirror),
"expected invalid mirror: {mirror}"
);
}
}
#[test]
fn validates_latency_targets() {
for target in ["gd-ct-dualstack.ip.zstaticcdn.com", "1.1.1.1"] {
+1 -1
View File
@@ -43,7 +43,6 @@ pub struct ServerInput {
pub collect_interval: i64,
pub rx_correction: i64,
pub tx_correction: i64,
#[serde(default)]
pub agent_mirror: String,
pub offline_notify_disabled: bool,
pub auto_update: bool,
@@ -334,6 +333,7 @@ pub struct AgentConfigView {
pub report_interval: i64,
pub collect_interval: i64,
pub network_interface: String,
pub agent_mirror: String,
pub auto_update: bool,
pub latency_tasks: Vec<AgentLatencyTask>,
}
+17 -37
View File
@@ -11,29 +11,10 @@ use crate::models::{
};
use crate::routes::{RouteContext, RouteOutcome};
use crate::{
cloudflare,
db,
error,
exchange,
json,
latency,
live,
no_content,
now,
notify,
request_json,
server_id,
settings_for_admin_response,
theme,
validate_alert_rule,
validate_latency_task,
validate_server,
validate_theme,
valid_cloudflare_account_id,
valid_cloudflare_api_token,
valid_password_derived,
submitted_secret,
API_JSON_MAX_BYTES,
cloudflare, db, error, exchange, json, latency, live, no_content, notify, now, request_json,
server_id, settings_for_admin_response, submitted_secret, theme, valid_cloudflare_account_id,
valid_cloudflare_api_token, valid_password_derived, validate_alert_rule, validate_latency_task,
validate_server, validate_theme, API_JSON_MAX_BYTES,
};
pub(crate) async fn route(mut req: Request, ctx: &RouteContext) -> Result<RouteOutcome> {
@@ -122,7 +103,9 @@ pub(crate) async fn route(mut req: Request, ctx: &RouteContext) -> Result<RouteO
if method == Method::Post && path == "/api/admin/themes" {
return handled(themes_post(&mut req, ctx).await);
}
if method == Method::Post && path.starts_with("/api/admin/themes/") && path.ends_with("/preview")
if method == Method::Post
&& path.starts_with("/api/admin/themes/")
&& path.ends_with("/preview")
{
return handled(theme_preview_post(ctx, &path).await);
}
@@ -336,8 +319,7 @@ async fn servers_order_patch(req: &mut Request, ctx: &RouteContext) -> Result<Re
}
let current = db::list_servers(&ctx.database, true).await?;
let current_ids: HashSet<_> = current.iter().map(|server| &server.id).collect();
if input.ids.len() != current_ids.len()
|| input.ids.iter().any(|id| !current_ids.contains(id))
if input.ids.len() != current_ids.len() || input.ids.iter().any(|id| !current_ids.contains(id))
{
return error("排序列表必须包含全部节点", 400);
}
@@ -354,11 +336,7 @@ async fn servers_post(req: &mut Request, ctx: &RouteContext) -> Result<Response>
return error(message, 400);
}
let id = short_durable_id(ctx)?;
let token = ctx
.env
.durable_object("LIVE_HUB")?
.unique_id()?
.to_string();
let token = ctx.env.durable_object("LIVE_HUB")?.unique_id()?.to_string();
db::create_server(&ctx.database, &id, &token, &input).await?;
latency::assign_defaults(&ctx.database, &id).await?;
json(&serde_json::json!({ "id": id, "agent_token": token }), 201)
@@ -587,7 +565,8 @@ async fn notifications_test(ctx: &RouteContext) -> Result<Response> {
if ctx.settings.notification_endpoint.trim().is_empty() {
return error("请先填写 Telegram Bot Token 和 Chat ID", 400);
}
if let Err(err) = notify::send(&ctx.settings, "NodeFlare 测试通知:通知渠道配置成功。").await {
if let Err(err) = notify::send(&ctx.settings, "NodeFlare 测试通知:通知渠道配置成功。").await
{
console_error!("test notification failed: {err}");
return error("测试通知发送失败,请检查 Bot Token 和 Chat ID", 502);
}
@@ -705,8 +684,7 @@ async fn settings_patch(req: &mut Request, ctx: &RouteContext) -> Result<Respons
} else {
configured_secret_key
};
let protection_activated = input.turnstile_enabled == Some(true)
&& !settings.turnstile_enabled
let protection_activated = input.turnstile_enabled == Some(true) && !settings.turnstile_enabled
|| input.turnstile_login_enabled == Some(true) && !settings.turnstile_login_enabled;
if protection_activated && (site_key.is_empty() || secret_key.is_empty()) {
return error("启用 Turnstile 前必须填写站点密钥和私钥", 400);
@@ -735,9 +713,11 @@ async fn settings_patch(req: &mut Request, ctx: &RouteContext) -> Result<Respons
{
return error("Cloudflare Account ID 应为 32 位十六进制字符", 400);
}
if input.cloudflare_api_token.as_deref().is_some_and(|value| {
value.trim() != db::SECRET_MASK && !valid_cloudflare_api_token(value)
}) {
if input
.cloudflare_api_token
.as_deref()
.is_some_and(|value| value.trim() != db::SECRET_MASK && !valid_cloudflare_api_token(value))
{
return error("Cloudflare API Token 格式无效", 400);
}
if input
+4 -4
View File
@@ -2,16 +2,16 @@ use std::net::IpAddr;
use worker::{Context, D1Database, Env, Request, Response, Result};
use crate::auth::bearer_token;
use crate::db;
use crate::live;
use crate::latency;
use crate::live;
use crate::models::AgentReportBatch;
use crate::sha256_hex;
use crate::{
client_ip, error, no_content, now, public_server, request_json, validate_report,
AGENT_JSON_MAX_BYTES, MAX_AGENT_LATENCY_RESULTS, MAX_AGENT_SAMPLES,
};
use crate::auth::bearer_token;
use crate::models::AgentReportBatch;
use crate::sha256_hex;
pub(crate) async fn report(
mut req: Request,
+15 -23
View File
@@ -2,31 +2,18 @@ use std::collections::HashMap;
use worker::{Method, Request, Response, Result};
use crate::routes::{RouteContext, RouteOutcome};
use crate::{
db,
error,
json,
latency,
live,
no_content,
now,
public_server,
requested_hours,
server_id,
cached_history_response,
history_cache_key,
request_json,
set_admin_session_cookie,
store_history_response,
API_JSON_MAX_BYTES,
};
use crate::auth::{
create_admin_jwt, create_turnstile_proof, hash_password, random_salt, verify_credentials,
};
use crate::models::LoginRequest;
use crate::turnstile;
use crate::client_ip;
use crate::models::LoginRequest;
use crate::routes::{RouteContext, RouteOutcome};
use crate::turnstile;
use crate::{
cached_history_response, db, error, history_cache_key, json, latency, live, no_content, now,
public_server, request_json, requested_hours, server_id, set_admin_session_cookie,
store_history_response, API_JSON_MAX_BYTES,
};
pub(crate) async fn route(mut req: Request, ctx: &RouteContext) -> Result<RouteOutcome> {
let method = req.method();
@@ -39,14 +26,19 @@ pub(crate) async fn route(mut req: Request, ctx: &RouteContext) -> Result<RouteO
return Ok(RouteOutcome::Handled(admin_login(&mut req, ctx).await?));
}
if method == Method::Post && path == "/api/turnstile/verify" {
return Ok(RouteOutcome::Handled(turnstile_verify(&mut req, ctx).await?));
return Ok(RouteOutcome::Handled(
turnstile_verify(&mut req, ctx).await?,
));
}
if method == Method::Get && path == "/api/exchange-rates" {
return Ok(RouteOutcome::Handled(exchange_rates(ctx).await?));
}
if method == Method::Get && path == "/api/ws" {
if !ctx.turnstile_verified {
return Ok(RouteOutcome::Handled(error("请先完成 Cloudflare 人机验证", 403)?));
return Ok(RouteOutcome::Handled(error(
"请先完成 Cloudflare 人机验证",
403,
)?));
}
if !ctx.settings.public_dashboard && !ctx.admin {
return Ok(RouteOutcome::Handled(error("此仪表盘需要登录后访问", 401)?));
+16 -13
View File
@@ -2,7 +2,10 @@ use worker::{Method, Request, Response, Result};
use crate::auth::verify_theme_preview_proof;
use crate::routes::{RouteContext, RouteOutcome};
use crate::{db, remote_theme_response, remote_theme_preview_response, secure_public_response, theme, ADMIN_HTML, ADMIN_SCRIPT, ADMIN_STYLE};
use crate::{
db, remote_theme_preview_response, remote_theme_response, secure_public_response, theme,
ADMIN_HTML, ADMIN_SCRIPT, ADMIN_STYLE,
};
/// 管理端内嵌资源(编译进 wasm,不受主题影响)。
pub(crate) fn embedded_asset(path: &str) -> Result<Option<Response>> {
@@ -10,11 +13,10 @@ pub(crate) fn embedded_asset(path: &str) -> Result<Option<Response>> {
"/admin" | "/admin/" | "/admin/index.html" => {
crate::embedded_admin_response(ADMIN_HTML, "text/html; charset=utf-8").map(Some)
}
"/admin-assets/admin.js" => crate::embedded_admin_response(
ADMIN_SCRIPT,
"application/javascript; charset=utf-8",
)
.map(Some),
"/admin-assets/admin.js" => {
crate::embedded_admin_response(ADMIN_SCRIPT, "application/javascript; charset=utf-8")
.map(Some)
}
"/admin-assets/admin.css" => {
crate::embedded_admin_response(ADMIN_STYLE, "text/css; charset=utf-8").map(Some)
}
@@ -31,16 +33,17 @@ pub(crate) async fn route(req: Request, ctx: &RouteContext) -> Result<RouteOutco
if let Some(preview_path) = path.strip_prefix("/__theme-preview/") {
let (proof, relative) = preview_path.split_once('/').unwrap_or((preview_path, ""));
let Some(theme_id) = verify_theme_preview_proof(proof, &ctx.env, &ctx.settings.admin_password_hash)
let Some(theme_id) =
verify_theme_preview_proof(proof, &ctx.env, &ctx.settings.admin_password_hash)
else {
return Ok(RouteOutcome::Handled(
secure_public_response(Response::error("主题预览链接已过期", 403)?)?,
));
return Ok(RouteOutcome::Handled(secure_public_response(
Response::error("主题预览链接已过期", 403)?,
)?));
};
let Some(url) = db::theme_url(&ctx.database, &theme_id).await? else {
return Ok(RouteOutcome::Handled(
secure_public_response(Response::error("主题不存在", 404)?)?,
));
return Ok(RouteOutcome::Handled(secure_public_response(
Response::error("主题不存在", 404)?,
)?));
};
let prefix = format!("/__theme-preview/{proof}");
return Ok(RouteOutcome::Handled(