main
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.vs
|
||||
x64
|
||||
@@ -0,0 +1 @@
|
||||
./skeet_loader.exe -true
|
||||
@@ -0,0 +1,3 @@
|
||||
PORT=3000
|
||||
JWT_PRIVATE_KEY_PATH=./private.pem
|
||||
JWT_PUBLIC_KEY_PATH=./public.pem
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
skeet_server.db
|
||||
logs/
|
||||
.env
|
||||
+157
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
}]
|
||||
};
|
||||
Generated
+1430
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
@@ -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)`);
|
||||
});
|
||||
@@ -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.
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define IDI_ICON1 101
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "resource.h"
|
||||
|
||||
IDI_ICON1 ICON "icon.ico"
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user