| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958 |
- package main
- import (
- "crypto/rand"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
- "network-topology-discovery/internal/config"
- "network-topology-discovery/internal/device"
- "network-topology-discovery/internal/scanner"
- "network-topology-discovery/internal/storage"
- "network-topology-discovery/internal/terminal"
- "network-topology-discovery/internal/topology"
- "network-topology-discovery/pkg/models"
- )
- // App 应用
- type App struct {
- config *config.Config
- builder *topology.Builder
- storage *storage.Storage
- topologyStorage *storage.TopologyStorage
- tasks map[string]*models.ScanTask
- mu sync.RWMutex
- httpServer *http.Server
- sessions map[string]time.Time // token -> expire time
- sessionMu sync.RWMutex
- }
- // NewApp 创建应用
- func NewApp(cfg *config.Config) *App {
- // 初始化拓扑存储(管理多个拓扑)
- topoStorage, err := storage.NewTopologyStorage("data")
- if err != nil {
- log.Printf("Warning: failed to initialize topology storage: %v", err)
- }
- // 初始化设备存储(使用默认文件,兼容旧版)
- store, err := storage.NewStorage("devices.json")
- if err != nil {
- log.Printf("Warning: failed to initialize storage: %v", err)
- }
- app := &App{
- config: cfg,
- builder: topology.NewBuilder(),
- storage: store,
- topologyStorage: topoStorage,
- tasks: make(map[string]*models.ScanTask),
- sessions: make(map[string]time.Time),
- }
- // 如果有拓扑存储,切换到第一个拓扑
- if topoStorage != nil {
- topos, err := topoStorage.GetAllTopologies()
- if err == nil && len(topos) > 0 {
- topoStorage.SetCurrentTopology(topos[0].ID)
- deviceFile := topoStorage.GetDeviceFilePath(topos[0].ID)
- app.storage.SetFilePath(deviceFile)
- log.Printf("Loaded topology: %s", topos[0].Name)
- }
- }
- // 加载设备到拓扑构建器
- if store != nil {
- devices, err := store.GetAllDevices()
- if err != nil {
- log.Printf("Warning: failed to load devices from database: %v", err)
- } else {
- log.Printf("Loaded %d devices from storage", len(devices))
- for _, dev := range devices {
- app.builder.AddDevice(dev)
- }
- }
- }
- return app
- }
- // Start 启动应用
- func (app *App) Start() error {
- // 设置路由
- mux := http.NewServeMux()
- // 登录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 {
- authMux.Handle("/", http.FileServer(http.Dir(webDir)))
- } else {
- log.Printf("警告: web目录不存在,静态文件服务不可用")
- authMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("<h1>网络拓扑发现系统</h1><p>Web界面文件未找到</p>"))
- })
- }
- // API路由
- 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
- authMux.HandleFunc("/api/topologies", app.handleTopologies)
- authMux.HandleFunc("/api/topology/switch", app.handleSwitchTopology)
- authMux.HandleFunc("/api/topology/{id}", app.handleTopologyDetail)
- // SSH终端API
- authMux.HandleFunc("/api/terminal", app.handleTerminalConnect)
- // 全局设备池API
- 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{
- Addr: addr,
- Handler: mux,
- }
- log.Printf("服务启动在 %s", addr)
- return app.httpServer.ListenAndServe()
- }
- // 生成唯一ID
- func generateID() string {
- b := make([]byte, 16)
- rand.Read(b)
- return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
- }
- // getWebDir 获取web目录路径
- func getWebDir() string {
- // 尝试多个可能的路径
- possiblePaths := []string{
- "web",
- filepath.Join("cmd", "web"),
- filepath.Join("..", "web"),
- }
- for _, path := range possiblePaths {
- if _, err := os.Stat(path); err == nil {
- absPath, _ := filepath.Abs(path)
- return absPath
- }
- }
- // 默认返回web
- return "web"
- }
- // 处理扫描请求
- func (app *App) handleScan(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- var req struct {
- ScanRange string `json:"scan_range"`
- SSHPort int `json:"ssh_port"`
- 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 req.SSHPort == 0 {
- req.SSHPort = 22
- }
- // 创建扫描任务
- taskID := generateID()
- task := &models.ScanTask{
- ID: taskID,
- Status: "running",
- StartTime: time.Now(),
- Devices: []models.Device{},
- }
- app.mu.Lock()
- app.tasks[taskID] = task
- app.mu.Unlock()
- // 确保当前有拓扑
- if app.topologyStorage != nil && app.topologyStorage.GetCurrentTopologyID() == "" {
- topos, err := app.topologyStorage.GetAllTopologies()
- if err == nil && len(topos) > 0 {
- app.topologyStorage.SetCurrentTopology(topos[0].ID)
- deviceFile := app.topologyStorage.GetDeviceFilePath(topos[0].ID)
- app.storage.SetFilePath(deviceFile)
- }
- }
- // 异步执行扫描
- go app.runScan(task, req.ScanRange, req.SSHPort, req.Username, req.Password)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"task_id": taskID})
- }
- // 执行扫描
- func (app *App) runScan(task *models.ScanTask, cidr string, sshPort int, username, password string) {
- defer func() {
- task.EndTime = time.Now()
- }()
- // 创建扫描器
- sc := scanner.NewScanner(app.config.Scanner.Concurrency, time.Duration(app.config.Scanner.Timeout)*time.Second)
- // 阶段1: 解析IP范围 (进度 0% -> 10%)
- ips, err := sc.ScanRange(cidr)
- if err != nil {
- task.Status = "failed"
- task.ErrorMessage = err.Error()
- return
- }
- task.TotalDevices = len(ips)
- task.Progress = 10
- log.Printf("[扫描] 阶段1: 解析CIDR %s, 共 %d 个IP", cidr, len(ips))
- // 阶段2: 检查存活主机 (进度 10% -> 30%)
- aliveHosts := sc.CheckHosts(ips)
- task.Progress = 30
- log.Printf("[扫描] 阶段2: 发现 %d 个存活主机", len(aliveHosts))
- // 阶段3: 检查SSH端口 (进度 30% -> 50%)
- sshHosts := sc.CheckSSHHosts(aliveHosts, sshPort)
- task.Progress = 50
- task.TotalDevices = len(sshHosts)
- log.Printf("[扫描] 阶段3: 发现 %d 个SSH主机", len(sshHosts))
- // 如果没有SSH主机,直接完成
- if len(sshHosts) == 0 {
- task.Status = "completed"
- task.Progress = 100
- log.Printf("[扫描] 未发现SSH主机,扫描完成")
- return
- }
- // 阶段4: 采集设备信息 (进度 50% -> 100%)
- var devices []models.Device
- for i, ip := range sshHosts {
- log.Printf("[扫描] 阶段4: 正在采集设备 %s (%d/%d)", ip, i+1, len(sshHosts))
- // 尝试不同设备类型
- deviceTypes := []models.DeviceType{
- models.DeviceTypeCisco,
- models.DeviceTypeHuawei,
- models.DeviceTypeH3C,
- models.DeviceTypeASA,
- models.DeviceTypeLinux,
- models.DeviceTypeWindows,
- }
- var discoveredDevice *models.Device
- for _, dtype := range deviceTypes {
- dev, err := device.DiscoverDevice(ip, dtype, username, password)
- if err == nil && dev.ScanStatus == "success" {
- discoveredDevice = dev
- break
- }
- }
- if discoveredDevice != nil {
- devices = append(devices, *discoveredDevice)
- app.builder.AddDevice(*discoveredDevice)
- // 保存到数据库
- if app.storage != nil {
- if err := app.storage.SaveDevice(discoveredDevice); err != nil {
- log.Printf("Warning: failed to save device %s to database: %v", ip, err)
- }
- }
- }
- // 更新进度: 50% ~ 100%
- task.ScannedDevices = i + 1
- task.Progress = 50 + (i+1)*50/len(sshHosts)
- task.Devices = devices
- }
- task.Status = "completed"
- task.Progress = 100
- task.Devices = devices
- log.Printf("[扫描] 完成,共发现 %d 台设备", len(devices))
- }
- // 处理扫描进度查询
- func (app *App) handleScanProgress(w http.ResponseWriter, r *http.Request) {
- id := r.PathValue("id")
- app.mu.RLock()
- task, exists := app.tasks[id]
- app.mu.RUnlock()
- if !exists {
- http.Error(w, "Task not found", http.StatusNotFound)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(task)
- }
- // 处理拓扑查询
- func (app *App) handleTopology(w http.ResponseWriter, r *http.Request) {
- graph := app.builder.Build()
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(graph)
- }
- // 处理获取所有设备
- func (app *App) handleGetDevices(w http.ResponseWriter, r *http.Request) {
- var devices []models.Device
- // 优先从存储获取
- if app.storage != nil {
- var err error
- devices, err = app.storage.GetAllDevices()
- if err != nil {
- log.Printf("Error: failed to get devices from storage: %v", err)
- // 降级到 builder获取
- devices = app.builder.GetDevices()
- }
- log.Printf("Returning %d devices from storage", len(devices))
- } else {
- devices = app.builder.GetDevices()
- log.Printf("Returning %d devices from builder", len(devices))
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(devices)
- }
- // 处理添加设备
- func (app *App) handleAddDevice(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- var req struct {
- IP string `json:"ip"`
- Type string `json:"type"`
- 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
- }
- deviceType := models.DeviceType(req.Type)
- log.Printf("Adding device: %s (type: %s)", req.IP, req.Type)
- dev, err := device.DiscoverDevice(req.IP, deviceType, req.Username, req.Password)
- if err != nil {
- log.Printf("Failed to discover device %s: %v", req.IP, err)
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusInternalServerError)
- json.NewEncoder(w).Encode(map[string]string{"message": err.Error()})
- return
- }
- log.Printf("Device discovered: %s, interfaces: %d, neighbors: %d",
- dev.IP, len(dev.Interfaces), len(dev.Neighbors))
- app.builder.AddDevice(*dev)
- // 保存到存储
- if app.storage != nil {
- if err := app.storage.SaveDevice(dev); err != nil {
- log.Printf("Error: failed to save device %s to storage: %v", req.IP, err)
- } else {
- log.Printf("Device %s saved to storage successfully", req.IP)
- }
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(dev)
- }
- // 处理设备详情查询
- 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 {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(dev)
- return
- }
- }
- http.Error(w, "Device not found", http.StatusNotFound)
- }
- // 处理拓扑列表(GET: 获取所有拓扑,POST: 创建新拓扑)
- func (app *App) handleTopologies(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- if r.Method == http.MethodGet {
- // 获取所有拓扑
- if app.topologyStorage == nil {
- json.NewEncoder(w).Encode([]models.Topology{})
- return
- }
- topos, err := app.topologyStorage.GetAllTopologies()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- // 更新每个拓扑的设备数量
- currentTopoID := app.topologyStorage.GetCurrentTopologyID()
- for i := range topos {
- deviceFile := app.topologyStorage.GetDeviceFilePath(topos[i].ID)
- store, err := storage.NewStorage(deviceFile)
- if err == nil {
- devices, err := store.GetAllDevices()
- if err == nil {
- topos[i].DeviceCount = len(devices)
- }
- }
- // 标记当前拓扑
- if topos[i].ID == currentTopoID {
- topos[i].Name = topos[i].Name + " (当前)"
- }
- }
- json.NewEncoder(w).Encode(topos)
- } else if r.Method == http.MethodPost {
- // 创建新拓扑
- var req struct {
- Name string `json:"name"`
- Description string `json:"description"`
- ScanRange string `json:"scan_range"`
- SSHPort int `json:"ssh_port"`
- Username string `json:"username"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if req.Name == "" {
- http.Error(w, "name is required", http.StatusBadRequest)
- return
- }
- topo, err := app.topologyStorage.CreateTopology(req.Name, req.Description, req.ScanRange, req.SSHPort, req.Username)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- // 自动切换到新拓扑
- app.topologyStorage.SetCurrentTopology(topo.ID)
- deviceFile := app.topologyStorage.GetDeviceFilePath(topo.ID)
- app.storage.SetFilePath(deviceFile)
- app.builder.Clear()
- log.Printf("Created and switched to new topology: %s", topo.Name)
- json.NewEncoder(w).Encode(topo)
- } else {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- }
- }
- // 处理切换拓扑
- func (app *App) handleSwitchTopology(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- var req struct {
- TopologyID string `json:"topology_id"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if app.topologyStorage == nil {
- http.Error(w, "Topology storage not available", http.StatusInternalServerError)
- return
- }
- // 验证拓扑存在
- topo, err := app.topologyStorage.GetTopology(req.TopologyID)
- if err != nil {
- http.Error(w, "Topology not found", http.StatusNotFound)
- return
- }
- // 切换拓扑
- err = app.topologyStorage.SetCurrentTopology(req.TopologyID)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- // 切换设备存储文件
- deviceFile := app.topologyStorage.GetDeviceFilePath(req.TopologyID)
- err = app.storage.SetFilePath(deviceFile)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- // 清空并重新加载拓扑构建器
- app.builder.Clear()
- devices, err := app.storage.GetAllDevices()
- if err == nil {
- for _, dev := range devices {
- app.builder.AddDevice(dev)
- }
- }
- log.Printf("Switched to topology: %s (%s)", topo.Name, req.TopologyID)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"message": "Topology switched successfully"})
- }
- // 处理拓扑详情
- func (app *App) handleTopologyDetail(w http.ResponseWriter, r *http.Request) {
- id := r.PathValue("id")
- if app.topologyStorage == nil {
- http.Error(w, "Topology storage not available", http.StatusInternalServerError)
- return
- }
- topo, err := app.topologyStorage.GetTopology(id)
- if err != nil {
- http.Error(w, "Topology not found", http.StatusNotFound)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(topo)
- }
- // 处理SSH终端连接
- func (app *App) handleTerminalConnect(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- // WebSocket 连接使用 GET,但我们通过 POST 获取凭据后再升级
- if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- }
- // 从查询参数获取设备IP
- ip := r.URL.Query().Get("ip")
- if ip == "" {
- http.Error(w, "ip parameter is required", http.StatusBadRequest)
- return
- }
- port := 22
- if p := r.URL.Query().Get("port"); p != "" {
- fmt.Sscanf(p, "%d", &port)
- }
- username := r.URL.Query().Get("username")
- password := r.URL.Query().Get("password")
- if username == "" || password == "" {
- http.Error(w, "username and password are required", http.StatusBadRequest)
- return
- }
- log.Printf("[终端] 正在连接 %s:%d (%s)", ip, port, username)
- // 使用 terminal handler 处理 WebSocket 连接
- 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 {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- deviceMap := make(map[string]models.Device) // 用IP去重
- if app.topologyStorage != nil {
- topos, err := app.topologyStorage.GetAllTopologies()
- if err == nil {
- for _, topo := range topos {
- deviceFile := app.topologyStorage.GetDeviceFilePath(topo.ID)
- store, err := storage.NewStorage(deviceFile)
- if err != nil {
- continue
- }
- devices, err := store.GetAllDevices()
- if err != nil {
- continue
- }
- for _, dev := range devices {
- deviceMap[dev.IP] = dev
- }
- }
- }
- }
- // 也从旧版 devices.json 加载
- if app.storage != nil {
- devices, err := app.storage.GetAllDevices()
- if err == nil {
- for _, dev := range devices {
- deviceMap[dev.IP] = dev
- }
- }
- }
- devices := make([]models.Device, 0, len(deviceMap))
- for _, dev := range deviceMap {
- devices = append(devices, dev)
- }
- log.Printf("[设备池] 返回 %d 个全局设备", len(devices))
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(devices)
- }
- // 向指定拓扑批量添加设备
- func (app *App) handleAddDevicesToTopology(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- topoID := r.PathValue("id")
- var req struct {
- DeviceIDs []string `json:"device_ids"` // IP列表或ID列表
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if len(req.DeviceIDs) == 0 {
- http.Error(w, "device_ids is required", http.StatusBadRequest)
- return
- }
- // 获取目标拓扑的存储
- if app.topologyStorage == nil {
- http.Error(w, "Topology storage not available", http.StatusInternalServerError)
- return
- }
- topo, err := app.topologyStorage.GetTopology(topoID)
- if err != nil {
- http.Error(w, "Topology not found", http.StatusNotFound)
- return
- }
- deviceFile := app.topologyStorage.GetDeviceFilePath(topoID)
- targetStore, err := storage.NewStorage(deviceFile)
- if err != nil {
- http.Error(w, "Failed to open target storage", http.StatusInternalServerError)
- return
- }
- // 从全局设备池中查找并添加
- allDeviceMap := make(map[string]models.Device)
- topos, _ := app.topologyStorage.GetAllTopologies()
- for _, t := range topos {
- df := app.topologyStorage.GetDeviceFilePath(t.ID)
- s, err := storage.NewStorage(df)
- if err != nil {
- continue
- }
- devs, err := s.GetAllDevices()
- if err != nil {
- continue
- }
- for _, d := range devs {
- allDeviceMap[d.IP] = d
- allDeviceMap[d.ID] = d
- }
- }
- added := 0
- for _, idOrIP := range req.DeviceIDs {
- if dev, ok := allDeviceMap[idOrIP]; ok {
- devCopy := dev // 复制一份
- targetStore.SaveDevice(&devCopy)
- added++
- }
- }
- // 如果是当前拓扑,刷新builder
- if app.topologyStorage.GetCurrentTopologyID() == topoID {
- app.builder.Clear()
- devices, _ := targetStore.GetAllDevices()
- for _, dev := range devices {
- app.builder.AddDevice(dev)
- }
- }
- log.Printf("[拓扑] 向 %s 添加了 %d/%d 设备", topo.Name, added, len(req.DeviceIDs))
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]interface{}{
- "added": added,
- "total": len(req.DeviceIDs),
- "topology": topo.Name,
- })
- }
- func main() {
- // 加载配置
- configFile := "config.json"
- if len(os.Args) > 1 {
- configFile = os.Args[1]
- }
- var cfg *config.Config
- if _, err := os.Stat(configFile); err == nil {
- cfg, err = config.LoadConfig(configFile)
- if err != nil {
- log.Printf("加载配置文件失败: %v, 使用默认配置", err)
- cfg = config.DefaultConfig()
- }
- } else {
- log.Printf("配置文件不存在, 使用默认配置")
- cfg = config.DefaultConfig()
- }
- // 创建并启动应用
- app := NewApp(cfg)
- log.Println("网络拓扑发现系统启动...")
- if err := app.Start(); err != nil {
- log.Fatalf("服务启动失败: %v", err)
- }
- }
|