gabefromsimplebit commited on
Commit
7279a02
·
verified ·
1 Parent(s): 7107715

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +203 -53
app.py CHANGED
@@ -1,83 +1,150 @@
1
  from flask import Flask, request, send_file, redirect, jsonify
2
  from flask_cors import CORS
 
3
  import subprocess
4
- import os
5
- import uuid
6
  import plistlib
7
  import zipfile
 
 
 
 
8
  from pathlib import Path
9
 
10
  app = Flask(__name__)
11
  CORS(app)
12
 
 
13
  BASE_URL = "https://gabefromsimplebit-simplebit-signer.hf.space"
14
 
15
 
16
  @app.route("/")
17
  def home():
18
- return "SimpleBit Signer is Online!"
 
 
 
19
 
20
 
21
  def get_actual_bundle_id(ipa_path):
22
  """
23
- Extract the real bundle ID from the IPA Info.plist safely.
24
  """
25
  try:
26
  with zipfile.ZipFile(ipa_path, "r") as ipa:
27
- plist_file = next(
28
- f for f in ipa.namelist()
29
- if f.startswith("Payload/") and f.endswith("Info.plist")
30
- )
31
 
32
- with ipa.open(plist_file) as fp:
33
- plist_data = plistlib.load(fp)
 
 
 
 
 
 
 
 
34
 
35
  return plist_data.get("CFBundleIdentifier")
36
 
37
  except Exception as e:
38
- print(f"Bundle ID extraction failed: {e}")
39
 
40
  return None
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  @app.route("/sign", methods=["POST"])
44
  def sign():
45
  try:
 
46
  # Validate uploads
 
 
47
  if "ipa" not in request.files:
48
- return jsonify({"error": "Missing IPA file"}), 400
 
 
49
 
50
  if "p12" not in request.files:
51
- return jsonify({"error": "Missing P12 certificate"}), 400
 
 
52
 
53
  if "provision" not in request.files:
54
- return jsonify({"error": "Missing mobileprovision file"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  job_id = str(uuid.uuid4())[:8]
 
57
  job_dir = Path(job_id)
58
  job_dir.mkdir(exist_ok=True)
59
 
 
 
 
 
60
  ipa = request.files["ipa"]
61
  p12 = request.files["p12"]
62
- prov = request.files["provision"]
63
 
64
- password = request.form.get("password", "1234")
65
 
66
  ipa_path = job_dir / "app.ipa"
67
  p12_path = job_dir / "cert.p12"
68
- prov_path = job_dir / "prov.mobileprovision"
69
  out_path = job_dir / "signed.ipa"
70
 
71
  ipa.save(ipa_path)
72
  p12.save(p12_path)
73
- prov.save(prov_path)
 
 
 
 
74
 
75
- # Extract bundle ID before signing
76
  bundle_id = get_actual_bundle_id(ipa_path)
77
 
78
- # Run zsign
 
 
 
 
 
79
  command = [
80
- "zsign",
81
  "-k", str(p12_path),
82
  "-p", password,
83
  "-m", str(prov_path),
@@ -86,33 +153,78 @@ def sign():
86
  str(ipa_path)
87
  ]
88
 
 
 
 
 
 
 
 
89
  result = subprocess.run(
90
  command,
91
  capture_output=True,
92
  text=True
93
  )
94
 
 
 
 
 
 
 
 
 
 
 
95
  if result.returncode != 0:
96
  return jsonify({
97
  "error": "Signing failed",
98
- "details": result.stderr
 
 
 
 
 
 
 
 
 
 
 
99
  }), 500
100
 
 
101
  # Fallback bundle ID
 
 
102
  if not bundle_id:
103
  bundle_id = "com.simplebit.dynamic"
104
 
105
- # Save bundle ID
106
- with open(job_dir / "bundle_id.txt", "w") as f:
107
- f.write(bundle_id)
 
 
108
 
109
  plist_url = f"{BASE_URL}/plist/{job_id}"
110
 
111
- return redirect(
112
- f"itms-services://?action=download-manifest&url={plist_url}"
 
113
  )
114
 
 
 
 
 
 
 
 
 
 
115
  except Exception as e:
 
 
116
  return jsonify({
117
  "error": str(e)
118
  }), 500
@@ -120,36 +232,45 @@ def sign():
120
 
121
  @app.route("/plist/<job_id>")
122
  def serve_plist(job_id):
123
- job_dir = Path(job_id)
124
-
125
  try:
126
- with open(job_dir / "bundle_id.txt", "r") as f:
127
- bundle_id = f.read().strip()
128
- except:
129
- bundle_id = "com.simplebit.dynamic"
 
 
 
 
130
 
131
- ipa_direct_url = f"{BASE_URL}/download/{job_id}"
132
 
133
- plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
 
 
134
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
135
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 
136
  <plist version="1.0">
137
  <dict>
 
138
  <key>items</key>
139
  <array>
140
  <dict>
 
141
  <key>assets</key>
142
  <array>
143
  <dict>
144
  <key>kind</key>
145
  <string>software-package</string>
 
146
  <key>url</key>
147
- <string>{ipa_direct_url}</string>
148
  </dict>
149
  </array>
150
 
151
  <key>metadata</key>
152
  <dict>
 
153
  <key>bundle-identifier</key>
154
  <string>{bundle_id}</string>
155
 
@@ -161,36 +282,65 @@ def serve_plist(job_id):
161
 
162
  <key>title</key>
163
  <string>SimpleBit Signed App</string>
 
164
  </dict>
 
165
  </dict>
166
  </array>
 
167
  </dict>
168
- </plist>"""
 
 
 
169
 
170
- plist_path = job_dir / "manifest.plist"
 
171
 
172
- with open(plist_path, "w") as f:
173
- f.write(plist_content)
 
 
174
 
175
- return send_file(
176
- plist_path,
177
- mimetype="application/xml"
178
- )
179
 
180
 
181
  @app.route("/download/<job_id>")
182
  def download_ipa(job_id):
183
- ipa_path = Path(job_id) / "signed.ipa"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- if not ipa_path.exists():
186
- return jsonify({"error": "IPA not found"}), 404
187
 
188
- return send_file(
189
- ipa_path,
190
- as_attachment=True,
191
- download_name="signed.ipa"
192
- )
 
193
 
194
 
195
  if __name__ == "__main__":
196
- app.run(host="0.0.0.0", port=7860)
 
 
 
 
 
1
  from flask import Flask, request, send_file, redirect, jsonify
2
  from flask_cors import CORS
3
+
4
  import subprocess
 
 
5
  import plistlib
6
  import zipfile
7
+ import shutil
8
+ import uuid
9
+ import os
10
+
11
  from pathlib import Path
12
 
13
  app = Flask(__name__)
14
  CORS(app)
15
 
16
+ # CHANGE THIS TO YOUR REAL SPACE URL
17
  BASE_URL = "https://gabefromsimplebit-simplebit-signer.hf.space"
18
 
19
 
20
  @app.route("/")
21
  def home():
22
+ return jsonify({
23
+ "status": "online",
24
+ "service": "SimpleBit Signer"
25
+ })
26
 
27
 
28
  def get_actual_bundle_id(ipa_path):
29
  """
30
+ Safely extract CFBundleIdentifier from IPA
31
  """
32
  try:
33
  with zipfile.ZipFile(ipa_path, "r") as ipa:
34
+ plist_name = None
 
 
 
35
 
36
+ for name in ipa.namelist():
37
+ if name.startswith("Payload/") and name.endswith("Info.plist"):
38
+ plist_name = name
39
+ break
40
+
41
+ if not plist_name:
42
+ return None
43
+
44
+ with ipa.open(plist_name) as plist_file:
45
+ plist_data = plistlib.load(plist_file)
46
 
47
  return plist_data.get("CFBundleIdentifier")
48
 
49
  except Exception as e:
50
+ print("Bundle ID extraction failed:", str(e))
51
 
52
  return None
53
 
54
 
55
+ def save_bundle_id(job_dir, bundle_id):
56
+ try:
57
+ with open(job_dir / "bundle_id.txt", "w") as f:
58
+ f.write(bundle_id)
59
+ except Exception as e:
60
+ print("Failed to save bundle ID:", str(e))
61
+
62
+
63
+ def load_bundle_id(job_dir):
64
+ try:
65
+ with open(job_dir / "bundle_id.txt", "r") as f:
66
+ return f.read().strip()
67
+ except:
68
+ return "com.simplebit.dynamic"
69
+
70
+
71
  @app.route("/sign", methods=["POST"])
72
  def sign():
73
  try:
74
+ # ---------------------------------------------------
75
  # Validate uploads
76
+ # ---------------------------------------------------
77
+
78
  if "ipa" not in request.files:
79
+ return jsonify({
80
+ "error": "Missing IPA file"
81
+ }), 400
82
 
83
  if "p12" not in request.files:
84
+ return jsonify({
85
+ "error": "Missing P12 certificate"
86
+ }), 400
87
 
88
  if "provision" not in request.files:
89
+ return jsonify({
90
+ "error": "Missing provisioning profile"
91
+ }), 400
92
+
93
+ # ---------------------------------------------------
94
+ # Check zsign installation
95
+ # ---------------------------------------------------
96
+
97
+ zsign_path = shutil.which("zsign")
98
+
99
+ print("ZSIGN PATH:", zsign_path)
100
+
101
+ if not zsign_path:
102
+ return jsonify({
103
+ "error": "zsign is not installed"
104
+ }), 500
105
+
106
+ # ---------------------------------------------------
107
+ # Create job directory
108
+ # ---------------------------------------------------
109
 
110
  job_id = str(uuid.uuid4())[:8]
111
+
112
  job_dir = Path(job_id)
113
  job_dir.mkdir(exist_ok=True)
114
 
115
+ # ---------------------------------------------------
116
+ # Uploaded files
117
+ # ---------------------------------------------------
118
+
119
  ipa = request.files["ipa"]
120
  p12 = request.files["p12"]
121
+ provision = request.files["provision"]
122
 
123
+ password = request.form.get("password", "")
124
 
125
  ipa_path = job_dir / "app.ipa"
126
  p12_path = job_dir / "cert.p12"
127
+ prov_path = job_dir / "profile.mobileprovision"
128
  out_path = job_dir / "signed.ipa"
129
 
130
  ipa.save(ipa_path)
131
  p12.save(p12_path)
132
+ provision.save(prov_path)
133
+
134
+ # ---------------------------------------------------
135
+ # Extract bundle ID
136
+ # ---------------------------------------------------
137
 
 
138
  bundle_id = get_actual_bundle_id(ipa_path)
139
 
140
+ print("BUNDLE ID:", bundle_id)
141
+
142
+ # ---------------------------------------------------
143
+ # zsign command
144
+ # ---------------------------------------------------
145
+
146
  command = [
147
+ zsign_path,
148
  "-k", str(p12_path),
149
  "-p", password,
150
  "-m", str(prov_path),
 
153
  str(ipa_path)
154
  ]
155
 
156
+ print("RUNNING COMMAND:")
157
+ print(" ".join(command))
158
+
159
+ # ---------------------------------------------------
160
+ # Run signing
161
+ # ---------------------------------------------------
162
+
163
  result = subprocess.run(
164
  command,
165
  capture_output=True,
166
  text=True
167
  )
168
 
169
+ print("RETURN CODE:", result.returncode)
170
+ print("STDOUT:")
171
+ print(result.stdout)
172
+ print("STDERR:")
173
+ print(result.stderr)
174
+
175
+ # ---------------------------------------------------
176
+ # Handle failure
177
+ # ---------------------------------------------------
178
+
179
  if result.returncode != 0:
180
  return jsonify({
181
  "error": "Signing failed",
182
+ "returncode": result.returncode,
183
+ "stdout": result.stdout,
184
+ "stderr": result.stderr
185
+ }), 500
186
+
187
+ # ---------------------------------------------------
188
+ # Verify signed IPA exists
189
+ # ---------------------------------------------------
190
+
191
+ if not out_path.exists():
192
+ return jsonify({
193
+ "error": "Signed IPA missing after signing"
194
  }), 500
195
 
196
+ # ---------------------------------------------------
197
  # Fallback bundle ID
198
+ # ---------------------------------------------------
199
+
200
  if not bundle_id:
201
  bundle_id = "com.simplebit.dynamic"
202
 
203
+ save_bundle_id(job_dir, bundle_id)
204
+
205
+ # ---------------------------------------------------
206
+ # OTA Install URL
207
+ # ---------------------------------------------------
208
 
209
  plist_url = f"{BASE_URL}/plist/{job_id}"
210
 
211
+ ota_url = (
212
+ "itms-services://?action=download-manifest"
213
+ f"&url={plist_url}"
214
  )
215
 
216
+ return jsonify({
217
+ "success": True,
218
+ "job_id": job_id,
219
+ "bundle_id": bundle_id,
220
+ "install_url": ota_url,
221
+ "plist_url": plist_url,
222
+ "download_url": f"{BASE_URL}/download/{job_id}"
223
+ })
224
+
225
  except Exception as e:
226
+ print("FATAL ERROR:", str(e))
227
+
228
  return jsonify({
229
  "error": str(e)
230
  }), 500
 
232
 
233
  @app.route("/plist/<job_id>")
234
  def serve_plist(job_id):
 
 
235
  try:
236
+ job_dir = Path(job_id)
237
+
238
+ signed_ipa = job_dir / "signed.ipa"
239
+
240
+ if not signed_ipa.exists():
241
+ return jsonify({
242
+ "error": "Signed IPA not found"
243
+ }), 404
244
 
245
+ bundle_id = load_bundle_id(job_dir)
246
 
247
+ ipa_url = f"{BASE_URL}/download/{job_id}"
248
+
249
+ plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
250
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
251
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
252
+
253
  <plist version="1.0">
254
  <dict>
255
+
256
  <key>items</key>
257
  <array>
258
  <dict>
259
+
260
  <key>assets</key>
261
  <array>
262
  <dict>
263
  <key>kind</key>
264
  <string>software-package</string>
265
+
266
  <key>url</key>
267
+ <string>{ipa_url}</string>
268
  </dict>
269
  </array>
270
 
271
  <key>metadata</key>
272
  <dict>
273
+
274
  <key>bundle-identifier</key>
275
  <string>{bundle_id}</string>
276
 
 
282
 
283
  <key>title</key>
284
  <string>SimpleBit Signed App</string>
285
+
286
  </dict>
287
+
288
  </dict>
289
  </array>
290
+
291
  </dict>
292
+ </plist>
293
+ """
294
+
295
+ plist_path = job_dir / "manifest.plist"
296
 
297
+ with open(plist_path, "w") as f:
298
+ f.write(plist_content)
299
 
300
+ return send_file(
301
+ plist_path,
302
+ mimetype="application/xml"
303
+ )
304
 
305
+ except Exception as e:
306
+ return jsonify({
307
+ "error": str(e)
308
+ }), 500
309
 
310
 
311
  @app.route("/download/<job_id>")
312
  def download_ipa(job_id):
313
+ try:
314
+ ipa_path = Path(job_id) / "signed.ipa"
315
+
316
+ if not ipa_path.exists():
317
+ return jsonify({
318
+ "error": "IPA not found"
319
+ }), 404
320
+
321
+ return send_file(
322
+ ipa_path,
323
+ as_attachment=True,
324
+ download_name="signed.ipa"
325
+ )
326
+
327
+ except Exception as e:
328
+ return jsonify({
329
+ "error": str(e)
330
+ }), 500
331
 
 
 
332
 
333
+ @app.route("/health")
334
+ def health():
335
+ return jsonify({
336
+ "status": "healthy",
337
+ "zsign_installed": shutil.which("zsign") is not None
338
+ })
339
 
340
 
341
  if __name__ == "__main__":
342
+ app.run(
343
+ host="0.0.0.0",
344
+ port=7860,
345
+ debug=True
346
+ )