mirror of
https://github.com/MirageNetwork/MirageServer.git
synced 2025-09-26 20:41:34 +08:00
1012 lines
26 KiB
Go
1012 lines
26 KiB
Go
package controller
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/rand"
|
||
"embed"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"io/fs"
|
||
"net"
|
||
"net/http"
|
||
"net/netip"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/go-webauthn/webauthn/protocol"
|
||
webauthn "github.com/go-webauthn/webauthn/webauthn"
|
||
"github.com/gorilla/mux"
|
||
"github.com/patrickmn/go-cache"
|
||
"github.com/robfig/cron/v3"
|
||
"github.com/rs/zerolog/log"
|
||
"go4.org/netipx"
|
||
"golang.org/x/sync/errgroup"
|
||
"gorm.io/gorm"
|
||
"tailscale.com/types/key"
|
||
"tailscale.com/version"
|
||
)
|
||
|
||
type Cockpit struct {
|
||
App *Mirage
|
||
db *gorm.DB
|
||
Addr string
|
||
|
||
serviceState bool
|
||
CtrlChn chan CtrlMsg
|
||
MsgChn chan CtrlMsg
|
||
// hasAdmin bool
|
||
|
||
author *webauthn.WebAuthn
|
||
superAdmin *MirageSuperAdmin
|
||
authCache *cache.Cache
|
||
|
||
BuildCron *cron.Cron
|
||
}
|
||
|
||
type CtrlMsg struct {
|
||
Msg string
|
||
Err error
|
||
SysCfg *Config
|
||
}
|
||
|
||
type MirageSuperAdmin struct {
|
||
cred AdminCredential
|
||
}
|
||
|
||
func (user *MirageSuperAdmin) WebAuthnID() []byte {
|
||
return []byte("MirageSuperAdmin")
|
||
}
|
||
func (user *MirageSuperAdmin) WebAuthnName() string {
|
||
return "MirageSuperAdmin"
|
||
}
|
||
func (user *MirageSuperAdmin) WebAuthnDisplayName() string {
|
||
return "蜃境超级管理员"
|
||
}
|
||
func (user *MirageSuperAdmin) WebAuthnIcon() string {
|
||
return ""
|
||
}
|
||
func (user *MirageSuperAdmin) WebAuthnCredentials() []webauthn.Credential {
|
||
return []webauthn.Credential{webauthn.Credential(user.cred)}
|
||
}
|
||
|
||
// NewCockpit 创建一个新的Cockpit实例
|
||
// @sysAddr 监听地址
|
||
// @ctrlChn 对外控制通道
|
||
// @db 数据库实例
|
||
// @return Cockpit实例
|
||
// @return 错误信息,如果没有错误则为nil
|
||
func NewCockpit(sysAddr string, ctrlChn, msgChn chan CtrlMsg, db *gorm.DB) (*Cockpit, error) {
|
||
cockpit := &Cockpit{
|
||
db: db,
|
||
Addr: sysAddr,
|
||
|
||
serviceState: false,
|
||
CtrlChn: ctrlChn,
|
||
MsgChn: msgChn,
|
||
BuildCron: cron.New(),
|
||
}
|
||
cockpit.authCache = cache.New(0, 0)
|
||
// cockpit.superAdmin, cockpit.hasAdmin = cockpit.GetAdmin()
|
||
cockpit.superAdmin = cockpit.GetSuperAdmin()
|
||
|
||
cockpit.BuildCron.AddFunc("CRON_TZ=Asia/Shanghai 00 02 * * *", cockpit.BuildLinuxClient)
|
||
|
||
return cockpit, nil
|
||
}
|
||
|
||
// isAdminSet 检查是否已经设置了管理员凭证
|
||
// @return 如果已经设置了管理员凭证则返回true,否则返回false
|
||
/* TODEL
|
||
func (c *Cockpit) GetAdmin() (*MirageSuperAdmin, bool) {
|
||
if sysadmin := c.GetSysAdmin(); sysadmin != nil {
|
||
return &MirageSuperAdmin{
|
||
cred: sysCfg.AdminCredential,
|
||
}, true
|
||
}
|
||
return nil, false
|
||
}
|
||
*/
|
||
|
||
func (c *Cockpit) GetSuperAdmin() *MirageSuperAdmin {
|
||
sysadmin := []SysAdmin{}
|
||
err := c.db.Find(&sysadmin).Error
|
||
if err != nil || sysadmin == nil || len(sysadmin) == 0 {
|
||
return nil
|
||
}
|
||
return &MirageSuperAdmin{
|
||
cred: sysadmin[0].AdminCredential,
|
||
}
|
||
}
|
||
func (c *Cockpit) GetAdmin() *SysAdmin {
|
||
sysadmin := []SysAdmin{}
|
||
err := c.db.Find(&sysadmin).Error
|
||
if err != nil || sysadmin == nil || len(sysadmin) == 0 {
|
||
return nil
|
||
}
|
||
return &sysadmin[0]
|
||
}
|
||
|
||
func (c *Cockpit) GetSysCfg() *SysConfig {
|
||
cfg := []SysConfig{}
|
||
err := c.db.Find(&cfg).Error
|
||
if err != nil || cfg == nil || len(cfg) == 0 {
|
||
return nil
|
||
}
|
||
if cfg[0].NaviDeployKey == "" {
|
||
pri, pub, err := genSSHKeypair()
|
||
if err != nil {
|
||
log.Fatal().Msg(err.Error())
|
||
}
|
||
cfg[0].NaviDeployPub = pub
|
||
cfg[0].NaviDeployKey = pri
|
||
err = c.db.Save(&cfg[0]).Error
|
||
if err != nil {
|
||
log.Fatal().Msg(err.Error())
|
||
}
|
||
}
|
||
return &cfg[0]
|
||
}
|
||
|
||
//go:embed cockpit_html
|
||
var cockpitFS embed.FS
|
||
|
||
// createRouter 创建路由
|
||
// @return 路由实例
|
||
func (c *Cockpit) createRouter() *mux.Router {
|
||
router := mux.NewRouter()
|
||
|
||
cockpitDir, err := fs.Sub(cockpitFS, "cockpit_html")
|
||
if err != nil {
|
||
log.Fatal().Msg(err.Error())
|
||
}
|
||
|
||
cockpit_router := router.PathPrefix("/cockpit").Subrouter()
|
||
cockpit_router.Use(c.Auth)
|
||
|
||
cockpit_router.HandleFunc("/api/regAdmin", c.RegisterAdmin).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/revokeAdmin", c.RevokeAdmin).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/login", c.Login).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/setting/general", c.SetSettingGeneral).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/service/start", c.DoServiceStart).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/service/stop", c.DoServiceStop).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/tenants", c.CAPIPostTenants).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/publish/{os}", c.CAPIPublishClient).Methods(http.MethodPost)
|
||
cockpit_router.HandleFunc("/api/derp/add", c.CAPIAddDERP).Methods(http.MethodPost)
|
||
|
||
cockpit_router.HandleFunc("/api/logout", c.Logout).Methods(http.MethodGet)
|
||
cockpit_router.HandleFunc("/api/service/state", c.GetServiceState).Methods(http.MethodGet)
|
||
cockpit_router.HandleFunc("/api/setting/general", c.GetSettingGeneral).Methods(http.MethodGet)
|
||
cockpit_router.HandleFunc("/api/tenants", c.CAPIGetTenant).Methods(http.MethodGet)
|
||
cockpit_router.HandleFunc("/api/publish", c.GetPublishInfo).Methods(http.MethodGet)
|
||
cockpit_router.HandleFunc("/api/derp/query", c.CAPIQueryDERP).Methods(http.MethodGet)
|
||
|
||
cockpit_router.PathPrefix("/api/derp/{id}").HandlerFunc(c.CAPIDelNaviNode).Methods(http.MethodDelete)
|
||
|
||
cockpit_router.PathPrefix("").Handler(http.StripPrefix("/cockpit", http.FileServer(http.FS(cockpitDir))))
|
||
|
||
router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
c.ErrMessage(w, r, 404, "你迷失在蜃境中了吗?这里什么都没有")
|
||
})
|
||
return router
|
||
}
|
||
|
||
// Auth 验证用户是否已经登录
|
||
func (c *Cockpit) Auth(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if strings.HasPrefix(r.URL.Path, "/cockpit/assets/") || strings.HasPrefix(r.URL.Path, "/cockpit/imgs/") {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
// If admin credential is not set, allow user to register admin credential
|
||
if c.superAdmin == nil {
|
||
if r.URL.Path == "/cockpit/api/regAdmin" {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
// If user is not trying to register admin credential, return noadmin error
|
||
if strings.Contains(r.URL.Path, "/cockpit/api/") {
|
||
c.doAPIResponse(w, "noadmin", nil)
|
||
return
|
||
}
|
||
next.ServeHTTP(w, r) // If user is not trying to access API, allow user to
|
||
return
|
||
}
|
||
|
||
if r.URL.Path == "/cockpit/api/login" {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
// If user is not trying to login, check if user is logged in
|
||
authCookie, err := r.Cookie("mirage_cockpit_auth")
|
||
AuthCode, ok := c.authCache.Get("AuthCode")
|
||
if err != nil || !ok || authCookie.Value != AuthCode.(string) {
|
||
// If user is not logged in, return unauthorized error
|
||
if strings.Contains(r.URL.Path, "/cockpit/api/") {
|
||
c.doAPIResponse(w, "unauthorized", nil)
|
||
return
|
||
}
|
||
next.ServeHTTP(w, r) // If user is not trying to access API, allow user to
|
||
return
|
||
}
|
||
next.ServeHTTP(w, r) // If user is logged in, allow user to access API
|
||
})
|
||
}
|
||
|
||
// RevokeAdmin 注销超级管理员
|
||
func (c *Cockpit) RevokeAdmin(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
if c.superAdmin == nil {
|
||
c.doAPIResponse(w, "超级管理员不存在", nil)
|
||
return
|
||
}
|
||
sysAdmin := c.GetAdmin()
|
||
err := c.db.Delete(&sysAdmin).Error
|
||
if err != nil {
|
||
c.doAPIResponse(w, "解绑超级管理员失败", nil)
|
||
return
|
||
}
|
||
c.superAdmin = nil
|
||
c.doAPIResponse(w, "", "ok")
|
||
}
|
||
|
||
// RegisterAdmin 注册超级管理员
|
||
func (c *Cockpit) RegisterAdmin(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
if r.URL.Query().Get("phase") == "response" { // 注册响应
|
||
response, err := protocol.ParseCredentialCreationResponseBody(r.Body)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "解析注册响应失败", nil)
|
||
return
|
||
}
|
||
|
||
sessionInt, ok := c.authCache.Get("MirageSuperAdmin")
|
||
if !ok || sessionInt == nil {
|
||
c.doAPIResponse(w, "注册会话不存在或已过期", nil)
|
||
return
|
||
}
|
||
session := sessionInt.(*webauthn.SessionData)
|
||
|
||
credential, err := c.author.CreateCredential(c.superAdmin, *session, response)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "创建超管凭证失败", nil)
|
||
return
|
||
}
|
||
// newSysCfg := c.GetSysCfg()
|
||
currentSysadmin := c.GetAdmin()
|
||
if currentSysadmin == nil {
|
||
newSysCfg := c.GetSysCfg()
|
||
if newSysCfg == nil {
|
||
dexSecret := c.GenAuthCode()
|
||
machineKey := key.NewMachine()
|
||
machineKeyStr, err := machineKey.MarshalText()
|
||
if err != nil {
|
||
c.doAPIResponse(w, "创建机器密钥失败", nil)
|
||
return
|
||
}
|
||
newSysCfg = &SysConfig{
|
||
DexSecret: dexSecret,
|
||
ServerKey: string(machineKeyStr),
|
||
}
|
||
c.db.Save(newSysCfg)
|
||
newSysCfg = c.GetSysCfg()
|
||
if newSysCfg.DexSecret == "" || newSysCfg.ServerKey == "" {
|
||
c.doAPIResponse(w, "创建dex认证随机数失败", nil)
|
||
return
|
||
}
|
||
if newSysCfg.ServerKey == "" {
|
||
c.doAPIResponse(w, "创建服务器私钥失败", nil)
|
||
return
|
||
}
|
||
}
|
||
if newSysCfg.DexSecret == "" {
|
||
dexSecret := c.GenAuthCode()
|
||
newSysCfg.DexSecret = dexSecret
|
||
c.db.Save(newSysCfg)
|
||
newSysCfg = c.GetSysCfg()
|
||
if newSysCfg.DexSecret == "" {
|
||
c.doAPIResponse(w, "创建dex认证随机数失败", nil)
|
||
return
|
||
}
|
||
}
|
||
if newSysCfg.ServerKey == "" {
|
||
machineKey := key.NewMachine()
|
||
machineKeyStr, err := machineKey.MarshalText()
|
||
if err != nil {
|
||
c.doAPIResponse(w, "创建机器密钥失败", nil)
|
||
return
|
||
}
|
||
newSysCfg.ServerKey = string(machineKeyStr)
|
||
c.db.Save(newSysCfg)
|
||
newSysCfg = c.GetSysCfg()
|
||
if newSysCfg.ServerKey == "" {
|
||
c.doAPIResponse(w, "创建服务器私钥失败", nil)
|
||
return
|
||
}
|
||
}
|
||
currentSysadmin = &SysAdmin{}
|
||
}
|
||
currentSysadmin.AdminCredential = AdminCredential(*credential)
|
||
c.db.Save(currentSysadmin)
|
||
// c.superAdmin, c.hasAdmin = c.GetAdmin()
|
||
c.superAdmin = c.GetSuperAdmin()
|
||
if c.superAdmin == nil {
|
||
c.doAPIResponse(w, "保存超管凭证失败", nil)
|
||
return
|
||
}
|
||
c.doAPIResponse(w, "", "ok")
|
||
return
|
||
} else { // 注册请求
|
||
//if c.author == nil {
|
||
wconfig := &webauthn.Config{
|
||
RPDisplayName: "蜃境网络", // Display Name for your site
|
||
RPID: r.Host, // Generally the FQDN for your site
|
||
RPOrigins: r.Header["Origin"], //[]string{"https://" + serverURL}, // The origin URLs allowed for WebAuthn requests
|
||
}
|
||
webAuthor, err := webauthn.New(wconfig)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "创建WebAuthn验证器失败", nil)
|
||
return
|
||
}
|
||
c.author = webAuthor
|
||
//}
|
||
options, webAuthSession, err := c.author.BeginRegistration(c.superAdmin)
|
||
c.authCache.Set("MirageSuperAdmin", webAuthSession, 5*time.Minute)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "启动超管注册失败", nil)
|
||
return
|
||
}
|
||
c.doAPIResponse(w, "", options)
|
||
}
|
||
}
|
||
|
||
func (c *Cockpit) Login(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
if r.URL.Query().Get("phase") == "response" { // 登录响应
|
||
response, err := protocol.ParseCredentialRequestResponseBody(r.Body)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "解析登录响应失败", nil)
|
||
return
|
||
}
|
||
sessionInt, ok := c.authCache.Get("MirageSuperAdmin")
|
||
if !ok || sessionInt == nil {
|
||
c.doAPIResponse(w, "登录会话不存在或已过期", nil)
|
||
return
|
||
}
|
||
_, err = c.author.ValidateLogin(c.superAdmin, *sessionInt.(*webauthn.SessionData), response)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "登录验证失败", nil)
|
||
return
|
||
}
|
||
|
||
// 登录成功,生成 authCode 并返回给客户端
|
||
authCode := c.GenAuthCode()
|
||
c.authCache.Set("AuthCode", authCode, 5*time.Hour)
|
||
authCookie := &http.Cookie{
|
||
Name: "mirage_cockpit_auth",
|
||
Value: authCode,
|
||
Domain: r.URL.Host,
|
||
Path: "/",
|
||
Secure: true,
|
||
HttpOnly: true,
|
||
SameSite: http.SameSiteLaxMode,
|
||
Expires: time.Now().Add(1 * time.Hour),
|
||
}
|
||
http.SetCookie(w, authCookie)
|
||
c.doAPIResponse(w, "", "ok")
|
||
} else { // 登录请求
|
||
//if c.author == nil {
|
||
wconfig := &webauthn.Config{
|
||
RPDisplayName: "蜃境网络", // Display Name for your site
|
||
RPID: r.Host, // Generally the FQDN for your site
|
||
RPOrigins: r.Header["Origin"], //[]string{"https://" + serverURL}, // The origin URLs allowed for WebAuthn requests
|
||
}
|
||
webAuthor, err := webauthn.New(wconfig)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "创建WebAuthn验证器失败", nil)
|
||
return
|
||
}
|
||
c.author = webAuthor
|
||
//}
|
||
options, session, err := c.author.BeginLogin(c.superAdmin)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "启动超管登录失败", nil)
|
||
return
|
||
}
|
||
c.authCache.Set("MirageSuperAdmin", session, 5*time.Minute)
|
||
c.doAPIResponse(w, "", options)
|
||
}
|
||
}
|
||
|
||
// Logout 登出
|
||
func (c *Cockpit) Logout(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
c.authCache.Delete("AuthCode")
|
||
authCookie := &http.Cookie{
|
||
Name: "mirage_cockpit_auth",
|
||
Value: "",
|
||
Domain: r.URL.Host,
|
||
Path: "/",
|
||
Secure: true,
|
||
HttpOnly: true,
|
||
SameSite: http.SameSiteLaxMode,
|
||
MaxAge: -1,
|
||
}
|
||
http.SetCookie(w, authCookie)
|
||
c.doAPIResponse(w, "", "ok")
|
||
}
|
||
|
||
type svcStateData struct {
|
||
ControllerVer string `json:"ctrlver"`
|
||
IsRunning bool `json:"isRunning"`
|
||
}
|
||
|
||
// GetServiceState 获取服务状态
|
||
func (c *Cockpit) GetServiceState(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
c.doAPIResponse(w, "", svcStateData{
|
||
ControllerVer: version.Long(),
|
||
IsRunning: c.serviceState,
|
||
})
|
||
}
|
||
|
||
// DoServiceStart 启动服务
|
||
func (c *Cockpit) DoServiceStart(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
if c.serviceState {
|
||
c.doAPIResponse(w, "", true)
|
||
return
|
||
}
|
||
if cfg, ok := c.CheckCfgValid(); ok {
|
||
c.serviceState = true
|
||
c.CtrlChn <- CtrlMsg{
|
||
Msg: "start",
|
||
SysCfg: cfg,
|
||
}
|
||
c.doAPIResponse(w, "", true)
|
||
return
|
||
}
|
||
c.doAPIResponse(w, "配置不完整", false)
|
||
}
|
||
|
||
// DoServiceStop 停止服务
|
||
func (c *Cockpit) DoServiceStop(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
if !c.serviceState {
|
||
c.doAPIResponse(w, "", true)
|
||
return
|
||
}
|
||
c.serviceState = false
|
||
c.CtrlChn <- CtrlMsg{
|
||
Msg: "stop",
|
||
}
|
||
c.doAPIResponse(w, "", false)
|
||
}
|
||
|
||
// CheckCfgValid 检查配置是否有效
|
||
func (c *Cockpit) CheckCfgValid() (cfg *Config, ok bool) {
|
||
var err error
|
||
if sysCfg := c.GetSysCfg(); sysCfg != nil {
|
||
cfg, err = sysCfg.toSrvConfig()
|
||
if err != nil {
|
||
return
|
||
}
|
||
if cfg.ServerURL == "" || cfg.Addr == "" || cfg.IPPrefixes == nil || cfg.BaseDomain == "" { //|| cfg.DERPURL == "" {
|
||
return
|
||
}
|
||
/*
|
||
if cfg.OIDC.Issuer == "" || cfg.OIDC.ClientID == "" || cfg.OIDC.ClientSecret == "" {
|
||
return
|
||
}
|
||
if cfg.IDaaS.App == "" || cfg.IDaaS.ClientID == "" || cfg.IDaaS.ClientKey == "" || cfg.IDaaS.Instance == "" || cfg.IDaaS.OrgID == "" {
|
||
return
|
||
}
|
||
if cfg.SMS.ID == "" || cfg.SMS.Key == "" || cfg.SMS.Sign == "" || cfg.SMS.Template == "" {
|
||
return
|
||
}
|
||
*/
|
||
ok = true
|
||
}
|
||
return
|
||
}
|
||
|
||
// GetConfiguration 获取系统配置
|
||
func (c *Cockpit) GetSettingGeneral(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
gCfg := sysCfg.toGeneralCfg()
|
||
c.doAPIResponse(w, "", gCfg)
|
||
}
|
||
|
||
// SetConfiguration 设置系统配置
|
||
func (c *Cockpit) SetSettingGeneral(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
) {
|
||
reqData := make(map[string]interface{})
|
||
json.NewDecoder(r.Body).Decode(&reqData)
|
||
reqState, ok := reqData["state"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求state解析失败", nil)
|
||
return
|
||
}
|
||
switch reqState {
|
||
case "set-mipv4":
|
||
mipv4, ok := reqData["mipv4"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求mipv4解析失败", nil)
|
||
return
|
||
}
|
||
mipv4Prefix, err := netip.ParsePrefix(mipv4)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "MIPv4格式有误!", nil)
|
||
return
|
||
}
|
||
mipv4Prefix, ok = netipx.RangeOfPrefix(mipv4Prefix).Prefix()
|
||
if !ok {
|
||
c.doAPIResponse(w, "MIPv4格式有误!", nil)
|
||
return
|
||
}
|
||
if !mipv4Prefix.Addr().Is4() {
|
||
c.doAPIResponse(w, "MIPv4格式有误!", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.Mip4 = IPPrefix(mipv4Prefix)
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-mipv6":
|
||
mipv6, ok := reqData["mipv6"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求mipv6解析失败", nil)
|
||
return
|
||
}
|
||
mipv6Prefix, err := netip.ParsePrefix(mipv6)
|
||
if err != nil {
|
||
c.doAPIResponse(w, "MIPv6格式有误!", nil)
|
||
return
|
||
}
|
||
mipv6Prefix, ok = netipx.RangeOfPrefix(mipv6Prefix).Prefix()
|
||
if !ok {
|
||
c.doAPIResponse(w, "MIPv6格式有误!", nil)
|
||
return
|
||
}
|
||
if !mipv6Prefix.Addr().Is6() {
|
||
c.doAPIResponse(w, "MIPv6格式有误!", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.Mip6 = IPPrefix(mipv6Prefix)
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-srvaddr":
|
||
srvaddr, ok := reqData["srvaddr"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求srvaddr解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.Addr = srvaddr
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-serverurl":
|
||
serverURL, ok := reqData["ServerURL"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求ServerURL解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.ServerURL = serverURL
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-basedomain":
|
||
baseDomain, ok := reqData["BaseDomain"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求BaseDomain解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.Basedomain = baseDomain
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
/*
|
||
case "set-derpurl":
|
||
derpURL, ok := reqData["DERPURL"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求DERPURL解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.DerpUrl = derpURL
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
*/
|
||
case "set-routeaccessduemachine":
|
||
SubnetAccessDueMachine, ok := reqData["SubnetAccessDueMachine"].(bool)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求SubnetAccessDueMachine解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.RouteAccessDueMachine = SubnetAccessDueMachine
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-es":
|
||
esURL, ok := reqData["ESURL"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求ESURL解析失败", nil)
|
||
return
|
||
}
|
||
esKey, ok := reqData["ESKey"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求ESKey解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.EsUrl = esURL
|
||
sysCfg.EsKey = esKey
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-wxscanurl":
|
||
wxScanURL, ok := reqData["WXScanURL"].(string)
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求WXScanURL解析失败", nil)
|
||
return
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.WXScanURL = wxScanURL
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-sms":
|
||
smsCfgInt, ok := reqData["SMS"].(map[string]interface{})
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求SMS解析失败", nil)
|
||
return
|
||
}
|
||
smsCfg := SMSConfig{
|
||
ID: smsCfgInt["id"].(string),
|
||
Key: smsCfgInt["key"].(string),
|
||
Sign: smsCfgInt["sign"].(string),
|
||
Template: smsCfgInt["template"].(string),
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.SMSConfig = smsCfg
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-idaas":
|
||
idaasCfgInt, ok := reqData["IDaaS"].(map[string]interface{})
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求IDaaS解析失败", nil)
|
||
return
|
||
}
|
||
idaasCfg := ALIConfig{
|
||
App: idaasCfgInt["app"].(string),
|
||
ClientID: idaasCfgInt["id"].(string),
|
||
ClientKey: idaasCfgInt["key"].(string),
|
||
Instance: idaasCfgInt["instance"].(string),
|
||
OrgID: idaasCfgInt["org"].(string),
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.IdaasConfig = idaasCfg
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-microsoft":
|
||
MSInt, ok := reqData["Microsoft"].(map[string]interface{})
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求Microsoft解析失败", nil)
|
||
return
|
||
}
|
||
MSCfg := MicrosoftCfg{
|
||
ApplicationID: MSInt["app_id"].(string),
|
||
ClientID: MSInt["client_id"].(string),
|
||
ClientSecret: MSInt["client_secret"].(string),
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.MicrosoftCfg = MSCfg
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-github":
|
||
GHInt, ok := reqData["Github"].(map[string]interface{})
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求Github解析失败", nil)
|
||
return
|
||
}
|
||
GHCfg := GithubCfg{
|
||
ClientID: GHInt["client_id"].(string),
|
||
ClientSecret: GHInt["client_secret"].(string),
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.GithubCfg = GHCfg
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-google":
|
||
GgInt, ok := reqData["Google"].(map[string]interface{})
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求Google解析失败", nil)
|
||
return
|
||
}
|
||
GgCfg := GoogleCfg{
|
||
ClientID: GgInt["client_id"].(string),
|
||
ClientSecret: GgInt["client_secret"].(string),
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.GoogleCfg = GgCfg
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
case "set-apple":
|
||
AppInt, ok := reqData["Apple"].(map[string]interface{})
|
||
if !ok {
|
||
c.doAPIResponse(w, "用户请求Github解析失败", nil)
|
||
return
|
||
}
|
||
AppCfg := AppleCfg{
|
||
ClientID: AppInt["client_id"].(string),
|
||
TeamID: AppInt["team_id"].(string),
|
||
KeyID: AppInt["key_id"].(string),
|
||
PrivateKey: AppInt["private_key"].(string),
|
||
}
|
||
sysCfg := c.GetSysCfg()
|
||
if sysCfg == nil {
|
||
c.doAPIResponse(w, "获取系统配置失败", nil)
|
||
return
|
||
}
|
||
sysCfg.AppleCfg = AppCfg
|
||
if err := c.db.Save(sysCfg).Error; err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
default:
|
||
c.doAPIResponse(w, "用户请求state不存在", nil)
|
||
return
|
||
}
|
||
|
||
if c.serviceState {
|
||
newCfg, err := c.GetSysCfg().toSrvConfig()
|
||
if err != nil {
|
||
c.doAPIResponse(w, "更新系统配置失败", nil)
|
||
return
|
||
}
|
||
c.CtrlChn <- CtrlMsg{
|
||
Msg: "update-config",
|
||
SysCfg: newCfg,
|
||
}
|
||
}
|
||
c.GetSettingGeneral(w, r)
|
||
}
|
||
|
||
// GenAuthCode 生成 authCode
|
||
func (c *Cockpit) GenAuthCode() string {
|
||
const letterBytes = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
b := make([]byte, 25)
|
||
b[0] = 'm'
|
||
b[1] = 'n'
|
||
b[2] = '-'
|
||
|
||
index := make([]byte, 22)
|
||
rand.Read(index)
|
||
for i := 0; i < 22; i++ {
|
||
b[i+3] = letterBytes[index[i]&63]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// API调用的统一响应发报
|
||
// @msg 响应状态:成功时data不为nil则忽略,自动设置为success,否则拼接error-{msg}
|
||
// @data 响应数据:key值为data的json对象
|
||
func (c *Cockpit) doAPIResponse(writer http.ResponseWriter, msg string, data interface{}) {
|
||
res := APIResponse{}
|
||
if msg == "" {
|
||
res.Status = "success"
|
||
res.Data = data
|
||
} else {
|
||
res.Status = "error-" + msg
|
||
}
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
err := json.NewEncoder(writer).Encode(&res)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
}
|
||
|
||
// cgao6: 用这个向前端返回错误页面
|
||
func (c *Cockpit) ErrMessage(
|
||
w http.ResponseWriter,
|
||
r *http.Request,
|
||
code int,
|
||
msg string,
|
||
) {
|
||
errT := template.Must(template.New("err").Parse(errTemplate))
|
||
|
||
config := map[string]interface{}{
|
||
"ErrCode": code,
|
||
"ErrMsg": msg,
|
||
}
|
||
|
||
var payload bytes.Buffer
|
||
if err := errT.Execute(&payload, config); err != nil {
|
||
log.Error().
|
||
Str("handler", "ErrMessage").
|
||
Err(err).
|
||
Msg("Could not render Error template")
|
||
|
||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
_, err := w.Write([]byte("Could not render Error template"))
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
w.WriteHeader(code)
|
||
_, err := w.Write(payload.Bytes())
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
}
|
||
|
||
func (c *Cockpit) Run() error {
|
||
errorGroup := new(errgroup.Group)
|
||
|
||
router := c.createRouter()
|
||
httpServer := &http.Server{
|
||
Addr: c.Addr,
|
||
Handler: router,
|
||
ReadTimeout: HTTPReadTimeout,
|
||
WriteTimeout: 0,
|
||
}
|
||
httpListener, err := net.Listen("tcp", c.Addr)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to bind to TCP address: %w", err)
|
||
}
|
||
|
||
errorGroup.Go(func() error { return httpServer.Serve(httpListener) })
|
||
|
||
// 启动buildCron
|
||
c.BuildCron.Start()
|
||
|
||
msgFunc := func(c *Cockpit) {
|
||
for {
|
||
msg := <-c.MsgChn
|
||
switch msg.Msg {
|
||
case "error":
|
||
log.Info().Msg("received service fatal error message, should be stopped")
|
||
c.serviceState = false
|
||
}
|
||
}
|
||
}
|
||
errorGroup.Go(func() error {
|
||
msgFunc(c)
|
||
return nil
|
||
})
|
||
|
||
if sysCfg, ok := c.CheckCfgValid(); ok {
|
||
c.serviceState = true
|
||
// 启动定时任务
|
||
c.CtrlChn <- CtrlMsg{
|
||
Msg: "start",
|
||
Err: nil,
|
||
SysCfg: sysCfg,
|
||
}
|
||
}
|
||
|
||
return errorGroup.Wait()
|
||
}
|