first commit

This commit is contained in:
XinJiang1 2025-01-16 15:24:07 +08:00
commit bcbf2a8d08
20 changed files with 1161 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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=

View File

@ -0,0 +1,3 @@
package config
var AutoRenewAllDevices bool = false

View 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
View 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
}

View 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)
}

View 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
}

View 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
View 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)
}

View 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))
}

View 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
}

View 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)
}

View 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"`
}

View 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
View 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
View 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
}

View 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
}

View 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")
}