mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-01 08:37:41 +02:00
207 lines
5.5 KiB
Go
207 lines
5.5 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,
|
|
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)
|
|
|
|
// 创建可取消的根 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
|
|
}
|