first commit
This commit is contained in:
commit
bcbf2a8d08
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
43
cmd/server/main.go
Normal file
43
cmd/server/main.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"licensing-cotton/internal/database"
|
||||
"licensing-cotton/internal/handlers"
|
||||
"licensing-cotton/internal/security"
|
||||
)
|
||||
|
||||
func main() {
|
||||
passphrase := "2718"
|
||||
|
||||
// 1. 初始化数据库
|
||||
if err := database.InitDB("mydb.db"); err != nil {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 初始化 ECC 密钥
|
||||
if err := security.InitECCKey(); err != nil {
|
||||
log.Fatalf("生成ECC密钥对失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化私钥
|
||||
if err := security.InitEd25519Keys(passphrase); err != nil {
|
||||
log.Fatalf("Failed to initialize keys: %v", err)
|
||||
}
|
||||
|
||||
// 3. 初始化默认管理员
|
||||
if err := handlers.EnsureDefaultAdmin("admin", "admin123"); err != nil {
|
||||
log.Printf("初始化默认管理员失败: %v", err)
|
||||
}
|
||||
|
||||
// 4. 注册路由
|
||||
mux := handlers.RegisterRoutes()
|
||||
|
||||
// 5. 启动HTTP服务器
|
||||
log.Println("服务器启动中,监听 :8080 ...")
|
||||
if err := http.ListenAndServe("0.0.0.0:8895", mux); err != nil {
|
||||
log.Fatal("服务器启动失败:", err)
|
||||
}
|
||||
}
|
||||
25
go.mod
Normal file
25
go.mod
Normal file
@ -0,0 +1,25 @@
|
||||
module licensing-cotton
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
golang.org/x/crypto v0.32.0
|
||||
modernc.org/sqlite v1.34.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
55
go.sum
Normal file
55
go.sum
Normal file
@ -0,0 +1,55 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
3
internal/config/config.go
Normal file
3
internal/config/config.go
Normal file
@ -0,0 +1,3 @@
|
||||
package config
|
||||
|
||||
var AutoRenewAllDevices bool = false
|
||||
67
internal/database/database.go
Normal file
67
internal/database/database.go
Normal file
@ -0,0 +1,67 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
func InitDB(dbFile string) error {
|
||||
var err error
|
||||
DB, err = sql.Open("sqlite", dbFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法打开数据库: %w", err)
|
||||
}
|
||||
|
||||
// 初始化表
|
||||
if err := createTables(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTables() error {
|
||||
// 创建 users 表
|
||||
_, err := DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建 devices 表
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT UNIQUE NOT NULL,
|
||||
expiration DATETIME NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// **创建 pending_requests 表**
|
||||
_, err = DB.Exec(`CREATE TABLE IF NOT EXISTS pending_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
request_time DATETIME NOT NULL,
|
||||
status TEXT NOT NULL CHECK(status IN ('pending', 'approved', 'rejected')),
|
||||
approved_by TEXT DEFAULT NULL,
|
||||
approved_at DATETIME DEFAULT NULL,
|
||||
expiration DATETIME DEFAULT NULL
|
||||
);`)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 pending_requests 失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
117
internal/handlers/auth.go
Normal file
117
internal/handlers/auth.go
Normal file
@ -0,0 +1,117 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"licensing-cotton/internal/security"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"licensing-cotton/internal/database"
|
||||
"licensing-cotton/internal/models"
|
||||
)
|
||||
|
||||
func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "只允许POST", 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, "请求解析失败", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 从数据库查询用户
|
||||
var user models.User
|
||||
err := database.DB.QueryRow(`SELECT id, username, password_hash, role
|
||||
FROM users WHERE username = ?`, req.Username).
|
||||
Scan(&user.ID, &user.Username, &user.PasswordHash, &user.Role)
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, "数据库查询错误", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 session
|
||||
token, _ := security.GenerateToken(user.Username) // session.go 或本文件里实现
|
||||
setSession(token, user.Username)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "登录成功",
|
||||
"token": token,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
// 注册(仅管理员能创建/更新用户)
|
||||
func HandleRegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "只允许POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"` // "admin" or "user"
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "解析请求失败", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
passHash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
|
||||
_, err := database.DB.Exec(`INSERT INTO users(username, password_hash, role) VALUES (?,?,?)`,
|
||||
req.Username, passHash, req.Role)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
http.Error(w, "用户名已存在", http.StatusConflict)
|
||||
} else {
|
||||
http.Error(w, "插入数据库失败: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "用户创建成功",
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化默认管理员
|
||||
func EnsureDefaultAdmin(username, password string) error {
|
||||
var count int
|
||||
err := database.DB.QueryRow(`SELECT COUNT(*) FROM users WHERE username = ?`, username).Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
passHash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
_, err := database.DB.Exec(`INSERT INTO users(username, password_hash, role) VALUES (?,?,?)`,
|
||||
username, passHash, "admin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
85
internal/handlers/device.go
Normal file
85
internal/handlers/device.go
Normal file
@ -0,0 +1,85 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"licensing-cotton/internal/database"
|
||||
"licensing-cotton/internal/models"
|
||||
)
|
||||
|
||||
// 创建/更新设备
|
||||
func HandleCreateOrUpdateDevice(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Expiration string `json:"expiration"` // RFC3339
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "解析请求失败", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
exp, err := time.Parse(time.RFC3339, req.Expiration)
|
||||
if err != nil {
|
||||
http.Error(w, "时间格式错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 先查是否存在
|
||||
var id int64
|
||||
err = database.DB.QueryRow(`SELECT id FROM devices WHERE device_id = ?`, req.DeviceID).
|
||||
Scan(&id)
|
||||
if err != nil {
|
||||
// 如果设备不存在,插入
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
_, err = database.DB.Exec(`INSERT INTO devices(device_id, expiration) VALUES (?, ?)`,
|
||||
req.DeviceID, exp)
|
||||
if err != nil {
|
||||
http.Error(w, "创建设备失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "设备 %s 创建成功\n", req.DeviceID)
|
||||
return
|
||||
}
|
||||
http.Error(w, "查询设备失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 若已存在,更新
|
||||
_, err = database.DB.Exec(`UPDATE devices SET expiration=? WHERE device_id=?`, exp, req.DeviceID)
|
||||
if err != nil {
|
||||
http.Error(w, "更新设备失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "设备 %s 已更新到期时间: %v\n", req.DeviceID, exp)
|
||||
}
|
||||
|
||||
func HandleListDevices(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := database.DB.Query(`SELECT id, device_id, expiration FROM devices`)
|
||||
if err != nil {
|
||||
http.Error(w, "查询失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var devices []models.Device
|
||||
for rows.Next() {
|
||||
var d models.Device
|
||||
if err := rows.Scan(&d.ID, &d.DeviceID, &d.Expiration); err != nil {
|
||||
http.Error(w, "解析记录失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
devices = append(devices, d)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(devices)
|
||||
}
|
||||
158
internal/handlers/license.go
Normal file
158
internal/handlers/license.go
Normal file
@ -0,0 +1,158 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"licensing-cotton/internal/database"
|
||||
"licensing-cotton/internal/security"
|
||||
)
|
||||
|
||||
// License 结构
|
||||
type License struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Expiration string `json:"expiration"`
|
||||
SignDate string `json:"sign_date"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type LicenseWithoutSign struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Expiration string `json:"expiration"`
|
||||
SignDate string `json:"sign_date"`
|
||||
}
|
||||
|
||||
// 签发License (仅管理员)
|
||||
func HandleSignLicense(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "解析请求失败", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 查设备
|
||||
var expiration time.Time
|
||||
err := database.DB.QueryRow(`SELECT expiration FROM devices WHERE device_id=?`, req.DeviceID).
|
||||
Scan(&expiration)
|
||||
if err != nil {
|
||||
http.Error(w, "设备不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 构造 license
|
||||
lic := License{
|
||||
DeviceID: req.DeviceID,
|
||||
Expiration: expiration.Format(time.RFC3339),
|
||||
}
|
||||
// 序列化并哈希
|
||||
dataBytes, _ := json.Marshal(lic)
|
||||
hash := sha256.Sum256(dataBytes)
|
||||
|
||||
// 签名
|
||||
priv, _, _ := security.GetKeys()
|
||||
rSig, sSig, err := ecdsa.Sign(nil, priv, hash[:])
|
||||
if err != nil {
|
||||
http.Error(w, "ECDSA签名失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sigBytes := append(rSig.Bytes(), sSig.Bytes()...)
|
||||
lic.Signature = base64.StdEncoding.EncodeToString(sigBytes)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(lic)
|
||||
}
|
||||
|
||||
// 验证License (可供服务端或第三方调用)
|
||||
func HandleVerifyLicense(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "只允许POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var lic License
|
||||
if err := json.NewDecoder(r.Body).Decode(&lic); err != nil {
|
||||
http.Error(w, "解析License失败", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sigStr := lic.Signature
|
||||
lic.Signature = ""
|
||||
|
||||
dataBytes, _ := json.Marshal(lic)
|
||||
hash := sha256.Sum256(dataBytes)
|
||||
|
||||
sigData, err := base64.StdEncoding.DecodeString(sigStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Signature base64解码失败", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
half := len(sigData) / 2
|
||||
rSig := new(big.Int).SetBytes(sigData[:half])
|
||||
sSig := new(big.Int).SetBytes(sigData[half:])
|
||||
|
||||
_, pub, _ := security.GetKeys()
|
||||
if !ecdsa.Verify(pub, hash[:], rSig, sSig) {
|
||||
http.Error(w, "签名验证失败", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
exp, err := time.Parse(time.RFC3339, lic.Expiration)
|
||||
if err != nil {
|
||||
http.Error(w, "时间格式错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if time.Now().After(exp) {
|
||||
http.Error(w, "License已过期", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 可进一步检查数据库中的device_id与expiration是否匹配
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"message": "License有效"}`))
|
||||
}
|
||||
|
||||
func SignLicense(deviceID string, expirationTime time.Time) (License, error) {
|
||||
// 1. 准备待签名数据
|
||||
licWithoutSign := LicenseWithoutSign{
|
||||
DeviceID: deviceID,
|
||||
Expiration: expirationTime.Format(time.RFC3339),
|
||||
SignDate: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// 2. 序列化为 JSON
|
||||
licBytes, err := json.Marshal(licWithoutSign)
|
||||
if err != nil {
|
||||
return License{}, fmt.Errorf("序列化License失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 调用你的 Ed25519Sign 函数签名
|
||||
signature, signErr := security.Ed25519Sign(licBytes) // <-- 这里是你已有的签名函数
|
||||
if signErr != nil {
|
||||
return License{}, fmt.Errorf("签名失败: %w", signErr)
|
||||
}
|
||||
|
||||
// 4. 封装最终 License
|
||||
lic := License{
|
||||
DeviceID: deviceID,
|
||||
Expiration: licWithoutSign.Expiration,
|
||||
SignDate: licWithoutSign.SignDate,
|
||||
Signature: base64.StdEncoding.EncodeToString(signature),
|
||||
}
|
||||
log.Println("Signed License for " + deviceID + ":" + expirationTime.Format(time.RFC3339))
|
||||
return lic, nil
|
||||
}
|
||||
54
internal/handlers/middleware.go
Normal file
54
internal/handlers/middleware.go
Normal file
@ -0,0 +1,54 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"licensing-cotton/internal/security"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 解析 Token 并获取用户角色
|
||||
func getUserRoleFromRequest(r *http.Request) (string, bool) {
|
||||
// 获取 Token (支持 `Authorization` 头)
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
token = r.Header.Get("X-Session-Token") // 兼容旧方式
|
||||
}
|
||||
if token == "" {
|
||||
token = r.URL.Query().Get("token") // 兼容 URL 方式
|
||||
}
|
||||
if token == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 解析 Bearer Token
|
||||
if strings.HasPrefix(token, "Bearer ") {
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
// 解析 Token 获取用户名
|
||||
username, err := security.ParseToken(token)
|
||||
if err != nil {
|
||||
fmt.Println("Token 解析失败:", err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 查询数据库获取用户角色
|
||||
var role string
|
||||
err = dbQueryRole(username, &role)
|
||||
if err != nil {
|
||||
fmt.Println("数据库查询角色失败:", err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
return role, true
|
||||
}
|
||||
|
||||
func isAdminRequest(w http.ResponseWriter, r *http.Request) bool {
|
||||
role, authed := getUserRoleFromRequest(r)
|
||||
if !authed || role != "admin" {
|
||||
http.Error(w, "需要管理员权限", http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
107
internal/handlers/renew.go
Normal file
107
internal/handlers/renew.go
Normal file
@ -0,0 +1,107 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"licensing-cotton/internal/config"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"licensing-cotton/internal/database"
|
||||
"licensing-cotton/internal/models"
|
||||
)
|
||||
|
||||
// HandleRenewLicense 让设备只续 1 小时;需满足某些逻辑才能续
|
||||
func HandleRenewLicense(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, `{"error":"Only POST allowed","code":405}`, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Expiration string `json:"expiration"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"error":"解析请求失败","code":400}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if config.AutoRenewAllDevices {
|
||||
// 计算新的到期时间
|
||||
newExpiration := time.Now().Add(1 * time.Hour)
|
||||
|
||||
// 更新数据库 expiration
|
||||
_, errUpdate := database.DB.Exec(`UPDATE devices SET expiration=? WHERE device_id=?`, newExpiration, req.DeviceID)
|
||||
if errUpdate != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":"更新Expiration失败:%s","code":500}`, errUpdate.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
lic, signErr := SignLicense(req.DeviceID, newExpiration)
|
||||
if signErr != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":"签名失败: %v","code":500}`, signErr.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 返回成功续期的 License
|
||||
resp := struct {
|
||||
License License `json:"license"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
License: lic,
|
||||
Message: "License renewed automatically (AutoRenewAllDevices).",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
|
||||
// **查询设备**
|
||||
var dev models.Device
|
||||
err := database.DB.QueryRow(`SELECT id, device_id, expiration FROM devices WHERE device_id=?`, req.DeviceID).
|
||||
Scan(&dev.ID, &dev.DeviceID, &dev.Expiration)
|
||||
if err == sql.ErrNoRows {
|
||||
if err := CreateRequest(req.DeviceID); err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":"续期申请失败: %s","code":409}`, err.Error()), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
http.Error(w, `{"error":"设备不存在,已提交申请,等待管理员审批","code":404}`, http.StatusAccepted)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":"数据库查询错误: %s","code":500}`, err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
newExpiration := time.Now().Add(1 * time.Hour)
|
||||
if dev.Expiration.Before(time.Now()) {
|
||||
if err := CreateRequest(req.DeviceID); err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":"续期申请失败: %s","code":409}`, err.Error()), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
http.Error(w, `{"error":"设备过期,已提交续期申请,等待管理员审批","code":404}`, http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
if dev.Expiration.Before(newExpiration) {
|
||||
// 如果时间不够1小时了,就续期到可以运行的极限
|
||||
newExpiration = dev.Expiration
|
||||
}
|
||||
// **创建新 License 并签名**
|
||||
lic, signErr := SignLicense(dev.DeviceID, newExpiration)
|
||||
if signErr != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":"%v","code":500}`, signErr.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// **返回成功续期的 License**
|
||||
resp := struct {
|
||||
License License `json:"license"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
License: lic,
|
||||
Message: "License renewed successfully.",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
213
internal/handlers/request.go
Normal file
213
internal/handlers/request.go
Normal file
@ -0,0 +1,213 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"licensing-cotton/internal/config"
|
||||
"licensing-cotton/internal/database"
|
||||
"licensing-cotton/internal/models"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateRequest 记录新的 renew 申请 (如果设备未在申请列表中)
|
||||
func CreateRequest(deviceID string) error {
|
||||
var existingStatus string
|
||||
|
||||
// 查询该设备是否已经有 `pending_requests` 记录
|
||||
err := database.DB.QueryRow(`SELECT status FROM pending_requests WHERE device_id = ? ORDER BY request_time DESC LIMIT 1`, deviceID).
|
||||
Scan(&existingStatus)
|
||||
|
||||
if err == nil {
|
||||
if existingStatus == "pending" {
|
||||
// 如果已有 `pending` 申请,则不允许重复提交
|
||||
return errors.New("该设备已有未处理的续期申请,等待管理员审批")
|
||||
}
|
||||
// 如果 `status` 是 `approved` 或 `rejected`,则允许提交新申请
|
||||
} else if err != sql.ErrNoRows {
|
||||
// 查询时发生错误
|
||||
return fmt.Errorf("查询续期申请失败: %w", err)
|
||||
}
|
||||
|
||||
// 插入新的申请(设为 `pending`)
|
||||
_, err = database.DB.Exec(
|
||||
`INSERT INTO pending_requests (device_id, request_time, status) VALUES (?, ?, ?)`,
|
||||
deviceID, time.Now(), "pending",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建续期申请失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleListPendingRequests(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 管理员权限检查
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// **查询所有未审批的请求**
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT id, device_id, request_time, status, approved_by, approved_at, expiration
|
||||
FROM pending_requests WHERE status='pending'
|
||||
`)
|
||||
if err != nil {
|
||||
http.Error(w, "查询待审批请求失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var pendingRequests []models.PendingRequest
|
||||
for rows.Next() {
|
||||
var req models.PendingRequest
|
||||
var approvedBy sql.NullString
|
||||
var approvedAt sql.NullTime
|
||||
var expiration sql.NullTime
|
||||
|
||||
// **扫描所有字段**
|
||||
if err := rows.Scan(&req.ID, &req.DeviceID, &req.RequestTime, &req.Status, &approvedBy, &approvedAt, &expiration); err != nil {
|
||||
http.Error(w, "解析查询结果失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// **处理 `NULL` 值**
|
||||
if approvedBy.Valid {
|
||||
req.ApprovedBy = &approvedBy.String
|
||||
}
|
||||
if approvedAt.Valid {
|
||||
req.ApprovedAt = &approvedAt.Time
|
||||
}
|
||||
if expiration.Valid {
|
||||
req.Expiration = &expiration.Time
|
||||
}
|
||||
|
||||
pendingRequests = append(pendingRequests, req)
|
||||
}
|
||||
|
||||
// **返回 JSON**
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(pendingRequests)
|
||||
}
|
||||
|
||||
func HandleDeviceRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Expiration string `json:"expiration"`
|
||||
ApprovedBy string `json:"approved_by"`
|
||||
Action string `json:"action"` // "approve" 或 "reject"
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "解析请求失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Action {
|
||||
case "approve":
|
||||
handleApprove(w, struct{ DeviceID, Expiration, ApprovedBy, Action string }(req))
|
||||
case "reject":
|
||||
handleReject(w, struct{ DeviceID, Expiration, ApprovedBy, Action string }(req))
|
||||
default:
|
||||
http.Error(w, "无效的操作", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func handleApprove(w http.ResponseWriter, req struct{ DeviceID, Expiration, ApprovedBy, Action string }) {
|
||||
expirationTime, err := time.Parse(time.RFC3339, req.Expiration)
|
||||
if err != nil {
|
||||
http.Error(w, "无效的 expiration 格式", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var exists bool
|
||||
err = database.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM devices WHERE device_id = ?)`, req.DeviceID).Scan(&exists)
|
||||
if err != nil {
|
||||
http.Error(w, "数据库查询失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
_, err = database.DB.Exec(`INSERT INTO devices (device_id, expiration) VALUES (?, ?)`, req.DeviceID, expirationTime)
|
||||
} else {
|
||||
_, err = database.DB.Exec(`UPDATE devices SET expiration = ? WHERE device_id = ?`, expirationTime, req.DeviceID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "处理设备记录失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = database.DB.Exec(`UPDATE pending_requests SET status='approved', approved_by=?, approved_at=? WHERE device_id=?`, req.ApprovedBy, time.Now(), req.DeviceID)
|
||||
if err != nil {
|
||||
http.Error(w, "更新请求状态失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": fmt.Sprintf("设备 %s 已审批,授权至 %s", req.DeviceID, req.Expiration),
|
||||
})
|
||||
}
|
||||
|
||||
func handleReject(w http.ResponseWriter, req struct{ DeviceID, Expiration, ApprovedBy, Action string }) {
|
||||
currentTime := time.Now()
|
||||
_, err := database.DB.Exec(`UPDATE pending_requests SET status='rejected', approved_by=?, approved_at=? WHERE device_id=?`, req.ApprovedBy, currentTime, req.DeviceID)
|
||||
if err != nil {
|
||||
http.Error(w, "拒绝请求失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": fmt.Sprintf("设备 %s 的请求已被拒绝", req.DeviceID),
|
||||
})
|
||||
}
|
||||
|
||||
func SetAutoRenewAllDevices(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request body: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 重置 r.Body 供后续使用
|
||||
|
||||
if !isAdminRequest(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体中的 "enabled" 字段
|
||||
var req struct {
|
||||
Enabled string `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Failed to read request body: "+err.Error(), http.StatusInternalServerError)
|
||||
//http.Error(w, `{"error":"解析请求失败","code":400}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置全局变量
|
||||
config.AutoRenewAllDevices = req.Enabled == "true"
|
||||
|
||||
// 返回成功响应
|
||||
msg := fmt.Sprintf(`{"message":"AutoRenewAllDevices set to %v","code":200}`, req.Enabled)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
27
internal/handlers/routes.go
Normal file
27
internal/handlers/routes.go
Normal file
@ -0,0 +1,27 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
|
||||
func RegisterRoutes() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Auth
|
||||
mux.HandleFunc("/login", HandleLogin)
|
||||
mux.HandleFunc("/register", HandleRegisterUser)
|
||||
|
||||
// Device
|
||||
mux.HandleFunc("/device/manage", HandleCreateOrUpdateDevice)
|
||||
mux.HandleFunc("/device/list", HandleListDevices)
|
||||
|
||||
// License
|
||||
mux.HandleFunc("/license/sign", HandleSignLicense)
|
||||
mux.HandleFunc("/license/verify", HandleVerifyLicense)
|
||||
|
||||
mux.HandleFunc("/license/renew", HandleRenewLicense)
|
||||
|
||||
mux.HandleFunc("/admin/pending_requests", HandleListPendingRequests)
|
||||
mux.HandleFunc("/admin/handle_request", HandleDeviceRequest)
|
||||
mux.HandleFunc("/admin/allow_auto_renew", SetAutoRenewAllDevices)
|
||||
|
||||
return mux
|
||||
}
|
||||
42
internal/handlers/session.go
Normal file
42
internal/handlers/session.go
Normal file
@ -0,0 +1,42 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"licensing-cotton/internal/database"
|
||||
)
|
||||
|
||||
var (
|
||||
sessionMap = make(map[string]string) // token -> username
|
||||
sessionMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// 生成一个简单的 session token
|
||||
func generateSessionToken(username string) string {
|
||||
data := fmt.Sprintf("%s:%d", username, time.Now().UnixNano())
|
||||
sum := sha256.Sum256([]byte(data))
|
||||
return base64.URLEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func setSession(token, username string) {
|
||||
sessionMutex.Lock()
|
||||
defer sessionMutex.Unlock()
|
||||
sessionMap[token] = username
|
||||
}
|
||||
|
||||
func getSessionUsername(token string) (string, bool) {
|
||||
sessionMutex.RLock()
|
||||
defer sessionMutex.RUnlock()
|
||||
u, ok := sessionMap[token]
|
||||
return u, ok
|
||||
}
|
||||
|
||||
// 也可以把 dbQueryRole 写这里或单独再抽一个地方
|
||||
func dbQueryRole(username string, role *string) error {
|
||||
return database.DB.QueryRow(`SELECT role FROM users WHERE username=?`, username).
|
||||
Scan(role)
|
||||
}
|
||||
9
internal/models/device.go
Normal file
9
internal/models/device.go
Normal file
@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Device struct {
|
||||
ID int64 `json:"id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
}
|
||||
14
internal/models/pending_request.go
Normal file
14
internal/models/pending_request.go
Normal file
@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// PendingRequest 记录设备申请信息
|
||||
type PendingRequest struct {
|
||||
ID int64 `json:"id"` // 申请 ID
|
||||
DeviceID string `json:"device_id"` // 申请的设备唯一 ID (MAC 地址)
|
||||
RequestTime time.Time `json:"request_time"` // 申请时间
|
||||
Status string `json:"status"` // 申请状态 ("pending", "approved", "rejected")
|
||||
ApprovedBy *string `json:"approved_by,omitempty"` // 审批管理员用户名 (仅限 approved)
|
||||
ApprovedAt *time.Time `json:"approved_at,omitempty"` // 审批时间 (仅限 approved)
|
||||
Expiration *time.Time `json:"expiration,omitempty"` // 审批通过后的授权时间 (仅限 approved)
|
||||
}
|
||||
8
internal/models/user.go
Normal file
8
internal/models/user.go
Normal file
@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"-"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
28
internal/security/ecc.go
Normal file
28
internal/security/ecc.go
Normal file
@ -0,0 +1,28 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var PrivateKey *ecdsa.PrivateKey
|
||||
var PublicKey ecdsa.PublicKey
|
||||
|
||||
func InitECCKey() error {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
PrivateKey = key
|
||||
PublicKey = key.PublicKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetKeys() (*ecdsa.PrivateKey, *ecdsa.PublicKey, error) {
|
||||
if PrivateKey == nil {
|
||||
return nil, nil, errors.New("ECC私钥尚未初始化")
|
||||
}
|
||||
return PrivateKey, &PublicKey, nil
|
||||
}
|
||||
54
internal/security/ed22519.go
Normal file
54
internal/security/ed22519.go
Normal file
@ -0,0 +1,54 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ed25519Priv ed25519.PrivateKey
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// InitEd25519Keys 加载并解密 OpenSSH 格式的 Ed25519 私钥
|
||||
func InitEd25519Keys(passphrase string) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
// 读取私钥文件
|
||||
data, e := os.ReadFile("keys/alaer_machines")
|
||||
if e != nil {
|
||||
err = errors.New("failed to load ed25519 private key file: " + e.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 OpenSSH 私钥格式(可能是加密的)
|
||||
decryptedKey, e := ssh.ParseRawPrivateKeyWithPassphrase(data, []byte(passphrase))
|
||||
if e != nil {
|
||||
err = errors.New("failed to decrypt private key: " + e.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查解析出的私钥类型
|
||||
switch key := decryptedKey.(type) {
|
||||
case ed25519.PrivateKey:
|
||||
ed25519Priv = key
|
||||
case *ed25519.PrivateKey:
|
||||
ed25519Priv = *key
|
||||
default:
|
||||
err = errors.New("parsed key is not an ed25519 private key, check your key format")
|
||||
return
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Ed25519Sign 进行 Ed25519 签名
|
||||
func Ed25519Sign(message []byte) ([]byte, error) {
|
||||
if ed25519Priv == nil {
|
||||
return nil, errors.New("private key not initialized")
|
||||
}
|
||||
return ed25519.Sign(ed25519Priv, message), nil
|
||||
}
|
||||
44
internal/security/token.go
Normal file
44
internal/security/token.go
Normal file
@ -0,0 +1,44 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"time"
|
||||
)
|
||||
|
||||
var secretKey = []byte("your-secret-key") // 服务器私钥
|
||||
|
||||
// 生成 Token
|
||||
func GenerateToken(username string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"username": username,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(), // 1 天有效期
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(secretKey)
|
||||
}
|
||||
|
||||
// 解析 Token
|
||||
func ParseToken(tokenString string) (string, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 解析 Claims
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
username, ok := claims["username"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("无效的 Token")
|
||||
}
|
||||
return username, nil
|
||||
}
|
||||
|
||||
return "", errors.New("无效的 Token")
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user