gabefromsimplebit's picture
Update app.py
5c2d1aa verified
Raw History Blame Contribute Delete
7.34 kB
from flask import Flask, request, send_file, jsonify, redirect
from flask_cors import CORS
import subprocess
import plistlib
import zipfile
import shutil
import uuid
import tempfile
from pathlib import Path
app = Flask(__name__)
CORS(app)
BASE_URL = "https://gabefromsimplebit-simplebit-signer.hf.space"
# -------------------------------------------------
# HOME
# -------------------------------------------------
@app.route("/")
def home():
return jsonify({"status": "online"})
# -------------------------------------------------
# FIND .APP BUNDLE
# -------------------------------------------------
def find_app_bundle(temp_dir):
for item in Path(temp_dir).rglob("*.app"):
if item.is_dir():
return item
return None
# -------------------------------------------------
# READ INFO.PLIST
# -------------------------------------------------
def read_info_plist(app_bundle):
for f in Path(app_bundle).rglob("Info.plist"):
with open(f, "rb") as fp:
return f, plistlib.load(fp)
return None, None
def write_info_plist(plist_path, data):
with open(plist_path, "wb") as fp:
plistlib.dump(data, fp)
# -------------------------------------------------
# SIGN ROUTE
# -------------------------------------------------
@app.route("/sign", methods=["POST"])
def sign():
try:
if "ipa" not in request.files:
return jsonify({"error": "Missing IPA"}), 400
if "p12" not in request.files:
return jsonify({"error": "Missing P12"}), 400
if "provision" not in request.files:
return jsonify({"error": "Missing provisioning profile"}), 400
zsign_path = shutil.which("zsign")
if not zsign_path:
return jsonify({"error": "zsign not installed"}), 500
job_id = str(uuid.uuid4())[:8]
job_dir = Path(job_id)
job_dir.mkdir(exist_ok=True)
ipa_path = job_dir / "app.ipa"
p12_path = job_dir / "cert.p12"
prov_path = job_dir / "profile.mobileprovision"
signed_path = job_dir / "signed.ipa"
request.files["ipa"].save(ipa_path)
request.files["p12"].save(p12_path)
request.files["provision"].save(prov_path)
password = request.form.get("password", "")
# -------------------------------------------------
# EXTRACT IPA
# -------------------------------------------------
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(ipa_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
app_bundle = find_app_bundle(temp_dir)
if not app_bundle:
shutil.rmtree(temp_dir)
return jsonify({"error": "No .app found"}), 500
# -------------------------------------------------
# READ ORIGINAL INFO
# -------------------------------------------------
plist_path, plist_data = read_info_plist(app_bundle)
if not plist_data:
shutil.rmtree(temp_dir)
return jsonify({"error": "Info.plist missing"}), 500
original_bundle_id = plist_data.get("CFBundleIdentifier", "unknown.bundle")
original_name = (
plist_data.get("CFBundleDisplayName")
or plist_data.get("CFBundleName")
or "App"
)
# -------------------------------------------------
# DO NOT CHANGE BUNDLE ID (ENTERPRISE MODE)
# -------------------------------------------------
target_bundle_id = original_bundle_id
plist_data["CFBundleIdentifier"] = target_bundle_id
plist_data["CFBundleDisplayName"] = original_name
plist_data["CFBundleName"] = original_name
write_info_plist(plist_path, plist_data)
# -------------------------------------------------
# SIGN
# -------------------------------------------------
command = [
zsign_path,
"-f",
"-k", str(p12_path),
"-p", password,
"-m", str(prov_path),
str(app_bundle)
]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
shutil.rmtree(temp_dir)
return jsonify({
"error": "Signing failed",
"stdout": result.stdout,
"stderr": result.stderr
}), 500
# -------------------------------------------------
# BUILD CLEAN IPA
# -------------------------------------------------
payload_dir = Path(job_dir) / "Payload"
payload_dir.mkdir(exist_ok=True)
shutil.copytree(app_bundle, payload_dir / app_bundle.name)
zip_path = Path(job_dir) / "signed"
shutil.make_archive(str(zip_path), "zip", job_dir, "Payload")
shutil.move(str(zip_path) + ".zip", signed_path)
shutil.rmtree(temp_dir)
# save ORIGINAL bundle id for manifest
(job_dir / "bundle_id.txt").write_text(original_bundle_id)
plist_url = f"{BASE_URL}/plist/{job_id}"
install_url = f"itms-services://?action=download-manifest&url={plist_url}"
return redirect(install_url)
except Exception as e:
return jsonify({"error": str(e)}), 500
# -------------------------------------------------
# MANIFEST
# -------------------------------------------------
@app.route("/plist/<job_id>")
def plist(job_id):
job_dir = Path(job_id)
bundle_id = "unknown"
if (job_dir / "bundle_id.txt").exists():
bundle_id = (job_dir / "bundle_id.txt").read_text().strip()
ipa_url = f"{BASE_URL}/download/{job_id}"
plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>{ipa_url}</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>{bundle_id}</string>
<key>bundle-version</key>
<string>1.0</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>Signed App</string>
</dict>
</dict>
</array>
</dict>
</plist>
"""
plist_path = job_dir / "manifest.plist"
plist_path.write_text(plist_content)
return send_file(plist_path, mimetype="application/xml")
# -------------------------------------------------
# DOWNLOAD IPA
# -------------------------------------------------
@app.route("/download/<job_id>")
def download(job_id):
ipa_path = Path(job_id) / "signed.ipa"
if not ipa_path.exists():
return jsonify({"error": "IPA not found"}), 404
response = send_file(ipa_path, mimetype="application/octet-stream")
response.headers["Content-Disposition"] = "inline; filename=signed.ipa"
return response
# -------------------------------------------------
# HEALTH
# -------------------------------------------------
@app.route("/health")
def health():
return jsonify({"ok": True, "zsign": shutil.which("zsign") is not None})
# -------------------------------------------------
# RUN
# -------------------------------------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860, debug=True)