This commit is contained in:
parent
9f144c7448
commit
08b1f41350
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
@ -20,8 +21,11 @@ import (
|
|||||||
|
|
||||||
// 这个服务只负责 HTTP 到 HTTPS 的安全跳转,不承载业务会话。
|
// 这个服务只负责 HTTP 到 HTTPS 的安全跳转,不承载业务会话。
|
||||||
var httpServer *http.Server
|
var httpServer *http.Server
|
||||||
|
var httpServerMu sync.Mutex
|
||||||
|
|
||||||
func HttpStop() {
|
func HttpStop() {
|
||||||
|
httpServerMu.Lock()
|
||||||
|
defer httpServerMu.Unlock()
|
||||||
if httpServer != nil {
|
if httpServer != nil {
|
||||||
if err := httpServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := httpServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.WithError(err).Warn("close HTTP server failed")
|
log.WithError(err).Warn("close HTTP server failed")
|
||||||
@ -276,8 +280,29 @@ func allowRouteMethod(w http.ResponseWriter, r *http.Request, allowed ...string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HttpStart() error {
|
func HttpStart() error {
|
||||||
|
server, err := prepareHTTPServer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return serveHTTPServer(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveHTTPServer(server *http.Server) error {
|
||||||
|
log.WithFields(log.Fields{"action": "启动 HTTP 监听", "object": server.Addr, "result": "started"}).Info("开始绑定监听端口")
|
||||||
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return fmt.Errorf("serve HTTP: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHTTPServer() (*http.Server, error) {
|
||||||
|
httpServerMu.Lock()
|
||||||
|
defer httpServerMu.Unlock()
|
||||||
|
if httpServer != nil {
|
||||||
|
return nil, fmt.Errorf("HTTP server is already started")
|
||||||
|
}
|
||||||
if config.CurrentConfig() == nil && config.CurrentManager() == nil {
|
if config.CurrentConfig() == nil && config.CurrentManager() == nil {
|
||||||
return fmt.Errorf("runtime configuration is unavailable")
|
return nil, fmt.Errorf("runtime configuration is unavailable")
|
||||||
}
|
}
|
||||||
cfg := config.CurrentHTTPConfig()
|
cfg := config.CurrentHTTPConfig()
|
||||||
httpPort := 80
|
httpPort := 80
|
||||||
@ -287,7 +312,7 @@ func HttpStart() error {
|
|||||||
|
|
||||||
handler, err := newHTTPHandler(cfg)
|
handler, err := newHTTPHandler(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
httpServer = &http.Server{
|
httpServer = &http.Server{
|
||||||
@ -299,11 +324,7 @@ func HttpStart() error {
|
|||||||
IdleTimeout: time.Second * 30,
|
IdleTimeout: time.Second * 30,
|
||||||
MaxHeaderBytes: 16 << 10,
|
MaxHeaderBytes: 16 << 10,
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{"action": "启动 HTTP 监听", "object": fmt.Sprintf(":%d", httpPort), "result": "started"}).Info("开始绑定监听端口")
|
return httpServer, nil
|
||||||
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
return fmt.Errorf("serve HTTP: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPHandler(cfg config.HTTPConfig) (http.Handler, error) {
|
func newHTTPHandler(cfg config.HTTPConfig) (http.Handler, error) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
@ -32,6 +33,7 @@ import (
|
|||||||
var local embed.FS
|
var local embed.FS
|
||||||
|
|
||||||
var httpsServer *http.Server
|
var httpsServer *http.Server
|
||||||
|
var httpsServerMu sync.Mutex
|
||||||
|
|
||||||
type nullWrite struct{}
|
type nullWrite struct{}
|
||||||
|
|
||||||
@ -41,25 +43,53 @@ func (w *nullWrite) Write(p []byte) (int, error) {
|
|||||||
|
|
||||||
// HttpsStart 启动仅承载业务的 HTTPS 服务。
|
// HttpsStart 启动仅承载业务的 HTTPS 服务。
|
||||||
func HttpsStart() error {
|
func HttpsStart() error {
|
||||||
|
server, err := prepareHTTPSServer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return serveHTTPSServer(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveHTTPSServer(server *http.Server) error {
|
||||||
|
if server == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.WithFields(log.Fields{"action": "启动 HTTPS 监听", "object": server.Addr, "result": "started"}).Info("开始绑定监听端口")
|
||||||
|
if err := server.ListenAndServeTLS("", ""); err != nil {
|
||||||
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.WithFields(log.Fields{"action": "停止 HTTPS 监听", "object": server.Addr, "result": "success"}).Info("监听已正常关闭")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("serve HTTPS on %s: %w", server.Addr, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHTTPSServer() (*http.Server, error) {
|
||||||
|
httpsServerMu.Lock()
|
||||||
|
defer httpsServerMu.Unlock()
|
||||||
|
if httpsServer != nil {
|
||||||
|
return nil, fmt.Errorf("HTTPS server is already started")
|
||||||
|
}
|
||||||
legacyCfg := config.CurrentConfig()
|
legacyCfg := config.CurrentConfig()
|
||||||
if legacyCfg == nil && config.CurrentManager() == nil {
|
if legacyCfg == nil && config.CurrentManager() == nil {
|
||||||
return fmt.Errorf("runtime configuration is unavailable")
|
return nil, fmt.Errorf("runtime configuration is unavailable")
|
||||||
}
|
}
|
||||||
httpCfg := config.CurrentHTTPConfig()
|
httpCfg := config.CurrentHTTPConfig()
|
||||||
if httpCfg.TLSMode == config.HTTPSTLSModeProxy {
|
if httpCfg.TLSMode == config.HTTPSTLSModeProxy {
|
||||||
log.WithFields(log.Fields{"action": "启动内置 HTTPS 监听", "result": "skipped"}).Info("HTTPS 由反向代理处理,跳过内置监听")
|
log.WithFields(log.Fields{"action": "启动内置 HTTPS 监听", "result": "skipped"}).Info("HTTPS 由反向代理处理,跳过内置监听")
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
if err := router(mux); err != nil {
|
if err := router(mux); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
publicRecovery, err := newPublicRecoveryHandler()
|
publicRecovery, err := newPublicRecoveryHandler()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if session.Instance == nil {
|
if session.Instance == nil {
|
||||||
return fmt.Errorf("session manager is unavailable")
|
return nil, fmt.Errorf("session manager is unavailable")
|
||||||
}
|
}
|
||||||
|
|
||||||
nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
|
nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
|
||||||
@ -69,14 +99,14 @@ func HttpsStart() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.DevelopmentMode && legacyCfg != nil && legacyCfg.HttpsEnabled == 2 {
|
if config.DevelopmentMode && legacyCfg != nil && legacyCfg.HttpsEnabled == 2 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if legacyCfg == nil {
|
if legacyCfg == nil {
|
||||||
return fmt.Errorf("legacy SSL configuration is unavailable")
|
return nil, fmt.Errorf("legacy SSL configuration is unavailable")
|
||||||
}
|
}
|
||||||
tlsConfig, err := config.NewRuntimeTLSConfig()
|
tlsConfig, err := config.NewRuntimeTLSConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve HTTPS certificate: %w", err)
|
return nil, fmt.Errorf("resolve HTTPS certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpsServer = &http.Server{
|
httpsServer = &http.Server{
|
||||||
@ -90,18 +120,12 @@ func HttpsStart() error {
|
|||||||
MaxHeaderBytes: 16 << 10,
|
MaxHeaderBytes: 16 << 10,
|
||||||
ErrorLog: nullLog,
|
ErrorLog: nullLog,
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{"action": "启动 HTTPS 监听", "object": httpsServer.Addr, "result": "started"}).Info("开始绑定监听端口")
|
return httpsServer, nil
|
||||||
if err := httpsServer.ListenAndServeTLS("", ""); err != nil {
|
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.WithFields(log.Fields{"action": "停止 HTTPS 监听", "object": httpsServer.Addr, "result": "success"}).Info("监听已正常关闭")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("serve HTTPS on port %d: %w", httpsPort, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HttpsStop() {
|
func HttpsStop() {
|
||||||
|
httpsServerMu.Lock()
|
||||||
|
defer httpsServerMu.Unlock()
|
||||||
if httpsServer != nil {
|
if httpsServer != nil {
|
||||||
if err := httpsServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := httpsServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.WithError(err).Warn("close HTTPS server failed")
|
log.WithError(err).Warn("close HTTPS server failed")
|
||||||
|
|||||||
16
backend/listen/http_server/listeners.go
Normal file
16
backend/listen/http_server/listeners.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package http_server
|
||||||
|
|
||||||
|
// PrepareWebServers 在提交监听任务前同步创建实例,防止停止后才发布新的监听器。
|
||||||
|
func PrepareWebServers() (httpRun, httpsRun func() error, err error) {
|
||||||
|
httpInstance, err := prepareHTTPServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
httpsInstance, err := prepareHTTPSServer()
|
||||||
|
if err != nil {
|
||||||
|
HttpStop()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return func() error { return serveHTTPServer(httpInstance) },
|
||||||
|
func() error { return serveHTTPSServer(httpsInstance) }, nil
|
||||||
|
}
|
||||||
89
backend/listen/http_server/listeners_test.go
Normal file
89
backend/listen/http_server/listeners_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package http_server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebListenersStopBeforeServe(t *testing.T) {
|
||||||
|
certificateServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||||
|
tlsConfig := certificateServer.TLS.Clone()
|
||||||
|
certificateServer.Close()
|
||||||
|
for _, secure := range []bool{false, true} {
|
||||||
|
server := &http.Server{Addr: "127.0.0.1:0", TLSConfig: tlsConfig.Clone()}
|
||||||
|
var stop func()
|
||||||
|
var serve func(*http.Server) error
|
||||||
|
if secure {
|
||||||
|
httpsServer = server
|
||||||
|
stop, serve = HttpsStop, serveHTTPSServer
|
||||||
|
} else {
|
||||||
|
httpServer = server
|
||||||
|
stop, serve = HttpStop, serveHTTPServer
|
||||||
|
}
|
||||||
|
stop()
|
||||||
|
if err := serve(server); err != nil {
|
||||||
|
t.Fatalf("secure=%v: stopped listener returned %v", secure, err)
|
||||||
|
}
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebListenersConcurrentStop(t *testing.T) {
|
||||||
|
certificateServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||||
|
certificates := append([]tls.Certificate(nil), certificateServer.TLS.Certificates...)
|
||||||
|
certificateServer.Close()
|
||||||
|
for _, secure := range []bool{false, true} {
|
||||||
|
server := &http.Server{Addr: "127.0.0.1:0", TLSConfig: &tls.Config{Certificates: certificates}}
|
||||||
|
var stop func()
|
||||||
|
var serve func(*http.Server) error
|
||||||
|
if secure {
|
||||||
|
httpsServer = server
|
||||||
|
stop, serve = HttpsStop, serveHTTPSServer
|
||||||
|
} else {
|
||||||
|
httpServer = server
|
||||||
|
stop, serve = HttpStop, serveHTTPServer
|
||||||
|
}
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- serve(server) }()
|
||||||
|
var workers sync.WaitGroup
|
||||||
|
for range 10 {
|
||||||
|
workers.Add(1)
|
||||||
|
go func() { defer workers.Done(); stop() }()
|
||||||
|
}
|
||||||
|
workers.Wait()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("listener did not stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareWebServersClosesHTTPOnHTTPSFailure(t *testing.T) {
|
||||||
|
oldConfig, oldManager, oldDevelopment := config.Instance, config.CurrentManager(), config.DevelopmentMode
|
||||||
|
config.SetRuntimeManager(nil)
|
||||||
|
config.Instance = &config.Config{WebDomain: "example.com", HttpsEnabled: 1}
|
||||||
|
config.DevelopmentMode = false
|
||||||
|
t.Cleanup(func() {
|
||||||
|
HttpStop()
|
||||||
|
HttpsStop()
|
||||||
|
config.Instance = oldConfig
|
||||||
|
config.SetRuntimeManager(oldManager)
|
||||||
|
config.DevelopmentMode = oldDevelopment
|
||||||
|
})
|
||||||
|
if _, _, err := PrepareWebServers(); err == nil {
|
||||||
|
t.Fatal("missing HTTPS dependencies were accepted")
|
||||||
|
}
|
||||||
|
if httpServer != nil {
|
||||||
|
t.Fatal("HTTP listener was retained after HTTPS preparation failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package http_server
|
package http_server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@ -8,8 +9,50 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
|
"git.pcux.cn/xuans/Smail/dto/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestSetupAuthenticationStatusRequiresBootstrapAuthorization(t *testing.T) {
|
||||||
|
const token = "0123456789abcdefghijklmnop"
|
||||||
|
for _, endpoint := range []string{"/api/user/info", "/api/v1/user/info"} {
|
||||||
|
t.Run(endpoint, func(t *testing.T) {
|
||||||
|
security := withSetupSecurity(t, token)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
registerSetupAPIRoutes(mux, security, func(http.ResponseWriter, *http.Request) { t.Fatal("setup mutation invoked") })
|
||||||
|
for _, scenario := range []struct {
|
||||||
|
method, source, token string
|
||||||
|
ready bool
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{http.MethodPost, "127.0.0.1:1234", token, false, http.StatusOK},
|
||||||
|
{http.MethodPost, "127.0.0.1:1234", "", false, http.StatusForbidden},
|
||||||
|
{http.MethodPost, "192.0.2.1:1234", token, false, http.StatusForbidden},
|
||||||
|
{http.MethodGet, "127.0.0.1:1234", token, false, http.StatusMethodNotAllowed},
|
||||||
|
{http.MethodPost, "127.0.0.1:1234", token, true, http.StatusForbidden},
|
||||||
|
} {
|
||||||
|
config.SetInitState(scenario.ready)
|
||||||
|
request := httptest.NewRequest(scenario.method, endpoint, nil)
|
||||||
|
request.RemoteAddr = scenario.source
|
||||||
|
request.AddCookie(&http.Cookie{Name: setupCookieName, Value: scenario.token})
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != scenario.status {
|
||||||
|
t.Fatalf("status = %d, want %d", recorder.Code, scenario.status)
|
||||||
|
}
|
||||||
|
if recorder.Code == http.StatusOK {
|
||||||
|
var result response.Response
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil || result.ErrorNo != response.NeedSetup {
|
||||||
|
t.Fatalf("setup state response = %+v, error = %v", result, err)
|
||||||
|
}
|
||||||
|
if recorder.Header().Get("Cache-Control") != "no-store" {
|
||||||
|
t.Fatal("setup state can be cached")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func withSetupSecurity(t *testing.T, token string) *setupSecurity {
|
func withSetupSecurity(t *testing.T, token string) *setupSecurity {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
oldToken := os.Getenv("SMAIL_SETUP_TOKEN")
|
oldToken := os.Getenv("SMAIL_SETUP_TOKEN")
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
"git.pcux.cn/xuans/Smail/controllers"
|
"git.pcux.cn/xuans/Smail/controllers"
|
||||||
|
"git.pcux.cn/xuans/Smail/dto/response"
|
||||||
"git.pcux.cn/xuans/Smail/utils/ip"
|
"git.pcux.cn/xuans/Smail/utils/ip"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
@ -89,6 +90,14 @@ func registerSetupAPIRoutes(mux *http.ServeMux, security *setupSecurity, next ht
|
|||||||
authorized := security.authorize(next)
|
authorized := security.authorize(next)
|
||||||
mux.HandleFunc("/api/setup", authorized)
|
mux.HandleFunc("/api/setup", authorized)
|
||||||
mux.HandleFunc("/api/v1/setup", authorized)
|
mux.HandleFunc("/api/v1/setup", authorized)
|
||||||
|
// 安装阶段尚无业务会话,通过受保护的状态接口引导前端进入安装向导。
|
||||||
|
status := security.authorize(setupOnlyPost(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
response.NewErrorResponse(response.NeedSetup, "请先完成初始化", "").FPrint(w)
|
||||||
|
}))
|
||||||
|
mux.HandleFunc("/api/user/info", status)
|
||||||
|
mux.HandleFunc("/api/v1/user/info", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupOnlyPost(next http.HandlerFunc) http.HandlerFunc {
|
func setupOnlyPost(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
|||||||
@ -215,6 +215,11 @@ func Init(serverVersion string) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
hooks.Init(serverVersion)
|
hooks.Init(serverVersion)
|
||||||
|
httpRun, httpsRun, err := http_server.PrepareWebServers()
|
||||||
|
if err != nil {
|
||||||
|
stopServices()
|
||||||
|
return fmt.Errorf("prepare web services: %w", err)
|
||||||
|
}
|
||||||
mailSecurity := config.CurrentMailSecurity()
|
mailSecurity := config.CurrentMailSecurity()
|
||||||
// SMTP 25 用于服务器间投递,始终保留并支持机会性 STARTTLS。
|
// SMTP 25 用于服务器间投递,始终保留并支持机会性 STARTTLS。
|
||||||
startService(serviceErrors, "SMTP", smtp_server.Start)
|
startService(serviceErrors, "SMTP", smtp_server.Start)
|
||||||
@ -225,8 +230,8 @@ func Init(serverVersion string) error {
|
|||||||
startService(serviceErrors, "SMTP STARTTLS", smtp_server.StartWithTLSNew)
|
startService(serviceErrors, "SMTP STARTTLS", smtp_server.StartWithTLSNew)
|
||||||
}
|
}
|
||||||
// http server start
|
// http server start
|
||||||
startService(serviceErrors, "HTTPS", http_server.HttpsStart)
|
startService(serviceErrors, "HTTPS", httpsRun)
|
||||||
startService(serviceErrors, "HTTP", http_server.HttpStart)
|
startService(serviceErrors, "HTTP", httpRun)
|
||||||
|
|
||||||
// 等待 HTTP 服务启动后再执行需要 HTTP 回调的 ACME 操作。
|
// 等待 HTTP 服务启动后再执行需要 HTTP 回调的 ACME 操作。
|
||||||
if err := waitHTTPReady(serviceErrors); err != nil {
|
if err := waitHTTPReady(serviceErrors); err != nil {
|
||||||
|
|||||||
@ -29,6 +29,8 @@
|
|||||||
|
|
||||||
## 验收说明
|
## 验收说明
|
||||||
|
|
||||||
Web 功能使用 MCP 模拟键鼠测试;终端仅用于源码检查、构建、部署以及本机重置工具本身的交互,不调用业务 API 或编写功能测试脚本。保留现有测试业务库,不清空邮件、账号或外部邮箱绑定。
|
Web 功能使用 MCP 模拟键鼠测试;终端用于源码检查、构建、部署、授权范围内的数据备份清理以及本机重置工具本身的交互,不调用业务 API 代替页面验收。2026 年 9 月 16 日用户已授权清理测试数据,本轮限定在 `yunx-test`,先备份所有测试数据库再清空,保留数据库文件并重新执行初始化。不得将此授权扩大到其他部署环境。
|
||||||
|
|
||||||
具体测试结果记录到 `test-log/日期/测试log`;收到验证码、页面提示请求已受理、构建成功均不单独等于密码恢复闭环完成。
|
具体测试结果记录到 `test-log/日期/测试log`;收到验证码、页面提示请求已受理、构建成功均不单独等于密码恢复闭环完成。
|
||||||
|
|
||||||
|
本轮结果见《清库部署测试记录》。
|
||||||
|
|||||||
55
docs/清库部署测试记录.md
Normal file
55
docs/清库部署测试记录.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# 清库部署测试记录
|
||||||
|
|
||||||
|
## 范围与状态
|
||||||
|
|
||||||
|
日期:2026 年 9 月 16 日。仅测试本机 `D:\Desktop\Smail\yunx-test`,使用正式构建包和嵌入前端,未启用开发模式或停用本机恢复通道。
|
||||||
|
|
||||||
|
- [✓] 备份三份 SQLite 数据库和配置,校验备份后清空数据库对象,保留数据库文件。
|
||||||
|
- [✓] 完整构建、部署及部署指纹校验通过。
|
||||||
|
- [x] 首轮通过浏览器完成数据库、超级管理员、域名、DNS 展示和手动证书初始化;正式业务服务未就绪。
|
||||||
|
- [✓] 修复安装根地址认证探测失败,隔离浏览器直接访问根地址后自动进入中文安装向导。
|
||||||
|
- [✓] 修复 HTTP、HTTPS 全局实例在停止期间被清空而导致的崩溃;启动任务发布前同步准备实例,关闭已准备但尚未监听的实例也能正常退出。
|
||||||
|
- [✓] 全量自动回归与定向竞态检查通过。
|
||||||
|
- [!] 正式登录、刷新会话、普通用户及管理员邮件恢复、旧密码与旧会话失效实机验收未完成。
|
||||||
|
- [!] 超级管理员受保护本机恢复通道实机验收未完成。
|
||||||
|
|
||||||
|
## 实机证据
|
||||||
|
|
||||||
|
第一轮直接访问安装根地址出现“认证服务暂时不可用”。安装服务缺少 `/api/v1/user/info` 的初始化状态响应,前端未得到需要初始化的业务码。修复后,两个版本的认证探测路径均复用安装令牌和来源检查,只返回需要初始化状态,不执行账号或配置写入。
|
||||||
|
|
||||||
|
在全新隔离浏览器中访问 `http://127.0.0.1:5201/`,页面自动进入 `/#/setup?redirect=/`,显示中文六步安装向导,未再出现认证服务不可用。已查看页面快照和桌面截图;截图工具不允许写入当前工作区,因此截图仅保留在会话中,未声称落盘。
|
||||||
|
|
||||||
|
第一轮完成初始化后,Windows Named Pipe 在接受连接时返回 `Access is denied`,随后 HTTP 停止与启动并发导致空指针崩溃。最新构建重试相同权限环境后,保留明确权限错误并退出,未再发生空指针崩溃,监听端口已释放。
|
||||||
|
|
||||||
|
当前进程使用未提升的中等完整性令牌,管理员组仅用于拒绝检查。本机通道 ACL 仍限定 SYSTEM 与管理员,未放宽 ACL,未改成公开 HTTP 或 TCP 接口。
|
||||||
|
|
||||||
|
## 自动验证
|
||||||
|
|
||||||
|
- `go test ./listen/http_server ./res_init` 通过。
|
||||||
|
- `node test/run.mjs all` 通过:前端 166 项、部署 4 项以及全部后端包。部分未变更包使用 Go 测试缓存,不能据此认定受权限限制的 Windows IPC 用例已实机执行。
|
||||||
|
- `node test/run.mjs backend -race ./listen/http_server ./res_init` 通过。
|
||||||
|
- 新增覆盖:HTTP 和 HTTPS 停止先于监听、多个停止任务并发、HTTPS 准备失败清理 HTTP,以及安装状态接口的有效令牌、缺少令牌、来源限制、方法限制和初始化完成后拒绝访问。
|
||||||
|
- `git diff --check` 通过,仅存在仓库已有换行策略提示。
|
||||||
|
|
||||||
|
首次误将构建与回归并行调度,触发工作区锁且导致竞态测试读取不到正在移动的嵌入资源。已改用串行调度重跑并通过,此次失败不作为产品回归结论。
|
||||||
|
|
||||||
|
## 备份与日志
|
||||||
|
|
||||||
|
以下路径包含本地测试数据或配置,均为忽略的运行产物,不提交仓库:
|
||||||
|
|
||||||
|
- `test-log/live-20260916-1938/backup/`:清库前配置和数据库。
|
||||||
|
- `test-log/live-20260916-1938/reset-partition-config/`:归档的旧分区配置。
|
||||||
|
- `test-log/live-20260916-1938/before-retest/`:首轮初始化后、第二次清库前的配置和数据库。
|
||||||
|
- `test-log/live-20260916-1938/fresh.stderr.log`:原始崩溃证据。
|
||||||
|
- `test-log/live-20260916-1938/fixed.stdout.log` 与 `fixed.stderr.log`:修复后正式启动权限失败与退出证据。
|
||||||
|
- `test-log/live-20260916-1938/setup-retest.stdout.log`:最新安装向导进程日志。
|
||||||
|
- `test-log/run-2026-09-16T11-49-49-921Z-5240/`:成功的全量回归。
|
||||||
|
- `test-log/run-2026-09-16T11-51-52-136Z-29980/`:成功的竞态检查。
|
||||||
|
|
||||||
|
第二次清库后保留安装向导运行于 `127.0.0.1:5201`,数据库为空、配置未初始化,证书与业务密钥未删除。没有将旧账号备份恢复到当前运行库。
|
||||||
|
|
||||||
|
## 后续条件
|
||||||
|
|
||||||
|
后续正式流程需要符合项目部署要求的 Windows 权限环境或受支持的 Linux 部署环境。当前会话无法提供已提升的 Windows 令牌,不通过绕过权限限制推进验收。
|
||||||
|
|
||||||
|
在具备权限的环境中重新完成初始化并确认服务稳定后,再验证登录、会话刷新和邮箱恢复。邮件恢复还需要确定实际可接收验证码的外部测试邮箱;验证码收到、恢复页面打开或接口返回成功均不单独构成完整验收。
|
||||||
Loading…
Reference in New Issue
Block a user