hursheesh commited on
Commit
4578ce6
·
verified ·
1 Parent(s): 0d57ecf

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +49 -6
  2. app.py +949 -0
  3. license_server.py +68 -0
  4. packages.txt +5 -0
  5. requirements.txt +1 -0
README.md CHANGED
@@ -1,13 +1,56 @@
1
  ---
2
  title: HarshItPDF
3
- emoji: 👁
4
- colorFrom: red
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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