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