mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-14 05:39:09 +02:00
The Web UI startup banner always printed 127.0.0.1, even when server.host was bound to 0.0.0.0 or a specific interface. This made users believe the host setting was ignored (the actual listen binding was correct). Pass the configured host into the banner and expand wildcard binds to the machine's real addresses so LAN URLs are shown. Fixes #301 Co-authored-by: sycpro <sycpro@sycprodeMacBook-Pro.local>
213 lines
5.7 KiB
Go
213 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"cyberstrike-ai/internal/app"
|
|
"cyberstrike-ai/internal/config"
|
|
"cyberstrike-ai/internal/database"
|
|
"cyberstrike-ai/internal/logger"
|
|
"cyberstrike-ai/internal/security"
|
|
"cyberstrike-ai/internal/termout"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"go.uber.org/zap"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
func main() {
|
|
var configPath = flag.String("config", "config.yaml", "Path to the configuration file")
|
|
var httpsBootstrap = flag.Bool("https", false, "Enable HTTPS for the main site; uses an in-memory self-signed certificate when no cert/key is configured")
|
|
var httpBootstrap = flag.Bool("http", false, "Force plain HTTP for the main site, overriding TLS settings in the configuration file")
|
|
var resetAdminPassword = flag.Bool("reset-admin-password", false, "Interactively reset the built-in admin password and exit")
|
|
flag.Parse()
|
|
|
|
// 环境变量兼容(便于 systemd/docker 等不传参场景)
|
|
if *httpsBootstrap && *httpBootstrap {
|
|
fmt.Fprintln(os.Stderr, "--http and --https cannot be used together")
|
|
os.Exit(2)
|
|
}
|
|
if !*httpsBootstrap && !*httpBootstrap {
|
|
v := strings.TrimSpace(os.Getenv("CYBERSTRIKE_HTTPS"))
|
|
if v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes") {
|
|
*httpsBootstrap = true
|
|
}
|
|
}
|
|
|
|
// 加载配置
|
|
cp := strings.TrimSpace(*configPath)
|
|
if cp == "" {
|
|
cp = "config.yaml"
|
|
}
|
|
if strings.HasPrefix(cp, "-") {
|
|
fmt.Fprintf(os.Stderr, "Invalid -config path %q.\nIf HTTPS is also needed, use: ./cyberstrike-ai --https -config config.yaml (-config must be followed by a yaml file path).\n", cp)
|
|
os.Exit(2)
|
|
}
|
|
localConfig, err := config.EnsureLocalConfig(cp)
|
|
if err != nil {
|
|
fmt.Printf("Failed to load config: %v\n", err)
|
|
return
|
|
}
|
|
|
|
cfg, err := config.Load(cp)
|
|
if err != nil {
|
|
fmt.Printf("Failed to load config: %v\n", err)
|
|
return
|
|
}
|
|
if localConfig.Created {
|
|
termout.PrintConfigCreated()
|
|
}
|
|
|
|
if *resetAdminPassword {
|
|
if err := runResetAdminPassword(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to reset admin password: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
if *httpBootstrap {
|
|
config.ApplyPlainHTTPBootstrap(cfg)
|
|
} else if *httpsBootstrap {
|
|
config.ApplyDevHTTPSBootstrap(cfg)
|
|
}
|
|
|
|
port := cfg.Server.Port
|
|
if port <= 0 {
|
|
port = 8080
|
|
}
|
|
scheme := "http"
|
|
if config.MainWebUIUsesHTTPS(&cfg.Server) {
|
|
scheme = "https"
|
|
}
|
|
termout.PrintStartupWebUI(termout.StartupWebUIOptions{
|
|
Scheme: scheme,
|
|
Host: cfg.Server.Host,
|
|
Port: port,
|
|
SelfSigned: scheme == "https" && cfg.Server.TLSAutoSelfSign,
|
|
HTTPRedirect: scheme == "https" && config.ServerHTTPRedirectEnabled(&cfg.Server),
|
|
})
|
|
|
|
// MCP 启用且 auth_header_value 为空时,自动生成随机密钥并写回配置
|
|
if err := config.EnsureMCPAuth(cp, cfg); err != nil {
|
|
fmt.Printf("Failed to configure MCP authentication: %v\n", err)
|
|
return
|
|
}
|
|
if cfg.MCP.Enabled {
|
|
config.PrintMCPConfigJSON(cfg.MCP)
|
|
}
|
|
|
|
// 初始化日志
|
|
log := logger.New(cfg.Log.Level, cfg.Log.Output, logger.DiagnosticOptions{
|
|
Dir: cfg.Log.DiagnosticDir,
|
|
Disabled: cfg.Log.DiagnosticDisabled,
|
|
RetentionDays: cfg.Log.DiagnosticRetentionDays,
|
|
})
|
|
defer log.Sync()
|
|
|
|
// 创建可取消的根 context,用于优雅关闭
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// 监听系统信号
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
// 创建应用
|
|
application, err := app.New(cfg, log, cp)
|
|
if err != nil {
|
|
log.Fatal("应用初始化失败", "error", err)
|
|
}
|
|
|
|
// 在后台监听信号
|
|
go func() {
|
|
sig := <-sigCh
|
|
log.Info("收到系统信号,开始优雅关闭: " + sig.String())
|
|
application.Shutdown()
|
|
cancel()
|
|
}()
|
|
|
|
// 启动服务器(传入 context 以支持优雅关闭)
|
|
if err := application.RunWithContext(ctx); err != nil {
|
|
// context 取消导致的关闭不视为错误
|
|
if ctx.Err() != nil {
|
|
log.Info("服务器已优雅关闭")
|
|
} else {
|
|
log.Fatal("服务器启动失败", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func runResetAdminPassword(cfg *config.Config) error {
|
|
dbPath := strings.TrimSpace(cfg.Database.Path)
|
|
if dbPath == "" {
|
|
dbPath = "data/conversations.db"
|
|
}
|
|
if _, err := os.Stat(dbPath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("database does not exist: %s; start the service once to initialize it first", dbPath)
|
|
}
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Reset built-in admin password")
|
|
fmt.Println()
|
|
|
|
password, err := readHiddenPassword("New admin password: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
password = strings.TrimSpace(password)
|
|
if len(password) < 8 {
|
|
return fmt.Errorf("new password must be at least 8 characters")
|
|
}
|
|
confirm, err := readHiddenPassword("Confirm new password: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if password != strings.TrimSpace(confirm) {
|
|
return fmt.Errorf("passwords do not match")
|
|
}
|
|
|
|
hash, err := security.HashPassword(password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
db, err := database.NewDB(dbPath, zap.NewNop())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = db.Close() }()
|
|
|
|
admin, err := db.GetRBACUserByUsername("admin")
|
|
if err != nil {
|
|
return fmt.Errorf("built-in admin account was not found; start the service once to initialize it first: %w", err)
|
|
}
|
|
if !admin.IsBuiltin {
|
|
return fmt.Errorf("admin account is not built in; refusing to reset it")
|
|
}
|
|
if err := db.UpdateRBACAdminPassword(hash); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("Admin password has been reset.")
|
|
fmt.Println("If the service is running, existing login sessions remain valid until the service restarts or the sessions expire.")
|
|
return nil
|
|
}
|
|
|
|
func readHiddenPassword(prompt string) (string, error) {
|
|
fmt.Fprint(os.Stderr, prompt)
|
|
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Fprintln(os.Stderr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(password), nil
|
|
}
|