mirror of
https://git.hexahost.dev/smueller/HexaHost-Frontend.git
synced 2026-06-02 05:08:43 +00:00
Merge branch 'dev'
This commit is contained in:
51
.gitea/workflows/obfuscate-main.yml
Normal file
51
.gitea/workflows/obfuscate-main.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Obfuscate Main Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
obfuscate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Skip loop commits
|
||||||
|
run: |
|
||||||
|
msg="$(git log -1 --pretty=%B)"
|
||||||
|
echo "Last commit message: $msg"
|
||||||
|
if echo "$msg" | grep -q "\[skip ci\]"; then
|
||||||
|
echo "Skip CI commit detected."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Run release obfuscation
|
||||||
|
run: python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
|
|
||||||
|
- name: Commit obfuscated build
|
||||||
|
run: |
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "actions@local"
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No build changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
|
||||||
|
git push
|
||||||
44
.github/workflows/obfuscate-main.yml
vendored
Normal file
44
.github/workflows/obfuscate-main.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Obfuscate Main Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
obfuscate:
|
||||||
|
if: github.actor != 'github-actions[bot]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Run release obfuscation
|
||||||
|
run: python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
|
|
||||||
|
- name: Commit obfuscated build
|
||||||
|
run: |
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No build changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
|
||||||
|
git push
|
||||||
23
README.md
23
README.md
@@ -166,25 +166,22 @@ Für den Produktivbetrieb `public/` als Webroot konfigurieren.
|
|||||||
|
|
||||||
### Production-Build & Veröffentlichung
|
### Production-Build & Veröffentlichung
|
||||||
|
|
||||||
Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main` (ohne Kommentare, obfuskiertes JS).
|
Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main`.
|
||||||
|
|
||||||
**Voraussetzungen:** Node.js 18+ (inkl. npm), PHP 8+ CLI, Git
|
Bei jedem Push/Merge auf `main` läuft die GitHub Action `.github/workflows/obfuscate-main.yml` automatisch und führt aus:
|
||||||
|
|
||||||
```powershell
|
- Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS
|
||||||
# Windows
|
- Minify + Obfuscate für JavaScript
|
||||||
.\scripts\run-build.ps1
|
- Minify für CSS
|
||||||
.\scripts\publish-to-main.ps1 -Push
|
- Kein Source-Map-Output
|
||||||
```
|
- Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung
|
||||||
|
|
||||||
|
Lokal ausführbar:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux / macOS
|
python scripts/obfuscate_release.py --root . --hash-assets
|
||||||
chmod +x scripts/*.sh
|
|
||||||
./scripts/run-build.sh
|
|
||||||
./scripts/publish-to-main.sh --push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Details: `scripts/build/README.md`
|
|
||||||
|
|
||||||
## 🔗 Backend-Integration
|
## 🔗 Backend-Integration
|
||||||
|
|
||||||
Das Backend-Repository enthält folgende wiederverwendbare Komponenten:
|
Das Backend-Repository enthält folgende wiederverwendbare Komponenten:
|
||||||
|
|||||||
BIN
scripts/__pycache__/obfuscate_release.cpython-314.pyc
Normal file
BIN
scripts/__pycache__/obfuscate_release.cpython-314.pyc
Normal file
Binary file not shown.
337
scripts/obfuscate_release.py
Normal file
337
scripts/obfuscate_release.py
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"}
|
||||||
|
|
||||||
|
|
||||||
|
def strip_comments_keep_strings(text: str) -> str:
|
||||||
|
out = []
|
||||||
|
i = 0
|
||||||
|
n = len(text)
|
||||||
|
in_single = False
|
||||||
|
in_double = False
|
||||||
|
in_template = False
|
||||||
|
escape = False
|
||||||
|
in_line_comment = False
|
||||||
|
in_block_comment = False
|
||||||
|
|
||||||
|
while i < n:
|
||||||
|
ch = text[i]
|
||||||
|
nxt = text[i + 1] if i + 1 < n else ""
|
||||||
|
|
||||||
|
if in_line_comment:
|
||||||
|
if ch == "\n":
|
||||||
|
in_line_comment = False
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_block_comment:
|
||||||
|
if ch == "*" and nxt == "/":
|
||||||
|
in_block_comment = False
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
if ch == "\n":
|
||||||
|
out.append("\n")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_single or in_double or in_template:
|
||||||
|
out.append(ch)
|
||||||
|
if escape:
|
||||||
|
escape = False
|
||||||
|
elif ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif in_single and ch == "'":
|
||||||
|
in_single = False
|
||||||
|
elif in_double and ch == '"':
|
||||||
|
in_double = False
|
||||||
|
elif in_template and ch == "`":
|
||||||
|
in_template = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "/" and nxt == "/":
|
||||||
|
in_line_comment = True
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if ch == "/" and nxt == "*":
|
||||||
|
in_block_comment = True
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "'":
|
||||||
|
in_single = True
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if ch == '"':
|
||||||
|
in_double = True
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if ch == "`":
|
||||||
|
in_template = True
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_php_comments(text: str) -> str:
|
||||||
|
out = []
|
||||||
|
i = 0
|
||||||
|
n = len(text)
|
||||||
|
in_single = False
|
||||||
|
in_double = False
|
||||||
|
in_line_comment = False
|
||||||
|
in_block_comment = False
|
||||||
|
escape = False
|
||||||
|
|
||||||
|
while i < n:
|
||||||
|
ch = text[i]
|
||||||
|
nxt = text[i + 1] if i + 1 < n else ""
|
||||||
|
|
||||||
|
if in_line_comment:
|
||||||
|
if ch == "\n":
|
||||||
|
in_line_comment = False
|
||||||
|
out.append("\n")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_block_comment:
|
||||||
|
if ch == "*" and nxt == "/":
|
||||||
|
in_block_comment = False
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
if ch == "\n":
|
||||||
|
out.append("\n")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_single or in_double:
|
||||||
|
out.append(ch)
|
||||||
|
if escape:
|
||||||
|
escape = False
|
||||||
|
elif ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif in_single and ch == "'":
|
||||||
|
in_single = False
|
||||||
|
elif in_double and ch == '"':
|
||||||
|
in_double = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "/" and nxt == "/":
|
||||||
|
in_line_comment = True
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if ch == "#":
|
||||||
|
in_line_comment = True
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if ch == "/" and nxt == "*":
|
||||||
|
in_block_comment = True
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "'":
|
||||||
|
in_single = True
|
||||||
|
elif ch == '"':
|
||||||
|
in_double = True
|
||||||
|
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def minify_css_fallback(text: str) -> str:
|
||||||
|
text = strip_comments_keep_strings(text)
|
||||||
|
text = re.sub(r"\s+", " ", text)
|
||||||
|
text = re.sub(r"\s*([{}:;,>+~])\s*", r"\1", text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def minify_js_fallback(text: str) -> str:
|
||||||
|
text = strip_comments_keep_strings(text)
|
||||||
|
text = re.sub(r"\s+", " ", text)
|
||||||
|
text = re.sub(r"\s*([{}:;,()=+\-*/<>!&|?])\s*", r"\1", text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(command: list[str], cwd: Path, input_text: str | None = None) -> str:
|
||||||
|
proc = subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=str(cwd),
|
||||||
|
input=input_text,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(proc.stderr.strip() or "command failed")
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def process_js(path: Path, cwd: Path) -> None:
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
if shutil.which("npx"):
|
||||||
|
try:
|
||||||
|
minified = run_cmd(
|
||||||
|
[
|
||||||
|
"npx",
|
||||||
|
"--yes",
|
||||||
|
"terser",
|
||||||
|
"--compress",
|
||||||
|
"--mangle",
|
||||||
|
"--comments",
|
||||||
|
"false",
|
||||||
|
],
|
||||||
|
cwd,
|
||||||
|
input_text=original,
|
||||||
|
)
|
||||||
|
obfuscated = run_cmd(
|
||||||
|
[
|
||||||
|
"npx",
|
||||||
|
"--yes",
|
||||||
|
"javascript-obfuscator",
|
||||||
|
"--compact",
|
||||||
|
"true",
|
||||||
|
"--control-flow-flattening",
|
||||||
|
"true",
|
||||||
|
"--dead-code-injection",
|
||||||
|
"true",
|
||||||
|
"--string-array",
|
||||||
|
"true",
|
||||||
|
"--string-array-encoding",
|
||||||
|
"base64",
|
||||||
|
"--target",
|
||||||
|
"browser-no-eval",
|
||||||
|
"--source-map",
|
||||||
|
"false",
|
||||||
|
"--output",
|
||||||
|
"stdout",
|
||||||
|
],
|
||||||
|
cwd,
|
||||||
|
input_text=minified,
|
||||||
|
)
|
||||||
|
path.write_text(obfuscated.strip() + "\n", encoding="utf-8")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def process_css(path: Path, cwd: Path) -> None:
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
if shutil.which("npx"):
|
||||||
|
try:
|
||||||
|
minified = run_cmd(
|
||||||
|
[
|
||||||
|
"npx",
|
||||||
|
"--yes",
|
||||||
|
"clean-css-cli",
|
||||||
|
"--skip-rebase",
|
||||||
|
"-O2",
|
||||||
|
],
|
||||||
|
cwd,
|
||||||
|
input_text=original,
|
||||||
|
)
|
||||||
|
path.write_text(minified.strip() + "\n", encoding="utf-8")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def process_php(path: Path) -> None:
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
stripped = strip_php_comments(original)
|
||||||
|
path.write_text(stripped, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
||||||
|
return digest
|
||||||
|
|
||||||
|
|
||||||
|
def replace_references(root: Path, mapping: dict[str, str]) -> None:
|
||||||
|
for candidate in root.rglob("*"):
|
||||||
|
if not candidate.is_file() or candidate.suffix.lower() not in TEXT_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
content = candidate.read_text(encoding="utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
updated = content
|
||||||
|
for src, dst in mapping.items():
|
||||||
|
updated = updated.replace(src, dst)
|
||||||
|
updated = updated.replace("/" + src, "/" + dst)
|
||||||
|
if updated != content:
|
||||||
|
candidate.write_text(updated, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def build_hash_mapping(public_root: Path) -> dict[str, str]:
|
||||||
|
mapping: dict[str, str] = {}
|
||||||
|
asset_root = public_root / "assets"
|
||||||
|
for ext in (".js", ".css"):
|
||||||
|
for file_path in sorted(asset_root.rglob(f"*{ext}")):
|
||||||
|
if ".min." in file_path.name or ".obf." in file_path.name:
|
||||||
|
continue
|
||||||
|
digest = hash_file(file_path)
|
||||||
|
new_name = f"{file_path.stem}.{digest}{file_path.suffix}"
|
||||||
|
new_path = file_path.with_name(new_name)
|
||||||
|
file_path.rename(new_path)
|
||||||
|
rel_old = file_path.relative_to(public_root).as_posix()
|
||||||
|
rel_new = new_path.relative_to(public_root).as_posix()
|
||||||
|
mapping[rel_old] = rel_new
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Release obfuscation build.")
|
||||||
|
parser.add_argument("--root", default=".", help="Repository root")
|
||||||
|
parser.add_argument("--hash-assets", action="store_true", help="Hash JS/CSS file names")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo_root = Path(args.root).resolve()
|
||||||
|
public_root = repo_root / "public"
|
||||||
|
|
||||||
|
if not public_root.exists():
|
||||||
|
print("public directory not found", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for js in sorted(public_root.rglob("*.js")):
|
||||||
|
process_js(js, repo_root)
|
||||||
|
for css in sorted(public_root.rglob("*.css")):
|
||||||
|
process_css(css, repo_root)
|
||||||
|
for php in sorted((repo_root / "public").rglob("*.php")):
|
||||||
|
process_php(php)
|
||||||
|
for php in sorted((repo_root / "backend").rglob("*.php")):
|
||||||
|
process_php(php)
|
||||||
|
|
||||||
|
if args.hash_assets:
|
||||||
|
mapping = build_hash_mapping(public_root)
|
||||||
|
replace_references(repo_root, mapping)
|
||||||
|
|
||||||
|
print("Release obfuscation complete.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user