feat: use worker versionId as jwt_secret

This commit is contained in:
zaunist committed 2025-04-06 04:56:29 +08:00
1 parent 6b99bec324
commit 3590fa7d58
12 files changed
+269 -222

No files matched your search

+41 -3
View File
@@ -42,12 +42,35 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整的 git 历史用于获取提交信息
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Get version info
id: get_version
shell: bash
run: |
# 从 GitHub 环境获取版本信息
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
else
VERSION="dev-$(git rev-parse --short HEAD)"
fi
COMMIT_HASH=$(git rev-parse --short HEAD)
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "COMMIT_HASH=$COMMIT_HASH" >> $GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $GITHUB_ENV
echo "版本: $VERSION"
echo "提交哈希: $COMMIT_HASH"
echo "构建日期: $BUILD_DATE"
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
env:
@@ -55,7 +78,12 @@ jobs:
GOARCH: ${{ matrix.arch }}
run: |
cd agent
go build -v -ldflags="-s -w -extldflags=-static" -tags netgo,osusergo -o ${{ matrix.artifact_name }} .
go build -v \
-ldflags="-s -w -extldflags=-static \
-X github.com/xugou/agent/cmd/agent.Version=${{ env.VERSION }} \
-X github.com/xugou/agent/cmd/agent.GitCommit=${{ env.COMMIT_HASH }} \
-X github.com/xugou/agent/cmd/agent.BuildDate=${{ env.BUILD_DATE }}" \
-tags netgo,osusergo -o ${{ matrix.artifact_name }} .
- name: Build macOS
if: matrix.os == 'macos-latest'
@@ -64,7 +92,12 @@ jobs:
GOARCH: ${{ matrix.arch }}
run: |
cd agent
go build -v -ldflags="-s -w" -tags netgo,osusergo -o ${{ matrix.artifact_name }} .
go build -v \
-ldflags="-s -w \
-X github.com/xugou/agent/cmd/agent.Version=${{ env.VERSION }} \
-X github.com/xugou/agent/cmd/agent.GitCommit=${{ env.COMMIT_HASH }} \
-X github.com/xugou/agent/cmd/agent.BuildDate=${{ env.BUILD_DATE }}" \
-tags netgo,osusergo -o ${{ matrix.artifact_name }} .
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -73,7 +106,12 @@ jobs:
GOARCH: ${{ matrix.arch }}
run: |
cd agent
go build -v -ldflags="-s -w" -o ${{ matrix.artifact_name }} .
go build -v `
-ldflags="-s -w `
-X github.com/xugou/agent/cmd/agent.Version=${{ env.VERSION }} `
-X github.com/xugou/agent/cmd/agent.GitCommit=${{ env.COMMIT_HASH }} `
-X github.com/xugou/agent/cmd/agent.BuildDate=${{ env.BUILD_DATE }}" `
-o ${{ matrix.artifact_name }} .
- name: Upload artifact
uses: actions/upload-artifact@v4
-4
View File
@@ -23,10 +23,6 @@ cd agent
go build -o xugou-agent
```
### 使用预编译二进制文件
从 [Releases](https://github.com/xugou/agent/releases) 页面下载适合您系统的预编译二进制文件。
## 使用方法
### 基本命令
-1
View File
@@ -36,7 +36,6 @@ func init() {
viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server"))
viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))
viper.BindPFlag("log_level", rootCmd.PersistentFlags().Lookup("log-level"))
viper.BindPFlag("agent_id", rootCmd.PersistentFlags().Lookup("agent-id"))
}
func initConfig() {
+5 -13
View File
@@ -18,11 +18,11 @@ func init() {
startCmd := &cobra.Command{
Use: "start",
Short: "启动 Xugou Agent",
Long: `启动 Xugou Agent 开始收集系统信息并上报到服务器`,
Long: `启动 Xugou Agent 开始采集系统信息并上报到服务器`,
Run: runStart,
}
startCmd.Flags().IntP("interval", "i", 60, "数据收集和上报间隔(秒)")
startCmd.Flags().IntP("interval", "i", 60, "数据采集和上报间隔(秒)")
viper.BindPFlag("interval", startCmd.Flags().Lookup("interval"))
rootCmd.AddCommand(startCmd)
@@ -55,16 +55,8 @@ func runStart(cmd *cobra.Command, args []string) {
// 初始化数据收集器和上报器
dataCollector := collector.NewCollector()
var dataReporter reporter.Reporter
// 根据配置决定使用哪种上报器
if server == "console" {
dataReporter = reporter.NewConsoleReporter()
fmt.Println("使用控制台上报器")
} else {
dataReporter = reporter.NewHTTPReporter(server, token)
fmt.Println("使用HTTP上报器")
}
dataReporter := reporter.NewHTTPReporter(server, token)
fmt.Println("使用HTTP上报器")
// 设置定时器,按指定间隔收集和上报数据
ticker := time.NewTicker(time.Duration(interval) * time.Second)
@@ -96,7 +88,7 @@ func collectAndReport(ctx context.Context, c collector.Collector, r reporter.Rep
// 收集系统信息
info, err := c.Collect(ctx)
if err != nil {
fmt.Printf("收集系统信息失败: %v\n", err)
fmt.Printf("采集系统信息失败: %v\n", err)
return
}
+29 -29
View File
@@ -118,7 +118,24 @@ func (r *HTTPReporter) ensureRegistered(ctx context.Context, info *collector.Sys
}
defer resp.Body.Close()
// 检查响应状态码
// 检查是否收到"客户端已存在"的响应
if resp.StatusCode == 400 {
var errorResponse RegisterResponse
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&errorResponse); err != nil {
return fmt.Errorf("解析错误响应失败: %w", err)
}
if errorResponse.Message == "客户端已存在" {
fmt.Printf("客户端已存在,将直接更新状态,token: %s\n", r.apiToken)
r.registered = true
return nil
}
return fmt.Errorf("注册失败: %s", errorResponse.Message)
}
// 检查其他错误状态码
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
}
@@ -145,13 +162,8 @@ func (r *HTTPReporter) ensureRegistered(ctx context.Context, info *collector.Sys
return nil
}
// Report 将系统信息上报到服务器
func (r *HTTPReporter) Report(ctx context.Context, info *collector.SystemInfo) error {
// 确保客户端已注册
if err := r.ensureRegistered(ctx, info); err != nil {
return fmt.Errorf("客户端注册或状态更新失败: %w", err)
}
// reportStatus 将系统信息上报到服务器,不检查注册状态
func (r *HTTPReporter) reportStatus(ctx context.Context, info *collector.SystemInfo) error {
// 提取所有磁盘的总容量和使用量
var diskTotal, diskUsed uint64
for _, disk := range info.DiskInfo {
@@ -241,29 +253,17 @@ func (r *HTTPReporter) Report(ctx context.Context, info *collector.SystemInfo) e
return nil
}
// ConsoleReporter 是将数据输出到控制台的上报器实现
type ConsoleReporter struct{}
// NewConsoleReporter 创建一个新的控制台数据上报器
func NewConsoleReporter() Reporter {
return &ConsoleReporter{}
}
// Report 将系统信息输出到控制台
func (r *ConsoleReporter) Report(ctx context.Context, info *collector.SystemInfo) error {
data, err := json.MarshalIndent(info, "", " ")
if err != nil {
return fmt.Errorf("序列化系统信息失败: %w", err)
// Report 将系统信息上报到服务器
func (r *HTTPReporter) Report(ctx context.Context, info *collector.SystemInfo) error {
// 确保客户端已注册
if err := r.ensureRegistered(ctx, info); err != nil {
return fmt.Errorf("客户端注册或状态更新失败: %w", err)
}
fmt.Println("系统信息收集时间:", info.Timestamp.Format("2006-01-02 15:04:05"))
fmt.Println("主机名:", info.Hostname)
fmt.Println("平台:", info.Platform, info.OS)
fmt.Println("CPU使用率:", info.CPUInfo.Usage, "%")
fmt.Println("内存使用率:", info.MemoryInfo.UsageRate, "%")
fmt.Println("系统负载:", info.LoadInfo.Load1, info.LoadInfo.Load5, info.LoadInfo.Load15)
fmt.Println("详细信息:")
fmt.Println(string(data))
// 如果注册成功但未直接上报状态(通过客户端已存在的分支),则调用上报
if r.registered {
return r.reportStatus(ctx, info)
}
return nil
}
-40
View File
@@ -9,36 +9,6 @@ import { ExecutionContext } from 'hono';
// 添加全局变量声明
declare global {
var isInitialized: boolean;
namespace NodeJS {
interface ProcessEnv {
NODE_ENV?: string;
PORT?: string;
JWT_SECRET?: string;
}
}
}
// 定义 D1 数据库类型
interface D1Database {
prepare(query: string): D1PreparedStatement;
dump(): Promise<ArrayBuffer>;
batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
exec<T = unknown>(query: string): Promise<D1Result<T>>;
}
interface D1PreparedStatement {
bind(...values: any[]): D1PreparedStatement;
first<T = unknown>(colName?: string): Promise<T>;
run<T = unknown>(): Promise<D1Result<T>>;
all<T = unknown>(): Promise<D1Result<T>>;
raw<T = unknown>(): Promise<T[]>;
}
interface D1Result<T = unknown> {
results?: T[];
success: boolean;
error?: string;
meta?: object;
}
// 导入路由
@@ -76,16 +46,6 @@ app.use('*', async (c, next) => {
// 公共路由
app.get('/', (c) => c.json({ message: 'XUGOU API 服务正在运行' }));
// 获取 JWT 密钥
const getJwtSecret = (c: any) => {
// 在 Cloudflare Workers 环境中,使用 env 变量
if (typeof process === 'undefined') {
return c.env.JWT_SECRET || 'your-secret-key-change-in-production';
}
// 在 Node.js 环境中,使用 process.env
return process.env.JWT_SECRET || 'your-secret-key-change-in-production';
};
// 路由注册
app.route('/api/auth', authRoutes);
app.route('/api/monitors', monitorRoutes);
-2
View File
@@ -24,6 +24,4 @@ export interface D1Result<T = unknown> {
// 通用绑定类型
export type Bindings = {
DB: D1Database;
JWT_SECRET: string;
ENABLE_DB_INIT?: string;
};
+24 -104
View File
@@ -3,7 +3,7 @@ import { jwt } from 'hono/jwt';
import { Context, Next } from 'hono';
import { Bindings } from '../models/db';
import { Agent } from '../models/agent';
import { getJwtSecret, generateToken } from '../utils/jwt';
import { getJwtSecret, generateToken, verifyToken } from '../utils/jwt';
import {
getAllAgents,
getAgentsByUser,
@@ -62,7 +62,7 @@ agents.post('/', async (c) => {
const { name } = await c.req.json();
const payload = c.get('jwtPayload');
const token = await generateToken();
const token = await generateToken(c.env);
// 创建新客户端
const newAgent = await createAgent(c.env.DB, name, token, payload.id);
@@ -161,49 +161,6 @@ agents.put('/:id', async (c) => {
}
});
// 更新客户端状态
agents.post('/:id/status', async (c) => {
try {
const agentId = Number(c.req.param('id'));
const {
cpu_usage,
memory_total,
memory_used,
disk_total,
disk_used,
network_rx,
network_tx,
hostname,
ip_address,
os,
version
} = await c.req.json();
// 更新客户端状态和资源指标
const result = await updateAgentStatus(c.env.DB, agentId, 'active', {
cpu_usage,
memory_total,
memory_used,
disk_total,
disk_used,
network_rx,
network_tx,
hostname,
ip_address,
os,
version
});
return c.json({
success: true,
message: '客户端状态已更新'
});
} catch (error) {
console.error('更新客户端状态错误:', error);
return c.json({ success: false, message: '更新客户端状态失败' }, 500);
}
});
// 删除客户端
agents.delete('/:id', async (c) => {
try {
@@ -235,48 +192,11 @@ agents.delete('/:id', async (c) => {
}
});
// 重新生成客户端令牌
agents.post('/:id/token', async (c) => {
try {
const agentId = Number(c.req.param('id'));
const payload = c.get('jwtPayload');
// 获取客户端信息
const agent = await getAgentById(c.env.DB, agentId);
if (!agent) {
return c.json({ success: false, message: '客户端不存在' }, 404);
}
// 检查权限
if (payload.role !== 'admin' && agent.created_by !== payload.id) {
return c.json({ success: false, message: '无权为此客户端重新生成令牌' }, 403);
}
// 生成新令牌
const newToken = await generateToken();
// 更新客户端令牌
const result = await updateAgentToken(c.env.DB, agent.id, newToken);
return c.json({
success: true,
message: '客户端令牌已重新生成',
token: newToken
});
} catch (error) {
console.error('重新生成客户端令牌错误:', error);
return c.json({ success: false, message: '重新生成客户端令牌失败' }, 500);
}
});
// 生成临时注册Token
// 生成客户端Token
agents.post('/token/generate', async (c) => {
try {
const payload = c.get('jwtPayload');
// 生成新令牌
const newToken = await generateToken();
const newToken = await generateToken(c.env);
// 可以选择将此token存储在临时表中,或者使用其他方式验证(例如,设置过期时间)
// 这里为简化操作,只返回令牌
@@ -301,29 +221,29 @@ agents.post('/register', async (c) => {
return c.json({ success: false, message: '缺少注册令牌' }, 400);
}
// 通过token查找客户端
const existingAgent = await getAgentByToken(c.env.DB, token);
if (existingAgent) {
return c.json({ success: false, message: '客户端已存在' }, 400);
}
// 验证令牌
const tokenVerification = await verifyToken(token, c.env);
// 如果令牌无效,返回错误
if (!tokenVerification.valid) {
return c.json({
success: false,
message: `注册令牌无效: ${tokenVerification.message}`
}, 401);
}
// 查找管理员用户作为客户端创建者
const adminId = await getAdminUserId(c.env.DB);
// 查找是否已存在使用相同token的客户端
const existingAgent = await getAgentByToken(c.env.DB, token);
if (existingAgent) {
// 如果客户端已存在,更新其状态信息
await updateAgent(c.env.DB, existingAgent.id, {
status: 'active',
hostname,
ip_address,
os,
version
});
return c.json({
success: true,
message: '客户端状态更新成功',
agent: { id: existingAgent.id }
});
}
// 如果客户端不存在,则创建新客户端
const newAgent = await createAgent(
c.env.DB,
-1
View File
@@ -8,7 +8,6 @@ import { getUserByUsername, createUser, getUserById } from '../db/auth';
// 导入 D1 数据库类型
type Bindings = {
DB: D1Database;
JWT_SECRET: string;
};
interface D1Database {
+5 -15
View File
@@ -4,10 +4,8 @@ import { cors } from 'hono/cors';
import { Bindings } from '../models/db';
import {
StatusPageConfig as DbStatusPageConfig,
Monitor,
Agent,
getUserStatusPageConfig,
getConfigMonitors,
getConfigAgents,
getStatusPageConfigById,
@@ -26,7 +24,7 @@ import {
getAdminUserId,
createDefaultConfig
} from '../db/status';
import { getJwtSecret } from '../utils/jwt';
// 状态页配置接口
interface StatusPageConfig {
title: string;
@@ -48,18 +46,10 @@ const adminRoutes = new Hono<{ Bindings: Bindings }>();
// 使用中间件验证JWT(仅对管理员路由)
adminRoutes.use('/*', async (c, next) => {
try {
// 使用环境变量中的JWT密钥进行验证
const jwtMiddleware = jwt({
secret: c.env.JWT_SECRET,
});
// 执行JWT认证
return await jwtMiddleware(c, next);
} catch (error) {
console.error('JWT验证失败:', error);
return c.json({ error: '未授权' }, 401);
}
const jwtMiddleware = jwt({
secret: getJwtSecret(c)
});
return jwtMiddleware(c, next);
});
// 获取状态页配置(管理员)
+162 -10
View File
@@ -10,22 +10,174 @@
* @returns JWT密钥
*/
export const getJwtSecret = (c: any): string => {
// 在Cloudflare Workers环境中,使用env变量
if (typeof process === 'undefined') {
return c.env.JWT_SECRET || 'your-secret-key-change-in-production';
}
// 在Node.js环境中,使用process.env
return process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const { id: versionId, tag: versionTag, timestamp: versionTimestamp } = c.env.CF_VERSION_METADATA;
return versionId || 'your-secret-key-change-in-production';
};
/**
* 生成随机令牌
* 生成用于API密钥或认证令牌的随机字符串
* 生成用于API密钥或认证令牌的随机字符串,包含时间戳、前缀和签名
*
* @param env 环境变量或上下文对象,用于获取密钥
* @returns 生成的随机令牌
*/
export async function generateToken(): Promise<string> {
const array = new Uint8Array(32);
export async function generateToken(env?: any): Promise<string> {
console.log('=== 开始生成令牌 ===');
console.log('env 参数:', env ? '已提供' : '未提供');
// 生成随机部分
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
const randomPart = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
console.log('生成的随机部分:', randomPart);
// 添加时间戳
const timestamp = Date.now().toString(36);
console.log('时间戳 (Base36):', timestamp, '原始时间戳:', Date.now());
// 添加特定前缀,用于标识这是一个XUGOU系统的令牌
const prefix = 'xugou';
console.log('使用前缀:', prefix);
// 创建基本令牌
const baseToken = `${prefix}_${timestamp}_${randomPart}`;
console.log('基本令牌 (无签名):', baseToken);
// 获取密钥
if (!env || !env.CF_VERSION_METADATA) {
console.error('错误: env.CF_VERSION_METADATA 不存在,无法生成签名');
return baseToken;
}
const { id: versionId, tag: versionTag, timestamp: versionTimestamp } = env.CF_VERSION_METADATA;
console.log("generateToken 使用的 versionId:", versionId);
console.log("generateToken 版本信息:", { tag: versionTag, timestamp: versionTimestamp });
try {
// 创建签名
const msgUint8 = new TextEncoder().encode(baseToken + versionId);
console.log('签名数据长度:', msgUint8.length, '字节');
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const signature = hashArray.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 16);
console.log('生成的签名:', signature);
// 返回带签名的令牌
const fullToken = `${baseToken}.${signature}`;
console.log('完整令牌:', fullToken);
console.log('=== 令牌生成完成 ===');
return fullToken;
} catch (error) {
console.error('生成令牌签名时出错:', error);
return baseToken;
}
}
/**
* 验证令牌是否有效
* 验证令牌的格式和签名是否正确
*
* @param token 需要验证的令牌
* @param env 环境变量或上下文对象,用于获取密钥
* @returns 验证结果对象,包含是否有效和相关信息
*/
export async function verifyToken(
token: string,
env: any,
): Promise<{ valid: boolean; message?: string; timestamp?: number; payload?: any }> {
console.log('=== 开始验证令牌 ===');
console.log('待验证令牌:', token);
try {
// 首先验证令牌格式
const parts = token.split('.');
console.log('令牌部分数量:', parts.length);
// 如果令牌没有签名部分(即不包含.),或者格式不正确,则无效
if (parts.length !== 2) {
console.log('验证失败: 令牌格式无效 (没有点号分隔符或分隔符数量不正确)');
return { valid: false, message: '令牌格式无效' };
}
// 解析令牌各部分
const [baseToken, signature] = parts;
console.log('基本令牌部分:', baseToken);
console.log('签名部分:', signature);
const baseTokenParts = baseToken.split('_');
console.log('基本令牌组件数量:', baseTokenParts.length);
// 验证基本令牌格式
if (baseTokenParts.length !== 3) {
console.log('验证失败: 基本令牌格式无效 (不包含3个由下划线分隔的部分)');
return { valid: false, message: '令牌格式无效' };
}
const [prefix, timestampStr, randomPart] = baseTokenParts;
console.log('解析出的令牌组件:', { prefix, timestampStr, randomPart });
// 验证前缀
if (prefix !== 'xugou') {
console.log(`验证失败: 令牌前缀无效 (期望 'xugou', 实际为 '${prefix}')`);
return { valid: false, message: '令牌前缀无效' };
}
// 解析时间戳
const timestamp = parseInt(timestampStr, 36);
console.log('解析的时间戳 (Base36 -> 数字):', timestampStr, '->', timestamp);
console.log('时间戳对应的日期:', new Date(timestamp).toISOString());
// 验证时间戳是否为有效数字
if (isNaN(timestamp)) {
console.log('验证失败: 时间戳无效 (不是有效的Base36数字)');
return { valid: false, message: '令牌时间戳无效' };
}
// 获取密钥
if (!env || !env.CF_VERSION_METADATA) {
console.error('错误: env.CF_VERSION_METADATA 不存在,无法验证签名');
return { valid: false, message: '环境变量不完整,无法验证签名' };
}
const { id: versionId, tag: versionTag, timestamp: versionTimestamp } = env.CF_VERSION_METADATA;
console.log("verifyToken 使用的 versionId:", versionId);
console.log("verifyToken 版本信息:", { tag: versionTag, timestamp: versionTimestamp });
// 重新计算签名并验证
const msgUint8 = new TextEncoder().encode(baseToken + versionId);
console.log('验证签名数据长度:', msgUint8.length, '字节');
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const calculatedSignature = hashArray.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 16);
console.log('计算出的签名:', calculatedSignature);
console.log('令牌中的签名:', signature);
console.log('签名是否匹配:', calculatedSignature === signature);
// 比较计算出的签名和令牌中的签名
if (calculatedSignature !== signature) {
console.log('验证失败: 签名不匹配');
return { valid: false, message: '令牌签名无效' };
}
// 如果所有验证都通过,令牌有效
console.log('验证成功: 令牌有效');
const result = {
valid: true,
timestamp,
payload: {
prefix,
timestamp,
randomPart
}
};
console.log('验证结果:', result);
console.log('=== 令牌验证完成 ===');
return result;
} catch (error) {
console.error('令牌验证过程出错:', error);
return { valid: false, message: '令牌验证过程出错' };
}
}
+3
View File
@@ -11,3 +11,6 @@ crons = ["* * * * *"]
binding = "DB"
database_name = "xugou_db"
database_id = "32a2ca5f-3a77-4b08-965e-804612557880"
[version_metadata]
binding = "CF_VERSION_METADATA"