feat: 完成了基于 FastAPI 的重制,重新写了 web 页面

This commit is contained in:
MeowCracker 2025-04-04 18:12:23 +08:00
commit a6efc64896
5 changed files with 332 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
venv

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# MobaGenkey
你应该知道这是干啥的
## 开始使用
先安装环境
```shell
$ pip install -r requirements.txt
```
然后直接运行
```shell
$ python app.py
```
打开浏览器访问控制台展示的地址(例如[http://127.0.0.1:8000](http://127.0.0.1:8000)),按照网页的指示自己玩

155
app.py Normal file
View File

@ -0,0 +1,155 @@
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import FileResponse, StreamingResponse, HTMLResponse
import os
import zipfile
from io import BytesIO
app = FastAPI()
VariantBase64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
VariantBase64Dict = {i: VariantBase64Table[i] for i in range(len(VariantBase64Table))}
VariantBase64ReverseDict = {
VariantBase64Table[i]: i for i in range(len(VariantBase64Table))
}
def VariantBase64Encode(bs: bytes):
result = b""
blocks_count, left_bytes = divmod(len(bs), 3)
for i in range(blocks_count):
coding_int = int.from_bytes(bs[3 * i : 3 * i + 3], "little")
block = VariantBase64Dict[coding_int & 0x3F]
block += VariantBase64Dict[(coding_int >> 6) & 0x3F]
block += VariantBase64Dict[(coding_int >> 12) & 0x3F]
block += VariantBase64Dict[(coding_int >> 18) & 0x3F]
result += block.encode()
if left_bytes == 0:
return result
elif left_bytes == 1:
coding_int = int.from_bytes(bs[3 * blocks_count :], "little")
block = VariantBase64Dict[coding_int & 0x3F]
block += VariantBase64Dict[(coding_int >> 6) & 0x3F]
result += block.encode()
return result
else:
coding_int = int.from_bytes(bs[3 * blocks_count :], "little")
block = VariantBase64Dict[coding_int & 0x3F]
block += VariantBase64Dict[(coding_int >> 6) & 0x3F]
block += VariantBase64Dict[(coding_int >> 12) & 0x3F]
result += block.encode()
return result
def VariantBase64Decode(s: str):
result = b""
blocks_count, left_bytes = divmod(len(s), 4)
for i in range(blocks_count):
block = VariantBase64ReverseDict[s[4 * i]]
block += VariantBase64ReverseDict[s[4 * i + 1]] << 6
block += VariantBase64ReverseDict[s[4 * i + 2]] << 12
block += VariantBase64ReverseDict[s[4 * i + 3]] << 18
result += block.to_bytes(3, "little")
if left_bytes == 0:
return result
elif left_bytes == 2:
block = VariantBase64ReverseDict[s[4 * blocks_count]]
block += VariantBase64ReverseDict[s[4 * blocks_count + 1]] << 6
result += block.to_bytes(1, "little")
return result
elif left_bytes == 3:
block = VariantBase64ReverseDict[s[4 * blocks_count]]
block += VariantBase64ReverseDict[s[4 * blocks_count + 1]] << 6
block += VariantBase64ReverseDict[s[4 * blocks_count + 2]] << 12
result += block.to_bytes(2, "little")
return result
else:
raise ValueError("Invalid encoding.")
def EncryptBytes(key: int, bs: bytes):
result = bytearray()
for i in range(len(bs)):
result.append(bs[i] ^ ((key >> 8) & 0xFF))
key = result[-1] & key | 0x482D
return bytes(result)
def DecryptBytes(key: int, bs: bytes):
result = bytearray()
for i in range(len(bs)):
result.append(bs[i] ^ ((key >> 8) & 0xFF))
key = bs[i] & key | 0x482D
return bytes(result)
class LicenseType:
Professional = 1
Educational = 3
Persional = 4
class LicenseType:
Professional = 1
Educational = 3
Persional = 4
def generate_license_bytes(
Type: LicenseType, Count: int, UserName: str, MajorVersion: int, MinorVersion: int
):
assert Count >= 0
LicenseString = f"{Type}#{UserName}|{MajorVersion}{MinorVersion}#{Count}#{MajorVersion}3{MinorVersion}6{MinorVersion}#0#0#0#"
EncodedLicenseString = VariantBase64Encode(
EncryptBytes(0x787, LicenseString.encode())
).decode()
FileName = EncodedLicenseString.replace("/", "").replace("\\", "")
# 使用内存文件
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr("Pro.key", EncodedLicenseString)
zip_buffer.seek(0)
return (FileName, zip_buffer)
@app.get("/api/generate")
async def generate_license(
name: str = Query(..., min_length=1),
ver: str = Query(..., regex=r"^\d+\.\d+$"),
count: int = Query(1, ge=1),
):
try:
MajorVersion, MinorVersion = map(int, ver.split(".")[:2])
except ValueError:
raise HTTPException(status_code=400, detail="Invalid version format")
try:
filename, file_data = generate_license_bytes(
LicenseType.Professional, count, name, MajorVersion, MinorVersion
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return StreamingResponse(
content=file_data,
media_type="application/zip",
headers={
"Content-Disposition": f"attachment; filename=Custom.mxtpro",
"Content-Type": "application/zip",
},
)
@app.get("/", response_class=HTMLResponse)
async def index():
return FileResponse("templates/index.html")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
uvicorn
fastapi

155
templates/index.html Normal file
View File

@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>License Generator</title>
<script src="https://unpkg.com/sober@latest/dist/sober.min.js"></script>
<style>
s-page {
--s-color-primary: #5b53a8;
--s-color-on-primary: #ffffff;
--s-color-primary-container: #e4dfff;
--s-color-on-primary-container: #150362;
--s-color-secondary: #5e5c71;
--s-color-on-secondary: #ffffff;
--s-color-secondary-container: #e4dff9;
--s-color-on-secondary-container: #1b192c;
--s-color-tertiary: #7b5266;
--s-color-on-tertiary: #ffffff;
--s-color-tertiary-container: #ffd8e8;
--s-color-on-tertiary-container: #2f1122;
--s-color-error: #ba1a1a;
--s-color-on-error: #ffffff;
--s-color-error-container: #ffdad6;
--s-color-on-error-container: #410002;
--s-color-background: #fffbff;
--s-color-on-background: #1c1b1f;
--s-color-outline: #787680;
--s-color-outline-variant: #c8c5d0;
--s-color-surface: #fffbff;
--s-color-on-surface: #1c1b1f;
--s-color-surface-variant: #e5e1ec;
--s-color-on-surface-variant: #47464f;
--s-color-inverse-surface: #313034;
--s-color-inverse-on-surface: #f4eff4;
--s-color-inverse-primary: #c6c0ff;
--s-color-surface-container: #f1ecf1;
--s-color-surface-container-lowest: #e5e1e6;
--s-color-surface-container-low: #ebe7ec;
--s-color-surface-container-high: #f7f2f7;
--s-color-surface-container-highest: #ffffff;
--s-color-dark-primary: #c6c0ff;
--s-color-dark-on-primary: #2c2276;
--s-color-dark-primary-container: #433b8e;
--s-color-dark-on-primary-container: #e4dfff;
--s-color-dark-secondary: #c8c3dc;
--s-color-dark-on-secondary: #302e41;
--s-color-dark-secondary-container: #474459;
--s-color-dark-on-secondary-container: #e4dff9;
--s-color-dark-tertiary: #ebb8cf;
--s-color-dark-on-tertiary: #482537;
--s-color-dark-tertiary-container: #613b4e;
--s-color-dark-on-tertiary-container: #ffd8e8;
--s-color-dark-error: #ffb4ab;
--s-color-dark-on-error: #690005;
--s-color-dark-error-container: #93000a;
--s-color-dark-on-error-container: #ffb4ab;
--s-color-dark-background: #1c1b1f;
--s-color-dark-on-background: #e5e1e6;
--s-color-dark-outline: #928f99;
--s-color-dark-outline-variant: #47464f;
--s-color-dark-surface: #1c1b1f;
--s-color-dark-on-surface: #e5e1e6;
--s-color-dark-surface-variant: #47464f;
--s-color-dark-on-surface-variant: #c8c5d0;
--s-color-dark-inverse-surface: #e5e1e6;
--s-color-dark-inverse-on-surface: #313034;
--s-color-dark-inverse-primary: #5b53a8;
--s-color-dark-surface-container: #201f23;
--s-color-dark-surface-container-lowest: #0e0e11;
--s-color-dark-surface-container-low: #1c1b1f;
--s-color-dark-surface-container-high: #2a292d;
--s-color-dark-surface-container-highest: #353438;
}
</style>
</head>
<body>
<s-page theme="auto">
<!--抽屉布局-->
<s-drawer>
<!--应用栏-->
<s-appbar>
<!--应用栏-标题-->
<div slot="headline"> MobaXterm Key Generator </div>
</s-appbar>
<!--主视图-->
<s-scroll-view style="flex-grow: 1">
<main style="height: 100%;display: flex;">
<s-card
style="margin-left: 15%; max-width: 70%; width: 100%; padding: 16px; margin-top: 16px; margin-bottom: 16px; ">
<div slot="headline">欢迎使用许可证生成器</div>
<div slot="text">
<p>请填写以下信息以生成许可证。</p>
<p>注意:生成的许可证仅供学习和测试使用,请勿用于商业目的。</p>
<a href="https://github.com/MeowCracker/MobaGenkey">https://github.com/MeowCracker/MobaGenkey</a>
<s-text-field label="授权用户" id="licenseUser" style="width: 100%; margin-top: 16px;">
</s-text-field>
<s-text-field label="授权版本 (如 25.1)" id="licenseVersion"
style="width: 100%; margin-top: 16px;"></s-text-field>
<s-text-field label="授权用户数量(默认为 1" id="licenseUserCount" type="number"
style="width: 100%; margin-top: 16px;"></s-text-field>
<div align="right" style="margin-top: 16px;">
<s-button id="generateBtn">生成并保存</s-button>
</div>
</div>
</s-card>
</main>
</s-scroll-view>
</s-drawer>
</s-page>
<script>
document.getElementById("generateBtn").addEventListener("click", async () => {
const licenseUser = document.getElementById("licenseUser").value.trim();
const licenseVersion = document.getElementById("licenseVersion").value.trim();
const licenseUserCount = document.getElementById("licenseUserCount").value.trim() || 1;
if(!licenseUser || !licenseVersion || !licenseUserCount) {
alert("你需要正确填写授权用户和授权版本!");
return;
}
try {
const response = await fetch(`/api/generate?name=${encodeURIComponent(licenseUser)}&ver=${encodeURIComponent(licenseVersion)}&count=${encodeURIComponent(licenseUserCount)}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
const error = await response.json();
alert(`Error: ${error.detail}`);
return;
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "Custom.mxtpro";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Error generating license:", error);
alert("在生成许可证的时候出现了问题,请检查网络连接或者查看日志!");
}
});
</script>
</body>
</html>