Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion admin-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lemwood Mirror 管理后台</title>
<title>柠泽资源站(miawa) 管理后台</title>
</head>
<body class="bg-background text-foreground antialiased selection:bg-zinc-200 selection:text-zinc-900 dark:selection:bg-zinc-800 dark:selection:text-zinc-100">
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion admin-app/src/layout/components/Sidebar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
class="flex flex-col truncate"
>
<span class="text-sm font-semibold tracking-tight text-zinc-900 dark:text-zinc-100 truncate">
Lemwood Mirror
柠泽资源站(miawa)
</span>
<span class="text-[11px] text-zinc-500 dark:text-zinc-400 font-mono tracking-tighter">
管理控制台
Expand Down
2 changes: 1 addition & 1 deletion admin-app/src/views/config/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
配置编辑
</h2>
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
管理 Lemwood Mirror 服务的核心网络、安全、启动器源及自更新参数
管理 柠泽资源站(miawa) 服务的核心网络、安全、启动器源及自更新参数
</p>
</div>

Expand Down
2 changes: 1 addition & 1 deletion admin-app/src/views/dashboard/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
控制台概览
</h2>
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
Lemwood Mirror 启动器分发镜像服务运行状态与快捷导航
柠泽资源站(miawa) 启动器分发镜像服务运行状态与快捷导航
</p>
</div>
<div class="flex items-center gap-2">
Expand Down
4 changes: 2 additions & 2 deletions admin-app/src/views/login/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/>
</div>
<h1 class="text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">
Lemwood Mirror
柠泽资源站(miawa)
</h1>
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
管理控制台身份认证
Expand Down Expand Up @@ -99,7 +99,7 @@

<!-- 底部版权与前台链接 -->
<div class="text-center mt-6 text-xs text-zinc-400 space-x-3">
<span>Lemwood Mirror</span>
<span>柠泽资源站(miawa)</span>
<span>•</span>
<a
href="/"
Expand Down
28 changes: 18 additions & 10 deletions cmd/mirror/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ func NewScanner(cfg *config.Config, base string, s *server.State, ghc *gh.Client

func buildSelfUpdateConfig(cfg *config.Config) selfupdate.Config {
return selfupdate.Config{
Enabled: cfg.SelfUpdateEnabled,
RepoURL: cfg.SelfUpdateRepoURL,
Channel: cfg.SelfUpdateChannel,
AutoRestart: cfg.SelfUpdateAutoRestart,
ProxyURL: cfg.ProxyURL,
AssetProxyURL: cfg.AssetProxyURL,
Enabled: cfg.SelfUpdateEnabled,
RepoURL: cfg.SelfUpdateRepoURL,
Channel: cfg.SelfUpdateChannel,
AutoRestart: cfg.SelfUpdateAutoRestart,
ProxyURL: cfg.ProxyURL,
AssetProxyURL: cfg.AssetProxyURL,
InsecureSkipVerify: cfg.TLSSkipVerify,
}
}

Expand Down Expand Up @@ -161,8 +162,15 @@ func (sc *Scanner) scanLauncher(lcfg config.LauncherConfig) {
}
}

downer := downloader.NewDownloader(sc.cfg.DownloadTimeoutMinutes, sc.cfg.ConcurrentDownloads)
infoPath, err := downer.DownloadLatest(ctx, lcfg.Name, sc.base, sc.cfg.ProxyURL, sc.cfg.AssetProxyURL, sc.cfg.XgetEnabled, sc.cfg.XgetDomain, rel, sc.cfg.ServerAddress, sc.cfg.ServerPort, sc.cfg.DownloadUrlBase, isLatest)
downer := downloader.NewDownloader(sc.cfg.DownloadTimeoutMinutes, sc.cfg.ConcurrentDownloads, sc.cfg.TLSSkipVerify)
// 每次扫描都从 GitHub API 拉取该 release 全部资产的 SHA-256 摘要(name → hex64),
// 用于下载后本地校验并写入 index.json 元数据;旧资产无 digest 时仅记录本地哈希。
digests, derr := sc.ghc.GetReleaseAssetDigests(ctx, owner, repo, version)
if derr != nil {
log.Printf("%s: 获取 GitHub 资产摘要失败(本次跳过远程校验): %v", lcfg.Name, derr)
digests = nil
}
infoPath, err := downer.DownloadLatest(ctx, lcfg.Name, sc.base, sc.cfg.ProxyURL, sc.cfg.AssetProxyURL, sc.cfg.XgetEnabled, sc.cfg.XgetDomain, rel, sc.cfg.ServerAddress, sc.cfg.ServerPort, sc.cfg.DownloadUrlBase, isLatest, digests)
if err != nil {
log.Printf("%s: 下载/检查失败: %v", lcfg.Name, err)
continue
Expand Down Expand Up @@ -195,7 +203,7 @@ func (sc *Scanner) ScanAll() {
if sc.cfg.ExternalBlacklistURL != "" {
log.Printf("[黑名单同步] 开始同步外部黑名单: %s", sc.cfg.ExternalBlacklistURL)
go func() {
if err := blacklist.SyncExternalBlacklist(sc.cfg.ExternalBlacklistURL); err != nil {
if err := blacklist.SyncExternalBlacklist(sc.cfg.ExternalBlacklistURL, sc.cfg.TLSSkipVerify); err != nil {
log.Printf("[黑名单同步] 同步外部黑名单失败: %v", err)
}
}()
Expand Down Expand Up @@ -307,7 +315,7 @@ func main() {
}
}

ghc := gh.NewClient(cfg.EffectiveGitHubToken(), cfg.ProxyURL)
ghc := gh.NewClient(cfg.EffectiveGitHubToken(), cfg.ProxyURL, cfg.TLSSkipVerify)
selfUpdateManager := selfupdate.NewManager(ghc, Version, resolveBinaryPath(), buildSelfUpdateConfig(cfg))
s.SetSelfUpdateManager(selfUpdateManager)

Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/globalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const globalConfig = {
downloadChallenge: '/downloads/challenge',
downloadAuthorize: '/downloads/authorize',
downloadPrepare: '/downloads/prepare',
downloadLanding: '/downloads/landing'
downloadLanding: '/downloads/landing',
fileIntegrity: '/files/integrity'
}
},

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ export const prepareDownload = (filePath, returnUrl, source) =>
export const getDownloadLanding = (token) =>
api.get(`${globalConfig.api.endpoints.downloadLanding}?token=${encodeURIComponent(token)}`)

// 文件完整性:SHA-256 校验值(PoW 验证页展示)
export const getFileIntegrity = (filePath) =>
api.get(`${globalConfig.api.endpoints.fileIntegrity}?file_path=${encodeURIComponent(filePath)}`)

73 changes: 67 additions & 6 deletions frontend/src/views/VerifyView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import {
PhHeart as Heart,
PhCircleNotch as Loader2,
PhArrowClockwise as RefreshCw,
PhShieldCheck as ShieldCheck,
PhUsers as Users,
PhXCircle as XCircle
} from '@phosphor-icons/vue'
import { getPowConfig, createDownloadChallenge, authorizeDownload } from '@/services/api'
import { getPowConfig, createDownloadChallenge, authorizeDownload, getFileIntegrity } from '@/services/api'
import { base64urlDecode, base64urlEncode, leadingZeroBits } from '@/lib/pow'
import { globalConfig } from '@/lib/globalConfig'
import Button from '@/components/ui/Button.vue'
Expand All @@ -31,6 +30,10 @@ const statusText = ref('正在获取验证挑战…')
const errorMessage = ref('')
const verifyStatus = ref('pending') // pending | error

const fileHash = ref('')
const fileSize = ref(0)
const githubUrl = globalConfig.links.githubOrg

let cancelled = false

// ---- PBKDF2 求解(编解码与零位统计见 lib/pow.js,与后端 internal/pow 协议一致) ----
Expand Down Expand Up @@ -87,6 +90,8 @@ const init = async () => {
errorMessage.value = ''
verifyStatus.value = 'pending'
filePath.value = route.query.file || ''
fileHash.value = ''
fileSize.value = 0

if (!filePath.value) {
errorMessage.value = '缺少文件参数,请从来源页面重新发起下载'
Expand All @@ -95,6 +100,9 @@ const init = async () => {
return
}

// 并行加载文件哈希(失败不影响验证主流程)
loadIntegrity()

if (!isPowSupported()) {
errorMessage.value =
'当前浏览器不支持 Web Crypto(需要较新的内核与 HTTPS 环境),无法自动完成验证。可升级浏览器,或使用下方备用下载入口。'
Expand Down Expand Up @@ -151,6 +159,29 @@ const init = async () => {
}
}

// 文件完整性信息:并行加载,失败不影响验证主流程
const loadIntegrity = async () => {
try {
const res = await getFileIntegrity(filePath.value)
fileHash.value = res.data?.sha256 || ''
fileSize.value = res.data?.size || 0
} catch {
// 哈希展示失败不影响验证与下载
}
}

const formatSize = (bytes) => {
if (!bytes) return ''
const units = ['B', 'KB', 'MB', 'GB']
let v = bytes
let i = 0
while (v >= 1024 && i < units.length - 1) {
v /= 1024
i++
}
return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${units[i]}`
}

const retry = () => {
init()
}
Expand Down Expand Up @@ -183,10 +214,26 @@ onUnmounted(() => {
<div class="flex min-h-[calc(100vh-10rem)] flex-col items-center justify-center gap-4 py-8 supports-[height:100dvh]:min-h-[calc(100dvh-10rem)]">
<Card class="w-full max-w-lg">
<CardHeader class="items-center text-center">
<div class="mb-2 rounded-full bg-primary/10 p-3 text-primary">
<ShieldCheck weight="duotone" class="h-8 w-8" />
<div class="mb-2 flex items-center justify-center gap-3">
<div
v-if="verifyStatus === 'error'"
class="rounded-full bg-destructive/10 p-2 text-destructive"
aria-label="验证失败"
>
<XCircle weight="duotone" class="h-8 w-8" />
</div>
<div
v-else-if="verifyStatus === 'success'"
class="rounded-full bg-emerald-500/10 p-2 text-emerald-500"
aria-label="验证成功"
>
<CheckCircle weight="duotone" class="h-8 w-8" />
</div>
<div v-else class="rounded-full bg-primary/10 p-2 text-primary" aria-label="验证进行中">
<Loader2 weight="duotone" class="h-8 w-8 animate-spin" />
</div>
<CardTitle class="text-2xl">安全验证</CardTitle>
</div>
<CardTitle class="text-2xl">安全验证</CardTitle>
<CardDescription>正在确认你是真实访客,无需任何操作</CardDescription>
</CardHeader>

Expand Down Expand Up @@ -232,7 +279,21 @@ onUnmounted(() => {
</CardContent>

<CardFooter v-if="filePath" class="border-t text-xs text-muted-foreground">
<span class="break-all">目标文件:{{ filePath.split('/').pop() }}</span>
<div class="w-full space-y-1.5">
<p class="break-all">
目标文件:{{ filePath.split('/').pop() }}<span v-if="fileSize">({{ formatSize(fileSize) }})</span>
</p>
<p v-if="fileHash" class="break-all font-mono text-[11px] leading-relaxed">SHA-256:{{ fileHash }}</p>
<a
v-if="fileHash"
:href="githubUrl"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-primary hover:underline"
>
GitHub 项目仓库 →
</a>
</div>
</CardFooter>
</Card>

Expand Down
20 changes: 18 additions & 2 deletions internal/blacklist/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package blacklist

import (
"bufio"
"crypto/tls"
"fmt"
"io"
"lemwood_mirror/internal/db"
Expand All @@ -25,16 +26,31 @@ func init() {
externalIPs = make(map[string]bool)
}

func SyncExternalBlacklist(url string) error {
// insecureTransport 返回跳过 TLS 证书校验的传输层(由 tls_skip_verify 配置显式开启)。
func insecureTransport() *http.Transport {
t := http.DefaultTransport.(*http.Transport).Clone()
if t.TLSClientConfig == nil {
t.TLSClientConfig = &tls.Config{}
}
t.TLSClientConfig.InsecureSkipVerify = true //nolint:gosec // 由 tls_skip_verify 配置显式开启
return t
}

func SyncExternalBlacklist(url string, insecureSkipVerify bool) error {
if url == "" {
return nil
}

syncMu.Lock()
defer syncMu.Unlock()

var transport http.RoundTripper = http.DefaultTransport
if insecureSkipVerify {
transport = insecureTransport()
}
client := &http.Client{
Timeout: 30 * time.Second,
Timeout: 30 * time.Second,
Transport: transport,
}

resp, err := client.Get(url)
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ proxy_url: {{ yaml .ProxyURL }}
asset_proxy_url: {{ yaml .AssetProxyURL }}
xget_domain: {{ yaml .XgetDomain }}
xget_enabled: {{ .XgetEnabled }}
tls_skip_verify: {{ .TLSSkipVerify }}

admin_enabled: {{ .AdminEnabled }}
admin_user: {{ yaml .AdminUser }}
Expand Down Expand Up @@ -191,6 +192,7 @@ type Config struct {
AssetProxyURL string `json:"asset_proxy_url" yaml:"asset_proxy_url"`
XgetDomain string `json:"xget_domain" yaml:"xget_domain"`
XgetEnabled bool `json:"xget_enabled" yaml:"xget_enabled"`
TLSSkipVerify bool `json:"tls_skip_verify" yaml:"tls_skip_verify"`
DownloadTimeoutMinutes int `json:"download_timeout_minutes" yaml:"download_timeout_minutes"`
ConcurrentDownloads int `json:"concurrent_downloads" yaml:"concurrent_downloads"`
DownloadUrlBase string `json:"download_url_base,omitempty" yaml:"download_url_base,omitempty"`
Expand Down
8 changes: 4 additions & 4 deletions internal/db/firewall_dialect_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func dialectIntegrationConfigs() map[string]*config.Config {
MySQLHost: "127.0.0.1", MySQLPort: 33306, MySQLUser: "lemwood", MySQLPassword: "testpass", MySQLDatabase: "lemwood_fw_test",
},
"postgres": {
DatabaseMode: "pgsql",
PostgresHost: "127.0.0.1", PostgresPort: 55432, PostgresUser: user, PostgresDatabase: "lemwood_fw_test", PostgresSSLMode: "disable",
DatabaseMode: "pgsql",
PostgresHost: "127.0.0.1", PostgresPort: 55432, PostgresUser: user, PostgresDatabase: "lemwood_fw_test", PostgresSSLMode: "disable",
},
}
}
Expand Down Expand Up @@ -120,8 +120,8 @@ func TestBlacklistPagedAcrossDialectsIntegration(t *testing.T) {
t.Fatalf("清空 ip_blacklist 失败: %v", err)
}
entries := []struct{ ip, source, reason string }{
{"192.0.2.101", "manual", "a_c"}, // 字面含下划线
{"192.0.2.102", "manual", "abc"}, // 若 _ 被当通配符会被 "a_c" 误命中
{"192.0.2.101", "manual", "a_c"}, // 字面含下划线
{"192.0.2.102", "manual", "abc"}, // 若 _ 被当通配符会被 "a_c" 误命中
{"198.51.100.7", "external", "外部同步说明"},
{"203.0.113.99", "local", "流量超限自动封禁"},
}
Expand Down
Loading
Loading