明确前后端开发端口分工
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-07 16:07:50 +08:00
parent 6ad6f449ca
commit 362cf0d4cc
8 changed files with 99 additions and 27 deletions

View File

@ -77,7 +77,9 @@ func CurrentHTTPConfig() HTTPConfig {
func ResolveHTTPPortDefaults(value HTTPConfig, setupPort int) HTTPConfig {
result := cloneHTTPConfig(value)
result.TLSMode = strings.ToLower(strings.TrimSpace(result.TLSMode))
if result.Port == 0 {
if override := environmentPort("SMAIL_HTTP_PORT_OVERRIDE"); override != 0 {
result.Port = override
} else if result.Port == 0 {
result.Port = environmentPort("SMAIL_HTTP_PORT")
if result.Port == 0 && validServicePort(setupPort) {
result.Port = setupPort
@ -86,6 +88,9 @@ func ResolveHTTPPortDefaults(value HTTPConfig, setupPort int) HTTPConfig {
result.Port = 80
}
}
if override := strings.ToLower(strings.TrimSpace(os.Getenv("SMAIL_HTTP_TLS_MODE_OVERRIDE"))); override == HTTPSTLSModeDirect || override == HTTPSTLSModeProxy {
result.TLSMode = override
}
if result.TLSMode == HTTPSTLSModeProxy {
result.HTTPSPort = 0
result.HTTPSEnabled = false

View File

@ -3,6 +3,8 @@ package config
import "testing"
func TestResolveHTTPPortDefaultsPreservesConfiguredPorts(t *testing.T) {
t.Setenv("SMAIL_HTTP_PORT_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_TLS_MODE_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_PORT", "19001")
t.Setenv("SMAIL_HTTPS_PORT", "19002")
@ -17,6 +19,8 @@ func TestResolveHTTPPortDefaultsPreservesConfiguredPorts(t *testing.T) {
}
func TestResolveHTTPPortDefaultsUsesEnvironmentPorts(t *testing.T) {
t.Setenv("SMAIL_HTTP_PORT_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_TLS_MODE_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_PORT", "9001")
t.Setenv("SMAIL_HTTPS_PORT", "9002")
@ -27,6 +31,8 @@ func TestResolveHTTPPortDefaultsUsesEnvironmentPorts(t *testing.T) {
}
func TestResolveHTTPPortDefaultsUsesSetupAndDirectDefaults(t *testing.T) {
t.Setenv("SMAIL_HTTP_PORT_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_TLS_MODE_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_PORT", "")
t.Setenv("SMAIL_HTTPS_PORT", "")
@ -37,6 +43,8 @@ func TestResolveHTTPPortDefaultsUsesSetupAndDirectDefaults(t *testing.T) {
}
func TestResolveHTTPPortDefaultsDisablesInternalHTTPSInProxyMode(t *testing.T) {
t.Setenv("SMAIL_HTTP_PORT_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_TLS_MODE_OVERRIDE", "")
t.Setenv("SMAIL_HTTP_PORT", "9001")
t.Setenv("SMAIL_HTTPS_PORT", "9002")
@ -49,3 +57,20 @@ func TestResolveHTTPPortDefaultsDisablesInternalHTTPSInProxyMode(t *testing.T) {
t.Fatalf("proxy HTTP config = %+v, want HTTP 9001 with internal HTTPS disabled", got)
}
}
func TestResolveHTTPPortDefaultsUsesExplicitRuntimeOverride(t *testing.T) {
t.Setenv("SMAIL_HTTP_PORT_OVERRIDE", "5201")
t.Setenv("SMAIL_HTTP_TLS_MODE_OVERRIDE", HTTPSTLSModeProxy)
t.Setenv("SMAIL_HTTP_PORT", "9001")
t.Setenv("SMAIL_HTTPS_PORT", "9002")
got := ResolveHTTPPortDefaults(HTTPConfig{
Port: 80,
HTTPSPort: 443,
HTTPSEnabled: true,
TLSMode: HTTPSTLSModeDirect,
}, 18080)
if got.Port != 5201 || got.HTTPSPort != 0 || got.HTTPSEnabled || got.TLSMode != HTTPSTLSModeProxy {
t.Fatalf("runtime override HTTP config = %+v, want proxy API on port 5201", got)
}
}

View File

@ -31,9 +31,10 @@ func ResolveWebAuthnSecurity(root RootConfig) (SecurityConfig, error) {
}
if len(security.WebAuthnRPOrigins) == 0 {
origin := "https://" + web
httpConfig := ResolveHTTPPortDefaults(root.HTTP, 0)
// 反代的外部端口不等于内部 HTTP 端口,非标准外部端口需显式配置。
if root.HTTP.TLSMode != HTTPSTLSModeProxy {
port := ResolveHTTPPortDefaults(root.HTTP, 0).HTTPSPort
if httpConfig.TLSMode != HTTPSTLSModeProxy {
port := httpConfig.HTTPSPort
if !validServicePort(port) {
return SecurityConfig{}, fmt.Errorf("安全密钥站点 HTTPS 端口无效")
}

View File

@ -463,10 +463,7 @@ func dirInit() error {
// 等待 HTTP 服务启动,最长约 90 秒。
func waitHTTPReady(serviceErrors <-chan error) error {
port := 80
if cfg := config.CurrentConfig(); cfg != nil && cfg.HttpPort > 0 {
port = cfg.HttpPort
}
port := config.CurrentHTTPConfig().Port
url := fmt.Sprintf("http://127.0.0.1:%d/api/ping", port)
client := &http.Client{Timeout: 2 * time.Second}

View File

@ -9,10 +9,13 @@ $logs = Join-Path $root 'test-log\runtime'
New-Item -ItemType Directory -Path $logs -Force | Out-Null
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
$previousRoot = $env:SMAIL_ROOT
$previousHTTPPortOverride = $env:SMAIL_HTTP_PORT_OVERRIDE
$previousHTTPTLSModeOverride = $env:SMAIL_HTTP_TLS_MODE_OVERRIDE
$previousSetupPort = $env:setup_port
try {
if ($Service -eq 'backend') {
$binary = Join-Path $root 'out\backend\smail-dev.exe'
if (Get-NetTCPConnection -State Listen -LocalPort 443 -ErrorAction SilentlyContinue) { throw '443 端口已占用,请先确认并停止旧后端' }
if (Get-NetTCPConnection -State Listen -LocalPort 5201 -ErrorAction SilentlyContinue) { throw '5201 端口已占用,请先确认并停止旧后端' }
$running = Get-CimInstance Win32_Process | Where-Object {
$_.ExecutablePath -and $_.ExecutablePath -eq $binary
}
@ -32,8 +35,11 @@ try {
if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw '请先运行 deploy/build.ps1 编译后端' }
# SMAIL_ROOT 是 config 的父目录,不能指向 config 本身或编译输出目录。
$env:SMAIL_ROOT = $root
$env:SMAIL_HTTP_PORT_OVERRIDE = '5201'
$env:SMAIL_HTTP_TLS_MODE_OVERRIDE = 'proxy'
$env:setup_port = '5201'
$arguments = @('--dev-disable-admin-reset')
Write-Output '开发启动:沿用 config/config.json 和现有数据库;明确停用管理员恢复通道,不请求权限提升。'
Write-Output '开发启动:后端 API 仅监听 5201;沿用 config/config.json 和现有数据库;明确停用管理员恢复通道,不请求权限提升。'
if ($SkipBuild) { Write-Warning '已跳过编译,请自行确认二进制与当前源码一致。' }
# 插件从 out/plugins 加载;账号、配置和密钥仍通过 SMAIL_ROOT 定位到根目录 config。
foreach ($name in @('spam_block.exe', 'pmail_telegram_push.exe', 'wechat_push.exe')) {
@ -43,11 +49,13 @@ try {
}
$workingDirectory = Join-Path $root 'out'
} else {
if (Get-NetTCPConnection -State Listen -LocalPort 15173 -ErrorAction SilentlyContinue) { throw '15173 端口已占用,请先确认并停止旧前端' }
foreach ($port in @(80, 443)) {
if (Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue) { throw "$port 端口已占用,请先确认并停止旧前端或旧后端" }
}
$binary = (Get-Command node -ErrorAction Stop).Source
$vite = Join-Path $root 'frontend\node_modules\vite\bin\vite.js'
if (-not (Test-Path -LiteralPath $vite)) { throw '请先安装 frontend 的依赖' }
$arguments = @(('"{0}"' -f $vite), '--host', '127.0.0.1', '--port', '15173', '--strictPort')
$arguments = @(('"{0}"' -f $vite), '--host', '127.0.0.1', '--port', '443', '--strictPort')
$workingDirectory = Join-Path $root 'frontend'
}
$stdout = Join-Path $logs "$Service-$stamp.stdout.log"
@ -61,4 +69,7 @@ try {
Write-Output "运行文件:$binary;标准输出:$stdout;错误日志:$stderr"
} finally {
$env:SMAIL_ROOT = $previousRoot
$env:SMAIL_HTTP_PORT_OVERRIDE = $previousHTTPPortOverride
$env:SMAIL_HTTP_TLS_MODE_OVERRIDE = $previousHTTPTLSModeOverride
$env:setup_port = $previousSetupPort
}

View File

@ -1 +1,5 @@
VITE_DEV_BACKEND_URL=https://127.0.0.1:443
VITE_DEV_BACKEND_URL=http://127.0.0.1:5201
VITE_DEV_BACKEND_ORIGIN=https://xuans.com
VITE_DEV_FRONTEND_ORIGIN=https://xuans.com
SMAIL_DEV_TLS_CERTIFICATE=../config/ssl/xuans.com.crt
SMAIL_DEV_TLS_PRIVATE_KEY=../config/ssl/xuans.com.key

View File

@ -1,4 +1,7 @@
/* eslint-env node */
import { readFileSync } from 'node:fs'
import { createServer as createHTTPServer } from 'node:http'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
@ -6,27 +9,40 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'
const developmentHTTPRedirect = (frontendOrigin, host = '127.0.0.1', port = 80) => ({
name: 'smail-development-http-redirect',
apply: 'serve',
async configureServer(server) {
const redirectServer = createHTTPServer((request, response) => {
const requestURL = new URL(request.url || '/', 'http://127.0.0.1')
const location = new URL(`${requestURL.pathname}${requestURL.search}`, frontendOrigin)
response.writeHead(308, {
Location: location.toString(),
'Cache-Control': 'no-store'
})
response.end()
})
await new Promise((resolve, reject) => {
redirectServer.once('error', reject)
redirectServer.listen(port, host, resolve)
})
server.httpServer?.once('close', () => redirectServer.close())
}
})
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const backendUrl = (env.VITE_DEV_BACKEND_URL || 'https://127.0.0.1:443').replace(/\/$/, '')
const backendUrl = (env.VITE_DEV_BACKEND_URL || 'http://127.0.0.1:5201').replace(/\/$/, '')
const backendOrigin = (env.VITE_DEV_BACKEND_ORIGIN || 'https://xuans.com').replace(/\/$/, '')
const rewriteDevelopmentCookies = (proxyResponse) => {
const setCookies = proxyResponse.headers['set-cookie']
if (!Array.isArray(setCookies)) {
return
}
proxyResponse.headers['set-cookie'] = setCookies.map((cookie) => (
cookie
.replace(/;\s*Secure/gi, '')
.replace(/;\s*Domain=[^;]*/gi, '')
))
}
const frontendOrigin = (env.VITE_DEV_FRONTEND_ORIGIN || 'https://xuans.com').replace(/\/$/, '')
const certificatePath = path.resolve(process.cwd(), env.SMAIL_DEV_TLS_CERTIFICATE || '../config/ssl/xuans.com.crt')
const privateKeyPath = path.resolve(process.cwd(), env.SMAIL_DEV_TLS_PRIVATE_KEY || '../config/ssl/xuans.com.key')
return {
plugins: [
developmentHTTPRedirect(frontendOrigin),
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
@ -41,6 +57,13 @@ export default defineConfig(({ mode }) => {
},
},
server: {
host: '127.0.0.1',
port: 443,
strictPort: true,
https: {
cert: readFileSync(certificatePath),
key: readFileSync(privateKeyPath)
},
cors: true,
proxy: {
"/api": {
@ -55,7 +78,6 @@ export default defineConfig(({ mode }) => {
proxyRequest.setHeader('x-forwarded-proto', originURL.protocol.replace(':', ''))
proxyRequest.removeHeader('referer')
})
proxy.on('proxyRes', rewriteDevelopmentCookies)
}
},
"/attachments": {

View File

@ -52,6 +52,13 @@ test('开发启动必须固定运行配置根目录并集中保存日志', async
assert.match(content, /-WindowStyle Hidden/)
assert.match(content, /\[switch\]\$SkipBuild/)
assert.match(content, /--dev-disable-admin-reset/)
assert.match(content, /SMAIL_HTTP_PORT_OVERRIDE = '5201'/)
assert.match(content, /SMAIL_HTTP_TLS_MODE_OVERRIDE = 'proxy'/)
assert.match(content, /LocalPort 5201/)
assert.match(content, /@\(80, 443\)/)
assert.match(await read('frontend/.env.development'), /VITE_DEV_BACKEND_URL=http:\/\/127\.0\.0\.1:5201/)
assert.match(await read('frontend/vite.config.js'), /port: 443/)
assert.match(await read('frontend/vite.config.js'), /smail-development-http-redirect/)
assert.match(content, /默认先编译当前工作区/)
assert.ok(content.charCodeAt(0) === 0xfeff, 'Windows PowerShell 5 脚本应有 UTF-8 BOM')
})