Compare commits
19 Commits
70311b2a0e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f47b280634 | |||
| 67db5c7a11 | |||
| 4a75e30671 | |||
| 68649ba214 | |||
| b052e82358 | |||
| 667d3c205a | |||
| adbf40240c | |||
| 6232b79ffa | |||
| 043526229e | |||
| 98e0f594df | |||
| 2be258380f | |||
| c6a8d7c804 | |||
| 06f0e5eb86 | |||
| 5402524456 | |||
| ced142e044 | |||
| 13ec20a19f | |||
| 2c16bea045 | |||
| e577e6a90f | |||
| 69fcabb4b9 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -119,8 +119,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|||||||
*.html linguist-language=HTML
|
*.html linguist-language=HTML
|
||||||
*.css linguist-language=CSS
|
*.css linguist-language=CSS
|
||||||
*.js linguist-language=JavaScript
|
*.js linguist-language=JavaScript
|
||||||
*.json linguist-language=JSON
|
#*.json linguist-language=JSON
|
||||||
*.md linguist-language=Markdown
|
#*.md linguist-language=Markdown
|
||||||
*.yml linguist-language=YAML
|
*.yml linguist-language=YAML
|
||||||
*.yaml linguist-language=YAML
|
*.yaml linguist-language=YAML
|
||||||
*.c linguist-language=C
|
*.c linguist-language=C
|
||||||
|
|||||||
43
.gitea/workflows/ci.yaml
Normal file
43
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r backend/requirements.txt
|
||||||
|
|
||||||
|
- name: Lint backend
|
||||||
|
run: |
|
||||||
|
pip install flake8
|
||||||
|
flake8 backend
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Check frontend files
|
||||||
|
run: |
|
||||||
|
test -f frontend/templates/index.html
|
||||||
|
test -f frontend/css/style.css
|
||||||
|
test -f frontend/js/main.js
|
||||||
40
README.md
40
README.md
@@ -1,6 +1,6 @@
|
|||||||
# MobaXterm Activator
|
# MobaXterm Activator
|
||||||
|
|
||||||
**Автор:** [icysanta](https://github.com/icysanta) (или ваш GitHub, если нужно изменить)
|
**Автор:** [icysanta](https://github.com/icysanta)
|
||||||
|
|
||||||
## 📄 Описание
|
## 📄 Описание
|
||||||
**MobaXterm Activator** — это веб-приложение для генерации лицензий для MobaXterm. Проект сочетает backend на **FastAPI** (Python) для API-логики и frontend на **HTML**, **CSS**, **JavaScript** для пользовательского интерфейса. Используется **Docker** для контейнеризации, что упрощает развертывание и тестирование.
|
**MobaXterm Activator** — это веб-приложение для генерации лицензий для MobaXterm. Проект сочетает backend на **FastAPI** (Python) для API-логики и frontend на **HTML**, **CSS**, **JavaScript** для пользовательского интерфейса. Используется **Docker** для контейнеризации, что упрощает развертывание и тестирование.
|
||||||
@@ -58,6 +58,44 @@
|
|||||||
|
|
||||||
3. Откройте в браузере: `http://localhost:8000`.
|
3. Откройте в браузере: `http://localhost:8000`.
|
||||||
|
|
||||||
|
### Запуск в Docker Compose
|
||||||
|
1. Создайте директорию:
|
||||||
|
```
|
||||||
|
sudo mkdir -p /opt/mobaxterm-activator
|
||||||
|
sudo cd /opt/mobaxterm-activator
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Создайте docker-compose.yml файл:
|
||||||
|
```
|
||||||
|
sudo tee /opt/mobaxterm-activator/docker-compose.yml > /dev/null <<'EOF'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: whyverum/mobax_panel:latest
|
||||||
|
container_name: mobax_panel_app
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 80
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:80/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
networks: # Для использования с NGINX PROXY MANAGER
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Запустите и настройте адрессацию.
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
## 📝 Использование
|
## 📝 Использование
|
||||||
1. На главной странице (`index.html`) введите имя пользователя и версию MobaXterm (формат: X.Y, например, 25.3).
|
1. На главной странице (`index.html`) введите имя пользователя и версию MobaXterm (формат: X.Y, например, 25.3).
|
||||||
2. Нажмите "Generate" — скачается файл `Custom.mxtpro`.
|
2. Нажмите "Generate" — скачается файл `Custom.mxtpro`.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""
|
"""
|
||||||
Конфигурация FastAPI приложения.
|
Конфигурация FastAPI приложения.
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ from fastapi.responses import FileResponse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Импорт роутера
|
# Импорт роутера
|
||||||
from .routes.license_routes import router as license_router # Изменено: один dot вместо двух
|
from .routes.license_routes import router as license_router
|
||||||
|
|
||||||
# Путь к корню проекта
|
# Путь к корню проекта
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""
|
"""
|
||||||
Создание и конфигурация FastAPI приложения.
|
Создание и конфигурация FastAPI приложения.
|
||||||
@@ -19,23 +20,38 @@ def create_app() -> FastAPI:
|
|||||||
description="API для генерации лицензий Custom.mxtpro",
|
description="API для генерации лицензий Custom.mxtpro",
|
||||||
version="1.0.0"
|
version="1.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Подключение роутеров
|
# Подключение роутеров
|
||||||
apps.include_router(license_router, prefix="/api")
|
apps.include_router(license_router, prefix="/api")
|
||||||
|
|
||||||
# Монтируем статические папки фронтенда
|
# Монтируем статические папки фронтенда
|
||||||
apps.mount("/static/css", StaticFiles(directory=BASE_DIR.parent / "frontend" / "css"), name="css")
|
apps.mount(
|
||||||
apps.mount("/static/js", StaticFiles(directory=BASE_DIR.parent / "frontend" / "js"), name="js")
|
"/static/css",
|
||||||
apps.mount("/static/assets", StaticFiles(directory=BASE_DIR.parent / "frontend" / "assets"), name="assets")
|
StaticFiles(directory=BASE_DIR.parent / "frontend" / "css"),
|
||||||
|
name="css"
|
||||||
|
)
|
||||||
|
apps.mount(
|
||||||
|
"/static/js",
|
||||||
|
StaticFiles(directory=BASE_DIR.parent / "frontend" / "js"),
|
||||||
|
name="js"
|
||||||
|
)
|
||||||
|
apps.mount(
|
||||||
|
"/static/assets",
|
||||||
|
StaticFiles(directory=BASE_DIR.parent / "frontend" / "assets"),
|
||||||
|
name="assets"
|
||||||
|
)
|
||||||
|
|
||||||
# Отдача главной страницы
|
# Отдача главной страницы
|
||||||
@apps.get("/", include_in_schema=False)
|
@apps.get("/", include_in_schema=False)
|
||||||
def read_index() -> FileResponse:
|
def read_index() -> FileResponse:
|
||||||
"""
|
"""
|
||||||
Отдает главную страницу сайта (index.html)
|
Отдает главную страницу сайта (index.html)
|
||||||
"""
|
"""
|
||||||
return FileResponse(BASE_DIR.parent / "frontend" / "templates" / "index.html")
|
index_file = BASE_DIR.parent / "frontend" / "templates" / "index.html"
|
||||||
|
return FileResponse(index_file)
|
||||||
|
|
||||||
return apps
|
return apps
|
||||||
|
|
||||||
|
|
||||||
# Экземпляр приложения
|
# Экземпляр приложения
|
||||||
app: FastAPI = create_app()
|
app: FastAPI = create_app()
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from .license_routes import *
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from ..services.license_service import generate_license_file
|
|||||||
|
|
||||||
router: APIRouter = APIRouter()
|
router: APIRouter = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate", response_class=FileResponse)
|
@router.post("/generate", response_class=FileResponse)
|
||||||
async def generate_license(
|
async def generate_license(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
@@ -20,17 +21,27 @@ async def generate_license(
|
|||||||
- FileResponse: сгенерированный ZIP файл с именем Custom.mxtpro
|
- FileResponse: сгенерированный ZIP файл с именем Custom.mxtpro
|
||||||
"""
|
"""
|
||||||
if not name.strip() or not version.strip():
|
if not name.strip() or not version.strip():
|
||||||
raise HTTPException(status_code=400, detail="NAME и VERSION обязательны.")
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="NAME и VERSION обязательны."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
# Создаём временный файл на сервере
|
# Создаём временный файл на сервере
|
||||||
filepath, temp_dir = generate_license_file(name, version)
|
filepath, temp_dir = generate_license_file(name, version)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Ошибка генерации лицензии: {e}")
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Ошибка генерации лицензии: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
# Удаляем директорию (и файл внутри) после отправки пользователю
|
# Удаляем директорию (и файл внутри) после отправки пользователю
|
||||||
background_tasks.add_task(temp_dir.cleanup)
|
background_tasks.add_task(temp_dir.cleanup)
|
||||||
|
|
||||||
# Отправляем с фиксированным именем Custom.mxtpro
|
# Отправляем с фиксированным именем Custom.mxtpro
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
filepath,
|
filepath,
|
||||||
media_type="application/octet-stream",
|
media_type="application/octet-stream",
|
||||||
headers={"Content-Disposition": 'attachment; filename="Custom.mxtpro"'}
|
headers={
|
||||||
|
"Content-Disposition": 'attachment; filename="Custom.mxtpro"'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from .license_service import *
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ from pathlib import Path
|
|||||||
from ..utils.encoding import variant_base64_encode
|
from ..utils.encoding import variant_base64_encode
|
||||||
from ..utils.crypto import encrypt_bytes
|
from ..utils.crypto import encrypt_bytes
|
||||||
|
|
||||||
|
|
||||||
class LicenseType:
|
class LicenseType:
|
||||||
Professional: int = 1
|
Professional: int = 1
|
||||||
Educational: int = 3
|
Educational: int = 3
|
||||||
Personal: int = 4
|
Personal: int = 4
|
||||||
|
|
||||||
|
|
||||||
def generate_license_file(
|
def generate_license_file(
|
||||||
user_name: str,
|
user_name: str,
|
||||||
version: str,
|
version: str,
|
||||||
@@ -26,18 +28,21 @@ def generate_license_file(
|
|||||||
minor_version: int = int(minor_str)
|
minor_version: int = int(minor_str)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Неверный формат версии: {version}") from e
|
raise ValueError(f"Неверный формат версии: {version}") from e
|
||||||
# Формирование строки лицензии (исправлено: убрали лишний #, интегрировали цифры в один блок)
|
|
||||||
|
# Формирование строки лицензии
|
||||||
license_str: str = (
|
license_str: str = (
|
||||||
f"{lic_type}#{user_name}|{major_version}{minor_version}"
|
f"{lic_type}#{user_name}|{major_version}{minor_version}"
|
||||||
f"#{count}#{major_version}3{minor_version}6{minor_version}#0#0#0#"
|
f"#{count}#{major_version}3{minor_version}6{minor_version}#0#0#0#"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Шифрование и кодирование
|
# Шифрование и кодирование
|
||||||
encrypted: bytes = encrypt_bytes(0x0787, license_str.encode("utf-8"))
|
encrypted: bytes = encrypt_bytes(0x0787, license_str.encode("utf-8"))
|
||||||
encoded: str = variant_base64_encode(encrypted).decode("ascii")
|
encoded: str = variant_base64_encode(encrypted).decode("ascii")
|
||||||
|
|
||||||
# Создание временной директории и файла с фиксированным именем
|
# Создание временной директории и файла с фиксированным именем
|
||||||
temp_dir = tempfile.TemporaryDirectory()
|
temp_dir = tempfile.TemporaryDirectory()
|
||||||
filepath = Path(temp_dir.name) / "Custom.mxtpro"
|
filepath = Path(temp_dir.name) / "Custom.mxtpro"
|
||||||
with ZipFile(filepath, "w") as zf:
|
with ZipFile(filepath, "w") as zf:
|
||||||
zf.writestr("Pro.key", encoded)
|
zf.writestr("Pro.key", encoded)
|
||||||
return str(filepath), temp_dir
|
|
||||||
|
return str(filepath), temp_dir
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
from .crypto import *
|
|
||||||
from .encoding import *
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
# Таблица Base64-подобного варианта
|
# Таблица Base64-подобного варианта
|
||||||
VariantBase64Table: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
|
VariantBase64Table: str = (
|
||||||
VariantBase64Dict: Dict[int, str] = {i: VariantBase64Table[i] for i in range(len(VariantBase64Table))}
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
|
||||||
VariantBase64ReverseDict: Dict[str, int] = {VariantBase64Table[i]: i for i in range(len(VariantBase64Table))}
|
)
|
||||||
|
VariantBase64Dict: Dict[int, str] = {
|
||||||
|
i: VariantBase64Table[i] for i in range(len(VariantBase64Table))
|
||||||
|
}
|
||||||
|
VariantBase64ReverseDict: Dict[str, int] = {
|
||||||
|
VariantBase64Table[i]: i for i in range(len(VariantBase64Table))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def variant_base64_encode(bs: bytes) -> bytes:
|
def variant_base64_encode(bs: bytes) -> bytes:
|
||||||
@@ -24,7 +30,8 @@ def variant_base64_encode(bs: bytes) -> bytes:
|
|||||||
b: bytes = bs[3*i:3*i+3]
|
b: bytes = bs[3*i:3*i+3]
|
||||||
coding_int: int = int.from_bytes(b, "little")
|
coding_int: int = int.from_bytes(b, "little")
|
||||||
block: str = "".join(
|
block: str = "".join(
|
||||||
VariantBase64Dict[(coding_int >> shift) & 0x3F] for shift in (0, 6, 12, 18)
|
VariantBase64Dict[(coding_int >> shift) & 0x3F]
|
||||||
|
for shift in (0, 6, 12, 18)
|
||||||
)
|
)
|
||||||
result.extend(block.encode("ascii"))
|
result.extend(block.encode("ascii"))
|
||||||
|
|
||||||
@@ -33,7 +40,8 @@ def variant_base64_encode(bs: bytes) -> bytes:
|
|||||||
b: bytes = bs[-left_bytes:]
|
b: bytes = bs[-left_bytes:]
|
||||||
coding_int: int = int.from_bytes(b, "little")
|
coding_int: int = int.from_bytes(b, "little")
|
||||||
block: str = "".join(
|
block: str = "".join(
|
||||||
VariantBase64Dict[(coding_int >> shift) & 0x3F] for shift in range(0, left_bytes*8 + 1, 6)
|
VariantBase64Dict[(coding_int >> shift) & 0x3F]
|
||||||
|
for shift in range(0, left_bytes * 8 + 1, 6)
|
||||||
)
|
)
|
||||||
result.extend(block.encode("ascii"))
|
result.extend(block.encode("ascii"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user