Files
MeowMusicServer/device.go
2025-12-09 16:33:44 +08:00

741 lines
18 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"os"
"strings"
"sync"
"time"
)
// Device 设备信息结构
type Device struct {
MAC string `json:"mac"`
Username string `json:"username"`
DeviceName string `json:"device_name"`
Token string `json:"token"`
BindTime time.Time `json:"bind_time"`
LastSeen time.Time `json:"last_seen"`
IsActive bool `json:"is_active"`
}
// BindingCode 绑定码结构
type BindingCode struct {
Code string `json:"code"`
Username string `json:"username"`
ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used"`
}
// DeviceManager 设备管理器
type DeviceManager struct {
devices map[string]*Device // MAC -> Device
bindingCodes map[string]*BindingCode // Code -> BindingCode
tokens map[string]string // Token -> MAC
mu sync.RWMutex
filePath string
}
var deviceManager *DeviceManager
var deviceManagerOnce sync.Once
// GetDeviceManager 获取设备管理器单例
func GetDeviceManager() *DeviceManager {
deviceManagerOnce.Do(func() {
deviceManager = &DeviceManager{
devices: make(map[string]*Device),
bindingCodes: make(map[string]*BindingCode),
tokens: make(map[string]string),
filePath: "./devices.json",
}
deviceManager.LoadFromFile()
})
return deviceManager
}
// GenerateBindingCode 生成6位数字绑定码
func (dm *DeviceManager) GenerateBindingCode(username string) (string, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
// 生成6位随机数字
var code string
for i := 0; i < 6; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
code += fmt.Sprintf("%d", n.Int64())
}
// 检查是否已存在(小概率)
if _, exists := dm.bindingCodes[code]; exists {
// 递归重新生成
return dm.GenerateBindingCode(username)
}
// 创建绑定码5分钟有效
bindingCode := &BindingCode{
Code: code,
Username: username,
ExpiresAt: time.Now().Add(5 * time.Minute),
Used: false,
}
dm.bindingCodes[code] = bindingCode
fmt.Printf("[Device] Generated binding code %s for user %s\n", code, username)
return code, nil
}
// generateDeviceToken 生成设备Token
func generateDeviceToken() string {
b := make([]byte, 32)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
// BindDevice 绑定设备
func (dm *DeviceManager) BindDevice(mac, bindingCode, deviceName string) (*Device, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
// 验证绑定码
code, exists := dm.bindingCodes[bindingCode]
if !exists {
return nil, fmt.Errorf("绑定码不存在")
}
if code.Used {
return nil, fmt.Errorf("绑定码已使用")
}
if time.Now().After(code.ExpiresAt) {
return nil, fmt.Errorf("绑定码已过期")
}
// 检查设备是否已绑定
if existingDevice, exists := dm.devices[mac]; exists {
return nil, fmt.Errorf("设备已绑定到用户 %s", existingDevice.Username)
}
// 生成Token
token := generateDeviceToken()
// 创建设备
device := &Device{
MAC: mac,
Username: code.Username,
DeviceName: deviceName,
Token: token,
BindTime: time.Now(),
LastSeen: time.Now(),
IsActive: true,
}
// 保存设备信息
dm.devices[mac] = device
dm.tokens[token] = mac
// 标记绑定码已使用
code.Used = true
// 保存到文件
dm.SaveToFile()
fmt.Printf("[Device] Device %s bound to user %s\n", mac, code.Username)
return device, nil
}
// VerifyToken 验证设备Token
func (dm *DeviceManager) VerifyToken(token string) (*Device, error) {
dm.mu.RLock()
defer dm.mu.RUnlock()
mac, exists := dm.tokens[token]
if !exists {
return nil, fmt.Errorf("无效的Token")
}
device, exists := dm.devices[mac]
if !exists {
return nil, fmt.Errorf("设备不存在")
}
if !device.IsActive {
return nil, fmt.Errorf("设备已停用")
}
return device, nil
}
// GetDeviceByMAC 根据MAC地址获取设备
func (dm *DeviceManager) GetDeviceByMAC(mac string) (*Device, error) {
dm.mu.RLock()
defer dm.mu.RUnlock()
device, exists := dm.devices[mac]
if !exists {
return nil, fmt.Errorf("设备未绑定")
}
return device, nil
}
// UpdateLastSeen 更新设备最后在线时间
func (dm *DeviceManager) UpdateLastSeen(mac string) {
dm.mu.Lock()
defer dm.mu.Unlock()
if device, exists := dm.devices[mac]; exists {
device.LastSeen = time.Now()
dm.SaveToFile()
}
}
// DirectBindDevice 直接绑定设备(不需要绑定码)
func (dm *DeviceManager) DirectBindDevice(mac, username, deviceName string) (*Device, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
// 检查设备是否已绑定
if existingDevice, exists := dm.devices[mac]; exists {
return nil, fmt.Errorf("设备已绑定到用户 %s", existingDevice.Username)
}
// 生成Token
token := generateDeviceToken()
// 创建设备
device := &Device{
MAC: mac,
Username: username,
DeviceName: deviceName,
Token: token,
BindTime: time.Now(),
LastSeen: time.Now(),
IsActive: true,
}
dm.devices[mac] = device
dm.tokens[token] = mac
dm.SaveToFile()
fmt.Printf("[Device] Device %s directly bound to user %s\n", mac, username)
return device, nil
}
// UnbindDevice 解绑设备
func (dm *DeviceManager) UnbindDevice(mac string) error {
dm.mu.Lock()
defer dm.mu.Unlock()
device, exists := dm.devices[mac]
if !exists {
return fmt.Errorf("设备不存在")
}
// 删除Token映射
delete(dm.tokens, device.Token)
// 删除设备
delete(dm.devices, mac)
dm.SaveToFile()
fmt.Printf("[Device] Device %s unbound\n", mac)
return nil
}
// GetDevice 获取单个设备信息
func (dm *DeviceManager) GetDevice(mac string) (*Device, error) {
dm.mu.RLock()
defer dm.mu.RUnlock()
device, exists := dm.devices[mac]
if !exists {
return nil, fmt.Errorf("设备不存在")
}
return device, nil
}
// GetUserDevices 获取用户的所有设备
func (dm *DeviceManager) GetUserDevices(username string) []*Device {
dm.mu.RLock()
defer dm.mu.RUnlock()
var devices []*Device
for _, device := range dm.devices {
if device.Username == username {
devices = append(devices, device)
}
}
return devices
}
// SaveToFile 保存设备信息到文件
func (dm *DeviceManager) SaveToFile() error {
data := struct {
Devices map[string]*Device `json:"devices"`
}{
Devices: dm.devices,
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Println("[Error] Failed to marshal devices:", err)
return err
}
err = os.WriteFile(dm.filePath, jsonData, 0644)
if err != nil {
fmt.Println("[Error] Failed to write devices.json:", err)
return err
}
return nil
}
// LoadFromFile 从文件加载设备信息
func (dm *DeviceManager) LoadFromFile() error {
file, err := os.Open(dm.filePath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("[Info] devices.json not found, creating new file")
return nil
}
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
var fileData struct {
Devices map[string]*Device `json:"devices"`
}
err = json.Unmarshal(data, &fileData)
if err != nil {
return err
}
dm.devices = fileData.Devices
if dm.devices == nil {
dm.devices = make(map[string]*Device)
}
// 重建Token索引
for mac, device := range dm.devices {
dm.tokens[device.Token] = mac
}
fmt.Printf("[Info] Loaded %d devices from file\n", len(dm.devices))
return nil
}
// HTTP处理器
// GenerateBindingCodeHandler 生成绑定码
func GenerateBindingCodeHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前登录用户
username := GetCurrentUser(r) // 需要从session获取
if username == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
dm := GetDeviceManager()
code, err := dm.GenerateBindingCode(username)
if err != nil {
http.Error(w, "Failed to generate binding code", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"code": code,
"expires_in": 300, // 5分钟
})
}
// BindDeviceHandler ESP32设备绑定接口
func BindDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
fmt.Println("[API] ESP32 device bind request received from", r.RemoteAddr)
// 解析请求
var req struct {
MAC string `json:"mac"`
BindingCode string `json:"binding_code"`
DeviceName string `json:"device_name"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.MAC == "" || req.BindingCode == "" {
http.Error(w, "Missing required fields", http.StatusBadRequest)
return
}
// 如果没有提供设备名称,使用默认名称
if req.DeviceName == "" {
req.DeviceName = "ESP32音乐播放器"
}
dm := GetDeviceManager()
device, err := dm.BindDevice(req.MAC, req.BindingCode, req.DeviceName)
if err != nil {
fmt.Printf("[Error] Device bind failed: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "设备绑定成功",
"token": device.Token,
"username": device.Username,
})
}
// VerifyDeviceHandler 验证设备Token
func VerifyDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
token := r.Header.Get("X-Device-Token")
if token == "" {
token = r.URL.Query().Get("token")
}
if token == "" {
http.Error(w, "Missing token", http.StatusBadRequest)
return
}
dm := GetDeviceManager()
device, err := dm.VerifyToken(token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// 更新最后在线时间
dm.UpdateLastSeen(device.MAC)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"device": map[string]interface{}{
"mac": device.MAC,
"username": device.Username,
"device_name": device.DeviceName,
"bind_time": device.BindTime,
"last_seen": device.LastSeen,
},
})
}
// GetCurrentUser 从请求中获取当前登录用户
func GetCurrentUser(r *http.Request) string {
userStore := GetUserStore()
// 1. 尝试从 Authorization Header 获取 (Bearer Token)
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
token := strings.TrimPrefix(authHeader, "Bearer ")
// 直接从UserStore验证Token
user, err := userStore.GetUserByToken(token)
if err == nil && user != nil {
return user.Username
}
}
// 2. 尝试从 Cookie 获取 session_token
cookie, err := r.Cookie("session_token")
if err == nil {
username := userStore.GetUsernameByToken(cookie.Value)
if username != "" {
return username
}
}
// 3. 尝试从 X-Device-Token Header 获取 (用于ESP32设备)
deviceToken := r.Header.Get("X-Device-Token")
if deviceToken != "" {
dm := GetDeviceManager()
device, err := dm.VerifyToken(deviceToken)
if err == nil && device != nil {
// 更新最后在线时间
dm.UpdateLastSeen(device.MAC)
return device.Username
}
}
return ""
}
// DirectBindDeviceHandler Web端直接绑定设备不需要绑定码
func DirectBindDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前用户
username := GetCurrentUser(r)
if username == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "未登录",
})
return
}
// 解析请求
var req struct {
MAC string `json:"mac"`
DeviceName string `json:"device_name"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "请求格式错误",
})
return
}
if req.MAC == "" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "MAC地址不能为空",
})
return
}
// 如果没有提供设备名称,使用默认名称
if req.DeviceName == "" {
req.DeviceName = "ESP32音乐播放器"
}
dm := GetDeviceManager()
device, err := dm.DirectBindDevice(req.MAC, username, req.DeviceName)
if err != nil {
fmt.Printf("[Error] Direct bind failed: %v\n", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "设备绑定成功",
"device": map[string]interface{}{
"mac": device.MAC,
"device_name": device.DeviceName,
"bind_time": device.BindTime,
},
})
}
// ListUserDevicesHandler 列出用户的所有设备
func ListUserDevicesHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前用户
username := GetCurrentUser(r)
if username == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "未登录",
})
return
}
dm := GetDeviceManager()
devices := dm.GetUserDevices(username)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"devices": devices,
})
}
// UnbindDeviceHandler 解绑设备
func UnbindDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前用户
username := GetCurrentUser(r)
if username == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "未登录",
})
return
}
// 解析请求
var req struct {
MAC string `json:"mac"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "请求格式错误",
})
return
}
if req.MAC == "" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "MAC地址不能为空",
})
return
}
dm := GetDeviceManager()
// 检查设备是否属于当前用户
device, err := dm.GetDevice(req.MAC)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "设备不存在",
})
return
}
if device.Username != username {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "无权解绑此设备",
})
return
}
// 解绑设备
err = dm.UnbindDevice(req.MAC)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "设备已解绑",
})
}
// SyncDeviceHandler ESP32用MAC地址同步Token用于网页端绑定后自动获取token
func SyncDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
MAC string `json:"mac"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.MAC == "" {
http.Error(w, "Missing MAC address", http.StatusBadRequest)
return
}
dm := GetDeviceManager()
device, err := dm.GetDevice(req.MAC)
if err != nil || device == nil {
// 设备未绑定
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "设备未绑定",
})
return
}
// 更新最后在线时间
dm.UpdateLastSeen(device.MAC)
// 返回 token 和用户名
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"token": device.Token,
"username": device.Username,
"message": "同步成功",
})
fmt.Printf("[Info] Device %s synced token for user: %s\n", device.MAC, device.Username)
}