This commit is contained in:
2026-06-08 15:51:52 +08:00
commit f51c0ee636
74 changed files with 1223619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.vs
x64
+1
View File
@@ -0,0 +1 @@
./skeet_loader.exe -true
+2
View File
@@ -0,0 +1,2 @@
# skeet_loader
+3
View File
@@ -0,0 +1,3 @@
PORT=3000
JWT_PRIVATE_KEY_PATH=./private.pem
JWT_PUBLIC_KEY_PATH=./public.pem
+4
View File
@@ -0,0 +1,4 @@
node_modules/
skeet_server.db
logs/
.env
+157
View File
@@ -0,0 +1,157 @@
const argon2 = require('argon2');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ─── Load RSA keys ────────────────────────────────────
function loadKeys() {
const privatePath = process.env.JWT_PRIVATE_KEY_PATH || path.join(__dirname, 'private.pem');
const publicPath = process.env.JWT_PUBLIC_KEY_PATH || path.join(__dirname, 'public.pem');
return {
privateKey: fs.readFileSync(privatePath, 'utf8'),
publicKey: fs.readFileSync(publicPath, 'utf8')
};
}
// ─── Argon2id (with client-side SHA-256 for HTTP transport) ──
/**
* Hash a raw password for storage.
* Step 1: SHA-256(raw) → 64-char hex
* Step 2: Argon2id(sha256_hex) → stored in DB
*
* This way the client sends SHA-256(password) over HTTP,
* and the server never sees or stores the raw password.
*/
async function hashPassword(password) {
const sha256 = crypto.createHash('sha256').update(password).digest('hex');
return argon2.hash(sha256, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4
});
}
/**
* Verify a client-submitted password hash against stored Argon2id hash.
* @param {string} storedHash — Argon2id(SHA-256(raw_password))
* @param {string} receivedSha256 — SHA-256(raw_password) from client (64-char hex)
*/
async function verifyPassword(storedHash, receivedSha256) {
return argon2.verify(storedHash, receivedSha256);
}
// ─── JWT ──────────────────────────────────────────────
function signAccessToken(user, deviceId) {
const { privateKey } = loadKeys();
return jwt.sign(
{
sub: user.id,
username: user.username,
role: user.role,
device_id: deviceId
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '30m'
}
);
}
function verifyAccessToken(token) {
const { publicKey } = loadKeys();
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
}
// ─── Refresh Token ────────────────────────────────────
function generateRefreshToken() {
return crypto.randomBytes(32).toString('hex'); // 256-bit → 64 hex chars
}
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
// ─── HWID Matching ────────────────────────────────────
/**
* Dynamic HWID matching.
* Rule 1: CPU + Board both match → immediate pass (core hardware).
* Rule 2: Weighted score ≥ 80%.
* CPU: 45%, Board: 45%, Disk: 8%, MAC: 2%
*/
function hwidMatchRate(device, incoming) {
const cpuMatch = device.cpu_hash === incoming.cpu;
const boardMatch = device.board_hash === incoming.board;
const diskMatch = device.disk_hash === incoming.disk;
const macMatch = device.mac_hash === incoming.mac;
// Rule 1: CPU + Board
if (cpuMatch && boardMatch) return { match: true, rate: 0.80 };
// Rule 2: weighted
let score = 0;
if (cpuMatch) score += 0.45;
if (boardMatch) score += 0.45;
if (diskMatch) score += 0.08;
if (macMatch) score += 0.02;
return { match: score >= 0.80, rate: score };
}
/**
* Compute a combined HWID hash for quick lookup.
*/
function combinedHwidHash(components) {
const combined = `${components.cpu}|${components.board}|${components.disk}|${components.mac}`;
return crypto.createHash('sha256').update(combined).digest('hex');
}
// ─── Admin RBAC ───────────────────────────────────────
function requireAdmin(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ success: false, error: 'UNAUTHORIZED' });
}
try {
const token = authHeader.slice(7);
const payload = verifyAccessToken(token);
if (payload.role !== 'admin') {
return res.status(403).json({ success: false, error: 'FORBIDDEN', message: 'Admin role required' });
}
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ success: false, error: 'TOKEN_INVALID' });
}
}
/**
* Hash a pre-computed SHA-256 hex string (from client) with Argon2id.
* Used for registration when the client sends SHA-256(password).
*/
async function hashPasswordFromHash(sha256hex) {
return argon2.hash(sha256hex, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4
});
}
module.exports = {
hashPassword, hashPasswordFromHash, verifyPassword,
signAccessToken, verifyAccessToken,
generateRefreshToken, hashToken,
hwidMatchRate, combinedHwidHash,
requireAdmin
};
+98
View File
@@ -0,0 +1,98 @@
/**
* Batch user creation script.
*
* Usage:
* node create_users.js user1 pass1 [user2 pass2 ...]
* node create_users.js --file users.json
* node create_users.js --interactive
*
* JSON format (users.json):
* [
* { "username": "user1", "password": "pass1", "role": "user", "expires_at": null },
* { "username": "user2", "password": "pass2", "role": "user", "expires_at": "2026-12-31" }
* ]
*/
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
const fs = require('fs');
const readline = require('readline');
const db = require('./db');
const auth = require('./auth');
db.initDB();
async function createOne(username, password, role = 'user', expiresAt = null) {
const existing = db.getUserByUsername(username);
if (existing) {
console.log(` [SKIP] '${username}' already exists`);
return false;
}
const hash = await auth.hashPassword(password);
db.createUser(username, hash, role, expiresAt || null);
console.log(` [OK] '${username}' created (role: ${role}${expiresAt ? ', expires: ' + expiresAt : ''})`);
return true;
}
async function main() {
const args = process.argv.slice(2);
let created = 0;
if (args.length === 0 || args.includes('--interactive') || args.includes('-i')) {
// ── Interactive mode ──
console.log('=== Interactive User Creation ===');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
while (true) {
const username = await new Promise(resolve => rl.question('\nUsername (empty to finish): ', resolve));
if (!username) break;
const password = await new Promise(resolve => rl.question('Password: ', resolve));
if (!password) { console.log(' Password required, skipping.'); continue; }
const role = await new Promise(resolve => rl.question('Role [user/admin] (default: user): ', resolve));
const expires = await new Promise(resolve => rl.question('Expiry YYYY-MM-DD (default: never): ', resolve));
if (await createOne(username, password, role || 'user', expires || null)) created++;
}
rl.close();
} else if (args.includes('--file') || args.includes('-f')) {
// ── JSON file mode ──
const fileIdx = args.indexOf('--file');
const fIdx = args.indexOf('-f');
const filePath = args[fileIdx !== -1 ? fileIdx + 1 : fIdx + 1];
if (!filePath || !fs.existsSync(filePath)) {
console.error('File not found:', filePath);
process.exit(1);
}
const users = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (!Array.isArray(users)) {
console.error('JSON must be an array of user objects.');
process.exit(1);
}
console.log(`Creating ${users.length} users from ${filePath}...\n`);
for (const u of users) {
if (await createOne(u.username, u.password, u.role || 'user', u.expires_at || null)) created++;
}
} else {
// ── CLI args mode: pairs of username password ──
if (args.length % 2 !== 0) {
console.error('Arguments must be in pairs: username password [username password ...]');
process.exit(1);
}
console.log(`Creating ${args.length / 2} users...\n`);
for (let i = 0; i < args.length; i += 2) {
if (await createOne(args[i], args[i + 1])) created++;
}
}
console.log(`\nDone. ${created} user(s) created.`);
process.exit(0);
}
main().catch(err => { console.error('Error:', err); process.exit(1); });
+198
View File
@@ -0,0 +1,198 @@
const Database = require('better-sqlite3');
const path = require('path');
let db;
function initDB(dbPath) {
db = new Database(dbPath || path.join(__dirname, 'skeet_server.db'));
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
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 DEFAULT 'user',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
display_name TEXT NOT NULL,
cpu_hash TEXT NOT NULL,
board_hash TEXT NOT NULL,
disk_hash TEXT NOT NULL,
mac_hash TEXT NOT NULL,
last_login TEXT,
last_ip TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash TEXT UNIQUE NOT NULL,
user_id INTEGER NOT NULL,
device_id INTEGER,
hwid_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS login_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
device_id INTEGER,
ip_address TEXT,
user_agent TEXT,
hwid_match_rate REAL,
success INTEGER NOT NULL,
reason TEXT,
attempted_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS rate_limits (
key TEXT NOT NULL,
action TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
window_start TEXT NOT NULL,
PRIMARY KEY (key, action)
);
`);
return db;
}
function getDB() {
if (!db) throw new Error('Database not initialized. Call initDB() first.');
return db;
}
// ─── Users ────────────────────────────────────────────
function getUserByUsername(username) {
return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username);
}
function getUserById(id) {
return getDB().prepare('SELECT * FROM users WHERE id = ?').get(id);
}
function createUser(username, passwordHash, role = 'user', expiresAt = null) {
return getDB().prepare(
'INSERT INTO users (username, password_hash, role, expires_at) VALUES (?, ?, ?, ?)'
).run(username, passwordHash, role, expiresAt);
}
// ─── Devices ──────────────────────────────────────────
function findMatchingDevice(userId, cpuHash, boardHash, diskHash, macHash) {
const devices = getDB().prepare(
'SELECT * FROM devices WHERE user_id = ?'
).all(userId);
return devices;
}
function createDevice(userId, displayName, cpuHash, boardHash, diskHash, macHash, ip) {
return getDB().prepare(
`INSERT INTO devices (user_id, display_name, cpu_hash, board_hash, disk_hash, mac_hash, last_login, last_ip)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), ?)`
).run(userId, displayName, cpuHash, boardHash, diskHash, macHash, ip);
}
function updateDeviceLogin(deviceId, ip) {
getDB().prepare(
'UPDATE devices SET last_login = datetime(\'now\'), last_ip = ? WHERE id = ?'
).run(ip, deviceId);
}
// ─── Refresh Tokens ───────────────────────────────────
function saveRefreshToken(tokenHash, userId, deviceId, hwidHash, expiresAt) {
return getDB().prepare(
'INSERT INTO refresh_tokens (token_hash, user_id, device_id, hwid_hash, expires_at) VALUES (?, ?, ?, ?, ?)'
).run(tokenHash, userId, deviceId, hwidHash, expiresAt);
}
function findRefreshToken(tokenHash) {
return getDB().prepare(
'SELECT * FROM refresh_tokens WHERE token_hash = ?'
).get(tokenHash);
}
function revokeRefreshToken(tokenHash) {
getDB().prepare(
'UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?'
).run(tokenHash);
}
// ─── Login Log ────────────────────────────────────────
function logLogin(userId, deviceId, ip, userAgent, hwidMatchRate, success, reason) {
getDB().prepare(
`INSERT INTO login_log (user_id, device_id, ip_address, user_agent, hwid_match_rate, success, reason)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run(userId, deviceId, ip, userAgent, hwidMatchRate, success ? 1 : 0, reason);
}
function countRecentFailures(username, windowMinutes = 10) {
const row = getDB().prepare(
`SELECT COUNT(*) as cnt FROM login_log
JOIN users ON login_log.user_id = users.id
WHERE users.username = ? AND login_log.success = 0
AND login_log.attempted_at > datetime('now', '-' || ? || ' minutes')`
).get(username, windowMinutes);
return row ? row.cnt : 0;
}
// ─── Rate Limits ──────────────────────────────────────
function checkRateLimit(key, action, maxCount, windowSeconds) {
const now = new Date().toISOString();
const db = getDB();
// UPSERT — single row per (key, action)
db.prepare(
`INSERT INTO rate_limits (key, action, count, window_start)
VALUES (?, ?, 1, ?)
ON CONFLICT(key, action) DO UPDATE SET
count = CASE
WHEN (unixepoch(?) - unixepoch(window_start)) > ? THEN 1
ELSE count + 1
END,
window_start = CASE
WHEN (unixepoch(?) - unixepoch(window_start)) > ? THEN ?
ELSE window_start
END`
).run(key, action, now, now, windowSeconds, now, windowSeconds, now);
const row = db.prepare(
'SELECT count, window_start FROM rate_limits WHERE key = ? AND action = ?'
).get(key, action);
if (!row) return { allowed: true, remaining: maxCount };
const windowAge = (Date.now() - new Date(row.window_start + 'Z').getTime()) / 1000;
if (windowAge > windowSeconds) return { allowed: true, remaining: maxCount };
return {
allowed: row.count <= maxCount,
remaining: Math.max(0, maxCount - row.count),
retryAfter: row.count > maxCount ? Math.ceil(windowSeconds - windowAge) : 0
};
}
module.exports = {
initDB, getDB,
getUserByUsername, getUserById, createUser,
findMatchingDevice, createDevice, updateDeviceLogin,
saveRefreshToken, findRefreshToken, revokeRefreshToken,
logLogin, countRecentFailures,
checkRateLimit
};
+89
View File
@@ -0,0 +1,89 @@
@echo off
chcp 65001 >nul
title Skeet Loader Server
echo ==========================================
echo Skeet Loader - Server Deployment
echo ==========================================
echo.
:: Check Node.js
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Node.js is not installed!
echo Download from: https://nodejs.org (LTS version)
pause
exit /b 1
)
echo Node.js: OK
:: Check npm
npm --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] npm not found!
pause
exit /b 1
)
echo npm: OK
:: Check if dependencies installed
if not exist "node_modules\" (
echo Installing dependencies...
npm install
if %errorlevel% neq 0 (
echo [ERROR] npm install failed!
pause
exit /b 1
)
)
echo Dependencies: OK
:: Check RSA keys
if not exist "private.pem" (
echo [ERROR] RSA keys not found! Generate them first:
echo openssl genrsa -out private.pem 2048
echo openssl rsa -in private.pem -pubout -out public.pem
pause
exit /b 1
)
echo RSA Keys: OK
:: Check if admin user exists
if not exist "skeet_server.db" (
echo.
echo No database found. Creating admin user...
node setup.js
if %errorlevel% neq 0 (
echo [ERROR] Admin setup failed!
pause
exit /b 1
)
)
:: Check PM2
echo.
where pm2 >nul 2>&1
if %errorlevel% equ 0 (
echo PM2 found - starting with PM2...
pm2 start ecosystem.config.js
pm2 status
echo.
echo Server started with PM2! Commands:
echo pm2 status - View status
echo pm2 logs - View logs
echo pm2 restart skeet-server
echo pm2 stop skeet-server
) else (
echo PM2 not installed - starting directly...
echo To install PM2: npm install -g pm2
echo.
echo Starting server directly (Ctrl+C to stop)...
node server.js
)
echo.
echo ==========================================
echo Server should be running on port 3000
echo Test: curl http://localhost:3000/api/status
echo ==========================================
pause
+32
View File
@@ -0,0 +1,32 @@
// PM2 process manager config
// Install: npm install -g pm2
// Start: pm2 start ecosystem.config.js
// Status: pm2 status
// Logs: pm2 logs skeet-server
// Restart: pm2 restart skeet-server
// Stop: pm2 stop skeet-server
// Auto-start on boot: pm2 startup && pm2 save
module.exports = {
apps: [{
name: 'skeet-server',
script: 'server.js',
cwd: __dirname,
env: {
NODE_ENV: 'production',
PORT: 3000
},
// Restart if memory exceeds 200MB
max_memory_restart: '200M',
// Restart if it crashes
autorestart: true,
// Max 5 restarts in 60 seconds, then stop
max_restarts: 5,
min_uptime: '60s',
// Log settings
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/error.log',
out_file: './logs/out.log',
merge_logs: true
}]
};
+1430
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "skeet-loader-server",
"version": "1.0.0",
"description": "Cloud verification server for skeet_loader",
"main": "server.js",
"scripts": {
"start": "node server.js",
"setup": "node setup.js"
},
"dependencies": {
"argon2": "^0.41.1",
"better-sqlite3": "^11.7.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"uuid": "^11.1.0"
}
}
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfL9+eXaLxbwOf
l42JstBlTgcPrfe5pqaq99bm+3cplEMCgfcX8rgJjae2vzjIdVA8CraS5dmPuV4R
vxbLN+Drz0P7PrzjGEf6gb8QJiQNQUsnIKjLaJtvgXj2r/qCpQN43fGnPbAakVCC
wYEgjsLTnwPFrOqPmjUSdhdqu+qe/umWc9ahqL4YyWyrMOEZsCOnJi8rrPlYd4uv
H0XOt7T4giSJPCQIRF2d+KujFOMwHdmutxQWgkPGmUM7npVQE+iBBpAj2IBhXHVV
2A5Dq4bznnT1Hh/ihCRWd1e6D1DRv6jL9BvL6tcASp+kaSiPwLaSjrWgj6kMnZDl
OL1WQFH7AgMBAAECggEASmNjQsysA9nmXhJC1Imoo+aKGzQnMuzjLrrhhJAXd+ku
RUI5l0nX4bL+IEKMxPQ5Wc2B0vSxLd4oTO+4tZ93roptUL/ZIeBrZm9yOxgvubf8
6Bx1dJR+KmXdAp5q2NlG3ZkNv6LupAOrFWdasb204pKI/zuWYyPjUCy6Naj583Eb
UmJxv8C/H95UL8BzGHkb4feFcJjv4Bm0dTLr45kt+GLaPSqTmMksXntCN7r64uNT
mZSBKN2/kRxmNjeyFPJvTbj6ZgsYCgyE6oeFTAKYXaAhEyyUTZqNYTQiADngvNCL
Us4RwRLY9rlw3RNFWATojyA6MgUWIWNuHFDxJst82QKBgQDfSoXThtv9cnUNMH72
qkyJdo4GJkbyvJk6TyyVgu4hd8eI0oFrgRCrKgCRbcmoi1oyu8tqeaCM5Y3qHtri
ggG8zUpmG5MqUv2LEd3cGAOd9ZP2oX5yoAg2QRoZTjoJJa9b8vg6y6JfYBNK4if6
37tTPvfDf+N2/Vc7c/fBRX4TyQKBgQC2gW35uWRR3thyCSLdTtD7TOhC93GzD1O8
psRQ7F9bcT25GZBp2YlK3LpTzVbPP2ek6x7ltyeq6PxXRbugE0ij7c+ywcMo91uZ
3Rcsj7TL8EpXpZU5V+BI4aeyN1KWKXa337jhcnZu8cJqomFkBv6sznLRyvLAxQu/
17f2oU5xowKBgDWjUjh42dmtJ/8OGkGosRAIYZ+KjFp9AZXnNP+JXyi8/DqazqoD
a/yh71b/94Q8TWOIhxnBs9aEwi1uUgg9UKuI6QlUMGrnWq6QkSnwvtWwC2Ygbx6b
4L0fsGRJzVkrK0+8MvL0vcGJc7j2UMJMAlTB+ISG8R5BRzYp6mcMCXg5AoGABhwb
JGsScrM1J8wqgKIs2NzgQa2q/sWalgw3MkZXguYtnM7ASOrhb8In8rpDF7kVrS25
4RLanxwhpoJNH7TFj8dcVq2p7OsrA+Gk7vb4pIMs2fZPIpZQieAUDyFPAHvu34T7
2YBNerVZPtykygZ57CsVKPTUX6O5GvkLzZPk3mUCgYEA14/OzSOSfYgO8AeZloTZ
IwBBMVTrK37vm/oP4nvnfhI34dpbPU6dT5PDR4WizUJsq5BqGfaxUgAJudMueAqb
E9q8RLF7l3ZEoiSut62VEllulrtk/EIBcGFJU4X0TthUskYrYE4GfO5UAuwfbxO5
O2Wkxljb2feNBBih8QDHJhY=
-----END PRIVATE KEY-----
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAny/fnl2i8W8Dn5eNibLQ
ZU4HD633uaamqvfW5vt3KZRDAoH3F/K4CY2ntr84yHVQPAq2kuXZj7leEb8Wyzfg
689D+z684xhH+oG/ECYkDUFLJyCoy2ibb4F49q/6gqUDeN3xpz2wGpFQgsGBII7C
058Dxazqj5o1EnYXarvqnv7plnPWoai+GMlsqzDhGbAjpyYvK6z5WHeLrx9Fzre0
+IIkiTwkCERdnfiroxTjMB3ZrrcUFoJDxplDO56VUBPogQaQI9iAYVx1VdgOQ6uG
85509R4f4oQkVndXug9Q0b+oy/Qby+rXAEqfpGkoj8C2ko61oI+pDJ2Q5Ti9VkBR
+wIDAQAB
-----END PUBLIC KEY-----
+365
View File
@@ -0,0 +1,365 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Skeet Loader — Admin</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f1117;color:#e0e0e0;min-height:100vh}
/* Login */
#loginBox{max-width:380px;margin:100px auto;padding:40px 30px;background:#1a1d27;border-radius:12px;border:1px solid #2a2d37}
#loginBox h1{text-align:center;font-size:22px;margin-bottom:8px;background:linear-gradient(90deg,#00b9ff,#ff01f7,#e8ff00);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
#loginBox .sub{text-align:center;font-size:12px;color:#666;margin-bottom:24px}
#loginBox input{width:100%;padding:12px;margin-bottom:12px;background:#0f1117;border:1px solid #2a2d37;border-radius:6px;color:#fff;font-size:14px;outline:none}
#loginBox input:focus{border-color:#00b9ff}
#loginBox button{width:100%;padding:12px;background:linear-gradient(90deg,#00b9ff,#ff01f7);border:none;border-radius:6px;color:#fff;font-size:15px;font-weight:600;cursor:pointer}
#loginBox button:hover{opacity:0.9}
#loginBox .err{color:#ff5050;font-size:13px;margin-top:8px;text-align:center}
/* Dashboard */
#dashboard{display:none;max-width:1200px;margin:0 auto;padding:20px}
header{display:flex;justify-content:space-between;align-items:center;padding:16px 0;border-bottom:1px solid #2a2d37;margin-bottom:24px}
header h1{font-size:20px;background:linear-gradient(90deg,#00b9ff,#ff01f7,#e8ff00);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
header .user{font-size:13px;color:#888;display:flex;gap:16px;align-items:center}
header .user button{background:#2a2d37;color:#ccc;border:1px solid #3a3d47;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:12px}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:24px}
.stat{background:#1a1d27;border:1px solid #2a2d37;border-radius:8px;padding:16px 20px}
.stat .n{font-size:28px;font-weight:700}
.stat .l{font-size:12px;color:#666;margin-top:4px}
.tabs{display:flex;gap:8px;margin-bottom:16px;border-bottom:1px solid #2a2d37;padding-bottom:8px}
.tab{padding:8px 16px;background:transparent;color:#888;border:none;cursor:pointer;font-size:13px;border-radius:4px 4px 0 0}
.tab.active{background:#1a1d27;color:#fff;border:1px solid #2a2d37;border-bottom:1px solid #1a1d27}
.toolbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
.toolbar button{background:#2a2d37;color:#ccc;border:1px solid #3a3d47;padding:7px 16px;border-radius:4px;cursor:pointer;font-size:12px}
.toolbar button:hover{background:#3a3d47}
.toolbar input{background:#1a1d27;border:1px solid #2a2d37;color:#fff;padding:7px 12px;border-radius:4px;font-size:12px;outline:none;width:180px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;padding:10px 12px;border-bottom:2px solid #2a2d37;color:#888;font-weight:600}
td{padding:9px 12px;border-bottom:1px solid #1a1d27}
tr:hover td{background:#1a1d2733}
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
.badge-ok{background:#0a3;color:#fff}
.badge-bad{background:#a00;color:#fff}
.badge-warn{background:#a80;color:#fff}
.badge-info{background:#049;color:#fff}
.btn-sm{background:#2a2d37;color:#ccc;border:1px solid #3a3d47;padding:4px 10px;border-radius:3px;cursor:pointer;font-size:11px;margin-right:4px}
.btn-sm:hover{background:#3a3d47}
.btn-sm.danger{color:#ff5050;border-color:#a03030}
.btn-sm.danger:hover{background:#3a1a1a}
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:100;justify-content:center;align-items:center}
.modal{background:#1a1d27;border:1px solid #2a2d37;border-radius:12px;padding:24px;min-width:350px}
.modal h3{margin-bottom:16px}
.modal input{width:100%;padding:10px;margin-bottom:10px;background:#0f1117;border:1px solid #2a2d37;border-radius:6px;color:#fff;font-size:13px}
.modal .btns{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
.modal .btns button{padding:8px 18px;border-radius:6px;border:none;cursor:pointer;font-size:13px}
.modal .btn-primary{background:linear-gradient(90deg,#00b9ff,#ff01f7);color:#fff}
.modal .btn-cancel{background:#2a2d37;color:#ccc}
.toast{position:fixed;top:20px;right:20px;background:#0a3;color:#fff;padding:12px 20px;border-radius:6px;font-size:13px;z-index:200;animation:fadein 0.3s}
@keyframes fadein{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
</style>
</head>
<body>
<!-- Login -->
<div id="loginBox">
<h1>Skeet Loader</h1>
<div class="sub">Admin Panel</div>
<input id="loginUser" placeholder="Username" autofocus>
<input id="loginPwd" type="password" placeholder="Password">
<button onclick="doLogin()">Login</button>
<div class="err" id="loginErr"></div>
</div>
<!-- Dashboard -->
<div id="dashboard">
<header>
<h1>Skeet Admin</h1>
<div class="user">
<span id="adminUser"></span>
<button onclick="logout()">Logout</button>
</div>
</header>
<div class="stats" id="stats"></div>
<div class="tabs">
<button class="tab active" onclick="switchTab('users')">Users</button>
<button class="tab" onclick="switchTab('logs')">Login Logs</button>
</div>
<div class="toolbar" id="userToolbar">
<input id="searchInput" placeholder="Search username..." oninput="renderUsers()">
<button onclick="showCreateModal()">+ Create User</button>
<button onclick="loadUsers()">↻ Refresh</button>
</div>
<table id="userTable"><thead><tr><th>ID</th><th>Username</th><th>Role</th><th>Status</th><th>HWID Bound</th><th>Created</th><th>Expires</th><th>Actions</th></tr></thead><tbody></tbody></table>
<table id="logTable" style="display:none"><thead><tr><th>Time</th><th>User</th><th>IP</th><th>Result</th><th>HWID Match</th><th>Reason</th></tr></thead><tbody></tbody></table>
</div>
<!-- Create User Modal -->
<div class="modal-overlay" id="createModal"><div class="modal">
<h3>Create User</h3>
<input id="newUser" placeholder="Username">
<input id="newPwd" type="password" placeholder="Password">
<input id="newExp" placeholder="Expiry (YYYY-MM-DD, optional)">
<div class="btns">
<button class="btn-cancel" onclick="closeModal('createModal')">Cancel</button>
<button class="btn-primary" onclick="createUser()">Create</button>
</div>
</div></div>
<!-- Toast -->
<div id="toast" class="toast" style="display:none"></div>
<script>
const API = ''; // same origin
let token = localStorage.getItem('admin_token');
let allUsers = [];
let allLogs = [];
let currentTab = 'users';
// ─── Init ──────────────────────────────
if (token) {
document.getElementById('loginBox').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
loadAll();
}
// ─── Login ─────────────────────────────
async function doLogin() {
const u = document.getElementById('loginUser').value.trim();
const p = document.getElementById('loginPwd').value;
if (!u || !p) return;
const h = sha256(p);
try {
const r = await fetch(API + '/api/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: u, password_hash: h,
hwid_components: { cpu: 'admin', board: 'panel', disk: 'panel', mac: 'panel' }
})
});
const d = await r.json();
if (!d.success) { document.getElementById('loginErr').textContent = d.error || 'Login failed'; return; }
// Verify admin role
const payload = JSON.parse(atob(d.access_token.split('.')[1]));
if (payload.role !== 'admin') {
document.getElementById('loginErr').textContent = 'Not an admin account';
return;
}
localStorage.setItem('admin_token', d.access_token);
token = d.access_token;
document.getElementById('loginBox').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
document.getElementById('adminUser').textContent = payload.username;
loadAll();
} catch(e) {
document.getElementById('loginErr').textContent = 'Connection failed';
}
}
function logout() {
localStorage.removeItem('admin_token');
token = null;
location.reload();
}
// ─── SHA-256 (pure JS — works without crypto.subtle, over HTTP) ──
function sha256(msg) {
function R(n,w){return((n>>>w)|(n<<(32-w)))>>>0}
const K=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];
// UTF-8 encode
const m=unescape(encodeURIComponent(msg));
const bytes=[];for(let i=0;i<m.length;i++)bytes.push(m.charCodeAt(i)&0xFF);
const l=bytes.length*8;
bytes.push(0x80);while((bytes.length+8)%64!==0)bytes.push(0);
// Big-endian 64-bit length (high 32 bits always 0 for <4GB messages)
bytes.push(0);bytes.push(0);bytes.push(0);bytes.push(0);
bytes.push((l>>>24)&0xFF);bytes.push((l>>>16)&0xFF);bytes.push((l>>>8)&0xFF);bytes.push(l&0xFF);
const H=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
for(let p=0;p<bytes.length;p+=64){
const W=[];for(let t=0;t<16;t++)W[t]=(bytes[p+t*4]<<24)|(bytes[p+t*4+1]<<16)|(bytes[p+t*4+2]<<8)|bytes[p+t*4+3];
for(let t=16;t<64;t++){const s0=R(W[t-15],7)^R(W[t-15],18)^(W[t-15]>>>3),s1=R(W[t-2],17)^R(W[t-2],19)^(W[t-2]>>>10);W[t]=(W[t-16]+s0+W[t-7]+s1)>>>0}
let[a,b,c,d,e,f,g,h]=H;
for(let t=0;t<64;t++){const S1=R(e,6)^R(e,11)^R(e,25),ch=(e&f)^(~e&g),t1=(h+S1+ch+K[t]+W[t])>>>0,S0=R(a,2)^R(a,13)^R(a,22),maj=(a&b)^(a&c)^(b&c),t2=(S0+maj)>>>0;h=g;g=f;f=e;e=(d+t1)>>>0;d=c;c=b;b=a;a=(t1+t2)>>>0}
H[0]=(H[0]+a)>>>0;H[1]=(H[1]+b)>>>0;H[2]=(H[2]+c)>>>0;H[3]=(H[3]+d)>>>0;H[4]=(H[4]+e)>>>0;H[5]=(H[5]+f)>>>0;H[6]=(H[6]+g)>>>0;H[7]=(H[7]+h)>>>0;
}
return H.map(v=>('0000000'+v.toString(16)).slice(-8)).join('');
}
// ─── Load Data ─────────────────────────
async function loadAll() {
await Promise.all([loadUsers(), loadStats()]);
}
async function loadUsers() {
try {
const r = await fetch(API + '/api/admin/users', { headers: authHeader() });
const d = await r.json();
allUsers = d.users || [];
renderUsers();
} catch(e) { console.error(e); }
}
async function loadStats() {
try {
const r = await fetch(API + '/api/admin/stats', { headers: authHeader() });
const d = await r.json();
document.getElementById('stats').innerHTML =
`<div class="stat"><div class="n">${d.totalUsers}</div><div class="l">Total Users</div></div>
<div class="stat"><div class="n" style="color:#0a3">${d.activeUsers}</div><div class="l">Active</div></div>
<div class="stat"><div class="n" style="color:#0af">${d.logins24h}</div><div class="l">Logins (24h)</div></div>
<div class="stat"><div class="n" style="color:#f55">${d.failures24h}</div><div class="l">Failures (24h)</div></div>`;
} catch(e) {}
}
async function loadLogs() {
try {
const r = await fetch(API + '/api/admin/logs', { headers: authHeader() });
const d = await r.json();
allLogs = d.logs || [];
renderLogs();
} catch(e) {}
}
function authHeader() {
return { 'Authorization': 'Bearer ' + token };
}
// ─── Render ────────────────────────────
function renderUsers() {
const q = (document.getElementById('searchInput')?.value || '').toLowerCase();
let users = q ? allUsers.filter(u => u.username.toLowerCase().includes(q)) : allUsers;
const tbody = document.querySelector('#userTable tbody');
tbody.innerHTML = users.map(u => `
<tr>
<td>${u.id}</td>
<td><b>${esc(u.username)}</b></td>
<td><span class="badge ${u.role==='admin'?'badge-warn':'badge-info'}">${u.role}</span></td>
<td><span class="badge ${u.is_active?'badge-ok':'badge-bad'}">${u.is_active?'Active':'Banned'}</span></td>
<td>${u.hwid_bound_at || '<span style="color:#666">—</span>'}</td>
<td>${fmtDate(u.created_at)}</td>
<td>${u.expires_at ? fmtDate(u.expires_at) : '<span style="color:#666">Never</span>'}</td>
<td>
<button class="btn-sm" onclick="toggleUser(${u.id},${u.is_active})">${u.is_active?'Ban':'Unban'}</button>
<button class="btn-sm danger" onclick="resetHwid(${u.id})">Reset HWID</button>
</td>
</tr>`).join('');
}
function renderLogs() {
const tbody = document.querySelector('#logTable tbody');
tbody.innerHTML = allLogs.map(l => `
<tr>
<td>${fmtDate(l.attempted_at)}</td>
<td>${esc(l.username||'—')}</td>
<td>${l.ip_address||'—'}</td>
<td><span class="badge ${l.success?'badge-ok':'badge-bad'}">${l.success?'OK':'FAIL'}</span></td>
<td>${l.hwid_match_rate != null ? (l.hwid_match_rate*100).toFixed(0)+'%' : '—'}</td>
<td style="color:#888">${esc(l.reason||'')}</td>
</tr>`).join('');
}
// ─── Tabs ──────────────────────────────
function switchTab(t) {
currentTab = t;
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('userTable').style.display = t === 'users' ? '' : 'none';
document.getElementById('userToolbar').style.display = t === 'users' ? '' : 'none';
document.getElementById('logTable').style.display = t === 'logs' ? '' : 'none';
if (t === 'logs') loadLogs();
if (t === 'users') loadUsers();
}
// ─── Actions ───────────────────────────
function showCreateModal() {
document.getElementById('createModal').style.display = 'flex';
document.getElementById('newUser').focus();
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
document.getElementById('newUser').value = '';
document.getElementById('newPwd').value = '';
document.getElementById('newExp').value = '';
}
async function createUser() {
const u = document.getElementById('newUser').value.trim();
const p = document.getElementById('newPwd').value;
const e = document.getElementById('newExp').value;
if (!u || !p) return;
// Admin creates user with raw password (server hashes internally)
try {
const r = await fetch(API + '/api/admin/create-user', {
method: 'POST', headers: { ...authHeader(), 'Content-Type': 'application/json' },
body: JSON.stringify({ username: u, password: p, expires_at: e || null })
});
const d = await r.json();
if (d.success) { toast('User created: ' + u); closeModal('createModal'); loadUsers(); }
else toast(d.error || 'Failed', true);
} catch(e) { toast('Connection error', true); }
}
async function toggleUser(id, current) {
try {
const r = await fetch(API + '/api/admin/user/' + id + '/toggle', {
method: 'POST', headers: authHeader()
});
const d = await r.json();
if (d.success) { toast('User ' + (d.is_active ? 'activated' : 'banned')); loadUsers(); }
} catch(e) { toast('Error', true); }
}
async function resetHwid(id) {
if (!confirm('Reset HWID for this user? They can bind to a new machine on next login.')) return;
try {
const r = await fetch(API + '/api/admin/user/' + id + '/reset-hwid', {
method: 'POST', headers: authHeader()
});
const d = await r.json();
if (d.success) toast('HWID reset — user can re-bind'); else toast(d.error||'Failed', true);
} catch(e) { toast('Error', true); }
}
// ─── Helpers ───────────────────────────
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function fmtDate(s) {
if (!s) return '—';
const d = new Date(s + (s.endsWith('Z')?'':'Z'));
return d.toLocaleString('zh-CN', { hour12: false });
}
function toast(msg, isErr) {
const el = document.getElementById('toast');
el.textContent = msg;
el.style.background = isErr ? '#a00' : '#0a3';
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 3000);
}
// Close modal on overlay click
document.getElementById('createModal').addEventListener('click', function(e) {
if (e.target === this) closeModal('createModal');
});
// Enter key on login
document.getElementById('loginPwd').addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
</script>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
const { checkRateLimit } = require('./db');
const LIMITS = {
ip: { max: 20, window: 300 }, // 20 req / 5 min
user: { max: 5, window: 600 }, // 5 failures / 10 min lock
hwid: { max: 10, window: 900 } // 10 failures / 15 min lock
};
/**
* Check all three rate limit dimensions.
* Returns { allowed, retryAfter } — retryAfter is the longest among triggered limits.
*/
function checkLoginRateLimit(ip, username, hwidCombined) {
let maxRetryAfter = 0;
// IP-based
const ipLimit = checkRateLimit(`ip:${ip}`, 'login', LIMITS.ip.max, LIMITS.ip.window);
if (!ipLimit.allowed) {
maxRetryAfter = Math.max(maxRetryAfter, ipLimit.retryAfter);
}
// Username-based (only for failed attempts — counted in login handler)
const userLimit = checkRateLimit(`user:${username}`, 'login', LIMITS.user.max, LIMITS.user.window);
if (!userLimit.allowed) {
maxRetryAfter = Math.max(maxRetryAfter, userLimit.retryAfter);
}
// HWID-based
if (hwidCombined) {
const hwidLimit = checkRateLimit(`hwid:${hwidCombined}`, 'login', LIMITS.hwid.max, LIMITS.hwid.window);
if (!hwidLimit.allowed) {
maxRetryAfter = Math.max(maxRetryAfter, hwidLimit.retryAfter);
}
}
return {
allowed: maxRetryAfter === 0,
retryAfter: maxRetryAfter
};
}
module.exports = { checkLoginRateLimit };
+322
View File
@@ -0,0 +1,322 @@
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
const express = require('express');
const db = require('./db');
const auth = require('./auth');
const rateLimit = require('./rate_limit');
const path = require('path');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Init DB
db.initDB();
// GET /api/status
app.get('/api/status', (req, res) => {
res.json({ status: 'ok', server_time: new Date().toISOString() });
});
// POST /api/login
app.post('/api/login', async (req, res) => {
const { username, password_hash, hwid_components } = req.body;
const ip = req.ip || req.socket.remoteAddress || 'unknown';
const userAgent = req.headers['user-agent'] || '';
if (!username || !password_hash || !hwid_components) {
return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
}
const { cpu, board, disk, mac } = hwid_components;
if (!cpu || !board || !disk || !mac) {
return res.status(400).json({ success: false, error: 'MISSING_HWID_COMPONENTS' });
}
const hwidCombined = auth.combinedHwidHash(hwid_components);
// Rate limit
const rl = rateLimit.checkLoginRateLimit(ip, username, hwidCombined);
if (!rl.allowed) {
return res.status(429).json({
success: false, error: 'RATE_LIMITED',
message: 'Too many attempts', retry_after: rl.retryAfter
});
}
// User lookup
const user = db.getUserByUsername(username);
if (!user) {
db.logLogin(null, null, ip, userAgent, 0, false, 'USER_NOT_FOUND');
return res.json({ success: false, error: 'INVALID_CREDENTIALS' });
}
if (!user.is_active) return res.json({ success: false, error: 'ACCOUNT_DISABLED' });
if (user.expires_at && new Date(user.expires_at) < new Date())
return res.json({ success: false, error: 'ACCOUNT_EXPIRED' });
// Verify password (Argon2id)
let passwordValid = false;
try {
passwordValid = await auth.verifyPassword(user.password_hash, password_hash);
} catch (err) {
console.error('[login] argon2 error:', err.message);
return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' });
}
if (!passwordValid) {
db.logLogin(user.id, null, ip, userAgent, 0, false, 'WRONG_PASSWORD');
return res.json({ success: false, error: 'INVALID_CREDENTIALS' });
}
// Admin accounts skip HWID check (for web panel access)
if (user.role === 'admin') {
const accessToken = auth.signAccessToken(user, 0);
const refreshToken = auth.generateRefreshToken();
const tokenHash = auth.hashToken(refreshToken);
const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
db.saveRefreshToken(tokenHash, user.id, 0, 'admin', expiresAt);
db.logLogin(user.id, 0, ip, userAgent, 1.0, true, null);
return res.json({
success: true,
access_token: accessToken,
refresh_token: refreshToken,
username: user.username,
match_rate: 1.0
});
}
// HWID matching
const userDevices = db.findMatchingDevice(user.id, cpu, board, disk, mac);
let matchedDevice = null;
let bestRate = 0;
for (const device of userDevices) {
const result = auth.hwidMatchRate(device, hwid_components);
if (result.match && result.rate > bestRate) {
matchedDevice = device;
bestRate = result.rate;
}
}
if (!matchedDevice) {
if (userDevices.length === 0) {
// First login — auto-create device
const displayName = req.headers['x-device-name'] || 'Unknown Device';
const result = db.createDevice(user.id, displayName, cpu, board, disk, mac, ip);
matchedDevice = { id: result.lastInsertRowid, cpu_hash: cpu, board_hash: board, disk_hash: disk, mac_hash: mac };
bestRate = 1.0;
} else {
// HWID threshold failed
db.logLogin(user.id, null, ip, userAgent, bestRate, false, 'HWID_THRESHOLD');
return res.json({
success: false, error: 'HWID_THRESHOLD',
message: 'Hardware does not match any bound device', match_rate: bestRate
});
}
} else {
db.updateDeviceLogin(matchedDevice.id, ip);
}
// Generate tokens
const accessToken = auth.signAccessToken(user, matchedDevice.id);
const refreshToken = auth.generateRefreshToken();
const tokenHash = auth.hashToken(refreshToken);
const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
db.saveRefreshToken(tokenHash, user.id, matchedDevice.id, hwidCombined, expiresAt);
db.logLogin(user.id, matchedDevice.id, ip, userAgent, bestRate, true, null);
res.json({
success: true,
access_token: accessToken,
refresh_token: refreshToken,
username: user.username,
match_rate: bestRate
});
});
// POST /api/refresh
app.post('/api/refresh', (req, res) => {
const { refresh_token, hwid_components } = req.body;
if (!refresh_token || !hwid_components) {
return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
}
const tokenHash = auth.hashToken(refresh_token);
const stored = db.findRefreshToken(tokenHash);
if (!stored || stored.revoked) return res.json({ success: false, error: 'TOKEN_EXPIRED' });
if (new Date(stored.expires_at) < new Date()) return res.json({ success: false, error: 'TOKEN_EXPIRED' });
// Verify HWID
const hwidCombined = auth.combinedHwidHash(hwid_components);
if (stored.hwid_hash !== hwidCombined) {
const user = db.getUserById(stored.user_id);
if (!user) return res.json({ success: false, error: 'TOKEN_EXPIRED' });
const userDevices = db.findMatchingDevice(user.id,
hwid_components.cpu, hwid_components.board,
hwid_components.disk, hwid_components.mac);
let matched = false;
for (const device of userDevices) {
if (auth.hwidMatchRate(device, hwid_components).match) { matched = true; break; }
}
if (!matched) return res.json({ success: false, error: 'HWID_THRESHOLD' });
}
// Rotate
db.revokeRefreshToken(tokenHash);
const user = db.getUserById(stored.user_id);
const newAccessToken = auth.signAccessToken(user, stored.device_id);
const newRefreshToken = auth.generateRefreshToken();
const newTokenHash = auth.hashToken(newRefreshToken);
const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
db.saveRefreshToken(newTokenHash, user.id, stored.device_id,
auth.combinedHwidHash(hwid_components), expiresAt);
if (stored.device_id) db.updateDeviceLogin(stored.device_id, req.ip || req.socket.remoteAddress);
res.json({ success: true, access_token: newAccessToken, refresh_token: newRefreshToken });
});
// POST /api/logout
app.post('/api/logout', (req, res) => {
const { refresh_token } = req.body;
if (refresh_token) db.revokeRefreshToken(auth.hashToken(refresh_token));
res.json({ success: true });
});
// POST /api/register (public self-registration)
app.post('/api/register', async (req, res) => {
const { username, password_hash } = req.body;
const ip = req.ip || req.socket.remoteAddress || 'unknown';
if (!username || !password_hash) {
return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
}
// Username rules
if (username.length < 3 || username.length > 32) {
return res.json({ success: false, error: 'INVALID_USERNAME', message: 'Username must be 3-32 characters' });
}
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
return res.json({ success: false, error: 'INVALID_USERNAME', message: 'Only letters, numbers, underscores' });
}
// Rate limit registration per IP (5 per hour)
const rl = db.checkRateLimit(`register:${ip}`, 'register', 5, 3600);
if (!rl.allowed) {
return res.status(429).json({
success: false, error: 'RATE_LIMITED',
message: 'Too many registrations. Try again later.', retry_after: rl.retryAfter
});
}
// Check if username taken
if (db.getUserByUsername(username)) {
return res.json({ success: false, error: 'USERNAME_TAKEN' });
}
// password_hash comes as SHA-256 from client — verify it's 64-char hex
if (password_hash.length !== 64 || !/^[a-f0-9]{64}$/.test(password_hash)) {
return res.json({ success: false, error: 'INVALID_PASSWORD_HASH' });
}
// password_hash is already SHA-256(password) — Argon2id it directly
let storedHash;
try {
storedHash = await auth.hashPasswordFromHash(password_hash);
} catch (err) {
return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' });
}
db.createUser(username, storedHash, 'user', null);
console.log(`[register] New user: ${username} (ip: ${ip})`);
res.json({ success: true, message: 'Registration successful. You can now login.' });
});
// Admin: POST /api/admin/create-user
app.post('/api/admin/create-user', auth.requireAdmin, async (req, res) => {
const { username, password, role, expires_at } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
}
if (db.getUserByUsername(username)) {
return res.json({ success: false, error: 'USERNAME_TAKEN' });
}
let passwordHash;
try { passwordHash = await auth.hashPassword(password); }
catch (err) { return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' }); }
db.createUser(username, passwordHash, role || 'user', expires_at || null);
res.json({ success: true, message: `User '${username}' created` });
});
// Admin: GET /api/admin/users (list all users)
app.get('/api/admin/users', auth.requireAdmin, (req, res) => {
const db = require('./db');
const users = db.getDB().prepare(
`SELECT u.id, u.username, u.role, u.is_active, u.created_at, u.expires_at,
(SELECT MIN(d.created_at) FROM devices d WHERE d.user_id = u.id) as hwid_bound_at
FROM users u ORDER BY u.id DESC`
).all();
res.json({ users });
});
// Admin: GET /api/admin/logs (recent login logs, last 100)
app.get('/api/admin/logs', auth.requireAdmin, (req, res) => {
const db = require('./db');
const logs = db.getDB().prepare(
`SELECT login_log.*, users.username FROM login_log
LEFT JOIN users ON login_log.user_id = users.id
ORDER BY login_log.id DESC LIMIT 100`
).all();
res.json({ logs });
});
// Admin: GET /api/admin/stats
app.get('/api/admin/stats', auth.requireAdmin, (req, res) => {
const db = require('./db');
const totalUsers = db.getDB().prepare('SELECT COUNT(*) as cnt FROM users').get().cnt;
const activeUsers = db.getDB().prepare('SELECT COUNT(*) as cnt FROM users WHERE is_active = 1').get().cnt;
const logins24h = db.getDB().prepare(
"SELECT COUNT(*) as cnt FROM login_log WHERE success = 1 AND attempted_at > datetime('now', '-1 day')"
).get().cnt;
const failures24h = db.getDB().prepare(
"SELECT COUNT(*) as cnt FROM login_log WHERE success = 0 AND attempted_at > datetime('now', '-1 day')"
).get().cnt;
res.json({ totalUsers, activeUsers, logins24h, failures24h });
});
// Admin: POST /api/admin/user/:id/toggle (ban/unban)
app.post('/api/admin/user/:id/toggle', auth.requireAdmin, (req, res) => {
const db = require('./db');
const user = db.getUserById(req.params.id);
if (!user) return res.json({ success: false, error: 'NOT_FOUND' });
const newStatus = user.is_active ? 0 : 1;
db.getDB().prepare('UPDATE users SET is_active = ? WHERE id = ?').run(newStatus, req.params.id);
res.json({ success: true, is_active: newStatus });
});
// Admin: POST /api/admin/user/:id/reset-hwid
app.post('/api/admin/user/:id/reset-hwid', auth.requireAdmin, (req, res) => {
const db = require('./db');
db.getDB().prepare('DELETE FROM devices WHERE user_id = ?').run(req.params.id);
db.getDB().prepare(
'UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?'
).run(req.params.id);
res.json({ success: true, message: 'HWID reset. User can bind to new machine on next login.' });
});
// Start
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`[server] running on port ${PORT}`);
console.log(` GET /api/status`);
console.log(` POST /api/login`);
console.log(` POST /api/refresh`);
console.log(` POST /api/logout`);
console.log(` POST /api/admin/create-user (admin)`);
});
+47
View File
@@ -0,0 +1,47 @@
/**
* Setup script — create the first admin user.
* Usage: node setup.js <admin_username> <admin_password>
*/
require('dotenv').config();
const readline = require('readline');
const db = require('./db');
const auth = require('./auth');
db.initDB();
async function main() {
const args = process.argv.slice(2);
let username = args[0];
let password = args[1];
if (!username || !password) {
// Prompt interactively
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
username = await new Promise(resolve => rl.question('Admin username: ', resolve));
password = await new Promise(resolve => rl.question('Admin password: ', resolve));
rl.close();
}
if (!username || !password) {
console.error('Username and password are required.');
process.exit(1);
}
const existing = db.getUserByUsername(username);
if (existing) {
console.error(`User '${username}' already exists.`);
process.exit(1);
}
const passwordHash = await auth.hashPassword(password);
db.createUser(username, passwordHash, 'admin', null);
console.log(`Admin user '${username}' created successfully.`);
process.exit(0);
}
main().catch(err => {
console.error('Setup failed:', err);
process.exit(1);
});
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.37314.3 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "skeet_loader", "skeet_loader\skeet_loader.vcxproj", "{68F549CE-1268-453D-A8C5-CB4AFE5F5381}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{68F549CE-1268-453D-A8C5-CB4AFE5F5381}.Debug|x64.ActiveCfg = Debug|x64
{68F549CE-1268-453D-A8C5-CB4AFE5F5381}.Debug|x64.Build.0 = Debug|x64
{68F549CE-1268-453D-A8C5-CB4AFE5F5381}.Release|x64.ActiveCfg = Release|x64
{68F549CE-1268-453D-A8C5-CB4AFE5F5381}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D188B6CB-458C-4BC0-B024-2BF1AA796290}
EndGlobalSection
EndGlobal
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,170 @@
#include "FontManager.h"
#include <string>
#include <unordered_set>
#include <cctype>
#include <windows.h>
#include <Shlobj.h>
// 单例
Font* Font::Get()
{
static Font instance;
return &instance;
}
// 内部工具
std::vector<int> Font::ExtractCodepoints(const char* text) {
int count = 0;
int* cps = RLLoadCodepoints(text, &count);
std::vector<int> result(cps, cps + count);
RLUnloadCodepoints(cps);
return result;
}
// 字体重建
void Font::Rebuild() {
if (m_codepoints.empty() || m_fontPath.empty()) return;
RLFont newFont = RLLoadFontEx(
m_fontPath.c_str(),
m_fontSizeGlobal,
m_codepoints.data(),
static_cast<int>(m_codepoints.size())
);
if (newFont.texture.id == 0 || newFont.baseSize <= 0) return;
if (m_font.texture.id != 0 && !m_usingDefaultFont) {
RLUnloadFont(m_font);
}
m_font = newFont;
m_usingDefaultFont = false;
RLSetTextureFilter(m_font.texture, RL_E_TEXTURE_FILTER_BILINEAR);
}
// 初始化加载字体
void Font::LoadCustom() {
if (m_font.baseSize != 0) return;
const char* initText = "1234567890:";
auto cps = ExtractCodepoints(initText);
for (int cp : cps) {
if (m_codepointSet.insert(cp).second) {
m_codepoints.push_back(cp);
}
}
// 1) 尝试从 exe 同路径的 font 文件夹加载
const char* appDir = RLGetApplicationDirectory();
if (appDir && appDir[0]) {
std::string fontDir = appDir;
fontDir += "\\font\\";
RLFilePathList allFiles = RLLoadDirectoryFilesEx(fontDir.c_str(), "*.*", false);
for (unsigned int i = 0; i < allFiles.count; ++i) {
std::string fullPath = allFiles.paths[i];
size_t dotPos = fullPath.find_last_of('.');
if (dotPos == std::string::npos) continue;
std::string ext = fullPath.substr(dotPos + 1);
for (char& c : ext) c = static_cast<char>(std::tolower(c));
if (ext != "ttf" && ext != "ttc" && ext != "otf") continue;
if (!RLFileExists(fullPath.c_str())) continue;
RLFont tryFont = RLLoadFontEx(fullPath.c_str(), m_fontSizeGlobal,
m_codepoints.data(), static_cast<int>(m_codepoints.size()));
if (tryFont.texture.id == 0 || tryFont.baseSize <= 0) continue;
m_font = tryFont;
m_fontPath = fullPath;
m_usingDefaultFont = false;
RLSetTextureFilter(m_font.texture, RL_E_TEXTURE_FILTER_BILINEAR);
RLUnloadDirectoryFiles(allFiles);
printf("[Font] > 已加载字体: %s\n", fullPath.c_str());
return;
}
RLUnloadDirectoryFiles(allFiles);
}
// 2) 回退到系统字体目录
std::string fontDir;
wchar_t fontPathW[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_FONTS, nullptr, 0, fontPathW))) {
char narrowPath[MAX_PATH];
WideCharToMultiByte(CP_UTF8, 0, fontPathW, -1, narrowPath, MAX_PATH, nullptr, nullptr);
fontDir = narrowPath;
} else {
fontDir = "C:\\Windows\\Fonts";
}
const char* fontFiles[] = { "simhei.ttf", "msyh.ttc", "verdana.ttf", nullptr };
for (int i = 0; fontFiles[i]; ++i) {
std::string fullPath = fontDir;
fullPath += "\\";
fullPath += fontFiles[i];
if (!RLFileExists(fullPath.c_str())) continue;
RLFont tryFont = RLLoadFontEx(fullPath.c_str(), m_fontSizeGlobal,
m_codepoints.data(), static_cast<int>(m_codepoints.size()));
if (tryFont.texture.id == 0 || tryFont.baseSize <= 0) continue;
m_font = tryFont;
m_fontPath = fullPath;
m_usingDefaultFont = false;
RLSetTextureFilter(m_font.texture, RL_E_TEXTURE_FILTER_BILINEAR);
printf("[Font] > 已加载系统字体: %s\n", fullPath.c_str());
return;
}
printf("[Font] > 警告: 未找到可用字体,使用默认字体\n");
}
// Ensure - 确保字符已加载
void Font::Ensure(const char* text) {
auto cps = ExtractCodepoints(text);
bool needRebuild = false;
for (int cp : cps) {
if (m_codepointSet.find(cp) == m_codepointSet.end()) {
m_codepointSet.insert(cp);
m_codepoints.push_back(cp);
needRebuild = true;
}
}
if (needRebuild) {
Rebuild();
}
}
void Font::DrawTextEX(const char* text, RLVector2 pos, int fontSize, RLColor color, int spacing) {
Ensure(text);
if (m_font.texture.id != 0) {
RLDrawTextEx(m_font, text, pos, static_cast<float>(fontSize), static_cast<float>(spacing), color);
} else {
RLDrawText(text, static_cast<int>(pos.x), static_cast<int>(pos.y), fontSize, color);
}
}
RLVector2 Font::MeasureText(const char* text, int fontSize, int spacing) {
Ensure(text);
if (m_font.texture.id != 0) {
return RLMeasureTextEx(m_font, text, static_cast<float>(fontSize), static_cast<float>(spacing));
}
return RLVector2{
static_cast<float>(RLMeasureText(text, fontSize)),
static_cast<float>(fontSize)
};
}
// 释放资源
void Font::Clear() {
if (m_font.texture.id != 0 && !m_usingDefaultFont) {
RLUnloadFont(m_font);
m_font = { 0 };
}
m_fontPath.clear();
m_codepointSet.clear();
m_codepoints.clear();
printf("[Font] > 已清理\n");
}
@@ -0,0 +1,45 @@
#pragma once
#include <vector>
#include <string>
#include <unordered_set>
#include <atomic>
#include "raylib.h"
class Font {
public:
static Font* Get();
// 初始化加载字体(搜索 font/ 目录 → 系统字体)
void LoadCustom();
// 确保文本中的字符已加载,必要时重建字体
void Ensure(const char* text);
// 绘制文本
void DrawTextEX(const char* text, RLVector2 pos, int fontSize, RLColor color, int spacing = 0);
// 测量文本尺寸
RLVector2 MeasureText(const char* text, int fontSize, int spacing = 2);
// 获取 raylib 字体引用
const RLFont& GetFont() const { return m_font; }
// 释放字体资源
void Clear();
private:
Font() = default;
~Font() = default;
Font(const Font&) = delete;
Font& operator=(const Font&) = delete;
std::vector<int> ExtractCodepoints(const char* text);
void Rebuild();
RLFont m_font = { 0 };
std::string m_fontPath;
int m_fontSizeGlobal = 32;
std::unordered_set<int> m_codepointSet;
std::vector<int> m_codepoints;
bool m_usingDefaultFont = false;
};
@@ -0,0 +1,283 @@
#include "HWIDCollector.h"
#include <windows.h>
#include <wincrypt.h>
#include <iphlpapi.h>
#include <wbemidl.h>
#include <comdef.h>
#include <cstdio>
#include <cstring>
#include <vector>
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "wbemuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
// ─── Singleton ────────────────────────────────────────
HWIDCollector* HWIDCollector::Get()
{
static HWIDCollector instance;
return &instance;
}
// ─── SHA-256 ──────────────────────────────────────────
std::string HWIDCollector::SHA256(const std::string& input)
{
if (input.empty()) return {};
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::string result;
if (!CryptAcquireContextA(&hProv, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT))
return {};
if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash))
{
CryptReleaseContext(hProv, 0);
return {};
}
if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(input.data()),
static_cast<DWORD>(input.size()), 0))
{
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return {};
}
BYTE hash[32];
DWORD hashLen = sizeof(hash);
if (CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLen, 0))
{
char hex[65];
for (int i = 0; i < 32; ++i)
snprintf(hex + i * 2, 3, "%02x", hash[i]);
result.assign(hex, 64);
}
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return result;
}
// ─── CPU: Registry ProcessorNameString ────────────────
std::string HWIDCollector::CollectCPUId()
{
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0",
0, KEY_READ, &hKey) != ERROR_SUCCESS)
{
printf("[HWID] CPU: registry key not found\n");
return {};
}
char name[256] = {};
DWORD size = sizeof(name);
if (RegQueryValueExA(hKey, "ProcessorNameString", nullptr, nullptr,
reinterpret_cast<BYTE*>(name), &size) != ERROR_SUCCESS)
{
printf("[HWID] CPU: ProcessorNameString not found\n");
RegCloseKey(hKey);
return {};
}
RegCloseKey(hKey);
std::string cpuName(name);
// Trim trailing whitespace/newlines
while (!cpuName.empty() && (cpuName.back() == ' ' || cpuName.back() == '\n' || cpuName.back() == '\r'))
cpuName.pop_back();
std::string hash = SHA256(cpuName);
printf("[HWID] CPU: %s -> %s\n", cpuName.c_str(), hash.c_str());
return hash;
}
// ─── Board: WMI Win32_BaseBoard.SerialNumber ──────────
std::string HWIDCollector::CollectBoardSerial()
{
std::string result;
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
bool comInitialized = SUCCEEDED(hr) || hr == S_FALSE || hr == RPC_E_CHANGED_MODE;
// note: RPC_E_CHANGED_MODE means COM was already initialized with a different mode
if (hr == RPC_E_CHANGED_MODE) comInitialized = false; // can't use safely
IWbemLocator* pLoc = nullptr;
IWbemServices* pSvc = nullptr;
IEnumWbemClassObject* pEnum = nullptr;
if (!comInitialized) goto cleanup;
hr = CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
IID_IWbemLocator, reinterpret_cast<void**>(&pLoc));
if (FAILED(hr) || !pLoc) { printf("[HWID] Board: CoCreateInstance failed 0x%lx\n", hr); goto cleanup; }
hr = pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), nullptr, nullptr, nullptr,
0, nullptr, nullptr, &pSvc);
if (FAILED(hr) || !pSvc) { printf("[HWID] Board: ConnectServer failed 0x%lx\n", hr); goto cleanup; }
// Set proxy security
CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE);
hr = pSvc->ExecQuery(
_bstr_t(L"WQL"),
_bstr_t(L"SELECT SerialNumber FROM Win32_BaseBoard"),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
nullptr, &pEnum);
if (FAILED(hr) || !pEnum) { printf("[HWID] Board: ExecQuery failed 0x%lx\n", hr); goto cleanup; }
{
IWbemClassObject* pObj = nullptr;
ULONG returned = 0;
hr = pEnum->Next(WBEM_INFINITE, 1, &pObj, &returned);
if (SUCCEEDED(hr) && pObj)
{
VARIANT vt;
VariantInit(&vt);
hr = pObj->Get(L"SerialNumber", 0, &vt, nullptr, nullptr);
if (SUCCEEDED(hr) && vt.vt == VT_BSTR && vt.bstrVal)
{
std::wstring ws(vt.bstrVal);
result = std::string(ws.begin(), ws.end());
}
VariantClear(&vt);
pObj->Release();
}
}
// Fallback: try Win32_BIOS
if (result.empty() || result == "To be filled by O.E.M.")
{
if (pEnum) { pEnum->Release(); pEnum = nullptr; }
hr = pSvc->ExecQuery(
_bstr_t(L"WQL"),
_bstr_t(L"SELECT SerialNumber FROM Win32_BIOS"),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
nullptr, &pEnum);
if (SUCCEEDED(hr) && pEnum)
{
IWbemClassObject* pObj = nullptr;
ULONG returned = 0;
hr = pEnum->Next(WBEM_INFINITE, 1, &pObj, &returned);
if (SUCCEEDED(hr) && pObj)
{
VARIANT vt;
VariantInit(&vt);
hr = pObj->Get(L"SerialNumber", 0, &vt, nullptr, nullptr);
if (SUCCEEDED(hr) && vt.vt == VT_BSTR && vt.bstrVal)
{
std::wstring ws(vt.bstrVal);
result = std::string(ws.begin(), ws.end());
}
VariantClear(&vt);
pObj->Release();
}
}
}
cleanup:
if (pEnum) pEnum->Release();
if (pSvc) pSvc->Release();
if (pLoc) pLoc->Release();
if (comInitialized) CoUninitialize();
std::string hash = SHA256(result);
printf("[HWID] Board: '%s' -> %s\n", result.c_str(), hash.c_str());
return hash;
}
// ─── Disk: GetVolumeInformationA ──────────────────────
std::string HWIDCollector::CollectVolumeSerial()
{
DWORD serial = 0;
if (!GetVolumeInformationA("C:\\", nullptr, 0, &serial, nullptr, nullptr, nullptr, 0))
{
printf("[HWID] Disk: GetVolumeInformation failed: 0x%lx\n", GetLastError());
return {};
}
char buf[16];
snprintf(buf, sizeof(buf), "%08lx", serial);
std::string serialStr(buf);
std::string hash = SHA256(serialStr);
printf("[HWID] Disk: %s -> %s\n", serialStr.c_str(), hash.c_str());
return hash;
}
// ─── MAC: GetAdaptersInfo ─────────────────────────────
std::string HWIDCollector::CollectMacAddress()
{
ULONG bufLen = sizeof(IP_ADAPTER_INFO);
std::vector<BYTE> buffer(bufLen);
PIP_ADAPTER_INFO pAdapterInfo = reinterpret_cast<PIP_ADAPTER_INFO>(buffer.data());
DWORD ret = GetAdaptersInfo(pAdapterInfo, &bufLen);
if (ret == ERROR_BUFFER_OVERFLOW)
{
buffer.resize(bufLen);
pAdapterInfo = reinterpret_cast<PIP_ADAPTER_INFO>(buffer.data());
ret = GetAdaptersInfo(pAdapterInfo, &bufLen);
}
if (ret != ERROR_SUCCESS)
{
printf("[HWID] MAC: GetAdaptersInfo failed: 0x%lx\n", ret);
return {};
}
// Find first physical adapter (Ethernet or Wi-Fi, skip loopback/tunnel)
for (PIP_ADAPTER_INFO p = pAdapterInfo; p; p = p->Next)
{
// Skip loopback and tunnel adapters
if (p->Type == MIB_IF_TYPE_LOOPBACK) continue;
if (p->Type == MIB_IF_TYPE_OTHER) continue;
if (p->AddressLength == 0) continue;
char mac[18];
snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x",
p->Address[0], p->Address[1], p->Address[2],
p->Address[3], p->Address[4], p->Address[5]);
std::string macStr(mac);
std::string hash = SHA256(macStr);
printf("[HWID] MAC: %s (%s) -> %s\n", p->Description, macStr.c_str(), hash.c_str());
return hash;
}
printf("[HWID] MAC: no physical adapter found\n");
return {};
}
// ─── Public API ───────────────────────────────────────
const HWIDComponents& HWIDCollector::Collect()
{
if (m_collected)
return m_components;
printf("[HWID] Collecting hardware identifiers...\n");
m_components.cpu = CollectCPUId();
m_components.board = CollectBoardSerial();
m_components.disk = CollectVolumeSerial();
m_components.mac = CollectMacAddress();
// Valid if we have at least CPU + Board (core 80%)
m_components.valid = !m_components.cpu.empty() && !m_components.board.empty();
m_collected = true;
printf("[HWID] Collection complete (valid=%d)\n", m_components.valid);
return m_components;
}
@@ -0,0 +1,42 @@
#pragma once
#include <string>
struct HWIDComponents {
std::string cpu; // SHA-256 of CPU name
std::string board; // SHA-256 of motherboard serial
std::string disk; // SHA-256 of volume serial
std::string mac; // SHA-256 of MAC address
bool valid = false; // true if at least CPU + Board collected
};
/**
* HWIDCollector — collects hardware identifiers and hashes each one individually.
* Each component is SHA-256 hashed before leaving the client.
* The server NEVER sees raw hardware IDs.
*/
class HWIDCollector
{
public:
static HWIDCollector* Get();
/// Collect all hardware components (cached after first call).
const HWIDComponents& Collect();
/// Return previously collected components (must call Collect() first).
const HWIDComponents& GetComponents() const { return m_components; }
private:
HWIDCollector() = default;
~HWIDCollector() = default;
std::string CollectCPUId();
std::string CollectBoardSerial();
std::string CollectVolumeSerial();
std::string CollectMacAddress();
/// SHA-256 hash a string, returns hex string.
static std::string SHA256(const std::string& input);
HWIDComponents m_components;
bool m_collected = false;
};
+324
View File
@@ -0,0 +1,324 @@
#include "Hash.h"
#include <windows.h>
#include <wincrypt.h>
#include <cstdio>
#pragma comment(lib, "advapi32.lib")
// ─── 内部辅助:字节数组 → 十六进制字符串 ─────────────────────
static std::string ToHexString(const BYTE* data, DWORD length)
{
std::string result;
result.reserve(length * 2);
for (DWORD i = 0; i < length; ++i)
{
char buf[3];
snprintf(buf, sizeof(buf), "%02x", data[i]);
result.append(buf);
}
return result;
}
// ─── 内部辅助:获取 MD5 的 CryptoAPI 提供者 ─────────────────
static bool AcquireCryptoProvider(HCRYPTPROV& hProv)
{
if (!CryptAcquireContextA(&hProv, nullptr, nullptr, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
{
printf("[Hash] > err: CryptAcquireContext failed (0x%lx)\n", GetLastError());
return false;
}
return true;
}
// ─── 内部:执行 MD5 哈希 ──────────────────────────────────────
static bool DoMD5Hash(HCRYPTPROV hProv, HCRYPTHASH& hHash, const BYTE* data, DWORD dataLen)
{
if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
{
printf("[Hash] > err: CryptCreateHash failed (0x%lx)\n", GetLastError());
return false;
}
if (!CryptHashData(hHash, data, dataLen, 0))
{
printf("[Hash] > err: CryptHashData failed (0x%lx)\n", GetLastError());
CryptDestroyHash(hHash);
return false;
}
return true;
}
// ─── 公开接口 ─────────────────────────────────────────────────
int64_t Hash::GetFileSize(const std::string& filePath)
{
HANDLE hFile = CreateFileA(
filePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE)
return -1;
LARGE_INTEGER size;
const bool ok = GetFileSizeEx(hFile, &size);
CloseHandle(hFile);
return ok ? static_cast<int64_t>(size.QuadPart) : -1;
}
std::string Hash::ComputeMD5(const std::string& filePath)
{
// 打开文件
HANDLE hFile = CreateFileA(
filePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("[Hash] > err: cannot open file: %s\n", filePath.c_str());
return {};
}
// 获取文件大小
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(hFile, &fileSize))
{
printf("[Hash] > err: GetFileSizeEx failed\n");
CloseHandle(hFile);
return {};
}
// 映射文件到内存
HANDLE hMapping = CreateFileMappingA(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!hMapping)
{
printf("[Hash] > err: CreateFileMapping failed (0x%lx)\n", GetLastError());
CloseHandle(hFile);
return {};
}
const BYTE* pData = static_cast<const BYTE*>(
MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0)
);
if (!pData)
{
printf("[Hash] > err: MapViewOfFile failed (0x%lx)\n", GetLastError());
CloseHandle(hMapping);
CloseHandle(hFile);
return {};
}
// CryptoAPI MD5
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::string result;
if (AcquireCryptoProvider(hProv))
{
if (DoMD5Hash(hProv, hHash, pData, static_cast<DWORD>(fileSize.QuadPart)))
{
BYTE hash[16];
DWORD hashLen = sizeof(hash);
if (CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLen, 0))
{
result = ToHexString(hash, hashLen);
}
else
{
printf("[Hash] > err: CryptGetHashParam failed (0x%lx)\n", GetLastError());
}
CryptDestroyHash(hHash);
}
CryptReleaseContext(hProv, 0);
}
UnmapViewOfFile(pData);
CloseHandle(hMapping);
CloseHandle(hFile);
if (!result.empty())
printf("[Hash] > %s -> %s\n", filePath.c_str(), result.c_str());
return result;
}
std::string Hash::ComputeMD5(const std::vector<uint8_t>& data)
{
if (data.empty())
return {};
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::string result;
if (AcquireCryptoProvider(hProv))
{
if (DoMD5Hash(hProv, hHash, data.data(), static_cast<DWORD>(data.size())))
{
BYTE hash[16];
DWORD hashLen = sizeof(hash);
if (CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLen, 0))
{
result = ToHexString(hash, hashLen);
}
CryptDestroyHash(hHash);
}
CryptReleaseContext(hProv, 0);
}
return result;
}
std::string Hash::ComputeMD5String(const std::string& str)
{
if (str.empty())
return {};
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::string result;
if (AcquireCryptoProvider(hProv))
{
if (DoMD5Hash(hProv, hHash, reinterpret_cast<const BYTE*>(str.data()), static_cast<DWORD>(str.size())))
{
BYTE hash[16];
DWORD hashLen = sizeof(hash);
if (CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLen, 0))
{
result = ToHexString(hash, hashLen);
}
CryptDestroyHash(hHash);
}
CryptReleaseContext(hProv, 0);
}
return result;
}
bool Hash::CompareFiles(const std::string& path1, const std::string& path2)
{
// 快速路径:先比较大小
const int64_t size1 = GetFileSize(path1);
const int64_t size2 = GetFileSize(path2);
if (size1 < 0 || size2 < 0)
{
printf("[Hash] > err: cannot stat one or both files for comparison\n");
return false;
}
if (size1 != size2)
{
printf("[Hash] > files differ: size mismatch (%lld vs %lld)\n", size1, size2);
return false;
}
// 大小相同,比较 MD5
const std::string hash1 = ComputeMD5(path1);
const std::string hash2 = ComputeMD5(path2);
if (hash1.empty() || hash2.empty())
{
printf("[Hash] > err: hash computation failed during comparison\n");
return false;
}
const bool same = (hash1 == hash2);
printf("[Hash] > compare: %s (%s vs %s)\n", same ? "MATCH" : "DIFFER", hash1.c_str(), hash2.c_str());
return same;
}
std::string Hash::ComputeSHA256String(const std::string& str)
{
if (str.empty())
return {};
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::string result;
if (!CryptAcquireContextA(&hProv, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT))
{
printf("[Hash] SHA256: CryptAcquireContext failed (0x%lx)\n", GetLastError());
return {};
}
if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash))
{
printf("[Hash] SHA256: CryptCreateHash failed (0x%lx)\n", GetLastError());
CryptReleaseContext(hProv, 0);
return {};
}
if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(str.data()), static_cast<DWORD>(str.size()), 0))
{
printf("[Hash] SHA256: CryptHashData failed (0x%lx)\n", GetLastError());
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return {};
}
BYTE hash[32];
DWORD hashLen = sizeof(hash);
if (CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLen, 0))
{
char hex[65];
for (int i = 0; i < 32; ++i)
snprintf(hex + i * 2, 3, "%02x", hash[i]);
result.assign(hex, 64);
}
else
{
printf("[Hash] SHA256: CryptGetHashParam failed (0x%lx)\n", GetLastError());
}
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return result;
}
bool Hash::CompareFileWithMemory(const std::string& filePath, const std::vector<uint8_t>& memData)
{
// 快速路径:先比较大小
const int64_t fileSize = GetFileSize(filePath);
if (fileSize < 0)
{
printf("[Hash] > err: cannot stat file for memory comparison: %s\n", filePath.c_str());
return false;
}
if (static_cast<size_t>(fileSize) != memData.size())
{
printf("[Hash] > file vs memory: size mismatch (%lld vs %zu)\n", fileSize, memData.size());
return false;
}
// 大小相同,比较 MD5
const std::string fileHash = ComputeMD5(filePath);
const std::string memHash = ComputeMD5(memData);
if (fileHash.empty() || memHash.empty())
{
printf("[Hash] > err: hash computation failed during file/memory comparison\n");
return false;
}
const bool same = (fileHash == memHash);
printf("[Hash] > file vs memory: %s (%s vs %s)\n", same ? "MATCH" : "DIFFER", fileHash.c_str(), memHash.c_str());
return same;
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
class Hash
{
public:
/// <summary>
/// 计算文件的 MD5 哈希,返回 32 字符的十六进制字符串。
/// 失败返回空字符串。
/// </summary>
static std::string ComputeMD5(const std::string& filePath);
/// <summary>
/// 计算字节数据的 MD5 哈希。
/// </summary>
static std::string ComputeMD5(const std::vector<uint8_t>& data);
/// <summary>
/// 计算字符串的 MD5 哈希。
/// </summary>
static std::string ComputeMD5String(const std::string& str);
/// <summary>
/// 计算字符串的 SHA-256 哈希,返回 64 字符的十六进制字符串。
/// </summary>
static std::string ComputeSHA256String(const std::string& str);
/// <summary>
/// 比较两个文件是否相同(先比大小,再比 MD5)。
/// </summary>
static bool CompareFiles(const std::string& path1, const std::string& path2);
/// <summary>
/// 比较磁盘文件与内存数据是否相同(先比大小,再比 MD5)。
/// </summary>
static bool CompareFileWithMemory(const std::string& filePath, const std::vector<uint8_t>& memData);
/// <summary>
/// 获取文件大小,失败返回 -1。
/// </summary>
static int64_t GetFileSize(const std::string& filePath);
private:
Hash() = default;
~Hash() = default;
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,197 @@
#include "Injector.h"
#include "SkeetInjector.h"
#include <windows.h>
#include <cstdio>
#include <thread>
#include <chrono>
// ─── 单例 ──────────────────────────────────────────────────────
injector* injector::Get()
{
static injector instance;
return &instance;
}
injector::~injector()
{
if (m_thread.joinable())
m_thread.join();
}
// ─── 公开接口 ─────────────────────────────────────────────────
bool injector::Launch()
{
if (m_running)
{
printf("[Injector] > warn: injection already in progress\n");
return false;
}
m_running = true;
m_success = false;
m_thread = std::thread(&injector::InjectionThread, this);
printf("[Injector] > injection thread launched\n");
return true;
}
bool injector::IsRunning() const
{
return m_running;
}
bool injector::HasSucceeded() const
{
return m_success;
}
void injector::Wait()
{
if (m_thread.joinable())
m_thread.join();
}
// ─── 内部实现 ─────────────────────────────────────────────────
std::string injector::WriteInjectorToTemp()
{
char tempPath[MAX_PATH];
DWORD len = GetTempPathA(sizeof(tempPath), tempPath);
if (len == 0 || len > sizeof(tempPath))
{
printf("[Injector] > err: GetTempPathA failed (0x%lx)\n", GetLastError());
return {};
}
std::string exePath = std::string(tempPath) + "SkeetInjector.exe";
HANDLE hFile = CreateFileA(
exePath.c_str(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("[Injector] > err: cannot create %s (0x%lx)\n", exePath.c_str(), GetLastError());
return {};
}
DWORD written = 0;
BOOL ok = WriteFile(hFile, SkeetInjector_exe, SkeetInjector_exe_len, &written, nullptr);
CloseHandle(hFile);
if (!ok || written != SkeetInjector_exe_len)
{
printf("[Injector] > err: write failed for %s (wrote %lu/%u)\n",
exePath.c_str(), written, SkeetInjector_exe_len);
DeleteFileA(exePath.c_str());
return {};
}
printf("[Injector] > written: %s (%u bytes)\n", exePath.c_str(), SkeetInjector_exe_len);
return exePath;
}
bool injector::RunInjector(const std::string& exePath)
{
// 使用 ShellExecuteExA + runas 以管理员权限启动(UAC 提权)
SHELLEXECUTEINFOA sei = {};
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
sei.hwnd = nullptr;
sei.lpVerb = "runas"; // 触发 UAC 管理员提权
sei.lpFile = exePath.c_str();
sei.lpParameters = nullptr;
sei.lpDirectory = nullptr;
sei.nShow = SW_HIDE;
printf("[Injector] > executing (runas): %s\n", exePath.c_str());
if (!ShellExecuteExA(&sei))
{
DWORD err = GetLastError();
if (err == ERROR_CANCELLED)
printf("[Injector] > err: user cancelled UAC prompt\n");
else
printf("[Injector] > err: ShellExecuteExA failed (0x%lx)\n", err);
return false;
}
// 等待注入器完成
if (sei.hProcess)
{
printf("[Injector] > process started (pid: %lu), waiting...\n", GetProcessId(sei.hProcess));
WaitForSingleObject(sei.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(sei.hProcess, &exitCode);
CloseHandle(sei.hProcess);
printf("[Injector] > process exited with code: %lu\n", exitCode);
}
return true;
}
void injector::CleanupTempFile(const std::string& exePath)
{
if (exePath.empty())
return;
// 短暂延迟,确保进程完全释放文件句柄
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (DeleteFileA(exePath.c_str()))
{
printf("[Injector] > cleaned up: %s\n", exePath.c_str());
}
else
{
// 可能仍在使用中,标记为重启后删除
MoveFileExA(exePath.c_str(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);
printf("[Injector] > cleanup deferred (reboot): %s\n", exePath.c_str());
}
}
// ─── 注入线程 ─────────────────────────────────────────────────
void injector::InjectionThread()
{
printf("[Injector] > injection thread started\n");
// 1. 将嵌入的 SkeetInjector.exe 写入临时目录
std::string exePath = WriteInjectorToTemp();
if (exePath.empty())
{
printf("[Injector] > err: failed to write injector to temp\n");
m_running = false;
return;
}
// 2. 执行注入器
bool ran = RunInjector(exePath);
if (!ran)
{
printf("[Injector] > err: injector execution failed\n");
CleanupTempFile(exePath);
m_running = false;
return;
}
// 3. 清理临时文件
CleanupTempFile(exePath);
// 4. 标记完成
m_success = true;
m_running = false;
printf("[Injector] > injection thread completed successfully\n");
}
@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <thread>
#include <atomic>
class injector
{
public:
static injector* Get();
/// <summary>
/// 在新线程中启动注入流程:
/// 1. 将嵌入的 SkeetInjector.exe 写入临时目录
/// 2. 执行注入器
/// 3. 清理临时文件
/// </summary>
bool Launch();
/// <summary>
/// 注入线程是否仍在运行
/// </summary>
bool IsRunning() const;
/// <summary>
/// 注入是否已成功完成
/// </summary>
bool HasSucceeded() const;
/// <summary>
/// 等待注入线程结束(阻塞调用)
/// </summary>
void Wait();
private:
injector() = default;
~injector();
injector(const injector&) = delete;
injector& operator=(const injector&) = delete;
/// <summary>
/// 注入线程入口:写文件 → 执行 → 等待完成 → 清理
/// </summary>
void InjectionThread();
std::string WriteInjectorToTemp();
bool RunInjector(const std::string& exePath);
void CleanupTempFile(const std::string& exePath);
std::thread m_thread;
std::atomic<bool> m_running = false;
std::atomic<bool> m_success = false;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,92 @@
#include "JWTDecoder.h"
#include "json/json.hpp"
#include <ctime>
#include <vector>
#include <cstdio>
using json = nlohmann::json;
// ─── Base64Url Decode ─────────────────────────────────
static const std::string BASE64_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static std::vector<unsigned char> Base64Decode(const std::string& input)
{
std::string s = input;
// Convert URL-safe to standard Base64
for (char& c : s) {
if (c == '-') c = '+';
if (c == '_') c = '/';
}
// Pad
while (s.size() % 4 != 0)
s += '=';
std::vector<unsigned char> result;
int i = 0;
int val = 0;
int valb = -8;
for (char c : s) {
if (c == '=') break;
size_t pos = BASE64_CHARS.find(c);
if (pos == std::string::npos) continue;
val = (val << 6) + static_cast<int>(pos);
valb += 6;
if (valb >= 0) {
result.push_back(static_cast<unsigned char>((val >> valb) & 0xFF));
valb -= 8;
}
}
return result;
}
std::string JWTDecoder::Base64UrlDecode(const std::string& input)
{
auto bytes = Base64Decode(input);
return std::string(bytes.begin(), bytes.end());
}
// ─── Parse Payload ────────────────────────────────────
JWTDecoder::Payload JWTDecoder::ParsePayload(const std::string& jsonStr)
{
Payload p;
try {
auto j = json::parse(jsonStr);
if (j.contains("sub")) p.sub = j["sub"].get<int64_t>();
if (j.contains("username")) p.username = j["username"].get<std::string>();
if (j.contains("role")) p.role = j["role"].get<std::string>();
if (j.contains("device_id")) p.device_id = j["device_id"].get<int64_t>();
if (j.contains("iat")) p.iat = j["iat"].get<int64_t>();
if (j.contains("exp")) p.exp = j["exp"].get<int64_t>();
p.valid = true;
} catch (const std::exception& e) {
printf("[JWTDecoder] parse error: %s\n", e.what());
p.valid = false;
}
return p;
}
// ─── Public API ───────────────────────────────────────
JWTDecoder::Payload JWTDecoder::Decode(const std::string& token)
{
Payload p;
// Split header.payload.signature
size_t dot1 = token.find('.');
if (dot1 == std::string::npos) return p;
size_t dot2 = token.find('.', dot1 + 1);
if (dot2 == std::string::npos) return p;
std::string payloadEncoded = token.substr(dot1 + 1, dot2 - dot1 - 1);
std::string payloadJson = Base64UrlDecode(payloadEncoded);
return ParsePayload(payloadJson);
}
bool JWTDecoder::IsExpired(int64_t expUnix)
{
return static_cast<int64_t>(std::time(nullptr)) > expUnix;
}
@@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <cstdint>
/**
* JWTDecoder — lightweight JWT payload decoder.
* Client-side ONLY: decodes the payload and checks exp.
* Does NOT verify the signature — that's the server's responsibility.
* The client NEVER has access to the JWT secret/private key.
*/
class JWTDecoder
{
public:
struct Payload {
int64_t sub = 0; // user_id
std::string username;
std::string role;
int64_t device_id = 0;
int64_t iat = 0;
int64_t exp = 0; // expiration timestamp (unix seconds)
bool valid = false;
};
/// Decode a JWT token (without signature verification).
/// Only extracts and validates the payload.
static Payload Decode(const std::string& token);
/// Check if a unix timestamp has expired.
static bool IsExpired(int64_t expUnix);
private:
JWTDecoder() = default;
static std::string Base64UrlDecode(const std::string& input);
static Payload ParsePayload(const std::string& json);
};
@@ -0,0 +1,140 @@
#include "CertPinner.h"
#include <wincrypt.h>
#include <cstdio>
#include <cstring>
#include <vector>
#pragma comment(lib, "crypt32.lib")
// ─── XOR-obfuscated pinned SPKI SHA-256 hash ──────────
// For development/testing, set to all-zeros to skip pinning.
// In production, replace with the actual SPKI hash of your server's public key.
//
// To generate: openssl x509 -in cert.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256
static const unsigned char OBFUSCATED_PIN[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char XOR_KEY = 0x00; // dev mode: XOR with 0 = no change
std::string CertPinner::GetPinnedHash()
{
std::string result(sizeof(OBFUSCATED_PIN), '\0');
for (size_t i = 0; i < sizeof(OBFUSCATED_PIN); ++i)
result[i] = OBFUSCATED_PIN[i] ^ XOR_KEY;
return result;
}
// ─── Public Key Hash Extraction ───────────────────────
std::string CertPinner::GetSpkiHash(PCCERT_CONTEXT pCertContext)
{
if (!pCertContext)
return {};
// Get the SubjectPublicKeyInfo from the certificate
PCERT_PUBLIC_KEY_INFO pKeyInfo = &pCertContext->pCertInfo->SubjectPublicKeyInfo;
// Hash the raw public key bytes with SHA-256
// Note: For proper SPKI pinning you'd encode the full SPKI (algorithm + key) as DER.
// For production, use: CryptEncodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO, ...)
// then hash the DER output. See OpenSSL command in the header comment.
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::string result;
if (CryptAcquireContextA(&hProv, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT))
{
if (CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash))
{
if (CryptHashData(hHash, pKeyInfo->PublicKey.pbData,
pKeyInfo->PublicKey.cbData, 0))
{
BYTE hash[32];
DWORD hashLen = sizeof(hash);
if (CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLen, 0))
{
result.assign(reinterpret_cast<char*>(hash), hashLen);
}
}
CryptDestroyHash(hHash);
}
CryptReleaseContext(hProv, 0);
}
return result;
}
// ─── Public API ───────────────────────────────────────
bool CertPinner::VerifyServerCert(HINTERNET hRequest)
{
const std::string pinned = GetPinnedHash();
// Check if all zeros — development mode, skip pinning
bool devMode = true;
for (char c : pinned) {
if (c != '\0') { devMode = false; break; }
}
if (devMode) {
printf("[CertPinner] DEV MODE — pinning skipped (all-zeros hash)\n");
return true;
}
// Retrieve server certificate from WinHTTP
PCCERT_CONTEXT pCertContext = nullptr;
DWORD certSize = sizeof(pCertContext);
if (!WinHttpQueryOption(
hRequest,
WINHTTP_OPTION_SERVER_CERT_CONTEXT,
&pCertContext,
&certSize))
{
printf("[CertPinner] WinHttpQueryOption(SERVER_CERT_CONTEXT) failed: 0x%lx\n", GetLastError());
return false;
}
// Verify certificate chain
CERT_CHAIN_PARA chainPara = {};
chainPara.cbSize = sizeof(chainPara);
PCCERT_CHAIN_CONTEXT pChainContext = nullptr;
if (!CertGetCertificateChain(
nullptr,
pCertContext,
nullptr,
pCertContext->hCertStore,
&chainPara,
0,
nullptr,
&pChainContext))
{
printf("[CertPinner] CertGetCertificateChain failed: 0x%lx\n", GetLastError());
CertFreeCertificateContext(pCertContext);
return false;
}
// Hash the server's public key
std::string serverHash = GetSpkiHash(pCertContext);
CertFreeCertificateChain(pChainContext);
CertFreeCertificateContext(pCertContext);
if (serverHash.empty())
{
printf("[CertPinner] ERROR: failed to hash server public key\n");
return false;
}
// Compare
if (serverHash == pinned)
{
printf("[CertPinner] SPKI hash MATCH — connection trusted\n");
return true;
}
printf("[CertPinner] SPKI hash MISMATCH — possible MITM detected!\n");
return false;
}
@@ -0,0 +1,32 @@
#pragma once
#include <windows.h>
#include <winhttp.h>
#include <string>
#include <vector>
#pragma comment(lib, "crypt32.lib")
/**
* SPKI SHA-256 Certificate Pinner — pure client-side.
* Prevents MITM proxies (Fiddler/Charles/Burp) from intercepting HTTPS traffic
* by verifying the server's public key matches a hardcoded hash.
*
* Pin format: SHA-256 of the SPKI (Subject Public Key Info) DER encoding.
* This survives certificate renewal as long as the same key pair is used.
*/
class CertPinner
{
public:
/// Verify the server certificate presented by an active WinHTTP request.
/// @return true if SPKI hash matches the pinned value, false (connection should be closed)
static bool VerifyServerCert(HINTERNET hRequest);
private:
CertPinner() = default;
/// Extract the SPKI (SubjectPublicKeyInfo) from a certificate context and hash it.
static std::string GetSpkiHash(PCCERT_CONTEXT pCertContext);
/// Returns the XOR-obfuscated pinned SPKI SHA-256 hash, deobfuscated at runtime.
static std::string GetPinnedHash();
};
@@ -0,0 +1,371 @@
#include "NetworkClient.h"
#include "CertPinner.h"
#include "json/json.hpp"
#include <cstdio>
#include <cstring>
#include <sstream>
using json = nlohmann::json;
// ─── Singleton ────────────────────────────────────────
NetworkClient* NetworkClient::Get()
{
static NetworkClient instance;
return &instance;
}
// ─── Connect / Close ──────────────────────────────────
HINTERNET NetworkClient::Connect()
{
HINTERNET hSession = WinHttpOpen(
L"skeet_loader/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (!hSession)
{
printf("[NetworkClient] WinHttpOpen failed: 0x%lx\n", GetLastError());
return nullptr;
}
// Set timeouts
WinHttpSetTimeouts(hSession, TIMEOUT_MS, TIMEOUT_MS, TIMEOUT_MS, TIMEOUT_MS);
HINTERNET hConnect = WinHttpConnect(hSession, SERVER_HOST, SERVER_PORT, 0);
if (!hConnect)
{
printf("[NetworkClient] WinHttpConnect failed: 0x%lx\n", GetLastError());
WinHttpCloseHandle(hSession);
return nullptr;
}
return hConnect;
}
void NetworkClient::Close(HINTERNET hSession, HINTERNET hConnect)
{
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);
}
// ─── Send Request ─────────────────────────────────────
NetworkClient::HttpResponse NetworkClient::SendRequest(
const std::wstring& endpoint,
const std::string& jsonBody,
const std::string& bearerToken)
{
HttpResponse resp;
HINTERNET hSession = nullptr;
HINTERNET hConnect = nullptr;
HINTERNET hRequest = nullptr;
DWORD flags = 0;
DWORD statusCode = 0;
DWORD statusCodeSize = 0;
DWORD bytesRead = 0;
LPCWSTR headers = nullptr;
hSession = WinHttpOpen(
L"skeet_loader/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (!hSession) { printf("[NetworkClient] WinHttpOpen failed\n"); return resp; }
WinHttpSetTimeouts(hSession, TIMEOUT_MS, TIMEOUT_MS, TIMEOUT_MS, TIMEOUT_MS);
hConnect = WinHttpConnect(hSession, SERVER_HOST, SERVER_PORT, 0);
if (!hConnect) { printf("[NetworkClient] WinHttpConnect failed\n"); goto cleanup; }
flags = SERVER_USE_SSL ? WINHTTP_FLAG_SECURE : 0;
hRequest = WinHttpOpenRequest(
hConnect,
L"POST",
endpoint.c_str(),
nullptr,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
flags);
if (!hRequest) { printf("[NetworkClient] WinHttpOpenRequest failed\n"); goto cleanup; }
// Set content type + optional bearer token
headers = L"Content-Type: application/json\r\n";
if (!bearerToken.empty())
{
std::string authHeader = "Authorization: Bearer " + bearerToken + "\r\n";
std::wstring wAuthHeader(authHeader.begin(), authHeader.end());
std::wstring allHeaders = std::wstring(headers) + wAuthHeader;
WinHttpAddRequestHeaders(hRequest, allHeaders.c_str(),
(DWORD)allHeaders.length(), WINHTTP_ADDREQ_FLAG_ADD);
}
else
{
WinHttpAddRequestHeaders(hRequest, headers, (DWORD)wcslen(headers),
WINHTTP_ADDREQ_FLAG_ADD);
}
// Send
if (!WinHttpSendRequest(
hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
(LPVOID)jsonBody.c_str(), (DWORD)jsonBody.size(),
(DWORD)jsonBody.size(), 0))
{
DWORD err = GetLastError();
printf("[NetworkClient] WinHttpSendRequest failed: 0x%lx\n", err);
if (err == ERROR_WINHTTP_SECURE_FAILURE)
printf("[NetworkClient] > SSL error (cert pinning or TLS issue)\n");
goto cleanup;
}
// Receive response
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
printf("[NetworkClient] WinHttpReceiveResponse failed: 0x%lx\n", GetLastError());
goto cleanup;
}
// Cert pinning check (only for HTTPS)
if (SERVER_USE_SSL)
{
if (!CertPinner::VerifyServerCert(hRequest))
{
printf("[NetworkClient] CertPinner rejected connection!\n");
resp.statusCode = 0;
goto cleanup;
}
}
// Get status code
statusCode = 0;
statusCodeSize = sizeof(statusCode);
WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX);
resp.statusCode = statusCode;
// Read response body
bytesRead = 0;
char buffer[4096];
while (WinHttpReadData(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0)
{
buffer[bytesRead] = '\0';
resp.body += buffer;
}
cleanup:
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);
return resp;
}
// ─── Send GET Request (for /api/status) ───────────────
static std::pair<DWORD, std::string> SendGetRequest(const std::wstring& endpoint)
{
DWORD statusCode = 0;
std::string body;
HINTERNET hSession = WinHttpOpen(
L"skeet_loader/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) return { 0, "" };
WinHttpSetTimeouts(hSession, 10000, 10000, 10000, 10000);
HINTERNET hConnect = WinHttpConnect(hSession, SERVER_HOST, SERVER_PORT, 0);
if (!hConnect) { WinHttpCloseHandle(hSession); return { 0, "" }; }
DWORD flags = SERVER_USE_SSL ? WINHTTP_FLAG_SECURE : 0;
HINTERNET hRequest = WinHttpOpenRequest(
hConnect, L"GET", endpoint.c_str(),
nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return { 0, "" }; }
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
{
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return { 0, "" };
}
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return { 0, "" };
}
if (SERVER_USE_SSL) CertPinner::VerifyServerCert(hRequest);
DWORD sc = 0, scSize = sizeof(sc);
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &sc, &scSize, WINHTTP_NO_HEADER_INDEX);
statusCode = sc;
DWORD bytesRead = 0;
char buffer[4096];
while (WinHttpReadData(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0)
{
buffer[bytesRead] = '\0';
body += buffer;
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return { statusCode, body };
}
// ─── JSON Builders ────────────────────────────────────
std::string NetworkClient::BuildLoginBody(
const std::string& username,
const std::string& passwordHash,
const std::string& cpuHash,
const std::string& boardHash,
const std::string& diskHash,
const std::string& macHash)
{
json j;
j["username"] = username;
j["password_hash"] = passwordHash; // SHA-256(password) — raw password never sent
j["hwid_components"]["cpu"] = cpuHash;
j["hwid_components"]["board"] = boardHash;
j["hwid_components"]["disk"] = diskHash;
j["hwid_components"]["mac"] = macHash;
return j.dump();
}
// ─── Public API ───────────────────────────────────────
StatusResult NetworkClient::CheckStatus()
{
StatusResult result;
auto [statusCode, body] = SendGetRequest(L"/api/status");
if (statusCode == 0)
{
result.online = false;
result.errorMessage = "Server unreachable";
return result;
}
try {
auto j = json::parse(body);
if (j.contains("status") && j["status"].get<std::string>() == "ok")
result.online = true;
else
result.errorMessage = "Invalid status response";
} catch (...) {
result.errorMessage = "Failed to parse status response";
}
return result;
}
LoginResult NetworkClient::Login(
const std::string& username,
const std::string& passwordHash,
const std::string& cpuHash,
const std::string& boardHash,
const std::string& diskHash,
const std::string& macHash)
{
LoginResult result;
std::string body = BuildLoginBody(username, passwordHash, cpuHash, boardHash, diskHash, macHash);
auto resp = SendRequest(L"/api/login", body);
if (resp.statusCode == 0)
{
result.errorCode = "NETWORK_ERROR";
result.errorMessage = "Server unreachable";
return result;
}
try {
auto j = json::parse(resp.body);
result.success = j.value("success", false);
if (result.success)
{
result.accessToken = j.value("access_token", "");
result.refreshToken = j.value("refresh_token", "");
result.username = j.value("username", "");
result.matchRate = j.value("match_rate", 0.0);
}
else
{
result.errorCode = j.value("error", "UNKNOWN");
result.errorMessage = j.value("message", "");
result.retryAfter = j.value("retry_after", 0);
}
} catch (const std::exception& e) {
printf("[NetworkClient] login parse error: %s\n", e.what());
result.errorCode = "PARSE_ERROR";
result.errorMessage = "Failed to parse server response";
}
return result;
}
RefreshResult NetworkClient::RefreshToken(
const std::string& refreshToken,
const std::string& cpuHash,
const std::string& boardHash,
const std::string& diskHash,
const std::string& macHash)
{
RefreshResult result;
// Build JSON body
json j;
j["refresh_token"] = refreshToken;
j["hwid_components"]["cpu"] = cpuHash;
j["hwid_components"]["board"] = boardHash;
j["hwid_components"]["disk"] = diskHash;
j["hwid_components"]["mac"] = macHash;
std::string body = j.dump();
auto resp = SendRequest(L"/api/refresh", body);
if (resp.statusCode == 0)
{
result.errorCode = "NETWORK_ERROR";
return result;
}
try {
auto respJson = json::parse(resp.body);
result.success = respJson.value("success", false);
if (result.success)
{
result.accessToken = respJson.value("access_token", "");
result.refreshToken = respJson.value("refresh_token", "");
}
else
{
result.errorCode = respJson.value("error", "UNKNOWN");
}
} catch (const std::exception& e) {
printf("[NetworkClient] refresh parse error: %s\n", e.what());
result.errorCode = "PARSE_ERROR";
}
return result;
}
@@ -0,0 +1,94 @@
#pragma once
#include <string>
#include <vector>
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
// ─── Server URL Configuration ─────────────────────────
// Change SERVER_HOST to your server's IP/domain before building.
#define SERVER_HOST L"202.60.232.225"
#define SERVER_PORT 3000
#define SERVER_USE_SSL FALSE // TRUE when you have HTTPS
// ───────────────────────────────────────────────────────
struct LoginResult {
bool success = false;
std::string accessToken;
std::string refreshToken;
std::string username;
double matchRate = 0.0;
std::string errorCode;
std::string errorMessage;
int retryAfter = 0;
};
struct StatusResult {
bool online = false;
std::string errorMessage;
};
struct RefreshResult {
bool success = false;
std::string accessToken;
std::string refreshToken;
std::string errorCode;
};
/**
* NetworkClient — WinHTTP-based HTTPS client.
* All methods are synchronous and should be called from a detached thread.
* Integrates with CertPinner for SPKI public key pinning.
*/
class NetworkClient
{
public:
static NetworkClient* Get();
/// Check server reachability (GET /api/status)
StatusResult CheckStatus();
/// Login (POST /api/login) — sends SHA-256(password) + HWID components
LoginResult Login(const std::string& username,
const std::string& passwordHash,
const std::string& cpuHash,
const std::string& boardHash,
const std::string& diskHash,
const std::string& macHash);
/// Refresh access token (POST /api/refresh)
RefreshResult RefreshToken(const std::string& refreshToken,
const std::string& cpuHash,
const std::string& boardHash,
const std::string& diskHash,
const std::string& macHash);
private:
NetworkClient() = default;
~NetworkClient() = default;
struct HttpResponse {
DWORD statusCode = 0;
std::string body;
};
/// Open WinHTTP session + connect
HINTERNET Connect();
void Close(HINTERNET hSession, HINTERNET hConnect);
/// Send a POST JSON request and get response
HttpResponse SendRequest(const std::wstring& endpoint,
const std::string& jsonBody,
const std::string& bearerToken = "");
/// Build JSON body for login
std::string BuildLoginBody(const std::string& username,
const std::string& passwordHash,
const std::string& cpuHash,
const std::string& boardHash,
const std::string& diskHash,
const std::string& macHash);
static constexpr int TIMEOUT_MS = 10000;
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+243
View File
@@ -0,0 +1,243 @@
#include "Steam.h"
#include <windows.h>
#include <tlhelp32.h>
#include <cstdio>
#include "../../Menu/Menu.h"
// 嵌入的 Steam 覆盖层文件数据(定义在各 .h 中)
extern unsigned int gameoverlayui_exe_len;
extern unsigned char gameoverlayui_exe[];
extern unsigned int gameoverlayui64_exe_len;
extern unsigned char gameoverlayui64_exe[];
extern unsigned int GameOverlayRenderer_dll_len;
extern unsigned char GameOverlayRenderer_dll[];
extern unsigned int GameOverlayRenderer64_dll_len;
extern unsigned char GameOverlayRenderer64_dll[];
// 单例
Steam* Steam::Get()
{
static Steam instance;
return &instance;
}
Steam::Steam() = default;
Steam::~Steam() = default;
const std::string& Steam::GetSteamPath() const {
// 每次调用都从注册表查询 Steam 安装路径
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Valve\\Steam", 0, KEY_READ | KEY_WOW64_32KEY, &hKey) == ERROR_SUCCESS) {
char path[MAX_PATH];
DWORD size = sizeof(path);
if (RegQueryValueExA(hKey, "InstallPath", nullptr, nullptr,
reinterpret_cast<BYTE*>(path), &size) == ERROR_SUCCESS)
{
m_SteamPath = path;
RegCloseKey(hKey);
printf("[Steam] > install path: %s\n", m_SteamPath.c_str());
return m_SteamPath;
}
RegCloseKey(hKey);
}
// 回退:当前用户注册表
if (RegOpenKeyExA(HKEY_CURRENT_USER,
"SOFTWARE\\Valve\\Steam", 0,
KEY_READ | KEY_WOW64_32KEY, &hKey) == ERROR_SUCCESS)
{
char path[MAX_PATH];
DWORD size = sizeof(path);
if (RegQueryValueExA(hKey, "SteamPath", nullptr, nullptr,
reinterpret_cast<BYTE*>(path), &size) == ERROR_SUCCESS)
{
m_SteamPath = path;
RegCloseKey(hKey);
printf("[Steam] > install path: %s\n", m_SteamPath.c_str());
return m_SteamPath;
}
RegCloseKey(hKey);
}
m_SteamPath.clear();
printf("[Steam] > warn: could not find steam install path\n");
return m_SteamPath;
}
bool Steam::IsProcessRunning(const wchar_t* exeName) const {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
return false;
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(pe32);
bool found = false;
if (Process32FirstW(hSnapshot, &pe32))
{
do
{
if (_wcsicmp(pe32.szExeFile, exeName) == 0)
{
found = true;
break;
}
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return found;
}
bool Steam::TerminateProcessByName(const wchar_t* exeName) const {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
return false;
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(pe32);
bool killed = false;
if (Process32FirstW(hSnapshot, &pe32))
{
do
{
if (_wcsicmp(pe32.szExeFile, exeName) == 0)
{
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID);
if (hProcess)
{
TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
killed = true;
}
}
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return killed;
}
bool Steam::IsSteamRunning() {
return IsProcessRunning(L"steam.exe");
}
bool Steam::StartSteam() {
const std::string& steamPath = GetSteamPath();
if (steamPath.empty())
{
printf("[Steam] > err: steam path unknown, cannot start\n");
return false;
}
std::string exePath = steamPath + "\\steam.exe";
HINSTANCE result = ShellExecuteA(
nullptr, "open", exePath.c_str(), nullptr, nullptr, SW_SHOW
);
if (reinterpret_cast<INT_PTR>(result) > 32)
{
printf("[Steam] > started: %s\n", exePath.c_str());
return true;
}
printf("[Steam] > err: failed to start (code %d)\n",
static_cast<int>(reinterpret_cast<INT_PTR>(result)));
return false;
}
bool Steam::KillSteam() {
auto Log = Menu::Get();
if (!IsSteamRunning())
{
printf("[Steam] > steam not running\n");
Log->AddLog("[Steam] > steam not running");
return false;
}
if (TerminateProcessByName(L"steam.exe"))
{
printf("[Steam] > steam killed\n");
Log->AddLog("[Steam] > steam killed");
return true;
}
printf("[Steam] > err: failed to kill steam\n");
Log->AddLog("[Steam] > err: failed to kill steam");
return false;
}
// ─── 写入嵌入文件到磁盘的辅助函数 ────────────────────────────
static bool WriteEmbeddedFile(const std::string& dir, const char* name, const file& f)
{
std::string fullPath = dir + "\\" + name;
HANDLE hFile = CreateFileA(
fullPath.c_str(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("[Steam] > err: cannot create file %s (0x%lx)\n", fullPath.c_str(), GetLastError());
return false;
}
DWORD written = 0;
const BOOL ok = WriteFile(hFile, f.file, f.len, &written, nullptr);
CloseHandle(hFile);
if (!ok || written != f.len)
{
printf("[Steam] > err: write failed for %s (wrote %lu/%u)\n", fullPath.c_str(), written, f.len);
return false;
}
printf("[Steam] > replaced: %s (%u bytes)\n", fullPath.c_str(), f.len);
return true;
}
// ─── 替换 Steam 覆盖层文件 ──────────────────────────────────
bool Steam::ReplaceOverlayFiles()
{
auto Log = Menu::Get();
const std::string& steamPath = GetSteamPath();
if (steamPath.empty())
{
printf("[Steam] > err: steam path unknown, cannot replace overlay files\n");
Log->AddLog("[Steam] > err: steam path unknown, cannot replace overlay files");
return false;
}
printf("[Steam] > replacing overlay files in: %s\n", steamPath.c_str());
bool allOk = true;
allOk &= WriteEmbeddedFile(steamPath, "gameoverlayui.exe", gameoverlayui());
allOk &= WriteEmbeddedFile(steamPath, "GameOverlayRenderer.dll", GameOverlayRenderer());
allOk &= WriteEmbeddedFile(steamPath, "gameoverlayui64.exe", gameoverlayui64());
allOk &= WriteEmbeddedFile(steamPath, "GameOverlayRenderer64.dll", GameOverlayRenderer64());
if (allOk) {
printf("[Steam] > all overlay files replaced successfully\n");
Log->AddLog("[Steam] > all overlay files replaced successfully");
}
else {
printf("[Steam] > warn: some overlay files failed to replace\n");
Log->AddLog("[Steam] > warn: some overlay files failed to replace");
}
return allOk;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <string>
struct file {
unsigned char* file;
unsigned int len;
const char* suffix;
};
class Steam
{
public:
static Steam* Get();
bool StartSteam();
bool IsSteamRunning();
bool KillSteam();
const std::string& GetSteamPath() const;
file gameoverlayui();
file gameoverlayui64();
file GameOverlayRenderer();
file GameOverlayRenderer64();
bool ReplaceOverlayFiles();
private:
Steam();
~Steam();
Steam(const Steam&) = delete;
Steam& operator=(const Steam&) = delete;
bool IsProcessRunning(const wchar_t* exeName) const;
bool TerminateProcessByName(const wchar_t* exeName) const;
mutable std::string m_SteamPath;
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,160 @@
#include "TokenStore.h"
#include <shlobj.h>
#include <cstdio>
#include <cstring>
#include <vector>
#include <fstream>
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "shell32.lib")
// ─── Singleton ────────────────────────────────────────
TokenStore* TokenStore::Get()
{
static TokenStore instance;
return &instance;
}
// ─── Storage Path ─────────────────────────────────────
std::string TokenStore::GetStoragePath()
{
char appData[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, appData)))
{
std::string dir = std::string(appData) + "\\skeet_loader";
CreateDirectoryA(dir.c_str(), nullptr);
return dir + "\\.session";
}
// Fallback: current directory
return ".session";
}
// ─── DPAPI Encrypt ────────────────────────────────────
std::vector<BYTE> TokenStore::Encrypt(const std::vector<BYTE>& plaintext)
{
DATA_BLOB in, out;
in.pbData = const_cast<BYTE*>(plaintext.data());
in.cbData = static_cast<DWORD>(plaintext.size());
if (!CryptProtectData(&in, L"skeet_loader_session", nullptr, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN | CRYPTPROTECT_LOCAL_MACHINE, &out))
{
printf("[TokenStore] CryptProtectData failed: 0x%lx\n", GetLastError());
return {};
}
std::vector<BYTE> result(out.pbData, out.pbData + out.cbData);
LocalFree(out.pbData);
return result;
}
// ─── DPAPI Decrypt ────────────────────────────────────
std::vector<BYTE> TokenStore::Decrypt(const std::vector<BYTE>& ciphertext)
{
DATA_BLOB in, out;
in.pbData = const_cast<BYTE*>(ciphertext.data());
in.cbData = static_cast<DWORD>(ciphertext.size());
if (!CryptUnprotectData(&in, nullptr, nullptr, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &out))
{
printf("[TokenStore] CryptUnprotectData failed: 0x%lx\n", GetLastError());
return {};
}
std::vector<BYTE> result(out.pbData, out.pbData + out.cbData);
LocalFree(out.pbData);
return result;
}
// ─── Public API ───────────────────────────────────────
bool TokenStore::Save(const std::string& refreshToken, const std::string& username)
{
// Build simple JSON
std::string json = "{\"refresh_token\":\"" + refreshToken + "\",\"username\":\"" + username + "\"}";
std::vector<BYTE> plaintext(json.begin(), json.end());
auto encrypted = Encrypt(plaintext);
if (encrypted.empty())
{
printf("[TokenStore] Save: encryption failed\n");
return false;
}
std::string path = GetStoragePath();
std::ofstream file(path, std::ios::binary);
if (!file)
{
printf("[TokenStore] Save: cannot open %s\n", path.c_str());
return false;
}
file.write(reinterpret_cast<const char*>(encrypted.data()), encrypted.size());
file.close();
printf("[TokenStore] Saved session to %s (%zu bytes encrypted)\n", path.c_str(), encrypted.size());
return true;
}
TokenStore::SessionData TokenStore::Load()
{
SessionData data;
std::string path = GetStoragePath();
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file)
{
printf("[TokenStore] Load: no saved session at %s\n", path.c_str());
return data;
}
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<BYTE> encrypted(size);
file.read(reinterpret_cast<char*>(encrypted.data()), size);
file.close();
auto decrypted = Decrypt(encrypted);
if (decrypted.empty())
{
printf("[TokenStore] Load: decryption failed\n");
// Clean up corrupted file
Clear();
return data;
}
std::string json(decrypted.begin(), decrypted.end());
printf("[TokenStore] Load: decrypted %zu bytes\n", decrypted.size());
// Simple JSON parsing (no dependency needed)
auto extract = [&json](const std::string& key) -> std::string {
std::string search = "\"" + key + "\":\"";
size_t start = json.find(search);
if (start == std::string::npos) return {};
start += search.length();
size_t end = json.find("\"", start);
if (end == std::string::npos) return {};
return json.substr(start, end - start);
};
data.refreshToken = extract("refresh_token");
data.username = extract("username");
data.valid = !data.refreshToken.empty();
printf("[TokenStore] Load: username=%s valid=%d\n", data.username.c_str(), data.valid);
return data;
}
void TokenStore::Clear()
{
std::string path = GetStoragePath();
DeleteFileA(path.c_str());
printf("[TokenStore] Cleared session: %s\n", path.c_str());
}
@@ -0,0 +1,48 @@
#pragma once
#include <string>
#include <vector>
#include <windows.h>
#pragma comment(lib, "crypt32.lib")
/**
* TokenStore — DPAPI-encrypted local storage for the refresh token.
*
* The refresh token is stored in %APPDATA%/skeet_loader/.session,
* encrypted with CryptProtectData (DPAPI). This binds the data to:
* - Current Windows user account
* - Current machine hardware
*
* On a different machine or Windows user, DPAPI decryption will fail,
* so the token naturally expires on machine/user change.
*
* Only the refresh_token is persisted. The access_token stays in memory only.
*/
class TokenStore
{
public:
static TokenStore* Get();
struct SessionData {
std::string refreshToken;
std::string username;
bool valid = false;
};
/// Save refresh token + username to encrypted local file
bool Save(const std::string& refreshToken, const std::string& username);
/// Load and decrypt the stored session data
SessionData Load();
/// Delete the stored session file
void Clear();
private:
TokenStore() = default;
~TokenStore() = default;
std::string GetStoragePath();
std::vector<BYTE> Encrypt(const std::vector<BYTE>& plaintext);
std::vector<BYTE> Decrypt(const std::vector<BYTE>& ciphertext);
};
@@ -0,0 +1,27 @@
#include "../UIFramework.h"
#include "../../Backend/Font/FontManager.h"
#include "raylib.h"
bool WidgetManager::Button(const char* label, RLRectangle rect, bool disabled) {
auto ws = BeginWidget(rect);
auto r = Render::Get();
auto Font = Font::Get();
RLDrawRectangleLines(rect.x, rect.y, rect.width, rect.height, { 0, 0, 0, 255 });
RLDrawRectangleLines(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, { 50, 50, 50, 255 });
RLVector2 size = Font->MeasureText(label, 18);
if (ws.pressed) r->Gradient({ rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4 }, RLColor{ 30, 30, 30, 255 }, RLColor{ 20, 20, 20, 255 }, true, true);
else if (ws.hover) r->Gradient({ rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4 }, RLColor{ 39, 39, 39, 255 }, RLColor{ 35, 35, 35, 255 }, true, true);
else r->Gradient({ rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4 }, RLColor{ 34, 34, 34, 255 }, RLColor{ 30, 30, 30, 255 }, true, true);
Font->DrawTextEX(label, { (rect.x + rect.width / 2) - size.x / 2, (rect.y + rect.height / 2) - (size.y / 2) }, 18, disabled ? RLColor{ 128, 128, 128, 255 } :RLColor{ 255, 255, 255, 255 },0);
return ws.released;
}
bool Framework::Button(const char* label, RLRectangle Rect, bool disabled) {
return WidgetManager::Get()->Button(label, Rect, disabled);
}
+11
View File
@@ -0,0 +1,11 @@
#include "../UIFramework.h"
#include "../../Backend/Font/FontManager.h"
void WidgetManager::Label(const char* label, RLVector2 Vec, int FontSize, int spacing) {
auto* font = Font::Get();
font->DrawTextEX(label, Vec, FontSize, RLColor{ 220, 220, 220, 255 }, spacing);
}
void Framework::Label(const char* label, RLVector2 Vec, int FontSize, int spacing) {
WidgetManager::Get()->Label(label, Vec, FontSize, spacing);
}
+381
View File
@@ -0,0 +1,381 @@
#include "../UIFramework.h"
#include "../../Backend/Font/FontManager.h"
#include <algorithm>
#define NOMINMAX
#include <windows.h>
#include <imm.h>
#pragma comment(lib, "imm32.lib")
static void SetImePos(int x, int y) {
HWND hwnd = (HWND)RLGetWindowHandle();
if (!hwnd) return;
HIMC himc = ImmGetContext(hwnd);
if (!himc) return;
COMPOSITIONFORM cf = {};
cf.dwStyle = CFS_POINT;
cf.ptCurrentPos.x = x;
cf.ptCurrentPos.y = y;
ImmSetCompositionWindow(himc, &cf);
CANDIDATEFORM candf = {};
candf.dwIndex = 0;
candf.dwStyle = CFS_CANDIDATEPOS;
candf.ptCurrentPos.x = x;
candf.ptCurrentPos.y = y;
ImmSetCandidateWindow(himc, &candf);
ImmReleaseContext(hwnd, himc);
}
void WidgetManager::UpdateImePosition(int x, int y) {
SetImePos(x, y);
}
bool WidgetManager::Textbox(const char* id, RLRectangle rect, TextboxState* state) {
(void)id;
auto ws = BeginWidget(rect);
auto* font = Font::Get();
const float kPad = 6.0f;
const int kFontSize = 16;
const float kInnerW = rect.width - 2.0f * kPad;
const float kTextStart = rect.x + kPad;
// ── UTF-8 helpers ──────────────────────────────────────
auto prevChar = [&](int off) -> int {
if (off <= 0) return 0;
int i = off - 1;
while (i > 0 && (state->text[i] & 0xC0) == 0x80) i--;
return i;
};
auto nextChar = [&](int off) -> int {
int len = (int)state->text.size();
if (off >= len) return len;
int i = off + 1;
while (i < len && (state->text[i] & 0xC0) == 0x80) i++;
return i;
};
// Convert mouse X (screen space) to byte offset in text
auto charAtX = [&](float mouseX, float scrollOff) -> int {
float x = kTextStart - scrollOff;
int pos = 0;
while (pos < (int)state->text.size()) {
int next = nextChar(pos);
float w = font->MeasureText(state->text.substr(pos, next - pos).c_str(), kFontSize, 2).x;
if (mouseX < x + w * 0.5f) break;
x += w;
pos = next;
}
return pos;
};
// ── Focus ──────────────────────────────────────────────
// Click on textbox → activate; click elsewhere or other widget consumed → deactivate
if (ws.clicked) {
state->active = true;
// Place cursor at click position, start potential selection
float mouseX = (float)RLGetMouseX();
// Compute scroll offset to determine actual text position at click time
float preScrollOff = 0.0f;
{
float preCursorX = kTextStart;
if (state->cursor > 0) {
std::string before = state->text.substr(0, state->cursor);
preCursorX += font->MeasureText(before.c_str(), kFontSize, 2).x;
}
float visibleRight = kTextStart + kInnerW;
if (preCursorX > visibleRight - 4.0f)
preScrollOff = preCursorX - visibleRight + 20.0f;
}
int clickPos = charAtX(mouseX, preScrollOff);
state->cursor = clickPos;
state->selectStart = clickPos;
state->selectEnd = clickPos;
state->selecting = true;
} else if (m_ctx.IsMousePressConsumed() || (RLIsMouseButtonPressed(RL_E_MOUSE_BUTTON_LEFT) && !ws.hover)) {
state->active = false;
state->selecting = false;
state->selectStart = state->selectEnd = state->cursor;
}
// ── Mouse drag selection ───────────────────────────────
if (state->selecting && RLIsMouseButtonDown(RL_E_MOUSE_BUTTON_LEFT)) {
// Need current scroll offset to map mouse X correctly
float curCursorX = kTextStart;
if (state->cursor > 0) {
std::string before = state->text.substr(0, state->cursor);
curCursorX += font->MeasureText(before.c_str(), kFontSize, 2).x;
}
float curScrollOff = 0.0f;
float visibleRight = kTextStart + kInnerW;
if (curCursorX > visibleRight - 4.0f)
curScrollOff = curCursorX - visibleRight + 20.0f;
float mouseX = (float)RLGetMouseX();
int dragPos = charAtX(mouseX, curScrollOff);
state->cursor = dragPos;
state->selectEnd = dragPos;
}
if (RLIsMouseButtonReleased(RL_E_MOUSE_BUTTON_LEFT))
state->selecting = false;
// ── Keyboard ───────────────────────────────────────────
bool changed = false;
bool interacted = false;
if (state->active) {
int selMin = std::min(state->selectStart, state->selectEnd);
int selMax = std::max(state->selectStart, state->selectEnd);
bool hasSel = (selMin != selMax);
bool shift = RLIsKeyDown(RL_E_KEY_LEFT_SHIFT) || RLIsKeyDown(RL_E_KEY_RIGHT_SHIFT);
// ── Character input ───────────────────────────────
int cp;
while ((cp = RLGetCharPressed()) > 0) {
if (cp < 32) continue;
// Replace selection if any
if (hasSel) {
state->text.erase(selMin, selMax - selMin);
state->cursor = selMin;
state->selectStart = state->selectEnd = selMin;
selMin = selMax = state->cursor;
hasSel = false;
}
char utf8[5];
int len = 0;
if (cp < 0x80) {
utf8[len++] = (char)cp;
} else if (cp < 0x800) {
utf8[len++] = (char)(0xC0 | (cp >> 6));
utf8[len++] = (char)(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
utf8[len++] = (char)(0xE0 | (cp >> 12));
utf8[len++] = (char)(0x80 | ((cp >> 6) & 0x3F));
utf8[len++] = (char)(0x80 | (cp & 0x3F));
} else if (cp < 0x110000) {
utf8[len++] = (char)(0xF0 | (cp >> 18));
utf8[len++] = (char)(0x80 | ((cp >> 12) & 0x3F));
utf8[len++] = (char)(0x80 | ((cp >> 6) & 0x3F));
utf8[len++] = (char)(0x80 | (cp & 0x3F));
}
utf8[len] = '\0';
state->text.insert(state->cursor, utf8);
state->cursor += len;
changed = true;
interacted = true;
}
// ── Backspace ─────────────────────────────────────
if (RLIsKeyPressed(RL_E_KEY_BACKSPACE) || RLIsKeyPressedRepeat(RL_E_KEY_BACKSPACE)) {
if (hasSel) {
state->text.erase(selMin, selMax - selMin);
state->cursor = selMin;
state->selectStart = state->selectEnd = selMin;
hasSel = false;
changed = true;
interacted = true;
} else if (state->cursor > 0) {
int prev = prevChar(state->cursor);
state->text.erase(prev, state->cursor - prev);
state->cursor = prev;
state->selectStart = state->selectEnd = prev;
changed = true;
interacted = true;
}
}
// ── Delete ───────────────────────────────────────
if (RLIsKeyPressed(RL_E_KEY_DELETE) || RLIsKeyPressedRepeat(RL_E_KEY_DELETE)) {
if (hasSel) {
state->text.erase(selMin, selMax - selMin);
state->cursor = selMin;
state->selectStart = state->selectEnd = selMin;
hasSel = false;
changed = true;
interacted = true;
} else if (state->cursor < (int)state->text.size()) {
int nxt = nextChar(state->cursor);
state->text.erase(state->cursor, nxt - state->cursor);
changed = true;
interacted = true;
}
}
// ── Arrow keys ───────────────────────────────────
if (RLIsKeyPressed(RL_E_KEY_LEFT) || RLIsKeyPressedRepeat(RL_E_KEY_LEFT)) {
int prev = prevChar(state->cursor);
state->cursor = prev;
if (shift) state->selectEnd = prev;
else state->selectStart = state->selectEnd = prev;
interacted = true;
}
if (RLIsKeyPressed(RL_E_KEY_RIGHT) || RLIsKeyPressedRepeat(RL_E_KEY_RIGHT)) {
int nxt = nextChar(state->cursor);
state->cursor = nxt;
if (shift) state->selectEnd = nxt;
else state->selectStart = state->selectEnd = nxt;
interacted = true;
}
// ── Home / End ───────────────────────────────────
if (RLIsKeyPressed(RL_E_KEY_HOME)) {
state->cursor = 0;
if (shift) state->selectEnd = 0;
else state->selectStart = state->selectEnd = 0;
interacted = true;
}
if (RLIsKeyPressed(RL_E_KEY_END)) {
int end = (int)state->text.size();
state->cursor = end;
if (shift) state->selectEnd = end;
else state->selectStart = state->selectEnd = end;
interacted = true;
}
// ── Ctrl shortcuts ───────────────────────────────
bool ctrl = RLIsKeyDown(RL_E_KEY_LEFT_CONTROL) || RLIsKeyDown(RL_E_KEY_RIGHT_CONTROL);
if (ctrl && RLIsKeyPressed(RL_E_KEY_A)) {
// Select all
state->selectStart = 0;
state->selectEnd = (int)state->text.size();
state->cursor = (int)state->text.size();
interacted = true;
}
if (ctrl && RLIsKeyPressed(RL_E_KEY_C)) {
// Copy
if (hasSel) {
std::string sel = state->text.substr(selMin, selMax - selMin);
RLSetClipboardText(sel.c_str());
}
}
if (ctrl && RLIsKeyPressed(RL_E_KEY_X)) {
// Cut
if (hasSel) {
std::string sel = state->text.substr(selMin, selMax - selMin);
RLSetClipboardText(sel.c_str());
state->text.erase(selMin, selMax - selMin);
state->cursor = selMin;
state->selectStart = state->selectEnd = selMin;
hasSel = false;
changed = true;
interacted = true;
}
}
if (ctrl && RLIsKeyPressed(RL_E_KEY_V)) {
// Paste
const char* clip = RLGetClipboardText();
if (clip && clip[0]) {
if (hasSel) {
state->text.erase(selMin, selMax - selMin);
state->cursor = selMin;
state->selectStart = state->selectEnd = selMin;
selMin = selMax = state->cursor;
hasSel = false;
}
state->text.insert(state->cursor, clip);
state->cursor += (int)strlen(clip);
state->selectStart = state->selectEnd = state->cursor;
changed = true;
interacted = true;
}
}
if (interacted)
state->lastInputTime = RLGetTime();
}
// ── Render ────────────────────────────────────────────
RLDrawRectangleLines(rect.x, rect.y, rect.width, rect.height, { 12, 12, 12, 255 });
RLDrawRectangleLines(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, RLColor{ 50, 50, 50, 255 });
RLDrawRectangleLines(rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4, RLColor{ 16, 16, 16, 255 });
RLDrawRectangle(rect.x + 3, rect.y + 3, rect.width - 6, rect.height - 6, RLColor{ 25, 25, 25, 255 });
// Compute normalized selection range
int selMin = std::min(state->selectStart, state->selectEnd);
int selMax = std::max(state->selectStart, state->selectEnd);
// Cursor X (before scroll offset)
float cursorX = kTextStart;
if (state->cursor > 0) {
std::string before = state->text.substr(0, state->cursor);
cursorX += font->MeasureText(before.c_str(), kFontSize, 2).x;
}
// Horizontal scroll to keep cursor visible
float scrollOff = 0.0f;
float visibleRight = kTextStart + kInnerW;
if (cursorX > visibleRight - 4.0f)
scrollOff = cursorX - visibleRight + 20.0f;
if (cursorX - scrollOff < kTextStart)
scrollOff = cursorX - kTextStart;
float drawX = kTextStart - scrollOff;
float textY = rect.y + (rect.height - kFontSize) / 2.0f;
// IME composition window follows cursor
if (state->active)
WidgetManager::UpdateImePosition((int)(cursorX - scrollOff), (int)(textY + kFontSize));
RLBeginScissorMode((int)(rect.x + kPad), (int)rect.y, (int)kInnerW, (int)rect.height);
// ── Selection highlight ──────────────────────────────
if (state->active && selMin != selMax) {
float selX1 = drawX;
if (selMin > 0) {
std::string before = state->text.substr(0, selMin);
selX1 += font->MeasureText(before.c_str(), kFontSize, 2).x;
}
std::string selText = state->text.substr(selMin, selMax - selMin);
float selW = font->MeasureText(selText.c_str(), kFontSize, 2).x;
RLDrawRectangle((int)selX1, (int)(textY + 1), (int)selW, kFontSize, { 50, 100, 200, 160 });
}
// ── Text color ───────────────────────────────────────
RLColor textColor;
if (state->active)
textColor = { 220, 220, 220, 255 };
else if (ws.hover)
textColor = { 200, 200, 200, 255 };
else
textColor = { 160, 160, 160, 255 };
// ── Draw text ────────────────────────────────────────
if (!state->text.empty()) {
if (state->password) {
int charCount = 0;
for (size_t i = 0; i < state->text.size(); ) {
if ((state->text[i] & 0xC0) != 0x80) charCount++;
i++;
}
std::string masked(charCount, '*');
font->DrawTextEX(masked.c_str(), { drawX, textY }, kFontSize, textColor, 2);
} else {
font->DrawTextEX(state->text.c_str(), { drawX, textY }, kFontSize, textColor, 2);
}
}
// ── Cursor blink ─────────────────────────────────────
if (state->active && !state->selecting) {
double elapsed = RLGetTime() - state->lastInputTime;
bool show = (elapsed < 0.5) || (((int)(RLGetTime() * 2.0)) % 2) == 0;
if (show)
RLDrawRectangle((int)(cursorX - scrollOff), (int)(textY + 2), 1, kFontSize - 4, { 220, 220, 220, 255 });
}
RLEndScissorMode();
return changed;
}
bool Framework::Textbox(const char* id, RLRectangle rect, TextboxState* state) {
return WidgetManager::Get()->Textbox(id, rect, state);
}
@@ -0,0 +1,31 @@
#include "../UIFramework.h"
#include "../../Backend/Font/FontManager.h"
bool WidgetManager::Toggle(const char* label, RLRectangle rect, bool* state) {
auto ws = BeginWidget(rect);
auto r = Render::Get();
auto Font = Font::Get();
if (ws.released && ws.hover)
*state = !*state;
bool on = *state;
RLDrawRectangleLines(rect.x, rect.y, rect.width, rect.height, { 0, 0, 0, 255 });
RLColor toggle = on ? RLColor{ 26, 26, 26, 255 } : RLColor{ 38, 38, 38, 255 };
RLDrawRectangle(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, toggle);
if(label != nullptr){
RLVector2 size = Font->MeasureText(label, 15);
Font->DrawTextEX(label,
{ (rect.x + rect.width / 2) - size.x / 2, (rect.y + rect.height / 2) - (size.y / 2) },
15, { 255, 255, 255, 255 });
}
return ws.released;
}
bool Framework::Toggle(const char* label, RLRectangle Rect, bool* state) {
return WidgetManager::Get()->Toggle(label, Rect, state);
}
+143
View File
@@ -0,0 +1,143 @@
#include "UIFramework.h"
#include "raylib.h"
#include "../Initialization/Initialization.h"
#include "../Backend/Font/FontManager.h"
// --- UIContext ---
void UIContext::NewFrame() {
m_mousePressConsumed = false;
m_mouseReleaseConsumed = false;
if (RLIsMouseButtonPressed(MOUSE_LEFT_BUTTON))
m_pressCaptured = false;
}
bool UIContext::ConsumeMousePress() {
if (m_mousePressConsumed)
return false;
m_mousePressConsumed = true;
return true;
}
bool UIContext::IsMousePressConsumed() const {
return m_mousePressConsumed;
}
bool UIContext::ConsumeMouseRelease() {
if (m_mouseReleaseConsumed)
return false;
m_mouseReleaseConsumed = true;
m_pressCaptured = false;
return true;
}
bool UIContext::IsMouseReleaseConsumed() const {
return m_mouseReleaseConsumed;
}
void UIContext::CapturePressOn(RLRectangle rect) {
m_pressCapturedRect = rect;
m_pressCaptured = true;
}
bool UIContext::IsPressCapturedOn(RLRectangle rect) const {
if (!m_pressCaptured)
return false;
return RLCheckCollisionPointRec(
{ rect.x + rect.width / 2, rect.y + rect.height / 2 },
m_pressCapturedRect);
}
// --- WidgetManager ---
WidgetManager* WidgetManager::Get() {
static WidgetManager instance;
return &instance;
}
void WidgetManager::NewFrame() {
m_ctx.NewFrame();
}
WidgetState WidgetManager::BeginWidget(RLRectangle rect) {
Initialization::Get()->AddDragBlockRegion(rect);
RLVector2 mouse = RLGetMousePosition();
bool hover = RLCheckCollisionPointRec(mouse, rect);
bool down = RLIsMouseButtonDown(MOUSE_LEFT_BUTTON);
bool pressed = hover && down;
bool clicked = hover && RLIsMouseButtonPressed(MOUSE_LEFT_BUTTON) && m_ctx.ConsumeMousePress();
if (clicked)
m_ctx.CapturePressOn(rect);
bool released = hover && RLIsMouseButtonReleased(MOUSE_LEFT_BUTTON)
&& m_ctx.IsPressCapturedOn(rect)
&& m_ctx.ConsumeMouseRelease();
return { hover, pressed, clicked, released };
}
// --- Framework (thin facade) ---
Framework* Framework::Get() {
static Framework instance;
return &instance;
}
void Framework::NewFrame() {
WidgetManager::Get()->NewFrame();
}
// --- Render ---
Render* Render::Get() {
static Render instance;
return &instance;
}
void Render::Gradient(RLRectangle Rect, RLColor LColor, RLColor ROtherColor, bool Vertical, bool Antialias) {
(void)Antialias;
if (Vertical) RLDrawRectangleGradientV((int)Rect.x, (int)Rect.y, (int)Rect.width, (int)Rect.height, LColor, ROtherColor);
else RLDrawRectangleGradientH((int)Rect.x, (int)Rect.y, (int)Rect.width, (int)Rect.height, LColor, ROtherColor);
}
void Render::CustomGradientBar(int x, int y, int width, int height, GradientStop* stops, int stopCount){
if (stopCount < 2) return;
int x_scaled = x;
int y_scaled = y;
int width_scaled = width;
int height_scaled = height;
for (int i = 0; i < stopCount - 1; i++)
{
GradientStop startStop = stops[i];
GradientStop endStop = stops[i + 1];
int startPixel = (int)(startStop.offset * width_scaled);
int endPixel = (int)(endStop.offset * width_scaled);
int segmentWidth = endPixel - startPixel;
if (segmentWidth <= 0) continue;
for (int j = 0; j < segmentWidth; j++)
{
float t = (segmentWidth == 1) ? 0.0f : (float)j / (float)(segmentWidth - 1);
RLColor c;
c.r = (unsigned char)ClampInt(startStop.color.r + (endStop.color.r - startStop.color.r) * t, 0, 255);
c.g = (unsigned char)ClampInt(startStop.color.g + (endStop.color.g - startStop.color.g) * t, 0, 255);
c.b = (unsigned char)ClampInt(startStop.color.b + (endStop.color.b - startStop.color.b) * t, 0, 255);
c.a = (unsigned char)ClampInt(startStop.color.a + (endStop.color.a - startStop.color.a) * t, 0, 255);
int current_pixel_x = x_scaled + startPixel + j;
RLDrawRectangle(current_pixel_x, y_scaled, 1, height_scaled, c);
float darkFactor = 0.5f;
RLColor c_dark = {
(unsigned char)(c.r * darkFactor),
(unsigned char)(c.g * darkFactor),
(unsigned char)(c.b * darkFactor),
c.a
};
RLDrawRectangle(current_pixel_x, y_scaled + 1, 1, height_scaled, c_dark);
}
}
}
+116
View File
@@ -0,0 +1,116 @@
#pragma once
#include "raylib.h"
#include <string>
// UI 状态上下文
class UIContext
{
public:
void NewFrame();
bool ConsumeMousePress();
bool IsMousePressConsumed() const;
bool ConsumeMouseRelease();
bool IsMouseReleaseConsumed() const;
void CapturePressOn(RLRectangle rect);
bool IsPressCapturedOn(RLRectangle rect) const;
private:
bool m_mousePressConsumed = false;
bool m_mouseReleaseConsumed = false;
RLRectangle m_pressCapturedRect = { 0 };
bool m_pressCaptured = false;
};
struct WidgetState
{
bool hover;
bool pressed;
bool clicked;
bool released;
};
struct TextboxState {
std::string text;
bool active = false;
int cursor = 0;
int selectStart = 0;
int selectEnd = 0;
bool selecting = false;
double lastInputTime = -1.0;
bool password = false;
};
// 控件管理器
class WidgetManager
{
public:
static WidgetManager* Get();
void NewFrame();
UIContext& Ctx() { return m_ctx; }
// Per-widget pre-processing: registers drag block, computes hover/pressed/clicked
WidgetState BeginWidget(RLRectangle rect);
// IME composition window positioning (Windows)
static void UpdateImePosition(int x, int y);
// Controls
bool Button(const char* label, RLRectangle rect, bool disabled);
bool Toggle(const char* label, RLRectangle rect, bool* state);
bool Textbox(const char* id, RLRectangle rect, TextboxState* state);
void Label(const char* label, RLVector2 Vec, int FontSize, int spacing);
private:
WidgetManager() = default;
~WidgetManager() = default;
UIContext m_ctx;
};
// 控件接口
class Framework
{
public:
static Framework* Get();
void NewFrame();
bool Button(const char* label, RLRectangle Rect, bool disabled = false);
bool Toggle(const char* label, RLRectangle Rect, bool* state);
bool Textbox(const char* id, RLRectangle rect, TextboxState* state);
void Label(const char* label, RLVector2 Vec, int FontSize, int spacing = 0);
UIContext& Ctx() { return WidgetManager::Get()->Ctx(); }
private:
Framework() = default;
~Framework() = default;
};
typedef struct {
RLColor color;
float offset;
} GradientStop;
// 渲染接口
class Render
{
public:
static Render* Get();
void Gradient(RLRectangle Rect, RLColor LColor, RLColor ROtherColor, bool Vertical, bool Antialias = false);
void CustomGradientBar(int x, int y, int width, int height, GradientStop* stops, int stopCount);
private:
Render() = default;
~Render() = default;
};
inline int ClampInt(int val, int min, int max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
inline float ClampF(float v, float a, float b) {
return v < a ? a : (v > b ? b : v);
}
@@ -0,0 +1,163 @@
#include "Initialization.h"
#include "../Framework/UIFramework.h"
#include "../Menu/Menu.h"
#include "raylib.h"
#include "../../resource.h"
#include <string.h>
#include <stdio.h>
#include "../Backend/Font/FontManager.h"
static auto Font = Font::Get();
Initialization* Initialization::Get()
{
static Initialization instance;
return &instance;
}
bool Initialization::Init_Windows() {
system("chcp 65001 > nul");
Win_Flags |= RL_E_FLAG_WINDOW_UNDECORATED;
RLSetConfigFlags(Win_Flags);
RLInitWindow((int)Win_size.x, (int)Win_size.y, Win_Tittle);
if (!RLIsWindowReady())
return false;
// 设置窗口图标(从资源加载)
HWND hwnd = (HWND)RLGetWindowHandle();
if (hwnd) {
HICON hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON1));
if (hIcon) {
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
}
}
RLSetExitKey(RL_E_KEY_NULL);
Font->LoadCustom();
return false;
}
void Initialization::Renderer() {
Framework::Get()->NewFrame();
ClearDragBlockRegions();
RLBeginDrawing();
{
RLClearBackground({0,0,0,0});
Menu::Get()->Render();
}
RLEndDrawing();
}
void Initialization::UpdateWindowDrag() {
if (!m_dragEnabled)
return;
bool leftDown = RLIsMouseButtonDown(RL_E_MOUSE_BUTTON_LEFT);
// Edge-triggered: start drag only when button just went down AND not in a widget area
if (leftDown && !m_drag.wasDown && !IsMouseInBlockRegion()) {
m_drag.dragging = true;
GetCursorPos(&m_drag.mouseStart);
HWND hwnd = (HWND)RLGetWindowHandle();
RECT rc;
GetWindowRect(hwnd, &rc);
m_drag.windowStart = { rc.left, rc.top };
}
// Release left button: stop drag
if (!leftDown)
m_drag.dragging = false;
m_drag.wasDown = leftDown;
if (!m_drag.dragging)
return;
HWND hwnd = (HWND)RLGetWindowHandle();
if (!hwnd)
return;
POINT mouse;
GetCursorPos(&mouse);
int dx = mouse.x - m_drag.mouseStart.x;
int dy = mouse.y - m_drag.mouseStart.y;
RECT win;
GetWindowRect(hwnd, &win);
int winW = win.right - win.left;
int winH = win.bottom - win.top;
// 当前屏幕工作区(多屏支持)
HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi{};
mi.cbSize = sizeof(mi);
GetMonitorInfo(monitor, &mi);
RECT work = mi.rcWork;
int targetX = m_drag.windowStart.x + dx;
int targetY = m_drag.windowStart.y + dy;
targetX = Clamp(targetX, work.left, work.right - winW);
targetY = Clamp(targetY, work.top, work.bottom - winH);
SetWindowPos(hwnd, nullptr, targetX, targetY, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
void Initialization::Clear() {
Font->Clear();
RLCloseWindow();
}
void Initialization::Set_WinSize(RLVector2 New_WinSize) {
if (New_WinSize.x == 0 && New_WinSize.y == 0)
return;
if (New_WinSize.x == Win_size.x && New_WinSize.y == Win_size.y)
return;
RLSetWindowSize((int)New_WinSize.x, (int)New_WinSize.y);
Win_size = New_WinSize;
}
bool Initialization::Set_Tittle(const char* New_Tittle) {
if (New_Tittle == nullptr)
return false;
if (strcmp(Win_Tittle, New_Tittle) == 0)
return false;
RLSetWindowTitle(New_Tittle);
Win_Tittle = New_Tittle;
return true;
}
void Initialization::Set_Flags(int New_Flags) {
RLSetWindowState(New_Flags);
}
void Initialization::Clear_Flags(int New_Flags) {
RLClearWindowState(New_Flags);
}
void Initialization::AddDragBlockRegion(RLRectangle Rect) {
m_dragBlockRegions.push_back({ Rect.x, Rect.y, Rect.width, Rect.height });
}
void Initialization::ClearDragBlockRegions() {
m_dragBlockRegions.clear();
}
bool Initialization::IsMouseInBlockRegion() const {
RLVector2 mouse = RLGetMousePosition();
for (const auto& r : m_dragBlockRegions) {
if (RLCheckCollisionPointRec(mouse, r))
return true;
}
return false;
}
@@ -0,0 +1,66 @@
#pragma once
#include <Windows.h>
#include <vector>
#include "raylib.h"
// 窗口拖拽状态
struct WindowDragState
{
bool dragging = false;
bool wasDown = false;
POINT mouseStart{};
POINT windowStart{};
};
class Initialization
{
public:
static Initialization* Get();
bool Init_Windows();
void Renderer();
void UpdateWindowDrag(); // 每帧调用,更新窗口拖拽
void Clear();
void Set_Flags(int New_Flags);
void Set_WinSize(RLVector2 New_WinSize);
bool Set_Tittle(const char* New_Tittle);
void Clear_Flags(int New_Flags);
RLVector2 Get_WinSize() const { return Win_size; }
const char* Get_WinTittle() const { return Win_Tittle; }
void SetDragEnabled(bool enabled) { m_dragEnabled = enabled; }
bool IsDragEnabled() const { return m_dragEnabled; }
void RequestClose() { m_requestClose = true; }
bool ShouldClose() const { return m_requestClose; }
// 注册拖拽屏蔽区域(窗口局部坐标),鼠标在此区域内按下不会触发拖拽
// 每帧需先 ClearDragBlockRegions() 再重新注册
void AddDragBlockRegion(RLRectangle Rect);
void ClearDragBlockRegions();
// 将窗口限制在当前显示器工作区内
static int Clamp(int v, int min, int max) { return (v < min) ? min : (v > max ? max : v); }
void SetFailedToFindPattern(bool failed) { m_failed_to_find_pattern = failed; }
bool GetFailedToFindPattern() const { return m_failed_to_find_pattern; }
private:
bool IsMouseInBlockRegion() const;
Initialization() :Win_size({600,420}) {};
~Initialization() = default;
RLVector2 Win_size = { 0,0 };
int Win_Flags = 0;
const char* Win_Tittle = "skeet_loader";
bool m_dragEnabled = true;
bool m_requestClose = false;
WindowDragState m_drag;
std::vector<RLRectangle> m_dragBlockRegions;
bool m_failed_to_find_pattern = false;
};
+128
View File
@@ -0,0 +1,128 @@
#include "Menu.h"
#include "raylib.h"
#include "../Initialization/Initialization.h"
#include "../Backend/Image.h"
#include "../Backend/TokenStore/TokenStore.h"
#include "../Backend/Network/NetworkClient.h"
#include "../Backend/HWID/HWIDCollector.h"
#include "../Backend/JWT/JWTDecoder.h"
#include <thread>
Menu* Menu::Get() {
static Menu instance;
return &instance;
}
static GradientStop stops[] = {
{ RLColor{ 0, 185, 255, 255 }, 0.0f },
{ RLColor{ 255, 1, 247, 255 }, 0.5f },
{ RLColor{ 232, 255, 0, 255 }, 1.0f }
};
static RLTexture2D GetBgTexture() {
static RLTexture2D tex = []() {
RLImage img = RLLoadImageFromMemory(".png", BgTexture, sizeof(BgTexture));
RLTexture2D t = RLLoadTextureFromImage(img);
RLUnloadImage(img);
return t;
}();
return tex;
}
static void BackGround() {
auto render = Render::Get();
RLVector2 Win_size = Initialization::Get()->Get_WinSize();
RLDrawRectangleLinesEx({ 0, 0, Win_size.x, Win_size.y }, 1, RLColor{0, 0, 0, 255});
RLDrawRectangleLinesEx({ 1, 1, Win_size.x - 2, Win_size.y - 2 }, 1, RLColor{60, 60, 60, 255});
RLDrawRectangleLinesEx({ 2, 2, Win_size.x - 4, Win_size.y - 4 }, 3, RLColor{40, 40, 40, 255});
RLDrawRectangleLinesEx({ 5, 5, Win_size.x - 10, Win_size.y - 10 }, 1, RLColor{60, 60, 60, 255});
RLDrawRectangleLinesEx({ 6, 6, Win_size.x - 12, Win_size.y - 12 }, 1, RLColor{0, 0, 0, 255});
RLTexture2D bg = GetBgTexture();
float texW = (float)bg.width;
float texH = (float)bg.height;
float areaX = 7, areaY = 7;
float areaW = Win_size.x - 14, areaH = Win_size.y - 14;
for (float y = areaY; y < areaY + areaH; y += texH) {
for (float x = areaX; x < areaX + areaW; x += texW) {
float drawW = texW;
float drawH = texH;
if (x + drawW > areaX + areaW) drawW = areaX + areaW - x;
if (y + drawH > areaY + areaH) drawH = areaY + areaH - y;
RLDrawTexturePro(bg,
{ 0, 0, drawW, drawH },
{ x, y, drawW, drawH },
{ 0, 0 }, 0, RLColor{255, 255, 255, 255});
}
}
render->CustomGradientBar(10, 10, Win_size.x - 20, 1, stops, sizeof(stops) / sizeof(GradientStop));
render->CustomGradientBar(10, 11, Win_size.x - 20, 1, stops, sizeof(stops) / sizeof(GradientStop));
render->CustomGradientBar(10, 12, Win_size.x - 20, 1, stops, sizeof(stops) / sizeof(GradientStop));
}
// ─── Startup auto-login ──────────────────────────────
static void TryAutoLogin(Menu* menu) {
printf("[Menu] Checking saved session for auto-login...\n");
auto session = TokenStore::Get()->Load();
if (!session.valid) {
printf("[Menu] No saved session found.\n");
menu->SetAutoLoginDone(false);
return;
}
printf("[Menu] Found saved session for user: %s\n", session.username.c_str());
// Collect HWID and try to refresh
const auto& hwid = HWIDCollector::Get()->Collect();
auto result = NetworkClient::Get()->RefreshToken(
session.refreshToken,
hwid.cpu, hwid.board, hwid.disk, hwid.mac);
if (result.success) {
printf("[Menu] Auto-login SUCCESS: %s\n", session.username.c_str());
auto* login = menu::Login::Get();
login->SetAutoLoggedIn(result.refreshToken, result.accessToken, session.username);
// Re-save the rotated refresh token
TokenStore::Get()->Save(result.refreshToken, session.username);
menu->SetAutoLoginDone(true);
} else {
printf("[Menu] Auto-login FAILED: %s\n", result.errorCode.c_str());
TokenStore::Get()->Clear();
menu->SetAutoLoginDone(false);
}
}
void Menu::Render() {
auto connect = menu::Connect::Get();
auto login = menu::Login::Get();
auto load = menu::Load::Get();
auto injector = menu::Injector::Get();
// ── Startup: attempt auto-login once ─────────────
if (!m_autoLoginChecked) {
m_autoLoginChecked = true;
std::thread(TryAutoLogin, this).detach();
}
BackGround();
// ── Menu flow ───────────────────────────────────
if (m_autoLoginSuccess) {
// Auto-login succeeded — skip directly to Load
load->DrawLoadMenu();
} else if (!connect->IsConnected()) {
connect->DrawConnectMenu(); // Step 1: Connect to server
} else if (!login->Get_loggedIn()) {
login->DrawLoginMenu(); // Step 2: Login
} else if (!load->Get_injecting()) {
load->DrawLoadMenu(); // Step 3: Load/Inject
} else {
injector->DrawInjectorMenu(); // Step 4: Injecting
}
}
+158
View File
@@ -0,0 +1,158 @@
#pragma once
#include "../Framework/UIFramework.h"
#include <atomic>
#include <mutex>
#include <string>
class Menu {
public:
static Menu* Get();
void Render();
void AddLog(const std::string& log) { Logs += log + "\n"; }
void ClearLogs() { Logs.clear(); }
const std::string& GetLogs() const { return Logs; }
// Auto-login state (set by Menu.cpp startup check)
void SetAutoLoginDone(bool success) { m_autoLoginSuccess = success; m_autoLoginChecked = true; }
bool IsAutoLoginDone() const { return m_autoLoginChecked; }
bool IsAutoLoginSuccess() const { return m_autoLoginSuccess; }
private:
Menu() = default;
~Menu() = default;
std::string Logs;
std::atomic<bool> m_autoLoginChecked{false};
std::atomic<bool> m_autoLoginSuccess{false};
};
namespace menu {
class Connect
{
public:
static Connect* Get();
void DrawConnectMenu();
bool IsConnected() const { return m_connected; }
// Allow Login to reset connect state on logout
void Reset() { m_connected = false; m_statusChecked = false; m_statusPending = false; }
private:
Connect() = default;
~Connect() = default;
bool m_connected = false;
double m_startTime = 0.0;
bool m_connecting = false;
// Network status check
std::atomic<bool> m_statusPending{false};
std::atomic<bool> m_statusChecked{false};
std::string m_statusError;
std::mutex m_mutex;
};
class Login
{
public:
static Login* Get();
void DrawLoginMenu();
bool Get_loggedIn() const { return m_loggedIn; }
std::string Get_UserName() const { return UserName.text; }
// Allow Load/Logout to reset
void Reset() { m_loggedIn = false; m_refreshToken.clear(); m_token.clear(); }
std::string GetRefreshToken() const { return m_refreshToken; }
// For auto-login: set login state without going through UI
void SetAutoLoggedIn(const std::string& refreshToken, const std::string& accessToken, const std::string& username) {
m_loggedIn = true;
m_refreshToken = refreshToken;
m_token = accessToken;
m_displayName = username;
}
std::string GetDisplayName() const { return m_displayName.empty() ? UserName.text : m_displayName; }
private:
Login() { PassWord.password = true; };
~Login() = default;
void TryLogin();
void ResetLoginForm();
TextboxState UserName = { "" };
TextboxState PassWord = { "" };
RLVector2 TextBox_UserName = { 180, 28 };
RLVector2 TextBox_PassWord = { 180, 28 };
bool m_loggedIn = false;
std::string m_errorMessage;
int m_loginAttempts = 0;
double m_lastFailedTime = 0.0;
bool m_blocked = false;
std::string m_prevUserText;
std::string m_prevPassText;
double m_errorSetTime = 0.0;
std::string m_prevErrorMessage;
// Cloud login async state
std::atomic<bool> m_loginPending{false};
std::string m_serverError;
std::string m_serverErrorCode;
std::string m_token; // JWT access token (memory only)
std::string m_refreshToken; // for DPAPI persistence
std::string m_displayName; // username for auto-login display
std::mutex m_mutex;
double m_loginStartTime = 0.0;
};
class Load
{
public:
static Load* Get();
void DrawLoadMenu();
bool Get_injecting() const { return m_injecting; }
// Reset on logout
void Reset() { m_injecting = false; m_welcomeShown = false; }
private:
Load() = default;
~Load() = default;
void bgDraw() const;
bool Load_lock = false;
bool is_cstoggle = true;
bool m_welcomeShown = false;
bool m_injecting = false;
double m_startTime = 0.0;
bool m_connecting = false;
RLRectangle Rect = { 10, 17, 289, 133 };
};
class Injector
{
public:
static Injector* Get();
void DrawInjectorMenu();
bool GetInjectionStatus() const { return m_injectionDone; }
private:
Injector() = default;
~Injector() = default;
bool m_connected = false;
int m_errorCode = 0;
double m_startTime = 0.0;
bool m_connecting = false;
bool m_injectionDone = false;
};
}
+84
View File
@@ -0,0 +1,84 @@
#include "../Menu.h"
#include "raylib.h"
#include "../../Initialization/Initialization.h"
#include "../../Backend/Font/FontManager.h"
#include "../../Backend/Network/NetworkClient.h"
#include <thread>
#include <mutex>
menu::Connect* menu::Connect::Get() {
static Connect instance;
return &instance;
}
void menu::Connect::DrawConnectMenu() {
auto* font = Font::Get();
RLVector2 Win_size = Initialization::Get()->Get_WinSize();
auto* ui = Framework::Get();
// ── Trigger status check on first frame ─────────
if (!m_connecting && !m_statusChecked) {
m_connecting = true;
m_startTime = RLGetTime();
m_statusPending = true;
// Spawn detached thread for server status check
std::thread([](Connect* self) {
auto result = NetworkClient::Get()->CheckStatus();
std::lock_guard<std::mutex> lock(self->m_mutex);
if (result.online) {
self->m_connected = true;
} else {
self->m_statusError = result.errorMessage;
}
self->m_statusPending = false;
self->m_statusChecked = true;
}, this).detach();
}
// ── Check completion ────────────────────────────
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_connected) {
// Connected — flow will advance to Login on next frame
return;
}
}
// ── Pulsing "Connecting..." animation ───────────
double elapsed = RLGetTime() - m_startTime;
float alpha = (float)(25 + (sin(elapsed * 2.5) + 1.0) * 0.5 * 180.0);
if (m_statusPending) {
auto size = font->MeasureText("Connecting...", 18);
font->DrawTextEX("Connecting...",
{ (Win_size.x - size.x) / 2, (Win_size.y - size.y) / 2 },
18, { 250, 250, 250, (unsigned char)alpha }, 2);
}
// ── Error state + retry/exit ────────────────────
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_statusPending && m_statusChecked && !m_connected) {
auto errSize = font->MeasureText(m_statusError.c_str(), 14);
font->DrawTextEX(m_statusError.c_str(),
{ (Win_size.x - errSize.x) / 2, (Win_size.y - errSize.y) / 2 - 40 },
14, { 255, 80, 80, 255 }, 2);
// Retry button
if (ui->Button("Retry", { (Win_size.x - 120) / 2, (Win_size.y - 35) / 2 + 10, 120, 35 })) {
m_statusChecked = false;
m_statusError.clear();
m_connecting = false;
}
}
// Exit button always shown during connect
if (!m_connected) {
if (ui->Button("Exit", { (Win_size.x - 120) / 2, (Win_size.y - 35) / 2 + 55, 120, 35 })) {
Initialization::Get()->RequestClose();
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
#include "../Menu.h"
#include "../../Backend/Font/FontManager.h"
#include "../../Initialization/Initialization.h"
#include "../../Backend/Injector/Injector.h"
#include <iostream>
menu::Injector* menu::Injector::Get() {
static Injector instance;
return &instance;
}
void menu::Injector::DrawInjectorMenu() {
auto* font = Font::Get();
RLVector2 Win_size = Initialization::Get()->Get_WinSize();
if (!m_connecting) {
m_connecting = true;
m_startTime = RLGetTime();
}
double elapsed = RLGetTime() - m_startTime;
float alpha = 25 + (float)((sin(elapsed * 2.5) + 1.0) * 0.5 * 195.0f);
auto size = font->MeasureText("Injector", 18);
font->DrawTextEX("Injector...", { (Win_size.x - size.x) / 2, (Win_size.y - size.y) / 2 }, 18, { 250, 250, 250, (unsigned char)alpha }, 2);
bool injectorReady = (elapsed >= 3.0); // 模拟注入准备时间
if (injectorReady && !m_injectionDone) {
m_injectionDone = true;
injector::Get()->Launch(); // 新线程启动 SkeetInjector
}
}
+136
View File
@@ -0,0 +1,136 @@
#include "../Menu.h"
#include "../../Backend/Image.h"
#include "../../Initialization/Initialization.h"
#include "../../Backend/Font/FontManager.h"
#include "../../Backend/TokenStore/TokenStore.h"
#include "../../Backend/Network/NetworkClient.h"
#include "../../Backend/Steam/Steam.h"
#include <iostream>
#include <chrono>
#include <thread>
menu::Load* menu::Load::Get() {
static Load instance;
return &instance;
}
static RLTexture2D GetCsTexture() {
static RLTexture2D tex = []() {
RLImage img = RLLoadImageFromMemory(".png", CSTexture, sizeof(CSTexture));
RLTexture2D t = RLLoadTextureFromImage(img);
RLUnloadImage(img);
return t;
}();
return tex;
}
static void DrawImage(RLTexture2D texture, RLRectangle dest, RLColor tint = {255, 255, 255, 255}) {
RLRectangle src = {0, 0, (float)texture.width, (float)texture.height};
RLDrawTexturePro(texture, src, dest, {0, 0}, 0, tint);
}
static void DoLogout() {
auto* login = menu::Login::Get();
auto* connect = menu::Connect::Get();
auto* load = menu::Load::Get();
// Clear local session (server token expires after 7 days naturally)
TokenStore::Get()->Clear();
login->Reset();
connect->Reset();
load->Reset();
Menu::Get()->ClearLogs();
Menu::Get()->SetAutoLoginDone(false);
}
void menu::Load::bgDraw() const {
auto win_size = Initialization::Get()->Get_WinSize();
RLDrawRectangleLines(Rect.x, Rect.y, Rect.width + 15, Rect.height, RLColor{ 0, 0, 0, 255 });
RLDrawRectangle(Rect.x + 1, Rect.y + 1, Rect.width - 2 + 15, Rect.height - 2, RLColor{ 35, 35, 35, 255 });
RLDrawRectangleLines(Rect.x + Rect.width + 8 + 15, Rect.y, Rect.width - 6 - 15, Rect.height, RLColor{ 0, 0, 0, 255 });
RLDrawRectangle(Rect.x + Rect.width + 8 + 1 + 15, Rect.y + 1, Rect.width - 8 - 15, Rect.height - 2, RLColor{ 23, 23, 23, 255 });
RLDrawRectangleLines(Rect.x, Rect.y + Rect.height + 8, win_size.x - (Rect.x * 2), win_size.y - (Rect.y + Rect.height + 18), RLColor{ 0, 0, 0, 255 });
RLDrawRectangle(Rect.x + 1, Rect.y + Rect.height + 9, win_size.x - (Rect.x * 2) - 2, win_size.y - (Rect.y + Rect.height + 18) - 2, RLColor{ 35, 35, 35, 255 });
}
void menu::Load::DrawLoadMenu() {
auto username = Login::Get()->GetDisplayName();
auto Log = Menu::Get();
if (!m_welcomeShown) {
Log->AddLog("Welcome back, " + username);
m_welcomeShown = true;
}
auto* font = Font::Get();
auto Win_size = Initialization::Get()->Get_WinSize();
auto* ui = Framework::Get();
bgDraw();
bool Cs_button = ui->Toggle(NULL, { Rect.x, Rect.y, Rect.width + 15, Rect.height / 2 - 11 }, &is_cstoggle);
DrawImage(GetCsTexture(), { Rect.x + 6, Rect.y + 6, 41.5, 42 }, is_cstoggle? RLColor{ 255,255,255,255 }:RLColor{155,155,155,255});
font->DrawTextEX("Counter-Strike Global Offensive", { Rect.x + 6 + 45, Rect.y + 6 }, 18, is_cstoggle ? RLColor{ 80, 118, 23, 255} : RLColor{ 120, 120, 120, 255 }, -1);
font->DrawTextEX("Updated 2023/03/24 13:32", { Rect.x + 6 + 45, Rect.y + 6 + font->MeasureText("Updated 2023/03/24 13:32", 17, -1).y}, 17, is_cstoggle ? RLColor{ 220, 220, 220, 255 } : RLColor{ 120, 120, 120, 255 }, -1);
if (Cs_button){
Log->ClearLogs();
m_welcomeShown = false;
}
// ── Load button ─────────────────────────────────
if (ui->Button("Load", { Rect.x + Rect.width + 17 + 15, Rect.y + 9, Rect.width - 24 - 15, 53 }, Load_lock) && !Load_lock) {
if (!is_cstoggle) {
Log->AddLog("Please select game!");
}
else {
Log->AddLog("Injector...");
auto Init = Initialization::Get();
if (Init->GetFailedToFindPattern())
std::thread([](Load* self) {
auto SteamMgr = Steam::Get();
SteamMgr->ReplaceOverlayFiles();
}, this).detach();
std::thread([](Load* self) {
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
self->m_injecting = true;
}, this).detach();
}
}
// ── Logout button ───────────────────────────────
//if (ui->Button("Logout", { Rect.x + Rect.width + 17 + 15, Rect.y + 9 + 33 + 6, Rect.width - 24 - 15, 25 })) {
// DoLogout();
// return; // Immediately rendered Connect on next frame
//}
// ── Exit button ─────────────────────────────────
if (ui->Button("Exit", { Rect.x + Rect.width + 17 + 15, Rect.y + 9 + 53 + 9, Rect.width - 24 - 15, 53 }))
Initialization::Get()->RequestClose();
// Log area with auto-scroll: clip to the log rectangle, then offset Y
// so the bottom of the text stays visible when it overflows.
{
const RLRectangle logArea = {
Rect.x + 6,
Rect.y + Rect.height + 14,
Win_size.x - (Rect.x * 2) - 12,
Win_size.y - (Rect.y + Rect.height + 18) - 2
};
const auto& logText = Log->GetLogs();
if (!logText.empty()) {
constexpr int fontSize = 18;
const char* text = logText.c_str();
const RLVector2 textSize = font->MeasureText(text, fontSize, 0);
const bool overflow = textSize.y > logArea.height;
if (overflow)
RLBeginScissorMode((int)logArea.x, (int)logArea.y, (int)logArea.width, (int)logArea.height);
const float offsetY = overflow ? textSize.y - logArea.height : 0.0f;
ui->Label(text, {Rect.x + 6, Rect.y + Rect.height + 12 - offsetY}, fontSize);
if (overflow)
RLEndScissorMode();
}
}
}
+243
View File
@@ -0,0 +1,243 @@
#include "raylib.h"
#include "../Menu.h"
#include "../../Initialization/Initialization.h"
#include "../../Backend/Font/FontManager.h"
#include "../../Backend/Network/NetworkClient.h"
#include "../../Backend/HWID/HWIDCollector.h"
#include "../../Backend/TokenStore/TokenStore.h"
#include "../../Backend/Hash/Hash.h"
#include <thread>
#include <cstdio>
menu::Login* menu::Login::Get() {
static Login instance;
return &instance;
}
// ── Centralized login validation (cloud-based) ───────
void menu::Login::TryLogin() {
// Cooldown check (local)
if (m_blocked) {
double elapsed = RLGetTime() - m_lastFailedTime;
if (elapsed >= 3.0) {
m_blocked = false;
m_loginAttempts = 0;
m_errorMessage.clear();
} else {
int remaining = (int)(3.0 - elapsed) + 1;
m_errorMessage = "Too many attempts. Wait " + std::to_string(remaining) + "s...";
return;
}
}
// Empty field checks
if (UserName.text.empty() && PassWord.text.empty()) {
m_errorMessage = "Please enter username and password";
return;
}
if (UserName.text.empty()) {
m_errorMessage = "Please enter username";
return;
}
if (PassWord.text.empty()) {
m_errorMessage = "Please enter password";
return;
}
// Prevent double-submit
if (m_loginPending) return;
m_loginPending = true;
m_loginStartTime = RLGetTime();
m_errorMessage.clear();
m_serverError.clear();
// Capture values for thread safety
std::string username = UserName.text;
std::string password = PassWord.text;
// Spawn network thread
std::thread([this, username, password]() {
printf("[Login] Attempting login for user: %s\n", username.c_str());
// Hash password client-side — raw password never leaves this machine
std::string passwordHash = Hash::ComputeSHA256String(password);
// Collect HWID components
const auto& hwid = HWIDCollector::Get()->Collect();
// Send login request (SHA-256 hash over HTTP)
auto result = NetworkClient::Get()->Login(
username, passwordHash,
hwid.cpu, hwid.board, hwid.disk, hwid.mac);
std::lock_guard<std::mutex> lock(m_mutex);
if (result.success) {
printf("[Login] Success: %s (match_rate=%.2f)\n", result.username.c_str(), result.matchRate);
m_token = result.accessToken;
m_refreshToken = result.refreshToken;
// Persist refresh token for auto-login
TokenStore::Get()->Save(m_refreshToken, result.username);
m_loggedIn = true;
m_loginAttempts = 0;
m_blocked = false;
} else {
printf("[Login] Failed: %s - %s\n", result.errorCode.c_str(), result.errorMessage.c_str());
if (result.errorCode == "HWID_THRESHOLD") {
m_serverError = "Hardware not recognized. Contact support.";
} else if (result.errorCode == "ACCOUNT_EXPIRED") {
m_serverError = "Account has expired";
} else if (result.errorCode == "ACCOUNT_DISABLED") {
m_serverError = "Account is disabled";
} else if (result.errorCode == "RATE_LIMITED") {
m_serverError = "Too many attempts. Wait " + std::to_string(result.retryAfter) + "s...";
} else if (result.errorCode == "NETWORK_ERROR") {
m_serverError = "Cannot reach server. Retry?";
} else {
m_serverError = "Invalid username or password";
m_loginAttempts++;
if (m_loginAttempts >= 5) {
m_blocked = true;
m_lastFailedTime = RLGetTime();
m_serverError = "Too many attempts. Wait 3s...";
}
}
m_serverErrorCode = result.errorCode;
}
m_loginPending = false;
}).detach();
}
// ── Reset form to initial state ───────────────────────
void menu::Login::ResetLoginForm() {
UserName.text.clear();
PassWord.text.clear();
UserName.cursor = UserName.selectStart = UserName.selectEnd = 0;
PassWord.cursor = PassWord.selectStart = PassWord.selectEnd = 0;
UserName.active = false;
PassWord.active = false;
m_errorMessage.clear();
m_serverError.clear();
m_loginAttempts = 0;
m_blocked = false;
m_prevUserText.clear();
m_prevPassText.clear();
}
void menu::Login::DrawLoginMenu() {
auto* ui = Framework::Get();
auto* font = Font::Get();
RLVector2 Win_size = Initialization::Get()->Get_WinSize();
// ── Cooldown tick with countdown ──────────────
if (m_blocked) {
double elapsed = RLGetTime() - m_lastFailedTime;
if (elapsed >= 3.0) {
m_blocked = false;
m_loginAttempts = 0;
m_errorMessage.clear();
} else {
int remaining = (int)(3.0 - elapsed) + 1;
m_errorMessage = "Too many attempts. Wait " + std::to_string(remaining) + "s...";
}
}
// ── Poll async login result ────────────────────
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_loginPending && !m_serverError.empty()) {
// Thread finished with error — transfer to UI error
m_errorMessage = m_serverError;
m_serverError.clear();
m_errorSetTime = RLGetTime();
m_prevErrorMessage = m_errorMessage;
}
}
// ── Clear error when user edits text ──────────
if (UserName.text != m_prevUserText || PassWord.text != m_prevPassText) {
if (!m_blocked && !m_loginPending)
m_errorMessage.clear();
}
m_prevUserText = UserName.text;
m_prevPassText = PassWord.text;
// ── Tab key to switch focus ───────────────────
if (RLIsKeyPressed(RL_E_KEY_TAB)) {
if (UserName.active) {
UserName.active = false;
PassWord.active = true;
} else if (PassWord.active) {
PassWord.active = false;
UserName.active = true;
} else {
UserName.active = true;
}
}
// ── Login form ────────────────────────────────
float pos_y = 95;
ui->Label("Name", { (Win_size.x - TextBox_UserName.x) / 2 + 5, pos_y }, 16, 2); pos_y += 15;
ui->Textbox("##UserName", { (Win_size.x - TextBox_UserName.x) / 2, pos_y, TextBox_UserName.x, 28 }, &UserName); pos_y += 50;
ui->Label("Password", { (Win_size.x - TextBox_PassWord.x) / 2 + 5, pos_y }, 16, 2); pos_y += 15;
ui->Textbox("##Password", { (Win_size.x - TextBox_PassWord.x) / 2, pos_y, TextBox_PassWord.x, 28 }, &PassWord); pos_y += 52;
// Collect login triggers
bool loginClicked = false;
bool enterPressed = RLIsKeyPressed(RL_E_KEY_ENTER) && (UserName.active || PassWord.active);
if (m_loginPending) {
// ── "Connecting..." animation while pending ──
double elapsed = RLGetTime() - m_loginStartTime;
float alpha = (float)(25 + (sin(elapsed * 2.5) + 1.0) * 0.5 * 180.0);
auto connSize = font->MeasureText("Connecting...", 15);
font->DrawTextEX("Connecting...",
{ (Win_size.x - connSize.x) / 2, pos_y + 8 },
15, { 255, 255, 255, (unsigned char)alpha }, 2);
// Disabled button
ui->Button("Login", { (Win_size.x - 180) / 2, pos_y, 180, 35 }, true);
} else {
loginClicked = ui->Button("Login", { (Win_size.x - 180) / 2, pos_y, 180, 35 });
}
pos_y += 60;
if (loginClicked || enterPressed)
TryLogin();
if (ui->Button("Exit", { (Win_size.x - 180) / 2, pos_y, 180, 35 }))
Initialization::Get()->RequestClose();
// ── Error message fade tracking ─────────────
if (m_errorMessage != m_prevErrorMessage) {
m_errorSetTime = RLGetTime();
m_prevErrorMessage = m_errorMessage;
}
// ── Error message with fade-out ─────────────
if (!m_errorMessage.empty()) {
double elapsed = RLGetTime() - m_errorSetTime;
unsigned char alpha = 255;
if (elapsed > 1.5) {
double fadeElapsed = elapsed - 1.5;
if (fadeElapsed >= 0.5) {
m_errorMessage.clear();
} else {
alpha = (unsigned char)(255.0 * (1.0 - fadeElapsed / 0.5));
}
}
if (!m_errorMessage.empty()) {
auto errSize = font->MeasureText(m_errorMessage.c_str(), 13);
font->DrawTextEX(m_errorMessage.c_str(),
{ (Win_size.x - errSize.x) / 2, pos_y + 45 },
13, { 255, 80, 80, alpha }, 2);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
#include "Initialization/Initialization.h"
#include "Backend/Steam/Steam.h"
#include "Backend/Injector/Injector.h"
#include <raylib.h>
#include "Menu/Menu.h"
int main(bool failed_to_find_pattern = false) {
auto Init = Initialization::Get();
auto MenuMgr = menu::Injector::Get();
auto BackendInjector = injector::Get();
if (Init->Init_Windows())
return 1;
if (failed_to_find_pattern)
Init->SetFailedToFindPattern(failed_to_find_pattern);
while (!RLWindowShouldClose() && !Init->ShouldClose() && !MenuMgr->GetInjectionStatus()) {
Init->UpdateWindowDrag();
Init->Renderer();
}
Init->Clear();
// 等待注入线程完成(已在 DrawInjectorMenu 中由新线程启动)
if (MenuMgr->GetInjectionStatus())
BackendInjector->Wait();
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#define IDI_ICON1 101
+3
View File
@@ -0,0 +1,3 @@
#include "resource.h"
IDI_ICON1 ICON "icon.ico"
+150
View File
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{68f549ce-1268-453d-a8c5-cb4afe5f5381}</ProjectGuid>
<RootNamespace>skeetloader</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<IntDir>$(SolutionDir)$(Platform)\out\$(Configuration)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<IntDir>$(SolutionDir)$(Platform)\out\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<AdditionalIncludeDirectories>$(ProjectDir)\include\raylib;$(ProjectDir)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(ProjectDir)\include\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>raylibd.lib;winmm.lib;opengl32.lib;gdi32.lib;shell32.lib;winhttp.lib;iphlpapi.lib;crypt32.lib;bcrypt.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<AdditionalIncludeDirectories>$(ProjectDir)\include\raylib;$(ProjectDir)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(ProjectDir)\include\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>raylib.lib;winmm.lib;opengl32.lib;gdi32.lib;shell32.lib;winhttp.lib;iphlpapi.lib;crypt32.lib;bcrypt.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ResourceCompile Include="resource.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="include\json\json.hpp" />
<ClInclude Include="main\Backend\Hash\Hash.h" />
<ClInclude Include="main\Backend\HWID\HWIDCollector.h" />
<ClInclude Include="main\Backend\Injector\Injector.h" />
<ClInclude Include="main\Backend\Injector\SkeetInjector.h" />
<ClInclude Include="main\Backend\JWT\JWTDecoder.h" />
<ClInclude Include="main\Backend\Network\CertPinner.h" />
<ClInclude Include="main\Backend\Network\NetworkClient.h" />
<ClInclude Include="main\Backend\Steam\Steam.h" />
<ClInclude Include="main\Backend\TokenStore\TokenStore.h" />
<ClInclude Include="include\raylib\raylib.h" />
<ClInclude Include="include\raylib\raymath.h" />
<ClInclude Include="include\raylib\rlgl.h" />
<ClInclude Include="main\Backend\Font\FontManager.h" />
<ClInclude Include="main\Backend\Image.h" />
<ClInclude Include="main\Initialization\Initialization.h" />
<ClInclude Include="main\Framework\UIFramework.h" />
<ClInclude Include="main\Menu\Menu.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="main\Backend\Hash\Hash.cpp" />
<ClCompile Include="main\Backend\HWID\HWIDCollector.cpp" />
<ClCompile Include="main\Backend\Injector\Injector.cpp">
<ObjectFileName>$(IntDir)injector_backend.obj</ObjectFileName>
</ClCompile>
<ClCompile Include="main\Backend\JWT\JWTDecoder.cpp" />
<ClCompile Include="main\Backend\Network\CertPinner.cpp" />
<ClCompile Include="main\Backend\Network\NetworkClient.cpp" />
<ClCompile Include="main\Backend\Steam\GameOverlayRenderer.h" />
<ClCompile Include="main\Backend\Steam\GameOverlayRenderer64.h" />
<ClCompile Include="main\Backend\Steam\gameoverlayui.h" />
<ClCompile Include="main\Backend\Steam\gameoverlayui64.h" />
<ClCompile Include="main\Backend\Steam\Steam.cpp" />
<ClCompile Include="main\Backend\TokenStore\TokenStore.cpp" />
<ClCompile Include="main\Menu\Menu\Connect.cpp" />
<ClCompile Include="main\Menu\Menu\Injector.cpp" />
<ClCompile Include="main\Menu\Menu\Load.cpp" />
<ClCompile Include="main\Menu\Menu\Login.cpp" />
<ClCompile Include="main\Backend\Font\FontManager.cpp" />
<ClCompile Include="main\Framework\GUI\Button.cpp" />
<ClCompile Include="main\Framework\GUI\Toggle.cpp" />
<ClCompile Include="main\Framework\GUI\Label.cpp" />
<ClCompile Include="main\Framework\GUI\Textbox.cpp" />
<ClCompile Include="main\Initialization\Initialization.cpp" />
<ClCompile Include="main\main.cpp" />
<ClCompile Include="main\Framework\UIFramework.cpp" />
<ClCompile Include="main\Menu\Menu.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
+128
View File
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="resource.rc">
<Filter>资源文件</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Image Include="icon.ico">
<Filter>资源文件</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<ClInclude Include="include\raylib\raylib.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\raylib\raymath.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\raylib\rlgl.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Initialization\Initialization.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Framework\UIFramework.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Backend\Font\FontManager.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Menu\Menu.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Backend\Image.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Backend\Steam\Steam.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Backend\Hash\Hash.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Backend\Injector\injector.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="main\Backend\Injector\skeet_dll.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main\Initialization\Initialization.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\main.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Framework\UIFramework.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Font\FontManager.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Menu\Menu.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Framework\GUI\Button.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Framework\GUI\Toggle.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Framework\GUI\Textbox.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Framework\GUI\Label.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Menu\Menu\Login.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Menu\Menu\Connect.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Menu\Menu\Load.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Menu\Menu\Injector.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Steam\Steam.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Hash\Hash.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Steam\GameOverlayRenderer.h">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Steam\GameOverlayRenderer64.h">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Steam\gameoverlayui.h">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Steam\gameoverlayui64.h">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="main\Backend\Injector\injector.cpp">
<Filter>源文件</Filter>
<ObjectFileName>$(IntDir)injector_backend.obj</ObjectFileName>
</ClCompile>
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>true</ShowAllFiles>
</PropertyGroup>
</Project>