Initial commit

This commit is contained in:
2026-01-04 23:00:21 +08:00
commit d3178871eb
124 changed files with 19300 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use eframe::egui;
const MAGIC: &[u8] = b"LSWRAP1";
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions::default();
eframe::run_native(
"授权打包器",
options,
Box::new(|_| Box::new(LauncherGui::new())),
)
}
struct LauncherGui {
api_base: String,
integration_code: String,
input_path: String,
output_path: String,
extract_to_self: bool,
status: String,
}
impl LauncherGui {
fn new() -> Self {
Self {
api_base: String::new(),
integration_code: String::new(),
input_path: String::new(),
output_path: String::new(),
extract_to_self: false,
status: String::new(),
}
}
fn pack(&mut self) -> Result<(), String> {
let api_base = normalize_api_base(&self.api_base)?;
let (project_id, project_key) = decode_integration_code(&self.integration_code)?;
let input = PathBuf::from(self.input_path.trim());
if !input.exists() {
return Err("未选择可执行文件".to_string());
}
let output = if self.output_path.trim().is_empty() {
derive_output_path(&input)?
} else {
PathBuf::from(self.output_path.trim())
};
if input == output {
return Err("输出文件不能与输入文件相同".to_string());
}
let exe_dir = env::current_exe()
.map_err(|e| format!("无法定位程序目录: {e}"))?
.parent()
.unwrap_or(Path::new("."))
.to_path_buf();
let stub_path = exe_dir.join("launcher_stub.exe");
if !stub_path.exists() {
return Err("未找到 launcher_stub.exe请放在同目录".to_string());
}
let payload_name = input
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("payload.exe")
.to_string();
let extract_to = if self.extract_to_self { "self" } else { "temp" };
let config = serde_json::json!({
"api_base": api_base,
"project_id": project_id,
"project_secret": project_key,
"license_file": "license.key",
"request_timeout_sec": 10,
"heartbeat_retries": 2,
"heartbeat_retry_delay_sec": 5,
"extract_to": extract_to,
"keep_payload": false,
"payload_name": payload_name
});
let config_bytes = serde_json::to_vec(&config)
.map_err(|e| format!("配置序列化失败: {e}"))?;
pack_files(&stub_path, &input, &output, &config_bytes)?;
self.output_path = output.to_string_lossy().to_string();
Ok(())
}
}
impl eframe::App for LauncherGui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
handle_drop(ctx, self);
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("授权打包器");
ui.add_space(8.0);
ui.label("授权系统地址");
ui.add(egui::TextEdit::singleline(&mut self.api_base).hint_text("http://118.145.218.2:39256"));
ui.add_space(6.0);
ui.label("项目对接码");
ui.add(egui::TextEdit::singleline(&mut self.integration_code).hint_text("LSC1.xxxxx"));
ui.add_space(6.0);
ui.label("选择要打包的 EXE");
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut self.input_path).desired_width(360.0));
if ui.button("浏览").clicked() {
if let Some(file) = rfd::FileDialog::new().add_filter("EXE", &["exe"]).pick_file() {
self.input_path = file.to_string_lossy().to_string();
if self.output_path.trim().is_empty() {
if let Ok(path) = derive_output_path(&file) {
self.output_path = path.to_string_lossy().to_string();
}
}
}
}
});
ui.add_space(6.0);
ui.label("输出文件");
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut self.output_path).desired_width(360.0));
if ui.button("另存为").clicked() {
if let Some(file) = rfd::FileDialog::new().add_filter("EXE", &["exe"]).set_file_name("packed.exe").save_file() {
self.output_path = file.to_string_lossy().to_string();
}
}
});
ui.add_space(6.0);
ui.checkbox(&mut self.extract_to_self, "解压到程序目录(需要本地资源时勾选)");
ui.add_space(10.0);
if ui.button("开始打包").clicked() {
match self.pack() {
Ok(_) => self.status = "打包完成".to_string(),
Err(err) => self.status = format!("打包失败: {err}"),
}
}
if !self.status.is_empty() {
ui.add_space(8.0);
ui.label(&self.status);
}
});
}
}
fn handle_drop(ctx: &egui::Context, app: &mut LauncherGui) {
let dropped = ctx.input(|i| i.raw.dropped_files.clone());
for file in dropped {
if let Some(path) = file.path {
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("exe")).unwrap_or(false) {
app.input_path = path.to_string_lossy().to_string();
if app.output_path.trim().is_empty() {
if let Ok(out) = derive_output_path(&path) {
app.output_path = out.to_string_lossy().to_string();
}
}
break;
}
}
}
}
fn pack_files(stub_path: &Path, payload_path: &Path, output_path: &Path, config: &[u8]) -> Result<(), String> {
let stub = fs::read(stub_path).map_err(|e| format!("读取 stub 失败: {e}"))?;
let payload = fs::read(payload_path).map_err(|e| format!("读取 EXE 失败: {e}"))?;
let mut out = fs::File::create(output_path).map_err(|e| format!("创建输出失败: {e}"))?;
out.write_all(&stub).map_err(|e| format!("写入 stub 失败: {e}"))?;
out.write_all(&payload).map_err(|e| format!("写入 EXE 失败: {e}"))?;
out.write_all(config).map_err(|e| format!("写入配置失败: {e}"))?;
out.write_all(MAGIC).map_err(|e| format!("写入标识失败: {e}"))?;
out.write_all(&(payload.len() as u64).to_le_bytes())
.map_err(|e| format!("写入长度失败: {e}"))?;
out.write_all(&(config.len() as u64).to_le_bytes())
.map_err(|e| format!("写入长度失败: {e}"))?;
out.flush().map_err(|e| format!("写入失败: {e}"))?;
Ok(())
}
fn normalize_api_base(value: &str) -> Result<String, String> {
let trimmed = value.trim().trim_end_matches('/');
if trimmed.is_empty() {
return Err("请输入授权系统地址".to_string());
}
Ok(trimmed.to_string())
}
fn derive_output_path(input: &Path) -> Result<PathBuf, String> {
let parent = input.parent().unwrap_or(Path::new("."));
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("app");
let ext = input
.extension()
.and_then(|s| s.to_str())
.unwrap_or("exe");
let name = format!("{stem}_packed.{ext}");
Ok(parent.join(name))
}
fn decode_integration_code(code: &str) -> Result<(String, String), String> {
let trimmed = code.trim();
if trimmed.is_empty() {
return Err("请输入对接码".to_string());
}
if trimmed.contains('|') {
return split_code(trimmed);
}
let raw = trimmed.strip_prefix("LSC1.").unwrap_or(trimmed);
let decoded = STANDARD.decode(raw).map_err(|_| "对接码格式错误".to_string())?;
let decoded_str = String::from_utf8(decoded).map_err(|_| "对接码解析失败".to_string())?;
split_code(&decoded_str)
}
fn split_code(value: &str) -> Result<(String, String), String> {
let mut parts = value.split('|');
let project_id = parts.next().unwrap_or("").trim();
let project_key = parts.next().unwrap_or("").trim();
if project_id.is_empty() || project_key.is_empty() {
return Err("对接码内容不完整".to_string());
}
Ok((project_id.to_string(), project_key.to_string()))
}

View File

@@ -0,0 +1,97 @@
use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
const MAGIC: &[u8] = b"LSWRAP1";
fn main() {
if let Err(err) = run() {
eprintln!("pack failed: {err}");
std::process::exit(1);
}
}
fn run() -> Result<(), String> {
let args = parse_args()?;
let stub = fs::read(&args.stub).map_err(|e| format!("read stub: {e}"))?;
let payload = fs::read(&args.input).map_err(|e| format!("read input: {e}"))?;
let mut config_value: serde_json::Value = {
let raw = fs::read(&args.config).map_err(|e| format!("read config: {e}"))?;
serde_json::from_slice(&raw).map_err(|e| format!("invalid config json: {e}"))?
};
let payload_name = Path::new(&args.input)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("payload.exe")
.to_string();
let obj = config_value
.as_object_mut()
.ok_or("config must be a json object")?;
let existing = obj.get("payload_name").and_then(|v| v.as_str()).unwrap_or("");
if existing.trim().is_empty() {
obj.insert("payload_name".to_string(), serde_json::Value::String(payload_name));
}
let config = serde_json::to_vec(&config_value).map_err(|e| format!("serialize config: {e}"))?;
let payload_len = payload.len() as u64;
let config_len = config.len() as u64;
let mut out = fs::File::create(&args.output).map_err(|e| format!("create output: {e}"))?;
out.write_all(&stub).map_err(|e| format!("write stub: {e}"))?;
out.write_all(&payload).map_err(|e| format!("write payload: {e}"))?;
out.write_all(&config).map_err(|e| format!("write config: {e}"))?;
out.write_all(MAGIC).map_err(|e| format!("write magic: {e}"))?;
out.write_all(&payload_len.to_le_bytes())
.map_err(|e| format!("write payload len: {e}"))?;
out.write_all(&config_len.to_le_bytes())
.map_err(|e| format!("write config len: {e}"))?;
out.flush().map_err(|e| format!("flush output: {e}"))?;
println!("packed: {}", args.output);
Ok(())
}
struct Args {
stub: String,
input: String,
config: String,
output: String,
}
fn parse_args() -> Result<Args, String> {
let mut stub = None;
let mut input = None;
let mut config = None;
let mut output = None;
let mut iter = env::args().skip(1);
while let Some(arg) = iter.next() {
match arg.as_str() {
"--stub" | "-s" => stub = iter.next(),
"--input" | "-i" => input = iter.next(),
"--config" | "-c" => config = iter.next(),
"--output" | "-o" => output = iter.next(),
"--help" | "-h" => {
print_help();
std::process::exit(0);
}
_ => return Err(format!("unknown arg: {arg}")),
}
}
let stub = stub.ok_or("missing --stub")?;
let input = input.ok_or("missing --input")?;
let config = config.ok_or("missing --config")?;
let output = output.ok_or("missing --output")?;
Ok(Args { stub, input, config, output })
}
fn print_help() {
println!("Usage: launcher_pack --stub <launcher_stub.exe> --input <app.exe> --config <config.json> --output <wrapped.exe>");
}

View File

@@ -0,0 +1,443 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac};
use rand::Rng;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const MAGIC: &[u8] = b"LSWRAP1";
const FOOTER_LEN: usize = 7 + 8 + 8;
fn main() {
if let Err(err) = run() {
eprintln!("launcher error: {err}");
std::process::exit(1);
}
}
fn run() -> Result<(), String> {
let exe_path = env::current_exe().map_err(|e| format!("current_exe: {e}"))?;
let exe_dir = exe_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let (payload, mut config) = read_embedded(&exe_path)?;
normalize_config(&mut config)?;
let card_key = load_card_key(&exe_dir, &config)?;
let device_id = resolve_device_id();
let timeout = config.request_timeout_sec.unwrap_or(10);
let client = Client::builder()
.timeout(Duration::from_secs(timeout))
.build()
.map_err(|e| format!("http client: {e}"))?;
let verify = verify_online(&client, &config, &card_key, &device_id)?;
let access_token = verify.access_token.ok_or("missing access_token")?;
let heartbeat_interval = if verify.heartbeat_interval > 0 {
verify.heartbeat_interval as u64
} else {
0
};
let payload_path = extract_payload(&payload, &config, &exe_dir)?;
let mut child = Command::new(&payload_path)
.current_dir(&exe_dir)
.args(env::args().skip(1))
.spawn()
.map_err(|e| format!("spawn payload: {e}"))?;
let stop = Arc::new(AtomicBool::new(false));
let heartbeat_failed = Arc::new(AtomicBool::new(false));
if heartbeat_interval > 0 {
let client = client.clone();
let config = config.clone();
let device_id = device_id.clone();
let access_token = access_token.clone();
let stop_flag = stop.clone();
let fail_flag = heartbeat_failed.clone();
thread::spawn(move || {
heartbeat_loop(&client, &config, &device_id, &access_token, heartbeat_interval, stop_flag, fail_flag);
});
}
loop {
if heartbeat_failed.load(Ordering::SeqCst) {
let _ = child.kill();
break;
}
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) => thread::sleep(Duration::from_millis(500)),
Err(err) => {
eprintln!("wait child failed: {err}");
break;
}
}
}
stop.store(true, Ordering::SeqCst);
let keep_payload = config.keep_payload.unwrap_or(false);
let extract_to = config.extract_to.clone().unwrap_or_else(|| "temp".to_string());
if !keep_payload || extract_to == "temp" {
let _ = fs::remove_file(&payload_path);
}
Ok(())
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct LauncherConfig {
api_base: String,
project_id: String,
project_secret: String,
client_version: Option<String>,
license_file: Option<String>,
card_key: Option<String>,
request_timeout_sec: Option<u64>,
heartbeat_retries: Option<u32>,
heartbeat_retry_delay_sec: Option<u64>,
extract_to: Option<String>,
payload_name: Option<String>,
keep_payload: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse<T> {
code: i32,
message: String,
data: Option<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VerifyData {
valid: bool,
access_token: Option<String>,
heartbeat_interval: i32,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct VerifyRequest<'a> {
project_id: &'a str,
key_code: &'a str,
device_id: &'a str,
client_version: Option<&'a str>,
timestamp: i64,
signature: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct HeartbeatRequest<'a> {
access_token: &'a str,
device_id: &'a str,
timestamp: i64,
signature: String,
}
struct VerifyResult {
access_token: Option<String>,
heartbeat_interval: i64,
}
fn read_embedded(exe_path: &Path) -> Result<(Vec<u8>, LauncherConfig), String> {
let bytes = fs::read(exe_path).map_err(|e| format!("read exe: {e}"))?;
if bytes.len() < FOOTER_LEN {
return Err("invalid launcher file".to_string());
}
let footer_offset = bytes.len() - FOOTER_LEN;
let magic = &bytes[footer_offset..footer_offset + MAGIC.len()];
if magic != MAGIC {
return Err("missing embedded payload".to_string());
}
let payload_len = read_u64(&bytes[footer_offset + MAGIC.len()..footer_offset + MAGIC.len() + 8])? as usize;
let config_len = read_u64(&bytes[footer_offset + MAGIC.len() + 8..footer_offset + MAGIC.len() + 16])? as usize;
let total_len = payload_len + config_len + FOOTER_LEN;
if total_len > bytes.len() {
return Err("embedded data corrupted".to_string());
}
let payload_start = bytes.len() - total_len;
let payload_end = payload_start + payload_len;
let config_end = payload_end + config_len;
let payload = bytes[payload_start..payload_end].to_vec();
let config_bytes = &bytes[payload_end..config_end];
let config: LauncherConfig = serde_json::from_slice(config_bytes)
.map_err(|e| format!("parse config: {e}"))?;
Ok((payload, config))
}
fn read_u64(slice: &[u8]) -> Result<u64, String> {
if slice.len() != 8 {
return Err("invalid footer".to_string());
}
let mut buf = [0u8; 8];
buf.copy_from_slice(slice);
Ok(u64::from_le_bytes(buf))
}
fn normalize_config(config: &mut LauncherConfig) -> Result<(), String> {
if config.api_base.trim().is_empty() {
return Err("api_base is empty".to_string());
}
config.api_base = config.api_base.trim_end_matches('/').to_string();
if config.project_id.trim().is_empty() {
return Err("project_id is empty".to_string());
}
if config.project_secret.trim().is_empty() {
return Err("project_secret is empty".to_string());
}
if config.request_timeout_sec.is_none() {
config.request_timeout_sec = Some(10);
}
if config.heartbeat_retries.is_none() {
config.heartbeat_retries = Some(2);
}
if config.heartbeat_retry_delay_sec.is_none() {
config.heartbeat_retry_delay_sec = Some(5);
}
if let Some(target) = config.extract_to.as_ref() {
config.extract_to = Some(target.trim().to_lowercase());
} else {
config.extract_to = Some("temp".to_string());
}
if config.payload_name.as_ref().map(|s| s.trim().is_empty()).unwrap_or(true) {
config.payload_name = Some("payload.exe".to_string());
}
if config.keep_payload.is_none() {
config.keep_payload = Some(false);
}
Ok(())
}
fn load_card_key(exe_dir: &Path, config: &LauncherConfig) -> Result<String, String> {
if let Some(card) = config.card_key.as_ref().filter(|s| !s.trim().is_empty()) {
return Ok(card.trim().to_string());
}
let license_file = config.license_file.clone().unwrap_or_else(|| "license.key".to_string());
let path = exe_dir.join(license_file);
let content = fs::read_to_string(&path).map_err(|_| "missing license.key".to_string())?;
let card = content
.lines()
.map(|line| line.trim())
.find(|line| !line.is_empty())
.ok_or("license.key is empty".to_string())?;
Ok(card.to_string())
}
fn resolve_device_id() -> String {
let mac = mac_address::get_mac_address()
.ok()
.flatten()
.map(|m| m.to_string())
.unwrap_or_else(|| "unknown".to_string());
let ip = local_ip_address::local_ip()
.ok()
.map(|addr| addr.to_string())
.unwrap_or_else(|| "unknown".to_string());
let raw = format!("{mac}|{ip}");
if raw.len() <= 64 {
raw
} else {
sha256_hex(raw.as_bytes())
}
}
fn verify_online(client: &Client, config: &LauncherConfig, card_key: &str, device_id: &str) -> Result<VerifyResult, String> {
let timestamp = unix_ts();
let payload = format!("{}|{}|{}", config.project_id, device_id, timestamp);
let signature = sign_hmac(&payload, &config.project_secret);
let request = VerifyRequest {
project_id: &config.project_id,
key_code: card_key,
device_id,
client_version: config.client_version.as_deref(),
timestamp,
signature,
};
let url = format!("{}/api/auth/verify", config.api_base);
let resp = client
.post(&url)
.header("X-Device-Id", device_id)
.json(&request)
.send()
.map_err(|e| format!("verify request: {e}"))?;
let text = resp.text().map_err(|e| format!("verify response: {e}"))?;
let api: ApiResponse<VerifyData> = serde_json::from_str(&text)
.map_err(|e| format!("verify decode: {e} ({text})"))?;
if api.code != 200 {
return Err(format!("verify failed: {}", api.message));
}
let data = api.data.ok_or("verify missing data".to_string())?;
if !data.valid {
return Err("license invalid".to_string());
}
Ok(VerifyResult {
access_token: data.access_token,
heartbeat_interval: data.heartbeat_interval as i64,
})
}
fn heartbeat_loop(client: &Client, config: &LauncherConfig, device_id: &str, access_token: &str, interval: u64, stop: Arc<AtomicBool>, failed: Arc<AtomicBool>) {
let retries = config.heartbeat_retries.unwrap_or(2);
let delay = config.heartbeat_retry_delay_sec.unwrap_or(5);
loop {
for _ in 0..interval {
if stop.load(Ordering::SeqCst) {
return;
}
thread::sleep(Duration::from_secs(1));
}
let mut ok = false;
for attempt in 0..=retries {
if stop.load(Ordering::SeqCst) {
return;
}
match send_heartbeat(client, config, device_id, access_token) {
Ok(_) => {
ok = true;
break;
}
Err(_) if attempt < retries => {
thread::sleep(Duration::from_secs(delay));
}
Err(_) => break,
}
}
if !ok {
failed.store(true, Ordering::SeqCst);
return;
}
}
}
fn send_heartbeat(client: &Client, config: &LauncherConfig, device_id: &str, access_token: &str) -> Result<(), String> {
let timestamp = unix_ts();
let payload = format!("{}|{}|{}", config.project_id, device_id, timestamp);
let signature = sign_hmac(&payload, &config.project_secret);
let request = HeartbeatRequest {
access_token,
device_id,
timestamp,
signature,
};
let url = format!("{}/api/auth/heartbeat", config.api_base);
let resp = client
.post(&url)
.header("X-Device-Id", device_id)
.json(&request)
.send()
.map_err(|e| format!("heartbeat request: {e}"))?;
let text = resp.text().map_err(|e| format!("heartbeat response: {e}"))?;
let api: ApiResponse<serde_json::Value> = serde_json::from_str(&text)
.map_err(|e| format!("heartbeat decode: {e} ({text})"))?;
if api.code != 200 {
return Err(format!("heartbeat failed: {}", api.message));
}
Ok(())
}
fn extract_payload(payload: &[u8], config: &LauncherConfig, exe_dir: &Path) -> Result<PathBuf, String> {
let payload_name = config.payload_name.clone().unwrap_or_else(|| "payload.exe".to_string());
let safe_name = Path::new(&payload_name)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("payload.exe")
.to_string();
let extract_to = config.extract_to.as_deref().unwrap_or("temp").to_lowercase();
let target_path = if extract_to == "self" {
exe_dir.join(safe_name)
} else {
let mut rng = rand::thread_rng();
let suffix: u32 = rng.gen();
let (stem, ext) = split_name(&safe_name);
let file_name = if ext.is_empty() {
format!("{stem}_{suffix}")
} else {
format!("{stem}_{suffix}.{ext}")
};
env::temp_dir().join(file_name)
};
fs::write(&target_path, payload).map_err(|e| format!("write payload: {e}"))?;
Ok(target_path)
}
fn split_name(name: &str) -> (String, String) {
let path = Path::new(name);
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("payload")
.to_string();
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("").to_string();
(stem, ext)
}
fn unix_ts() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs() as i64
}
fn sign_hmac(payload: &str, secret: &str) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.expect("hmac can take key of any size");
mac.update(payload.as_bytes());
let result = mac.finalize().into_bytes();
to_hex_lower(&result)
}
fn to_hex_lower(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}
fn sha256_hex(data: &[u8]) -> String {
let hash = Sha256::digest(data);
to_hex_lower(&hash)
}