From 1cdcfa2c2d63df89ddde44c160105bb169a1b6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 15 May 2026 11:47:34 +0800 Subject: [PATCH] Add files via upload --- internal/app/app.go | 56 ++++++++++++++++++--- internal/app/main_server_tls.go | 86 ++++++++++++++++++++++++++++++++ internal/handler/conversation.go | 2 + 3 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 internal/app/main_server_tls.go diff --git a/internal/app/app.go b/internal/app/app.go index dacbf0fd..fef5010b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,8 +3,10 @@ package app import ( "context" "crypto/subtle" + "crypto/tls" "database/sql" "fmt" + "net" "net/http" "os" "path/filepath" @@ -30,6 +32,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" + "golang.org/x/net/http2" ) // App 应用 @@ -60,7 +63,7 @@ type App struct { } // New 创建新应用 -func New(cfg *config.Config, log *logger.Logger) (*App, error) { +func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error) { gin.SetMode(gin.ReleaseMode) router := gin.Default() @@ -292,10 +295,10 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { }() } - // 获取配置文件路径 - configPath := "config.yaml" - if len(os.Args) > 1 { - configPath = os.Args[1] + // 配置文件路径必须由入口传入(与 flag -config 一致)。勿再用 os.Args[1],否则 ./cyberstrike-ai --https 会把 --https 当成路径。 + configPath = strings.TrimSpace(configPath) + if configPath == "" { + configPath = "config.yaml" } skillsDir := skillpackage.SkillsRootFromConfig(cfg.SkillsDir, configPath) @@ -530,11 +533,33 @@ func (a *App) RunWithContext(ctx context.Context) error { }() } - // 启动主服务器 + // 启动主服务器(可选 HTTPS + HTTP/2,见 config server.tls_*) addr := fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port) - a.logger.Info("启动HTTP服务器", zap.String("address", addr)) + tlsMode, tlsConf, certFile, keyFile, tlsErr := prepareMainServerTLS(&a.config.Server) + if tlsErr != nil { + return tlsErr + } srv := &http.Server{Addr: addr, Handler: a.router} + if tlsMode != mainTLSOff { + srv.TLSConfig = tlsConf + if err := http2.ConfigureServer(srv, &http2.Server{}); err != nil { + return fmt.Errorf("主服务 HTTP/2 配置失败: %w", err) + } + switch tlsMode { + case mainTLSFromFiles: + a.logger.Info("启动 HTTPS 主服务(已启用 HTTP/2 协商)", + zap.String("address", addr), + zap.String("cert", certFile), + ) + case mainTLSInMemorySelfSigned: + a.logger.Info("启动 HTTPS 主服务(内存自签证书,仅测试;已启用 HTTP/2 协商)", + zap.String("address", addr), + ) + } + } else { + a.logger.Info("启动 HTTP 主服务", zap.String("address", addr)) + } // 监听 context 取消,优雅关闭 HTTP 服务器 go func() { @@ -551,7 +576,22 @@ func (a *App) RunWithContext(ctx context.Context) error { } }() - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + var err error + switch tlsMode { + case mainTLSOff: + err = srv.ListenAndServe() + case mainTLSFromFiles: + err = srv.ListenAndServeTLS(certFile, keyFile) + case mainTLSInMemorySelfSigned: + var ln net.Listener + ln, err = tls.Listen("tcp", addr, srv.TLSConfig) + if err == nil { + err = srv.Serve(ln) + } + default: + err = srv.ListenAndServe() + } + if err != nil && err != http.ErrServerClosed { return err } return nil diff --git a/internal/app/main_server_tls.go b/internal/app/main_server_tls.go new file mode 100644 index 00000000..19b546d6 --- /dev/null +++ b/internal/app/main_server_tls.go @@ -0,0 +1,86 @@ +package app + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "strings" + "time" + + "cyberstrike-ai/internal/config" +) + +// mainTLSMode 主 Web 服务 TLS 启动方式。 +type mainTLSMode int + +const ( + mainTLSOff mainTLSMode = iota + mainTLSFromFiles + mainTLSInMemorySelfSigned +) + +// prepareMainServerTLS 根据 server 配置决定主站是否启用 HTTPS(及 HTTP/2 协商)。 +// fromFiles:使用 tls_cert_path + tls_key_path,由 http.Server.ListenAndServeTLS 加载 PEM。 +// inMemory:tls_auto_self_sign 生成的自签证书,仅用于本地/测试。 +func prepareMainServerTLS(cfg *config.ServerConfig) (mode mainTLSMode, tlsConf *tls.Config, certFile, keyFile string, err error) { + if cfg == nil || !config.MainWebUIUsesHTTPS(cfg) { + return mainTLSOff, nil, "", "", nil + } + certFile = strings.TrimSpace(cfg.TLSCertPath) + keyFile = strings.TrimSpace(cfg.TLSKeyPath) + if certFile != "" && keyFile != "" { + // 证书由 ListenAndServeTLS 从文件加载;此处仅提供最小 TLS 配置供 http2.ConfigureServer 合并 ALPN。 + return mainTLSFromFiles, &tls.Config{MinVersion: tls.VersionTLS12}, certFile, keyFile, nil + } + if cfg.TLSAutoSelfSign { + cert, genErr := generateMainServerSelfSignedCert() + if genErr != nil { + return mainTLSOff, nil, "", "", fmt.Errorf("生成自签 TLS 证书: %w", genErr) + } + tlsConf = &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{cert}, + } + return mainTLSInMemorySelfSigned, tlsConf, "", "", nil + } + return mainTLSOff, nil, "", "", fmt.Errorf("server: 已启用 TLS(tls_enabled / tls_auto_self_sign / 证书路径),请设置 tls_cert_path 与 tls_key_path,或将 tls_auto_self_sign 设为 true(仅测试环境)") +} + +func generateMainServerSelfSignedCert() (tls.Certificate, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + serial, err := rand.Int(rand.Reader, big.NewInt(1<<62)) + if err != nil { + return tls.Certificate{}, err + } + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: "CyberStrikeAI"}, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + DNSNames: []string{"localhost"}, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, err + } + keyDER, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return tls.Certificate{}, err + } + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + return tls.X509KeyPair(certPEM, keyPEM) +} diff --git a/internal/handler/conversation.go b/internal/handler/conversation.go index 0bd538ec..2bb5c920 100644 --- a/internal/handler/conversation.go +++ b/internal/handler/conversation.go @@ -117,6 +117,8 @@ func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) { return } + details = database.DedupeConsecutiveProcessDetails(details) + // 转换为前端期望的 JSON 结构(与 GetConversation 中 processDetails 结构一致) out := make([]map[string]interface{}, 0, len(details)) for _, d := range details {