Files
CyberPigeon/internal/server/server.go
T

442 lines
10 KiB
Go

package server
import (
"context"
"embed"
"encoding/json"
"fmt"
"io/fs"
"log/slog"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/sms-forwarder/internal/config"
"github.com/sms-forwarder/internal/forwarder"
"github.com/sms-forwarder/internal/modem"
"github.com/sms-forwarder/internal/notifier"
"github.com/sms-forwarder/internal/storage"
)
//go:embed web/*
var webFiles embed.FS
// Server Web 服务器
type Server struct {
cfg *config.Config
forwarder *forwarder.Forwarder
storage *storage.Storage
server *http.Server
configPath string
clients map[*websocket.Conn]bool
clientsMu sync.RWMutex
upgrader websocket.Upgrader
}
// New 创建服务器
func New(cfg *config.Config, fwd *forwarder.Forwarder, store *storage.Storage, configPath string) *Server {
return &Server{
cfg: cfg,
forwarder: fwd,
storage: store,
configPath: configPath,
clients: make(map[*websocket.Conn]bool),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有源
},
},
}
}
// Run 运行服务器
func (s *Server) Run(ctx context.Context) error {
if !s.cfg.Server.Enabled {
<-ctx.Done()
return nil
}
mux := http.NewServeMux()
// API 路由
mux.HandleFunc("/api/modems", s.handleModems)
mux.HandleFunc("/api/messages", s.handleMessages)
mux.HandleFunc("/api/messages/delete", s.handleDeleteMessage)
mux.HandleFunc("/api/channels", s.handleChannels)
mux.HandleFunc("/api/channels/save", s.handleSaveChannels)
mux.HandleFunc("/api/channels/test", s.handleTestChannel)
mux.HandleFunc("/api/ussd", s.handleUSSD)
mux.HandleFunc("/ws", s.handleWebSocket)
// 静态文件 - 使用 web 子目录
webFS, err := fs.Sub(webFiles, "web")
if err != nil {
return err
}
mux.Handle("/", http.FileServer(http.FS(webFS)))
s.server = &http.Server{
Addr: s.cfg.Server.Listen,
Handler: mux,
}
slog.Info("Web 服务器启动", "listen", s.cfg.Server.Listen)
// 启动服务器
go func() {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("服务器错误", "error", err)
}
}()
<-ctx.Done()
// 优雅关闭
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.server.Shutdown(shutdownCtx)
}
// ModemInfo 调制解调器信息
type ModemInfo struct {
IMEI string `json:"imei"`
Model string `json:"model"`
Manufacturer string `json:"manufacturer"`
Number string `json:"number"`
SignalQuality uint32 `json:"signal_quality"`
OperatorName string `json:"operator_name"`
ICCID string `json:"iccid"`
}
// handleModems 处理调制解调器信息请求
func (s *Server) handleModems(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
modems := s.forwarder.GetModems()
infos := make([]ModemInfo, 0, len(modems))
for _, modem := range modems {
// 更新实时信息
modem.UpdateSignalQuality()
modem.UpdateOperatorName()
modem.UpdateICCID()
info := ModemInfo{
IMEI: modem.EquipmentIdentifier,
Model: modem.Model,
Manufacturer: modem.Manufacturer,
Number: modem.Number,
SignalQuality: modem.SignalQuality,
OperatorName: modem.OperatorName,
ICCID: modem.ICCID,
}
infos = append(infos, info)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(infos)
}
// handleMessages 处理短信列表请求
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
if s.storage == nil {
json.NewEncoder(w).Encode(map[string]interface{}{
"items": []storage.Message{},
"total": 0,
})
return
}
// 解析分页参数
query := r.URL.Query()
limit := 50
offset := 0
if l := query.Get("limit"); l != "" {
fmt.Sscanf(l, "%d", &limit)
}
if o := query.Get("offset"); o != "" {
fmt.Sscanf(o, "%d", &offset)
}
messages, total := s.storage.ListWithPagination(limit, offset)
json.NewEncoder(w).Encode(map[string]interface{}{
"items": messages,
"total": total,
})
}
// handleUSSD 处理 USSD 请求
func (s *Server) handleUSSD(w http.ResponseWriter, r *http.Request) {
// Helper to send JSON error
sendError := func(w http.ResponseWriter, msg string, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{
"error": msg,
})
}
if r.Method != http.MethodPost {
sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
IMEI string `json:"imei"`
Code string `json:"code"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendError(w, "Invalid request", http.StatusBadRequest)
return
}
modems := s.forwarder.GetModems()
var targetModem *modem.Modem
for _, m := range modems {
if m.EquipmentIdentifier == req.IMEI {
targetModem = m
break
}
}
if targetModem == nil {
sendError(w, "Device not found", http.StatusNotFound)
return
}
slog.Info("执行 USSD", "imei", req.IMEI, "code", req.Code)
reply, err := targetModem.RunUSSD(req.Code)
if err != nil {
slog.Error("USSD 执行失败", "error", err)
sendError(w, "执行失败: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"reply": reply,
})
}
// handleDeleteMessage 处理删除短信请求
func (s *Server) handleDeleteMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if s.storage == nil {
http.Error(w, "Storage not enabled", http.StatusServiceUnavailable)
return
}
var req struct {
ID string `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if err := s.storage.Delete(req.ID); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// handleChannels 处理获取通道配置请求
func (s *Server) handleChannels(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 定义所有预设通道模板
channels := []config.ChannelConfig{
{
Type: "email",
Enabled: false,
Port: 587,
UseTLS: true,
},
{
Type: "bark",
Enabled: false,
},
{
Type: "gotify",
Enabled: false,
Priority: 5,
},
{
Type: "serverchan",
Enabled: false,
},
{
Type: "webhook",
Enabled: false,
Method: "POST",
},
}
// 将配置文件中的值合并到模板中
for i := range channels {
for j := range s.cfg.Channels {
if channels[i].Type == s.cfg.Channels[j].Type {
channels[i] = s.cfg.Channels[j]
break
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(channels)
}
// handleSaveChannels 处理保存通道配置请求
func (s *Server) handleSaveChannels(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var channels []config.ChannelConfig
if err := json.NewDecoder(r.Body).Decode(&channels); err != nil {
http.Error(w, "Invalid request: "+err.Error(), http.StatusBadRequest)
return
}
// 直接保存所有通道(包括未启用的)
s.cfg.Channels = channels
// 保存到配置文件
if err := s.cfg.Save(s.configPath); err != nil {
http.Error(w, "Failed to save config: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// handleTestChannel 处理测试通道请求
func (s *Server) handleTestChannel(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var channels []config.ChannelConfig
if err := json.NewDecoder(r.Body).Decode(&channels); err != nil {
http.Error(w, "Invalid request: "+err.Error(), http.StatusBadRequest)
return
}
// 创建通知发送器(包含所有要测试的通道)
testCfg := &config.Config{
Channels: channels,
}
n, err := notifier.New(testCfg)
if err != nil {
http.Error(w, "创建通知器失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 创建测试消息
testMsg := notifier.Message{
Modem: "测试设备",
From: "测试号码",
Text: "这是一条测试推送消息,如果您收到此消息,说明推送通道配置正确。",
Timestamp: time.Now(),
Incoming: true,
}
// 发送测试消息
if err := n.Send(testMsg); err != nil {
http.Error(w, "测试失败: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "测试消息已发送",
})
}
// handleWebSocket 处理 WebSocket 连接
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
slog.Error("WebSocket 升级失败", "error", err)
return
}
// 注册客户端
s.clientsMu.Lock()
s.clients[conn] = true
s.clientsMu.Unlock()
slog.Info("WebSocket 客户端连接", "remote", r.RemoteAddr)
// 客户端断开时清理
defer func() {
s.clientsMu.Lock()
delete(s.clients, conn)
s.clientsMu.Unlock()
conn.Close()
slog.Info("WebSocket 客户端断开", "remote", r.RemoteAddr)
}()
// 保持连接并处理 ping/pong
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}
// BroadcastMessage 广播新消息给所有 WebSocket 客户端
func (s *Server) BroadcastMessage(msg storage.Message) {
s.clientsMu.RLock()
defer s.clientsMu.RUnlock()
data, err := json.Marshal(map[string]interface{}{
"type": "new_message",
"message": msg,
})
if err != nil {
slog.Error("序列化消息失败", "error", err)
return
}
for client := range s.clients {
err := client.WriteMessage(websocket.TextMessage, data)
if err != nil {
slog.Error("发送 WebSocket 消息失败", "error", err)
}
}
}