Spaces:
Paused
Paused
Upload 5 files
Browse files- README.md +49 -6
- app.py +949 -0
- license_server.py +68 -0
- packages.txt +5 -0
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -1,13 +1,56 @@
|
|
| 1 |
---
|
| 2 |
title: HarshItPDF
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: HarshItPDF
|
| 3 |
+
emoji: 🖥️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "5.23.3"
|
| 8 |
+
python_version: "3.12"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# HarshItPDF — HF Spaces Gradio (Free Tier)
|
| 14 |
+
|
| 15 |
+
HarshItPDF is a rebranded, fully-unlocked Stirling-PDF deployed on Hugging Face Spaces Free Tier using Gradio.
|
| 16 |
+
|
| 17 |
+
## What you get
|
| 18 |
+
|
| 19 |
+
- **All premium / enterprise features enabled** (3-layer bytecode patch + mock license server)
|
| 20 |
+
- **OCR support** (Tesseract pre-installed via `packages.txt`)
|
| 21 |
+
- **Gradio splash screen** with JavaScript auto-redirect to the Java backend
|
| 22 |
+
- **No credit card required**
|
| 23 |
+
|
| 24 |
+
## Default login
|
| 25 |
+
|
| 26 |
+
- **Username:** `admin`
|
| 27 |
+
- **Password:** `changeme123`
|
| 28 |
+
|
| 29 |
+
## How to deploy
|
| 30 |
+
|
| 31 |
+
1. Create a new **HF Space** → choose **Gradio** as the SDK
|
| 32 |
+
2. Upload these files to the repo root:
|
| 33 |
+
- `app.py`
|
| 34 |
+
- `license_server.py`
|
| 35 |
+
- `requirements.txt`
|
| 36 |
+
- `packages.txt`
|
| 37 |
+
- `README.md`
|
| 38 |
+
3. HF builds the environment automatically (~3–5 minutes)
|
| 39 |
+
4. Once the Space status shows **Running**, click the **Open** button
|
| 40 |
+
5. The splash screen will auto-redirect to the app once the Java backend is ready (~30–60 seconds)
|
| 41 |
+
|
| 42 |
+
## Files
|
| 43 |
+
|
| 44 |
+
| File | Purpose |
|
| 45 |
+
|---|---|
|
| 46 |
+
| `app.py` | Runtime patcher + orchestrator + Gradio UI |
|
| 47 |
+
| `license_server.py` | Mock Keygen API on port 7871 |
|
| 48 |
+
| `requirements.txt` | Python dependencies (gradio) |
|
| 49 |
+
| `packages.txt` | System packages (Java 17 JDK, Tesseract, unzip, wget) |
|
| 50 |
+
|
| 51 |
+
## Important notes
|
| 52 |
+
|
| 53 |
+
- **Cold start:** Free CPU Spaces hibernate after ~48 hours of inactivity. First visit after sleep triggers a ~1–2 minute cold start (JAR download + extraction + patching + Java startup).
|
| 54 |
+
- **While awake:** The app stays responsive. The Gradio UI proxies to the Java backend via HF Spaces' internal port proxy (`/proxy/7870/`).
|
| 55 |
+
- **OCR languages:** English is pre-installed. Add more `tesseract-ocr-*` packages in `packages.txt` if needed.
|
| 56 |
+
- **Premium license:** Three independent patches ensure Enterprise status — bytecode flip (`iconst_0→1`), `LicenseKeyChecker` getter override, and Spring bean `runningProOrHigher()→true`.
|
app.py
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HarshItPDF — HF Spaces Gradio deployment (Free Tier).
|
| 3 |
+
Build-time (packages.txt): installs Java 17 JDK, Tesseract, unzip, wget.
|
| 4 |
+
Runtime: downloads JAR, patches bytecode, starts mock license + Java backend.
|
| 5 |
+
Gradio UI: splash screen with JS auto-redirect to /proxy/{BACKEND_PORT}/
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
import json
|
| 12 |
+
import shutil
|
| 13 |
+
import struct
|
| 14 |
+
import glob
|
| 15 |
+
import re
|
| 16 |
+
import threading
|
| 17 |
+
import subprocess
|
| 18 |
+
import urllib.request
|
| 19 |
+
import zipfile
|
| 20 |
+
import logging
|
| 21 |
+
|
| 22 |
+
logging.basicConfig(
|
| 23 |
+
level=logging.INFO,
|
| 24 |
+
format="[%(name)s] %(levelname)s %(message)s",
|
| 25 |
+
)
|
| 26 |
+
logger = logging.getLogger("harshitpdf")
|
| 27 |
+
|
| 28 |
+
# ═══════════════════════════════════════════════════════════════
|
| 29 |
+
# Configuration
|
| 30 |
+
# ═══════════════════════════════════════════════════════════════
|
| 31 |
+
PUBLIC_PORT = int(os.environ.get("PORT", "7860"))
|
| 32 |
+
BACKEND_PORT = int(os.environ.get("BACKEND_PORT", "7870"))
|
| 33 |
+
LICENCE_SERVER_PORT = int(os.environ.get("LICENCE_SERVER_PORT", "7871"))
|
| 34 |
+
|
| 35 |
+
RELEASE_TAG = "v2.11.0"
|
| 36 |
+
JAR_URL = (
|
| 37 |
+
f"https://github.com/Stirling-Tools/Stirling-PDF/releases/download/"
|
| 38 |
+
f"{RELEASE_TAG}/Stirling-PDF-with-login.jar"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
JAR_PATH = "Stirling-PDF-with-login.jar"
|
| 42 |
+
EXTRACT_DIR = "jar-extracted"
|
| 43 |
+
STATIC_DIR = os.path.join(EXTRACT_DIR, "BOOT-INF", "classes", "static")
|
| 44 |
+
ASSETS_DIR = os.path.join(STATIC_DIR, "assets")
|
| 45 |
+
CLASSES_DIR = os.path.join(EXTRACT_DIR, "BOOT-INF", "classes")
|
| 46 |
+
LIB_DIR = os.path.join(EXTRACT_DIR, "BOOT-INF", "lib")
|
| 47 |
+
CONFIGS_DIR = "configs"
|
| 48 |
+
PATCHED_FLAG = os.path.join(EXTRACT_DIR, ".harshitpdf-patched-v9")
|
| 49 |
+
|
| 50 |
+
BRAND_REPLACEMENTS = [
|
| 51 |
+
("Stirling PDF", "HarshItPDF"),
|
| 52 |
+
("Stirling-PDF", "HarshItPDF"),
|
| 53 |
+
("StirlingPDF", "HarshItPDF"),
|
| 54 |
+
("stirling-pdf", "harshitpdf"),
|
| 55 |
+
("stirlingpdf", "harshitpdf"),
|
| 56 |
+
("Stirling Tools", "HarshIt"),
|
| 57 |
+
("Stirling-Tools", "HarshIt"),
|
| 58 |
+
("Stirling Tools ", "HarshIt "),
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
# ═══════════════════════════════════════════════════════════════
|
| 62 |
+
# Branding helper
|
| 63 |
+
# ═══════════════════════════════════════════════════════════════
|
| 64 |
+
|
| 65 |
+
def _brand(text):
|
| 66 |
+
for old, new in BRAND_REPLACEMENTS:
|
| 67 |
+
text = text.replace(old, new)
|
| 68 |
+
return text
|
| 69 |
+
|
| 70 |
+
# ═══════════════════════════════════════════════════════════════
|
| 71 |
+
# Import mock license server
|
| 72 |
+
# ═══════════════════════════════════════════════════════════════
|
| 73 |
+
|
| 74 |
+
SELF_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 75 |
+
sys.path.insert(0, SELF_DIR)
|
| 76 |
+
from license_server import start_license_server
|
| 77 |
+
|
| 78 |
+
# ═══════════════════════════════════════════════════════════════
|
| 79 |
+
# JAR lifecycle
|
| 80 |
+
# ═══════════════════════════════════════════════════════════════
|
| 81 |
+
|
| 82 |
+
def _download_jar():
|
| 83 |
+
if os.path.exists(JAR_PATH):
|
| 84 |
+
return
|
| 85 |
+
logger.info(f"Downloading {JAR_URL} ...")
|
| 86 |
+
urllib.request.urlretrieve(JAR_URL, JAR_PATH)
|
| 87 |
+
logger.info("Download complete.")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _extract_jar():
|
| 91 |
+
if os.path.exists(os.path.join(EXTRACT_DIR, "BOOT-INF")):
|
| 92 |
+
if not os.path.exists(PATCHED_FLAG):
|
| 93 |
+
logger.info("Patch version changed — re-extracting JAR...")
|
| 94 |
+
shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
|
| 95 |
+
else:
|
| 96 |
+
return
|
| 97 |
+
logger.info("Extracting JAR...")
|
| 98 |
+
try:
|
| 99 |
+
subprocess.run(
|
| 100 |
+
["unzip", "-q", "-o", JAR_PATH, "-d", EXTRACT_DIR],
|
| 101 |
+
check=True, timeout=120,
|
| 102 |
+
)
|
| 103 |
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
| 104 |
+
with zipfile.ZipFile(JAR_PATH) as zf:
|
| 105 |
+
zf.extractall(EXTRACT_DIR)
|
| 106 |
+
logger.info("Extraction complete.")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _get_main_class():
|
| 110 |
+
with zipfile.ZipFile(JAR_PATH) as zf:
|
| 111 |
+
manifest = zf.read("META-INF/MANIFEST.MF").decode()
|
| 112 |
+
for line in manifest.split("\n"):
|
| 113 |
+
s = line.strip()
|
| 114 |
+
if s.startswith("Main-Class:"):
|
| 115 |
+
return s.split(":", 1)[1].strip()
|
| 116 |
+
return "org.springframework.boot.loader.launch.JarLauncher"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ═══════════════════════════════════════════════════════════════
|
| 120 |
+
# Constant-pool parser (robust)
|
| 121 |
+
# ═══════════════════════════════════════════════════════════════
|
| 122 |
+
|
| 123 |
+
CP_TAG_SIZES = {
|
| 124 |
+
1: None, 2: None,
|
| 125 |
+
3: 4, 4: 4, 5: 8, 6: 8,
|
| 126 |
+
7: 2, 8: 2, 16: 2, 19: 2, 20: 2,
|
| 127 |
+
9: 4, 10: 4, 11: 4, 12: 4, 17: 4, 18: 4,
|
| 128 |
+
15: 3,
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _parse_constant_pool(class_data):
|
| 133 |
+
if len(class_data) < 10:
|
| 134 |
+
return [], 10
|
| 135 |
+
cp_count = struct.unpack(">H", class_data[8:10])[0]
|
| 136 |
+
idx = 10
|
| 137 |
+
cp = [None]
|
| 138 |
+
i = 1
|
| 139 |
+
while i < cp_count:
|
| 140 |
+
if idx >= len(class_data):
|
| 141 |
+
break
|
| 142 |
+
tag = class_data[idx]
|
| 143 |
+
idx += 1
|
| 144 |
+
if tag == 1 or tag == 2:
|
| 145 |
+
if idx + 2 > len(class_data):
|
| 146 |
+
break
|
| 147 |
+
length = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 148 |
+
if idx + 2 + length > len(class_data):
|
| 149 |
+
break
|
| 150 |
+
val = class_data[idx+2:idx+2+length].decode("utf-8", errors="replace")
|
| 151 |
+
cp.append((tag, val))
|
| 152 |
+
idx += 2 + length
|
| 153 |
+
i += 1
|
| 154 |
+
elif tag in (5, 6):
|
| 155 |
+
if idx + 8 > len(class_data):
|
| 156 |
+
break
|
| 157 |
+
cp.append((tag, None))
|
| 158 |
+
cp.append((tag, None))
|
| 159 |
+
idx += 8
|
| 160 |
+
i += 2
|
| 161 |
+
elif tag in CP_TAG_SIZES and CP_TAG_SIZES[tag] is not None:
|
| 162 |
+
size = CP_TAG_SIZES[tag]
|
| 163 |
+
if idx + size > len(class_data):
|
| 164 |
+
break
|
| 165 |
+
if tag in (7, 8, 16, 19, 20):
|
| 166 |
+
val = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 167 |
+
cp.append((tag, val))
|
| 168 |
+
elif tag in (9, 10, 11, 12, 17, 18):
|
| 169 |
+
a = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 170 |
+
b = struct.unpack(">H", class_data[idx+2:idx+4])[0]
|
| 171 |
+
cp.append((tag, (a, b)))
|
| 172 |
+
elif tag == 15:
|
| 173 |
+
cp.append((tag, (class_data[idx], struct.unpack(">H", class_data[idx+1:idx+3])[0])))
|
| 174 |
+
else:
|
| 175 |
+
cp.append((tag, None))
|
| 176 |
+
idx += size
|
| 177 |
+
i += 1
|
| 178 |
+
else:
|
| 179 |
+
cp.append((tag, None))
|
| 180 |
+
break
|
| 181 |
+
return cp, idx
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _get_method_code_ranges(class_data):
|
| 185 |
+
_, idx = _parse_constant_pool(class_data)
|
| 186 |
+
if not idx:
|
| 187 |
+
return []
|
| 188 |
+
idx += 6
|
| 189 |
+
if idx + 2 > len(class_data):
|
| 190 |
+
return []
|
| 191 |
+
interfaces_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 192 |
+
idx += 2 + interfaces_count * 2
|
| 193 |
+
if idx + 2 > len(class_data):
|
| 194 |
+
return []
|
| 195 |
+
fields_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 196 |
+
idx += 2
|
| 197 |
+
for _ in range(fields_count):
|
| 198 |
+
if idx + 6 > len(class_data):
|
| 199 |
+
return []
|
| 200 |
+
idx += 6
|
| 201 |
+
attr_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 202 |
+
idx += 2
|
| 203 |
+
for _ in range(attr_count):
|
| 204 |
+
if idx + 6 > len(class_data):
|
| 205 |
+
return []
|
| 206 |
+
idx += 2
|
| 207 |
+
attr_len = struct.unpack(">I", class_data[idx:idx+4])[0]
|
| 208 |
+
idx += 4 + attr_len
|
| 209 |
+
if idx + 2 > len(class_data):
|
| 210 |
+
return []
|
| 211 |
+
methods_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 212 |
+
idx += 2
|
| 213 |
+
code_ranges = []
|
| 214 |
+
for _ in range(methods_count):
|
| 215 |
+
if idx + 6 > len(class_data):
|
| 216 |
+
break
|
| 217 |
+
idx += 6
|
| 218 |
+
attr_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 219 |
+
idx += 2
|
| 220 |
+
for _ in range(attr_count):
|
| 221 |
+
if idx + 6 > len(class_data):
|
| 222 |
+
break
|
| 223 |
+
idx += 2
|
| 224 |
+
attr_len = struct.unpack(">I", class_data[idx:idx+4])[0]
|
| 225 |
+
idx += 4
|
| 226 |
+
attr_start = idx
|
| 227 |
+
attr_end = idx + attr_len
|
| 228 |
+
if attr_end <= len(class_data) and attr_len >= 8:
|
| 229 |
+
code_len = struct.unpack(">I", class_data[attr_start+4:attr_start+8])[0]
|
| 230 |
+
code_start = attr_start + 8
|
| 231 |
+
code_end = code_start + code_len
|
| 232 |
+
if code_end <= attr_end:
|
| 233 |
+
code_ranges.append((code_start, code_end))
|
| 234 |
+
idx = attr_end
|
| 235 |
+
return code_ranges
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _get_method_code_info(class_data):
|
| 239 |
+
cp, idx = _parse_constant_pool(class_data)
|
| 240 |
+
if not idx:
|
| 241 |
+
return []
|
| 242 |
+
idx += 6
|
| 243 |
+
if idx + 2 > len(class_data):
|
| 244 |
+
return []
|
| 245 |
+
interfaces_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 246 |
+
idx += 2 + interfaces_count * 2
|
| 247 |
+
if idx + 2 > len(class_data):
|
| 248 |
+
return []
|
| 249 |
+
fields_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 250 |
+
idx += 2
|
| 251 |
+
for _ in range(fields_count):
|
| 252 |
+
if idx + 6 > len(class_data):
|
| 253 |
+
return []
|
| 254 |
+
idx += 6
|
| 255 |
+
attr_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 256 |
+
idx += 2
|
| 257 |
+
for _ in range(attr_count):
|
| 258 |
+
if idx + 6 > len(class_data):
|
| 259 |
+
return []
|
| 260 |
+
idx += 2
|
| 261 |
+
attr_len = struct.unpack(">I", class_data[idx:idx+4])[0]
|
| 262 |
+
idx += 4 + attr_len
|
| 263 |
+
if idx + 2 > len(class_data):
|
| 264 |
+
return []
|
| 265 |
+
methods_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 266 |
+
idx += 2
|
| 267 |
+
methods = []
|
| 268 |
+
for _ in range(methods_count):
|
| 269 |
+
if idx + 6 > len(class_data):
|
| 270 |
+
break
|
| 271 |
+
name_idx = struct.unpack(">H", class_data[idx+2:idx+4])[0]
|
| 272 |
+
idx += 6
|
| 273 |
+
attr_count = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 274 |
+
idx += 2
|
| 275 |
+
name = ""
|
| 276 |
+
if 0 < name_idx < len(cp) and cp[name_idx] and cp[name_idx][0] == 1:
|
| 277 |
+
name = cp[name_idx][1]
|
| 278 |
+
for _ in range(attr_count):
|
| 279 |
+
if idx + 6 > len(class_data):
|
| 280 |
+
break
|
| 281 |
+
attr_name_idx = struct.unpack(">H", class_data[idx:idx+2])[0]
|
| 282 |
+
attr_len = struct.unpack(">I", class_data[idx+2:idx+6])[0]
|
| 283 |
+
idx += 6
|
| 284 |
+
attr_start = idx
|
| 285 |
+
attr_end = idx + attr_len
|
| 286 |
+
if 0 < attr_name_idx < len(cp) and cp[attr_name_idx] and cp[attr_name_idx][0] == 1:
|
| 287 |
+
attr_name = cp[attr_name_idx][1]
|
| 288 |
+
if attr_name == "Code" and attr_len >= 8:
|
| 289 |
+
code_len = struct.unpack(">I", class_data[attr_start+4:attr_start+8])[0]
|
| 290 |
+
code_start = attr_start + 8
|
| 291 |
+
code_end = code_start + code_len
|
| 292 |
+
if code_end <= attr_end:
|
| 293 |
+
methods.append((name, code_start, code_end))
|
| 294 |
+
idx = attr_end
|
| 295 |
+
return methods
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def _find_fieldref_index(class_data, class_name_substr, field_name):
|
| 299 |
+
cp, _ = _parse_constant_pool(class_data)
|
| 300 |
+
if not cp:
|
| 301 |
+
return None
|
| 302 |
+
field_name_idx = None
|
| 303 |
+
for i, entry in enumerate(cp):
|
| 304 |
+
if entry and entry[0] == 1 and entry[1] == field_name:
|
| 305 |
+
field_name_idx = i
|
| 306 |
+
break
|
| 307 |
+
if not field_name_idx:
|
| 308 |
+
return None
|
| 309 |
+
nat_idx = None
|
| 310 |
+
for i, entry in enumerate(cp):
|
| 311 |
+
if entry and entry[0] == 12 and isinstance(entry[1], tuple) and entry[1][0] == field_name_idx:
|
| 312 |
+
nat_idx = i
|
| 313 |
+
break
|
| 314 |
+
if not nat_idx:
|
| 315 |
+
return None
|
| 316 |
+
class_idx = None
|
| 317 |
+
for i, entry in enumerate(cp):
|
| 318 |
+
if entry and entry[0] == 7 and isinstance(entry[1], int):
|
| 319 |
+
cn_entry = cp[entry[1]]
|
| 320 |
+
if cn_entry and cn_entry[0] == 1 and class_name_substr in cn_entry[1]:
|
| 321 |
+
class_idx = i
|
| 322 |
+
break
|
| 323 |
+
if not class_idx:
|
| 324 |
+
return None
|
| 325 |
+
for i, entry in enumerate(cp):
|
| 326 |
+
if entry and entry[0] == 9 and isinstance(entry[1], tuple) and entry[1][0] == class_idx and entry[1][1] == nat_idx:
|
| 327 |
+
return i
|
| 328 |
+
return None
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
# ═══════════════════════════════════════════════════════════════
|
| 332 |
+
# Bytecode patching
|
| 333 |
+
# ═══════════════════════════════════════════════════════════════
|
| 334 |
+
|
| 335 |
+
def _patch_method_to_return_true(class_data, class_name=""):
|
| 336 |
+
data = bytearray(class_data)
|
| 337 |
+
ranges = _get_method_code_ranges(class_data)
|
| 338 |
+
modified = False
|
| 339 |
+
for start, end in ranges:
|
| 340 |
+
i = start
|
| 341 |
+
while i < end - 1:
|
| 342 |
+
if data[i] == 0x03 and data[i + 1] == 0xac:
|
| 343 |
+
data[i] = 0x04
|
| 344 |
+
modified = True
|
| 345 |
+
i += 2
|
| 346 |
+
continue
|
| 347 |
+
if data[i] == 0x04 and data[i + 1] == 0xac:
|
| 348 |
+
i += 2
|
| 349 |
+
continue
|
| 350 |
+
i += 1
|
| 351 |
+
return modified, bytes(data)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def _patch_class_file(path):
|
| 355 |
+
with open(path, "rb") as fh:
|
| 356 |
+
data = fh.read()
|
| 357 |
+
modified, new_data = _patch_method_to_return_true(data, os.path.basename(path))
|
| 358 |
+
if modified:
|
| 359 |
+
with open(path, "wb") as fh:
|
| 360 |
+
fh.write(new_data)
|
| 361 |
+
return True
|
| 362 |
+
return False
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def _is_license_class(filename):
|
| 366 |
+
name = filename.lower()
|
| 367 |
+
return any(w in name for w in [
|
| 368 |
+
"license", "licenseverifier", "licensechecker",
|
| 369 |
+
"dynamiclicense", "premiumenabled",
|
| 370 |
+
])
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _is_license_jar(filename):
|
| 374 |
+
name = filename.lower()
|
| 375 |
+
return "proprietary" in name or "license" in name or "ee" in name
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def _patch_license_key_checker(path):
|
| 379 |
+
"""Patch getPremiumLicenseEnabledResult() → ENTERPRISE robustly."""
|
| 380 |
+
with open(path, "rb") as fh:
|
| 381 |
+
data = bytearray(fh.read())
|
| 382 |
+
enterprise_idx = _find_fieldref_index(data, "License", "ENTERPRISE")
|
| 383 |
+
if enterprise_idx:
|
| 384 |
+
methods = _get_method_code_info(data)
|
| 385 |
+
for name, cstart, cend in methods:
|
| 386 |
+
if name == "getPremiumLicenseEnabledResult":
|
| 387 |
+
for i in range(cstart, cend - 4):
|
| 388 |
+
if data[i] == 0x2a and data[i+1] == 0xb4 and data[i+4] == 0xb0:
|
| 389 |
+
data[i] = 0xb2
|
| 390 |
+
data[i+1] = (enterprise_idx >> 8) & 0xff
|
| 391 |
+
data[i+2] = enterprise_idx & 0xff
|
| 392 |
+
data[i+3] = 0x00
|
| 393 |
+
data[i+4] = 0xb0
|
| 394 |
+
with open(path, "wb") as fh:
|
| 395 |
+
fh.write(bytes(data))
|
| 396 |
+
logger.info(f"Robust-patched {name} → ENTERPRISE (idx={enterprise_idx})")
|
| 397 |
+
return True
|
| 398 |
+
logger.warning(f"Found {name} but no getfield+areturn pattern")
|
| 399 |
+
return False
|
| 400 |
+
logger.warning("ENTERPRISE not found in constant pool, trying hardcoded fallback...")
|
| 401 |
+
if len(data) > 5643:
|
| 402 |
+
expected = bytes([0xb4, 0x00, 0x0d, 0xb0])
|
| 403 |
+
if data[5638:5642] == expected:
|
| 404 |
+
data[5638:5643] = bytes([0xb2, 0x00, 0x4f, 0x00, 0xb0])
|
| 405 |
+
with open(path, "wb") as fh:
|
| 406 |
+
fh.write(bytes(data))
|
| 407 |
+
logger.info("Patched LicenseKeyChecker using hardcoded offset fallback")
|
| 408 |
+
return True
|
| 409 |
+
logger.error("Could not patch LicenseKeyChecker")
|
| 410 |
+
return False
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def _extract_and_patch_license_classes_from_jar(jar_path):
|
| 414 |
+
patched_files = []
|
| 415 |
+
with zipfile.ZipFile(jar_path, "r") as zf:
|
| 416 |
+
for name in zf.namelist():
|
| 417 |
+
if not name.endswith(".class"):
|
| 418 |
+
continue
|
| 419 |
+
if not _is_license_class(os.path.basename(name)):
|
| 420 |
+
continue
|
| 421 |
+
data = zf.read(name)
|
| 422 |
+
mod1, d1 = _patch_method_to_return_true(data, os.path.basename(name))
|
| 423 |
+
if mod1:
|
| 424 |
+
dest_path = os.path.join(CLASSES_DIR, name)
|
| 425 |
+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
| 426 |
+
with open(dest_path, "wb") as fh:
|
| 427 |
+
fh.write(d1)
|
| 428 |
+
patched_files.append(name)
|
| 429 |
+
if patched_files:
|
| 430 |
+
logger.info(f"Extracted/patched from {os.path.basename(jar_path)}: {len(patched_files)} classes")
|
| 431 |
+
return len(patched_files)
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def _patch_license_classes():
|
| 435 |
+
patched = 0
|
| 436 |
+
if os.path.exists(CLASSES_DIR):
|
| 437 |
+
for root, dirs, files in os.walk(CLASSES_DIR):
|
| 438 |
+
for f in files:
|
| 439 |
+
if not f.endswith(".class") or not _is_license_class(f):
|
| 440 |
+
continue
|
| 441 |
+
path = os.path.join(root, f)
|
| 442 |
+
try:
|
| 443 |
+
if _patch_class_file(path):
|
| 444 |
+
patched += 1
|
| 445 |
+
logger.info(f"Patched license class: {f}")
|
| 446 |
+
except Exception as e:
|
| 447 |
+
logger.warning(f"Could not patch {f}: {e}")
|
| 448 |
+
if os.path.exists(LIB_DIR):
|
| 449 |
+
for f in os.listdir(LIB_DIR):
|
| 450 |
+
if not f.endswith(".jar") or not _is_license_jar(f):
|
| 451 |
+
continue
|
| 452 |
+
try:
|
| 453 |
+
patched += _extract_and_patch_license_classes_from_jar(os.path.join(LIB_DIR, f))
|
| 454 |
+
except Exception as e:
|
| 455 |
+
logger.warning(f"Could not process {f}: {e}")
|
| 456 |
+
|
| 457 |
+
lck_path = os.path.join(
|
| 458 |
+
CLASSES_DIR,
|
| 459 |
+
"stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.class"
|
| 460 |
+
)
|
| 461 |
+
if not os.path.exists(lck_path) and os.path.exists(LIB_DIR):
|
| 462 |
+
for jf in os.listdir(LIB_DIR):
|
| 463 |
+
if "proprietary" not in jf.lower():
|
| 464 |
+
continue
|
| 465 |
+
with zipfile.ZipFile(os.path.join(LIB_DIR, jf)) as zf:
|
| 466 |
+
cp = "stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.class"
|
| 467 |
+
if cp in zf.namelist():
|
| 468 |
+
os.makedirs(os.path.dirname(lck_path), exist_ok=True)
|
| 469 |
+
with open(lck_path, "wb") as fh:
|
| 470 |
+
fh.write(zf.read(cp))
|
| 471 |
+
logger.info("Extracted LicenseKeyChecker.class for patching")
|
| 472 |
+
break
|
| 473 |
+
if os.path.exists(lck_path):
|
| 474 |
+
try:
|
| 475 |
+
if _patch_license_key_checker(lck_path):
|
| 476 |
+
patched += 1
|
| 477 |
+
except Exception as e:
|
| 478 |
+
logger.warning(f"Could not patch LicenseKeyChecker: {e}")
|
| 479 |
+
else:
|
| 480 |
+
logger.warning("LicenseKeyChecker.class not found")
|
| 481 |
+
|
| 482 |
+
if patched:
|
| 483 |
+
logger.info(f"Patched {patched} license verifier classes total.")
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
# ═══════════════════════════════════════════════════════════════
|
| 487 |
+
# HTML / JS / SVG / CSS patching
|
| 488 |
+
# ═══════════════════════════════════════════════════════════════
|
| 489 |
+
|
| 490 |
+
def _patch_html():
|
| 491 |
+
count = 0
|
| 492 |
+
if not os.path.exists(STATIC_DIR):
|
| 493 |
+
return
|
| 494 |
+
for root, dirs, files in os.walk(STATIC_DIR):
|
| 495 |
+
for f in files:
|
| 496 |
+
if not f.endswith(".html"):
|
| 497 |
+
continue
|
| 498 |
+
path = os.path.join(root, f)
|
| 499 |
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
| 500 |
+
content = fh.read()
|
| 501 |
+
content = _brand(content)
|
| 502 |
+
content = content.replace(
|
| 503 |
+
"The Free Adobe Acrobat alternative (10M+ Downloads)",
|
| 504 |
+
"Your private PDF powerhouse by HarshIt",
|
| 505 |
+
)
|
| 506 |
+
css_link = '<link rel="stylesheet" href="/harshitpdf-liquidglass.css">'
|
| 507 |
+
if "harshitpdf-liquidglass.css" not in content:
|
| 508 |
+
content = content.replace("</head>", css_link + "</head>")
|
| 509 |
+
with open(path, "w", encoding="utf-8") as fh:
|
| 510 |
+
fh.write(content)
|
| 511 |
+
count += 1
|
| 512 |
+
logger.info(f"Patched {count} HTML files.")
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def _patch_js():
|
| 516 |
+
count = 0
|
| 517 |
+
if not os.path.exists(ASSETS_DIR):
|
| 518 |
+
return
|
| 519 |
+
for f in os.listdir(ASSETS_DIR):
|
| 520 |
+
if not f.endswith(".js"):
|
| 521 |
+
continue
|
| 522 |
+
path = os.path.join(ASSETS_DIR, f)
|
| 523 |
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
| 524 |
+
content = fh.read()
|
| 525 |
+
orig = content
|
| 526 |
+
content = _brand(content)
|
| 527 |
+
content = re.sub(
|
| 528 |
+
r'requiresPremium\s*[=:]=?\s*(?:!0|!1|true|false|null)',
|
| 529 |
+
'requiresPremium=!1',
|
| 530 |
+
content,
|
| 531 |
+
)
|
| 532 |
+
content = re.sub(
|
| 533 |
+
r'["\'](?:Premium feature[:\s].*?|Enterprise only[:\s].*?|Enterprise Seats|Upgrade to Server Plan)["\']',
|
| 534 |
+
'""', content,
|
| 535 |
+
)
|
| 536 |
+
content = re.sub(
|
| 537 |
+
r'premiumEnabled\s*[=:]=?\s*[se][?\w]+\.premiumEnabled\?\??[!\d]+',
|
| 538 |
+
'premiumEnabled:!0', content,
|
| 539 |
+
)
|
| 540 |
+
if content != orig:
|
| 541 |
+
with open(path, "w", encoding="utf-8") as fh:
|
| 542 |
+
fh.write(content)
|
| 543 |
+
count += 1
|
| 544 |
+
logger.info(f"Patched {count} JS files.")
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def _write_svgs():
|
| 548 |
+
LOGO_SVGS = [
|
| 549 |
+
(
|
| 550 |
+
"StirlingPDFLogoBlackText.svg",
|
| 551 |
+
'<svg width="140" height="26" viewBox="0 0 140 26" fill="none" '
|
| 552 |
+
'xmlns="http://www.w3.org/2000/svg">'
|
| 553 |
+
'<path d="M3 2h12l4 4v18a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" '
|
| 554 |
+
'fill="#6366f1" opacity="0.9"/>'
|
| 555 |
+
'<path d="M15 2l4 4h-4V2z" fill="#818cf8"/>'
|
| 556 |
+
'<text x="6" y="17" font-family="system-ui,sans-serif" font-size="7" '
|
| 557 |
+
'font-weight="700" fill="#fff">PDF</text>'
|
| 558 |
+
'<text x="24" y="18" font-family="system-ui,-apple-system,sans-serif" '
|
| 559 |
+
'font-size="14" font-weight="700" fill="#1a1a2e" letter-spacing="-0.3">'
|
| 560 |
+
'HarshIt<tspan fill="#6366f1">PDF</tspan></text></svg>',
|
| 561 |
+
),
|
| 562 |
+
(
|
| 563 |
+
"favicon.svg",
|
| 564 |
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">'
|
| 565 |
+
'<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">'
|
| 566 |
+
'<stop offset="0" stop-color="#6366f1"/>'
|
| 567 |
+
'<stop offset="1" stop-color="#a855f7"/>'
|
| 568 |
+
"</linearGradient></defs>"
|
| 569 |
+
'<rect width="512" height="512" rx="100" fill="url(#g)"/>'
|
| 570 |
+
'<path d="M140 120h160l60 60v200a20 20 0 0 1-20 20H140a20 20 0 0 1-20-20'
|
| 571 |
+
'V140a20 20 0 0 1 20-20z" fill="#fff" opacity="0.95"/>'
|
| 572 |
+
'<path d="M300 120l60 60h-60V120z" fill="#ddd" opacity="0.8"/>'
|
| 573 |
+
'<text x="170" y="280" font-family="system-ui,sans-serif" font-size="80" '
|
| 574 |
+
'font-weight="800" fill="#6366f1">PDF</text></svg>',
|
| 575 |
+
),
|
| 576 |
+
]
|
| 577 |
+
logo_dir = os.path.join(STATIC_DIR, "modern-logo")
|
| 578 |
+
os.makedirs(logo_dir, exist_ok=True)
|
| 579 |
+
for name, svg in LOGO_SVGS:
|
| 580 |
+
with open(os.path.join(logo_dir, name), "w") as fh:
|
| 581 |
+
fh.write(svg)
|
| 582 |
+
logger.info("Wrote SVGs.")
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def _write_manifests():
|
| 586 |
+
manifest = {
|
| 587 |
+
"short_name": "HarshItPDF",
|
| 588 |
+
"name": "HarshItPDF",
|
| 589 |
+
"icons": [],
|
| 590 |
+
"start_url": ".",
|
| 591 |
+
"display": "standalone",
|
| 592 |
+
"theme_color": "#0a0a1a",
|
| 593 |
+
"background_color": "#0a0a1a",
|
| 594 |
+
}
|
| 595 |
+
for name in ("manifest.json", "manifest-classic.json"):
|
| 596 |
+
path = os.path.join(STATIC_DIR, name)
|
| 597 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 598 |
+
with open(path, "w") as fh:
|
| 599 |
+
json.dump(manifest, fh, indent=2)
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def _write_css():
|
| 603 |
+
candidates = glob.glob(os.path.join(SELF_DIR, "liquidglass*.css"))
|
| 604 |
+
css_src = candidates[0] if candidates else None
|
| 605 |
+
css_dst = os.path.join(STATIC_DIR, "harshitpdf-liquidglass.css")
|
| 606 |
+
if css_src and os.path.exists(css_src):
|
| 607 |
+
shutil.copy(css_src, css_dst)
|
| 608 |
+
logger.info(f"Copied CSS: {css_src} -> {css_dst}")
|
| 609 |
+
else:
|
| 610 |
+
with open(css_dst, "w") as fh:
|
| 611 |
+
fh.write("/* HarshItPDF Liquid Glass fallback */\n")
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def _patch_settings_template():
|
| 615 |
+
template_path = os.path.join(CLASSES_DIR, "settings.yml.template")
|
| 616 |
+
if not os.path.exists(template_path):
|
| 617 |
+
logger.warning("settings.yml.template not found.")
|
| 618 |
+
return
|
| 619 |
+
with open(template_path, "r") as fh:
|
| 620 |
+
content = fh.read()
|
| 621 |
+
content = _brand(content)
|
| 622 |
+
with open(template_path, "w") as fh:
|
| 623 |
+
fh.write(content)
|
| 624 |
+
logger.info("Patched settings.yml.template.")
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
def _write_settings_yml():
|
| 628 |
+
os.makedirs(CONFIGS_DIR, exist_ok=True)
|
| 629 |
+
settings_path = os.path.join(CONFIGS_DIR, "settings.yml")
|
| 630 |
+
template_path = os.path.join(CLASSES_DIR, "settings.yml.template")
|
| 631 |
+
if os.path.exists(template_path):
|
| 632 |
+
with open(template_path, "r") as fh:
|
| 633 |
+
content = fh.read()
|
| 634 |
+
else:
|
| 635 |
+
content = """# HarshItPDF Settings
|
| 636 |
+
security:
|
| 637 |
+
enableLogin: true
|
| 638 |
+
initialLogin:
|
| 639 |
+
username: admin
|
| 640 |
+
password: changeme123
|
| 641 |
+
ui:
|
| 642 |
+
appName: HarshItPDF
|
| 643 |
+
appNavbarName: HarshItPDF
|
| 644 |
+
homeDescription: Your private PDF powerhouse by HarshIt
|
| 645 |
+
system:
|
| 646 |
+
showUpdate: false
|
| 647 |
+
rootUriPath: ""
|
| 648 |
+
defaultLocale: en-US
|
| 649 |
+
premium:
|
| 650 |
+
enabled: false
|
| 651 |
+
enterpriseEdition:
|
| 652 |
+
enabled: true
|
| 653 |
+
maxUsers: 999
|
| 654 |
+
endpoints:
|
| 655 |
+
groupsToRemove: []
|
| 656 |
+
"""
|
| 657 |
+
content = _brand(content)
|
| 658 |
+
with open(settings_path, "w") as fh:
|
| 659 |
+
fh.write(content)
|
| 660 |
+
logger.info(f"Wrote settings.yml ({len(content)} chars).")
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def _patch_banner():
|
| 664 |
+
banner_path = os.path.join(CLASSES_DIR, "banner.txt")
|
| 665 |
+
os.makedirs(os.path.dirname(banner_path), exist_ok=True)
|
| 666 |
+
with open(banner_path, "w") as fh:
|
| 667 |
+
fh.write(r""" _ __ _ _ ___ ___ ___ ___
|
| 668 |
+
| |/ /__ _ _ __| |_ |_ _| _ \ _ _| __| _ \
|
| 669 |
+
| ' // _` | '_ \ ' \ | || _/| || _|| /
|
| 670 |
+
| . \ (_| | | | | || | | || | | |_| |_| _|
|
| 671 |
+
|_|\_\__,_|_| |_|
|
| 672 |
+
|__/
|
| 673 |
+
Powered by Spring Boot ${spring-boot.version}
|
| 674 |
+
""")
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
def _patch_meta():
|
| 678 |
+
for name in ("og-metadata.json",):
|
| 679 |
+
path = os.path.join(STATIC_DIR, name)
|
| 680 |
+
if os.path.exists(path):
|
| 681 |
+
with open(path, "r") as fh:
|
| 682 |
+
content = fh.read()
|
| 683 |
+
with open(path, "w") as fh:
|
| 684 |
+
fh.write(_brand(content))
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
def _fix_python_path():
|
| 688 |
+
venv_dir = "/opt/venv"
|
| 689 |
+
venv_bin = os.path.join(venv_dir, "bin")
|
| 690 |
+
venv_python = os.path.join(venv_bin, "python3")
|
| 691 |
+
if not os.path.exists(venv_dir):
|
| 692 |
+
os.makedirs(venv_bin, exist_ok=True)
|
| 693 |
+
system_python = shutil.which("python3")
|
| 694 |
+
if system_python and not os.path.exists(venv_python):
|
| 695 |
+
try:
|
| 696 |
+
os.symlink(system_python, venv_python)
|
| 697 |
+
except Exception as e:
|
| 698 |
+
logger.warning(f"Could not create python3 symlink: {e}")
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def _install_opencv():
|
| 702 |
+
try:
|
| 703 |
+
import cv2
|
| 704 |
+
logger.info(f"OpenCV available: {cv2.__version__}")
|
| 705 |
+
except ImportError:
|
| 706 |
+
logger.info("OpenCV not found, installing...")
|
| 707 |
+
try:
|
| 708 |
+
subprocess.run(
|
| 709 |
+
[sys.executable, "-m", "pip", "install", "--quiet", "opencv-python-headless"],
|
| 710 |
+
check=False, timeout=60,
|
| 711 |
+
)
|
| 712 |
+
import cv2
|
| 713 |
+
logger.info(f"OpenCV installed: {cv2.__version__}")
|
| 714 |
+
except Exception as e:
|
| 715 |
+
logger.warning(f"Could not install OpenCV: {e}")
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
def _compile_license_override():
|
| 719 |
+
java_src = """package stirling.software.SPDF.config;
|
| 720 |
+
import org.springframework.context.annotation.Bean;
|
| 721 |
+
import org.springframework.context.annotation.Configuration;
|
| 722 |
+
import org.springframework.beans.factory.annotation.Qualifier;
|
| 723 |
+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
| 724 |
+
|
| 725 |
+
@Configuration
|
| 726 |
+
public class HarshItLicenseOverride {
|
| 727 |
+
@Bean
|
| 728 |
+
@ConditionalOnMissingBean(name = "runningProOrHigher")
|
| 729 |
+
@Qualifier("runningProOrHigher")
|
| 730 |
+
public boolean runningProOrHigher() {
|
| 731 |
+
return true;
|
| 732 |
+
}
|
| 733 |
+
}
|
| 734 |
+
"""
|
| 735 |
+
src_path = os.path.join(EXTRACT_DIR, "HarshItLicenseOverride.java")
|
| 736 |
+
with open(src_path, "w") as fh:
|
| 737 |
+
fh.write(java_src)
|
| 738 |
+
lib_dir = os.path.join(EXTRACT_DIR, "BOOT-INF", "lib")
|
| 739 |
+
if not os.path.exists(lib_dir):
|
| 740 |
+
logger.warning("BOOT-INF/lib not found, skipping license override compile")
|
| 741 |
+
return False
|
| 742 |
+
jars = [os.path.join(lib_dir, j) for j in os.listdir(lib_dir) if j.endswith(".jar")]
|
| 743 |
+
classpath = ":".join(jars)
|
| 744 |
+
dest_dir = os.path.join(EXTRACT_DIR, "BOOT-INF", "classes")
|
| 745 |
+
try:
|
| 746 |
+
result = subprocess.run(
|
| 747 |
+
["javac", "-cp", classpath, "-d", dest_dir, src_path],
|
| 748 |
+
capture_output=True, text=True, timeout=60
|
| 749 |
+
)
|
| 750 |
+
if result.returncode == 0:
|
| 751 |
+
logger.info("Compiled HarshItLicenseOverride.class")
|
| 752 |
+
return True
|
| 753 |
+
else:
|
| 754 |
+
logger.warning(f"javac failed: {result.stderr[:200]}")
|
| 755 |
+
return False
|
| 756 |
+
except Exception as e:
|
| 757 |
+
logger.warning(f"Could not compile override: {e}")
|
| 758 |
+
return False
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
def _patch_files():
|
| 762 |
+
if os.path.exists(PATCHED_FLAG):
|
| 763 |
+
return
|
| 764 |
+
logger.info("Applying HarshItPDF patches...")
|
| 765 |
+
_patch_html()
|
| 766 |
+
_patch_js()
|
| 767 |
+
_write_svgs()
|
| 768 |
+
_write_manifests()
|
| 769 |
+
_write_css()
|
| 770 |
+
_patch_settings_template()
|
| 771 |
+
_write_settings_yml()
|
| 772 |
+
_patch_banner()
|
| 773 |
+
_patch_meta()
|
| 774 |
+
_fix_python_path()
|
| 775 |
+
_install_opencv()
|
| 776 |
+
_patch_license_classes()
|
| 777 |
+
_compile_license_override()
|
| 778 |
+
with open(PATCHED_FLAG, "w") as fh:
|
| 779 |
+
fh.write(str(time.time()))
|
| 780 |
+
logger.info("All patches applied.")
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
# ═══════════════════════════════════════════════════════════════
|
| 784 |
+
# Backend lifecycle
|
| 785 |
+
# ═══════════════════════════════════════════════════════════════
|
| 786 |
+
|
| 787 |
+
_backend_process = None
|
| 788 |
+
_backend_ready = threading.Event()
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
def _stream_output(pipe):
|
| 792 |
+
try:
|
| 793 |
+
for line in pipe:
|
| 794 |
+
print(line, end="")
|
| 795 |
+
if "Started SPDFApplication" in line:
|
| 796 |
+
_backend_ready.set()
|
| 797 |
+
except Exception as e:
|
| 798 |
+
logger.warning(f"Backend stream error: {e}")
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
def _find_tessdata():
|
| 802 |
+
candidates = [
|
| 803 |
+
"/usr/share/tesseract-ocr/5/tessdata/",
|
| 804 |
+
"/usr/share/tesseract-ocr/4.00/tessdata/",
|
| 805 |
+
"/usr/share/tesseract-ocr/4/tessdata/",
|
| 806 |
+
"/usr/share/tessdata/",
|
| 807 |
+
]
|
| 808 |
+
for c in candidates:
|
| 809 |
+
if os.path.exists(c):
|
| 810 |
+
return c
|
| 811 |
+
return ""
|
| 812 |
+
|
| 813 |
+
|
| 814 |
+
def start_backend():
|
| 815 |
+
logger.info("Launching HarshItPDF...")
|
| 816 |
+
main_class = _get_main_class()
|
| 817 |
+
|
| 818 |
+
env = os.environ.copy()
|
| 819 |
+
env["SERVER_PORT"] = str(BACKEND_PORT)
|
| 820 |
+
env["SYSTEM_ROOTURIPATH"] = ""
|
| 821 |
+
env["SECURITY_ENABLELOGIN"] = "true"
|
| 822 |
+
env["SECURITY_INITIALLOGIN_USERNAME"] = "admin"
|
| 823 |
+
env["SECURITY_INITIALLOGIN_PASSWORD"] = "changeme123"
|
| 824 |
+
env["UI_APPNAME"] = "HarshItPDF"
|
| 825 |
+
env["UI_APPNAVBARNAME"] = "HarshItPDF"
|
| 826 |
+
env["UI_HOMEDESCRIPTION"] = "Your private PDF powerhouse by HarshIt"
|
| 827 |
+
env["SYSTEM_SHOWUPDATE"] = "false"
|
| 828 |
+
env["PREMIUM_ENABLED"] = "true"
|
| 829 |
+
env["PREMIUM_KEY"] = "harshitpdf-unlocked-0000-0000-0000-000000000001"
|
| 830 |
+
env["DISABLE_ADDITIONAL_FEATURES"] = "false"
|
| 831 |
+
env["PYTHON_PATH"] = shutil.which("python3") or "/usr/bin/python3"
|
| 832 |
+
tessdata = _find_tessdata()
|
| 833 |
+
if tessdata:
|
| 834 |
+
env["TESSDATA_PREFIX"] = tessdata
|
| 835 |
+
|
| 836 |
+
logger.info(f"Launching Java backend on internal port {BACKEND_PORT}...")
|
| 837 |
+
|
| 838 |
+
global _backend_process
|
| 839 |
+
_backend_process = subprocess.Popen(
|
| 840 |
+
[
|
| 841 |
+
"java",
|
| 842 |
+
"-Xmx2G",
|
| 843 |
+
"-XX:+UseSerialGC",
|
| 844 |
+
"-XX:MaxRAMPercentage=60.0",
|
| 845 |
+
"-cp", EXTRACT_DIR,
|
| 846 |
+
main_class,
|
| 847 |
+
],
|
| 848 |
+
env=env,
|
| 849 |
+
stdout=subprocess.PIPE,
|
| 850 |
+
stderr=subprocess.STDOUT,
|
| 851 |
+
text=True,
|
| 852 |
+
)
|
| 853 |
+
threading.Thread(target=_stream_output, args=(_backend_process.stdout,), daemon=True).start()
|
| 854 |
+
time.sleep(2)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def check_backend():
|
| 858 |
+
try:
|
| 859 |
+
resp = urllib.request.urlopen(f"http://127.0.0.1:{BACKEND_PORT}/login", timeout=3)
|
| 860 |
+
return resp.status == 200
|
| 861 |
+
except Exception:
|
| 862 |
+
return False
|
| 863 |
+
|
| 864 |
+
|
| 865 |
+
# ═══════════════════════════════════════════════════════════════
|
| 866 |
+
# Service orchestrator (runs at module import on HF Spaces)
|
| 867 |
+
# ═══════════════════════════════════════════════════════════════
|
| 868 |
+
|
| 869 |
+
def _start_services():
|
| 870 |
+
logger.info("=" * 60)
|
| 871 |
+
logger.info("HarshItPDF Starting...")
|
| 872 |
+
logger.info("=" * 60)
|
| 873 |
+
|
| 874 |
+
_download_jar()
|
| 875 |
+
_extract_jar()
|
| 876 |
+
_patch_files()
|
| 877 |
+
|
| 878 |
+
logger.info(f"Starting mock license server on port {LICENCE_SERVER_PORT}...")
|
| 879 |
+
lic_thread = threading.Thread(target=start_license_server, daemon=True)
|
| 880 |
+
lic_thread.start()
|
| 881 |
+
time.sleep(0.5)
|
| 882 |
+
|
| 883 |
+
start_backend()
|
| 884 |
+
|
| 885 |
+
logger.info("Waiting for backend to be ready...")
|
| 886 |
+
_backend_ready.wait(timeout=120)
|
| 887 |
+
|
| 888 |
+
if check_backend():
|
| 889 |
+
logger.info(f"HarshItPDF backend is live on port {BACKEND_PORT}")
|
| 890 |
+
else:
|
| 891 |
+
logger.warning("Backend health check failed — app may still be starting")
|
| 892 |
+
|
| 893 |
+
# Keep-alive monitor
|
| 894 |
+
while True:
|
| 895 |
+
if _backend_process and _backend_process.poll() is not None:
|
| 896 |
+
logger.error("Java backend exited! Restarting...")
|
| 897 |
+
_backend_ready.clear()
|
| 898 |
+
start_backend()
|
| 899 |
+
_backend_ready.wait(timeout=120)
|
| 900 |
+
time.sleep(10)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
# ═══════════════════════════════════════════════════════════════
|
| 904 |
+
# Gradio UI
|
| 905 |
+
# ═══════════════════════════════════════════════════════════════
|
| 906 |
+
import gradio as gr
|
| 907 |
+
|
| 908 |
+
# ZeroGPU guard (required on GPU-backed spaces, harmless on CPU)
|
| 909 |
+
try:
|
| 910 |
+
import spaces
|
| 911 |
+
@spaces.GPU(duration=1)
|
| 912 |
+
def _gpuguard():
|
| 913 |
+
pass
|
| 914 |
+
except ImportError:
|
| 915 |
+
pass
|
| 916 |
+
|
| 917 |
+
|
| 918 |
+
def build_gradio_ui():
|
| 919 |
+
proxy_url = f"/proxy/{BACKEND_PORT}/"
|
| 920 |
+
with gr.Blocks(title="HarshItPDF", theme=gr.themes.Base()) as demo:
|
| 921 |
+
gr.HTML(f"""
|
| 922 |
+
<script>
|
| 923 |
+
(function(){{
|
| 924 |
+
const check = () => {{
|
| 925 |
+
fetch("{proxy_url}login", {{mode: "no-cors"}})
|
| 926 |
+
.then(() => window.location.href = "{proxy_url}")
|
| 927 |
+
.catch(() => setTimeout(check, 3000));
|
| 928 |
+
}};
|
| 929 |
+
setTimeout(check, 2000);
|
| 930 |
+
}})();
|
| 931 |
+
</script>
|
| 932 |
+
<div style="text-align:center;padding-top:20vh;color:#e0e0ff;background:#0f0f23;min-height:100vh;font-family:sans-serif;">
|
| 933 |
+
<h1 style="font-size:2.5rem;background:linear-gradient(135deg,#6366f1,#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">HarshItPDF</h1>
|
| 934 |
+
<p style="opacity:0.7;">Your private PDF powerhouse</p>
|
| 935 |
+
<div style="width:40px;height:40px;border:3px solid rgba(99,102,241,0.3);border-top-color:#6366f1;border-radius:50%;animation:spin 1s linear infinite;margin:1rem auto;"></div>
|
| 936 |
+
<p style="font-family:monospace;font-size:0.9rem;opacity:0.6;margin-top:1rem;">Starting backend... This may take 1–2 min on cold start.</p>
|
| 937 |
+
<p style="font-size:0.85rem;opacity:0.5;margin-top:0.5rem;">Default login: admin / changeme123</p>
|
| 938 |
+
<style>@keyframes spin{{to{{transform:rotate(360deg)}}}}</style>
|
| 939 |
+
</div>
|
| 940 |
+
""")
|
| 941 |
+
return demo
|
| 942 |
+
|
| 943 |
+
|
| 944 |
+
# Start services in background thread when module is imported
|
| 945 |
+
_svc_thread = threading.Thread(target=_start_services, daemon=True)
|
| 946 |
+
_svc_thread.start()
|
| 947 |
+
|
| 948 |
+
# HF Spaces Gradio SDK requires `demo` at module scope
|
| 949 |
+
demo = build_gradio_ui()
|
license_server.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
mock-license-server — Standalone HTTP server that validates licenses.
|
| 3 |
+
Replaces api.keygen.sh for the HarshItPDF license verification flow.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
| 10 |
+
|
| 11 |
+
PORT = int(os.environ.get("LICENCE_SERVER_PORT", "7871"))
|
| 12 |
+
logger = logging.getLogger("license-server")
|
| 13 |
+
|
| 14 |
+
VALID_RESPONSE = {
|
| 15 |
+
"meta": {"valid": True},
|
| 16 |
+
"data": {
|
| 17 |
+
"type": "license",
|
| 18 |
+
"id": "mock-license-001",
|
| 19 |
+
"attributes": {
|
| 20 |
+
"name": "HarshItPDF Enterprise License",
|
| 21 |
+
"key": "harshitpdf-unlocked-0000-0000-0000-000000000001",
|
| 22 |
+
"status": "ACTIVE",
|
| 23 |
+
"expiry": None,
|
| 24 |
+
"metadata": {
|
| 25 |
+
"isEnterprise": True,
|
| 26 |
+
"isPremium": True,
|
| 27 |
+
"maxUsers": 999,
|
| 28 |
+
"plan": "enterprise",
|
| 29 |
+
"tier": "enterprise",
|
| 30 |
+
},
|
| 31 |
+
"maxMachines": 999,
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class LicenseHandler(BaseHTTPRequestHandler):
|
| 38 |
+
def do_POST(self):
|
| 39 |
+
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
| 40 |
+
logger.info(f"License check from {self.client_address}: {self.path}")
|
| 41 |
+
self.send_response(200)
|
| 42 |
+
self.send_header("Content-Type", "application/json")
|
| 43 |
+
self.send_header("Connection", "close")
|
| 44 |
+
self.end_headers()
|
| 45 |
+
self.wfile.write(json.dumps(VALID_RESPONSE).encode())
|
| 46 |
+
|
| 47 |
+
def do_GET(self):
|
| 48 |
+
self.send_response(200)
|
| 49 |
+
self.send_header("Content-Type", "application/json")
|
| 50 |
+
self.send_header("Connection", "close")
|
| 51 |
+
self.end_headers()
|
| 52 |
+
self.wfile.write(
|
| 53 |
+
json.dumps({"ok": True, "service": "mock-license-server", "version": "1.0"}).encode()
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
def log_message(self, fmt, *args):
|
| 57 |
+
logger.info(fmt % args)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def start_license_server():
|
| 61 |
+
server = HTTPServer(("0.0.0.0", PORT), LicenseHandler)
|
| 62 |
+
logger.info(f"Mock license server listening on port {PORT}")
|
| 63 |
+
server.serve_forever()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
logging.basicConfig(level=logging.INFO, format="[license-server] %(message)s")
|
| 68 |
+
start_license_server()
|
packages.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openjdk-17-jdk-headless
|
| 2 |
+
tesseract-ocr
|
| 3 |
+
tesseract-ocr-eng
|
| 4 |
+
unzip
|
| 5 |
+
wget
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0
|