prod
This commit is contained in:
+216
-15
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +30,8 @@ type App struct {
|
||||
tasks map[string]*models.ScanTask
|
||||
mu sync.RWMutex
|
||||
httpServer *http.Server
|
||||
sessions map[string]time.Time // token -> expire time
|
||||
sessionMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewApp 创建应用
|
||||
@@ -51,6 +54,7 @@ func NewApp(cfg *config.Config) *App {
|
||||
storage: store,
|
||||
topologyStorage: topoStorage,
|
||||
tasks: make(map[string]*models.ScanTask),
|
||||
sessions: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
// 如果有拓扑存储,切换到第一个拓扑
|
||||
@@ -85,36 +89,56 @@ func (app *App) Start() error {
|
||||
// 设置路由
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 静态文件服务 - 使用文件系统而非embed
|
||||
// 登录API(无需认证)
|
||||
mux.HandleFunc("/api/login", app.handleLogin)
|
||||
mux.HandleFunc("/api/logout", app.handleLogout)
|
||||
|
||||
// 登录页面(无需认证)
|
||||
mux.HandleFunc("/login", app.handleLoginPage)
|
||||
|
||||
// 认证中间件保护的路由
|
||||
authMux := http.NewServeMux()
|
||||
|
||||
// 静态文件服务
|
||||
webDir := getWebDir()
|
||||
if _, err := os.Stat(webDir); err == nil {
|
||||
mux.Handle("/", http.FileServer(http.Dir(webDir)))
|
||||
authMux.Handle("/", http.FileServer(http.Dir(webDir)))
|
||||
} else {
|
||||
log.Printf("警告: web目录不存在,静态文件服务不可用")
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
authMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("<h1>网络拓扑发现系统</h1><p>Web界面文件未找到</p>"))
|
||||
})
|
||||
}
|
||||
|
||||
// API路由
|
||||
mux.HandleFunc("/api/scan", app.handleScan)
|
||||
mux.HandleFunc("/api/scan/{id}", app.handleScanProgress)
|
||||
mux.HandleFunc("/api/topology", app.handleTopology)
|
||||
mux.HandleFunc("/api/devices", app.handleGetDevices)
|
||||
mux.HandleFunc("/api/device", app.handleAddDevice)
|
||||
mux.HandleFunc("/api/device/{id}", app.handleDeviceDetail)
|
||||
authMux.HandleFunc("/api/scan", app.handleScan)
|
||||
authMux.HandleFunc("/api/scan/{id}", app.handleScanProgress)
|
||||
authMux.HandleFunc("/api/topology", app.handleTopology)
|
||||
authMux.HandleFunc("/api/devices", app.handleGetDevices)
|
||||
authMux.HandleFunc("/api/device", app.handleAddDevice)
|
||||
authMux.HandleFunc("/api/device/{id}", app.handleDeviceDetail)
|
||||
|
||||
// 拓扑管理API
|
||||
mux.HandleFunc("/api/topologies", app.handleTopologies)
|
||||
mux.HandleFunc("/api/topology/switch", app.handleSwitchTopology)
|
||||
mux.HandleFunc("/api/topology/{id}", app.handleTopologyDetail)
|
||||
authMux.HandleFunc("/api/topologies", app.handleTopologies)
|
||||
authMux.HandleFunc("/api/topology/switch", app.handleSwitchTopology)
|
||||
authMux.HandleFunc("/api/topology/{id}", app.handleTopologyDetail)
|
||||
|
||||
// SSH终端API
|
||||
mux.HandleFunc("/api/terminal", app.handleTerminalConnect)
|
||||
authMux.HandleFunc("/api/terminal", app.handleTerminalConnect)
|
||||
|
||||
// 全局设备池API
|
||||
mux.HandleFunc("/api/devices/all", app.handleGetAllDevices)
|
||||
mux.HandleFunc("/api/topology/{id}/devices", app.handleAddDevicesToTopology)
|
||||
authMux.HandleFunc("/api/devices/all", app.handleGetAllDevices)
|
||||
authMux.HandleFunc("/api/topology/{id}/devices", app.handleAddDevicesToTopology)
|
||||
|
||||
// 根据认证配置决定是否启用中间件
|
||||
var handler http.Handler = authMux
|
||||
if app.config.Auth.Enabled {
|
||||
handler = app.authMiddleware(authMux)
|
||||
log.Printf("认证已启用, 用户: %s", app.config.Auth.Username)
|
||||
} else {
|
||||
log.Printf("认证未启用")
|
||||
}
|
||||
mux.Handle("/", handler)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", app.config.Web.Host, app.config.Web.Port)
|
||||
app.httpServer = &http.Server{
|
||||
@@ -394,6 +418,64 @@ func (app *App) handleAddDevice(w http.ResponseWriter, r *http.Request) {
|
||||
func (app *App) handleDeviceDetail(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
|
||||
if r.Method == http.MethodDelete {
|
||||
// 删除设备
|
||||
if app.storage == nil {
|
||||
http.Error(w, "Storage not available", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := app.storage.DeleteDevice(id); err != nil {
|
||||
log.Printf("Failed to delete device %s: %v", id, err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 同时从 builder 中移除
|
||||
app.builder.RemoveDevice(id)
|
||||
log.Printf("Deleted device: %s", id)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPut {
|
||||
// 修改设备信息(Hostname、Type)
|
||||
var req struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if app.storage == nil {
|
||||
http.Error(w, "Storage not available", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dev, err := app.storage.GetDevice(id)
|
||||
if err != nil {
|
||||
http.Error(w, "Device not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if req.Hostname != "" {
|
||||
dev.Hostname = req.Hostname
|
||||
}
|
||||
if req.Type != "" {
|
||||
dev.Type = models.DeviceType(req.Type)
|
||||
}
|
||||
if err := app.storage.SaveDevice(dev); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 同时更新 builder 中的设备
|
||||
app.builder.AddDevice(*dev)
|
||||
log.Printf("Updated device: %s", id)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(dev)
|
||||
return
|
||||
}
|
||||
|
||||
// GET: 获取设备详情
|
||||
devices := app.builder.GetDevices()
|
||||
for _, dev := range devices {
|
||||
if dev.ID == id || dev.IP == id {
|
||||
@@ -592,6 +674,125 @@ func (app *App) handleTerminalConnect(w http.ResponseWriter, r *http.Request) {
|
||||
terminal.HandleTerminal(w, r, ip, port, username, password)
|
||||
}
|
||||
|
||||
// ==================== 认证相关 ====================
|
||||
|
||||
// authMiddleware 认证中间件
|
||||
func (app *App) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 从 cookie 获取 token
|
||||
cookie, err := r.Cookie("session_token")
|
||||
if err != nil || !app.isValidSession(cookie.Value) {
|
||||
// API 请求返回 401,页面请求重定向到登录
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized)
|
||||
} else {
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// isValidSession 验证会话是否有效
|
||||
func (app *App) isValidSession(token string) bool {
|
||||
if token == "" {
|
||||
return false
|
||||
}
|
||||
app.sessionMu.RLock()
|
||||
expire, exists := app.sessions[token]
|
||||
app.sessionMu.RUnlock()
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
if time.Now().After(expire) {
|
||||
app.sessionMu.Lock()
|
||||
delete(app.sessions, token)
|
||||
app.sessionMu.Unlock()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// generateToken 生成随机 token
|
||||
func generateToken() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// handleLogin 处理登录
|
||||
func (app *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !app.config.Auth.Enabled {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"success": true})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == app.config.Auth.Username && req.Password == app.config.Auth.Password {
|
||||
token := generateToken()
|
||||
expire := time.Now().Add(24 * time.Hour)
|
||||
|
||||
app.sessionMu.Lock()
|
||||
app.sessions[token] = expire
|
||||
app.sessionMu.Unlock()
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session_token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Expires: expire,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
log.Printf("用户 %s 登录成功", req.Username)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"success": true})
|
||||
} else {
|
||||
log.Printf("登录失败: 用户名或密码错误 (username=%s)", req.Username)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "用户名或密码错误"})
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogout 处理登出
|
||||
func (app *App) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if cookie, err := r.Cookie("session_token"); err == nil {
|
||||
app.sessionMu.Lock()
|
||||
delete(app.sessions, cookie.Value)
|
||||
app.sessionMu.Unlock()
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
})
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
// handleLoginPage 返回登录页面
|
||||
func (app *App) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
if !app.config.Auth.Enabled {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, filepath.Join(getWebDir(), "login.html"))
|
||||
}
|
||||
|
||||
// 获取所有拓扑中的全部设备(全局设备池)
|
||||
func (app *App) handleGetAllDevices(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
|
||||
Reference in New Issue
Block a user