gabefromsimplebit commited on
Commit
30f1d28
·
verified ·
1 Parent(s): 82cf52c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -248
app.py CHANGED
@@ -6,13 +6,13 @@ import plistlib
6
  import zipfile
7
  import shutil
8
  import uuid
 
9
 
10
  from pathlib import Path
11
 
12
  app = Flask(__name__)
13
  CORS(app)
14
 
15
- # CHANGE THIS TO YOUR REAL HF SPACE URL
16
  BASE_URL = "https://gabefromsimplebit-simplebit-signer.hf.space"
17
 
18
 
@@ -24,380 +24,242 @@ def home():
24
  })
25
 
26
 
27
- # ---------------------------------------------------------
28
- # Extract REAL main app bundle identifier
29
- # ---------------------------------------------------------
30
  def get_actual_bundle_id(ipa_path):
31
-
32
  try:
33
  with zipfile.ZipFile(ipa_path, "r") as ipa:
34
 
35
- app_plist = None
36
-
37
  for name in ipa.namelist():
38
 
39
- # ONLY match:
40
- # Payload/App.app/Info.plist
41
  if (
42
  name.startswith("Payload/")
43
  and name.count("/") == 2
44
  and name.endswith(".app/Info.plist")
45
  ):
46
- app_plist = name
47
- break
48
-
49
- if not app_plist:
50
- print("Main app Info.plist not found")
51
- return None
52
-
53
- print("MAIN APP PLIST:", app_plist)
54
-
55
- with ipa.open(app_plist) as plist_file:
56
- plist_data = plistlib.load(plist_file)
57
-
58
- bundle_id = plist_data.get("CFBundleIdentifier")
59
-
60
- print("REAL BUNDLE ID:", bundle_id)
61
-
62
- return bundle_id
63
 
64
  except Exception as e:
65
- print("Bundle extraction error:", str(e))
66
 
67
  return None
68
 
69
 
70
- # ---------------------------------------------------------
71
- # Save bundle ID
72
- # ---------------------------------------------------------
73
  def save_bundle_id(job_dir, bundle_id):
 
 
74
 
75
- try:
76
- with open(job_dir / "bundle_id.txt", "w") as f:
77
- f.write(bundle_id)
78
-
79
- except Exception as e:
80
- print("Save bundle ID error:", str(e))
81
 
82
-
83
- # ---------------------------------------------------------
84
- # Load bundle ID
85
- # ---------------------------------------------------------
86
  def load_bundle_id(job_dir):
87
-
88
  try:
89
- with open(job_dir / "bundle_id.txt", "r") as f:
90
- return f.read().strip()
91
-
92
  except:
93
  return "com.simplebit.dynamic"
94
 
95
 
96
- # ---------------------------------------------------------
97
  # SIGN IPA
98
- # ---------------------------------------------------------
99
  @app.route("/sign", methods=["POST"])
100
  def sign():
101
 
102
  try:
103
 
104
- # -------------------------------------------------
105
- # Validate uploads
106
- # -------------------------------------------------
107
-
108
  if "ipa" not in request.files:
109
- return jsonify({
110
- "error": "Missing IPA file"
111
- }), 400
112
 
113
  if "p12" not in request.files:
114
- return jsonify({
115
- "error": "Missing P12 file"
116
- }), 400
117
 
118
  if "provision" not in request.files:
119
- return jsonify({
120
- "error": "Missing provisioning profile"
121
- }), 400
122
-
123
- # -------------------------------------------------
124
- # Check zsign installation
125
- # -------------------------------------------------
126
 
127
  zsign_path = shutil.which("zsign")
128
 
129
- print("ZSIGN PATH:", zsign_path)
130
-
131
  if not zsign_path:
132
- return jsonify({
133
- "error": "zsign is not installed"
134
- }), 500
135
-
136
- # -------------------------------------------------
137
- # Create unique job folder
138
- # -------------------------------------------------
139
 
140
  job_id = str(uuid.uuid4())[:8]
141
-
142
  job_dir = Path(job_id)
143
  job_dir.mkdir(exist_ok=True)
144
 
145
- # -------------------------------------------------
146
- # Paths
147
- # -------------------------------------------------
148
-
149
  ipa_path = job_dir / "app.ipa"
150
  p12_path = job_dir / "cert.p12"
151
  prov_path = job_dir / "profile.mobileprovision"
152
  signed_path = job_dir / "signed.ipa"
153
 
154
- # -------------------------------------------------
155
- # Save uploaded files
156
- # -------------------------------------------------
157
-
158
  request.files["ipa"].save(ipa_path)
159
  request.files["p12"].save(p12_path)
160
  request.files["provision"].save(prov_path)
161
 
162
  password = request.form.get("password", "")
163
 
 
 
164
  # -------------------------------------------------
165
- # Extract bundle ID
166
  # -------------------------------------------------
 
 
 
 
 
 
167
 
168
- bundle_id = get_actual_bundle_id(ipa_path)
 
 
169
 
170
- if not bundle_id:
171
- bundle_id = "com.simplebit.dynamic"
 
 
 
 
 
 
 
 
 
172
 
173
  # -------------------------------------------------
174
- # zsign command
175
  # -------------------------------------------------
176
-
177
  command = [
178
  zsign_path,
 
179
  "-k", str(p12_path),
180
  "-p", password,
181
  "-m", str(prov_path),
182
- "-o", str(signed_path),
183
- str(ipa_path)
184
  ]
185
 
186
- print("RUNNING:")
187
- print(" ".join(command))
188
-
189
- # -------------------------------------------------
190
- # Execute zsign
191
- # -------------------------------------------------
192
-
193
  result = subprocess.run(
194
  command,
195
  capture_output=True,
196
  text=True
197
  )
198
 
199
- print("RETURN CODE:", result.returncode)
200
-
201
- print("STDOUT:")
202
  print(result.stdout)
203
-
204
- print("STDERR:")
205
  print(result.stderr)
206
 
207
- # -------------------------------------------------
208
- # Signing failed
209
- # -------------------------------------------------
210
-
211
  if result.returncode != 0:
212
-
213
  return jsonify({
214
  "error": "Signing failed",
215
- "returncode": result.returncode,
216
  "stdout": result.stdout,
217
  "stderr": result.stderr
218
  }), 500
219
 
220
  # -------------------------------------------------
221
- # Ensure IPA exists
222
  # -------------------------------------------------
 
 
 
 
 
223
 
224
- if not signed_path.exists():
225
 
226
- return jsonify({
227
- "error": "Signed IPA missing"
228
- }), 500
229
-
230
- # -------------------------------------------------
231
- # Save bundle ID
232
- # -------------------------------------------------
233
 
234
  save_bundle_id(job_dir, bundle_id)
235
 
236
- # -------------------------------------------------
237
- # Generate install link
238
- # -------------------------------------------------
239
-
240
  plist_url = f"{BASE_URL}/plist/{job_id}"
241
 
242
- install_url = (
243
- "itms-services://?action=download-manifest"
244
- f"&url={plist_url}"
245
- )
246
-
247
- print("INSTALL URL:")
248
- print(install_url)
249
-
250
- # -------------------------------------------------
251
- # AUTO REDIRECT TO INSTALL
252
- # -------------------------------------------------
253
 
254
  return redirect(install_url)
255
 
256
  except Exception as e:
 
257
 
258
- print("FATAL ERROR:")
259
- print(str(e))
260
 
261
- return jsonify({
262
- "error": str(e)
263
- }), 500
264
-
265
-
266
- # ---------------------------------------------------------
267
- # OTA plist
268
- # ---------------------------------------------------------
269
  @app.route("/plist/<job_id>")
270
- def serve_plist(job_id):
271
 
272
- try:
273
 
274
- job_dir = Path(job_id)
275
-
276
- signed_ipa = job_dir / "signed.ipa"
277
 
278
- if not signed_ipa.exists():
279
-
280
- return jsonify({
281
- "error": "Signed IPA not found"
282
- }), 404
283
 
284
- bundle_id = load_bundle_id(job_dir)
285
-
286
- ipa_url = f"{BASE_URL}/download/{job_id}"
287
-
288
- plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
289
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
290
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
291
-
292
  <plist version="1.0">
293
  <dict>
294
-
295
- <key>items</key>
296
- <array>
297
- <dict>
298
-
299
- <key>assets</key>
300
- <array>
301
-
302
- <dict>
303
- <key>kind</key>
304
- <string>software-package</string>
305
-
306
- <key>url</key>
307
- <string>{ipa_url}</string>
308
- </dict>
309
-
310
- </array>
311
-
312
- <key>metadata</key>
313
- <dict>
314
-
315
- <key>bundle-identifier</key>
316
- <string>{bundle_id}</string>
317
-
318
- <key>bundle-version</key>
319
- <string>1.0</string>
320
-
321
- <key>kind</key>
322
- <string>software</string>
323
-
324
- <key>title</key>
325
- <string>SimpleBit Signed App</string>
326
-
327
- </dict>
328
-
329
- </dict>
330
- </array>
331
-
332
  </dict>
333
- </plist>
334
- """
335
-
336
- plist_path = job_dir / "manifest.plist"
337
-
338
- with open(plist_path, "w") as f:
339
- f.write(plist_content)
 
 
 
 
 
 
 
 
 
340
 
341
- return send_file(
342
- plist_path,
343
- mimetype="application/xml"
344
- )
345
 
346
- except Exception as e:
347
 
348
- return jsonify({
349
- "error": str(e)
350
- }), 500
351
 
352
 
353
- # ---------------------------------------------------------
354
- # Download signed IPA
355
- # ---------------------------------------------------------
356
  @app.route("/download/<job_id>")
357
- def download_ipa(job_id):
358
 
359
- try:
360
 
361
- ipa_path = Path(job_id) / "signed.ipa"
 
362
 
363
- if not ipa_path.exists():
 
 
364
 
365
- return jsonify({
366
- "error": "IPA not found"
367
- }), 404
368
-
369
- return send_file(
370
- ipa_path,
371
- as_attachment=True,
372
- download_name="signed.ipa"
373
- )
374
 
375
- except Exception as e:
376
-
377
- return jsonify({
378
- "error": str(e)
379
- }), 500
380
-
381
-
382
- # ---------------------------------------------------------
383
- # Health endpoint
384
- # ---------------------------------------------------------
385
  @app.route("/health")
386
  def health():
387
-
388
  return jsonify({
389
- "status": "healthy",
390
- "zsign_installed": shutil.which("zsign") is not None
391
  })
392
 
393
 
394
- # ---------------------------------------------------------
395
- # Run Flask
396
- # ---------------------------------------------------------
397
  if __name__ == "__main__":
398
-
399
- app.run(
400
- host="0.0.0.0",
401
- port=7860,
402
- debug=True
403
- )
 
6
  import zipfile
7
  import shutil
8
  import uuid
9
+ import tempfile
10
 
11
  from pathlib import Path
12
 
13
  app = Flask(__name__)
14
  CORS(app)
15
 
 
16
  BASE_URL = "https://gabefromsimplebit-simplebit-signer.hf.space"
17
 
18
 
 
24
  })
25
 
26
 
27
+ # -------------------------------------------------
28
+ # Extract main bundle ID
29
+ # -------------------------------------------------
30
  def get_actual_bundle_id(ipa_path):
 
31
  try:
32
  with zipfile.ZipFile(ipa_path, "r") as ipa:
33
 
 
 
34
  for name in ipa.namelist():
35
 
 
 
36
  if (
37
  name.startswith("Payload/")
38
  and name.count("/") == 2
39
  and name.endswith(".app/Info.plist")
40
  ):
41
+ with ipa.open(name) as f:
42
+ plist_data = plistlib.load(f)
43
+ return plist_data.get("CFBundleIdentifier")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  except Exception as e:
46
+ print("Bundle error:", e)
47
 
48
  return None
49
 
50
 
51
+ # -------------------------------------------------
52
+ # Save / load bundle ID
53
+ # -------------------------------------------------
54
  def save_bundle_id(job_dir, bundle_id):
55
+ with open(job_dir / "bundle_id.txt", "w") as f:
56
+ f.write(bundle_id)
57
 
 
 
 
 
 
 
58
 
 
 
 
 
59
  def load_bundle_id(job_dir):
 
60
  try:
61
+ return (job_dir / "bundle_id.txt").read_text().strip()
 
 
62
  except:
63
  return "com.simplebit.dynamic"
64
 
65
 
66
+ # -------------------------------------------------
67
  # SIGN IPA
68
+ # -------------------------------------------------
69
  @app.route("/sign", methods=["POST"])
70
  def sign():
71
 
72
  try:
73
 
 
 
 
 
74
  if "ipa" not in request.files:
75
+ return jsonify({"error": "Missing IPA"}), 400
 
 
76
 
77
  if "p12" not in request.files:
78
+ return jsonify({"error": "Missing P12"}), 400
 
 
79
 
80
  if "provision" not in request.files:
81
+ return jsonify({"error": "Missing provisioning profile"}), 400
 
 
 
 
 
 
82
 
83
  zsign_path = shutil.which("zsign")
84
 
 
 
85
  if not zsign_path:
86
+ return jsonify({"error": "zsign not installed"}), 500
 
 
 
 
 
 
87
 
88
  job_id = str(uuid.uuid4())[:8]
 
89
  job_dir = Path(job_id)
90
  job_dir.mkdir(exist_ok=True)
91
 
 
 
 
 
92
  ipa_path = job_dir / "app.ipa"
93
  p12_path = job_dir / "cert.p12"
94
  prov_path = job_dir / "profile.mobileprovision"
95
  signed_path = job_dir / "signed.ipa"
96
 
 
 
 
 
97
  request.files["ipa"].save(ipa_path)
98
  request.files["p12"].save(p12_path)
99
  request.files["provision"].save(prov_path)
100
 
101
  password = request.form.get("password", "")
102
 
103
+ bundle_id = get_actual_bundle_id(ipa_path) or "com.simplebit.dynamic"
104
+
105
  # -------------------------------------------------
106
+ # Extract IPA manually
107
  # -------------------------------------------------
108
+ temp_dir = tempfile.mkdtemp()
109
+
110
+ with zipfile.ZipFile(ipa_path, "r") as zip_ref:
111
+ zip_ref.extractall(temp_dir)
112
+
113
+ payload_path = Path(temp_dir) / "Payload"
114
 
115
+ if not payload_path.exists():
116
+ shutil.rmtree(temp_dir)
117
+ return jsonify({"error": "Payload missing"}), 500
118
 
119
+ app_folder = None
120
+ for item in payload_path.iterdir():
121
+ if item.suffix == ".app":
122
+ app_folder = item
123
+ break
124
+
125
+ if not app_folder:
126
+ shutil.rmtree(temp_dir)
127
+ return jsonify({"error": ".app not found"}), 500
128
+
129
+ print("APP:", app_folder)
130
 
131
  # -------------------------------------------------
132
+ # Sign extracted app
133
  # -------------------------------------------------
 
134
  command = [
135
  zsign_path,
136
+ "-f",
137
  "-k", str(p12_path),
138
  "-p", password,
139
  "-m", str(prov_path),
140
+ str(app_folder)
 
141
  ]
142
 
 
 
 
 
 
 
 
143
  result = subprocess.run(
144
  command,
145
  capture_output=True,
146
  text=True
147
  )
148
 
 
 
 
149
  print(result.stdout)
 
 
150
  print(result.stderr)
151
 
 
 
 
 
152
  if result.returncode != 0:
153
+ shutil.rmtree(temp_dir)
154
  return jsonify({
155
  "error": "Signing failed",
 
156
  "stdout": result.stdout,
157
  "stderr": result.stderr
158
  }), 500
159
 
160
  # -------------------------------------------------
161
+ # Rebuild IPA manually
162
  # -------------------------------------------------
163
+ shutil.make_archive(
164
+ str(job_dir / "signed"),
165
+ 'zip',
166
+ temp_dir
167
+ )
168
 
169
+ shutil.move(job_dir / "signed.zip", signed_path)
170
 
171
+ shutil.rmtree(temp_dir)
 
 
 
 
 
 
172
 
173
  save_bundle_id(job_dir, bundle_id)
174
 
 
 
 
 
175
  plist_url = f"{BASE_URL}/plist/{job_id}"
176
 
177
+ install_url = f"itms-services://?action=download-manifest&url={plist_url}"
 
 
 
 
 
 
 
 
 
 
178
 
179
  return redirect(install_url)
180
 
181
  except Exception as e:
182
+ return jsonify({"error": str(e)}), 500
183
 
 
 
184
 
185
+ # -------------------------------------------------
186
+ # PLIST
187
+ # -------------------------------------------------
 
 
 
 
 
188
  @app.route("/plist/<job_id>")
189
+ def plist(job_id):
190
 
191
+ job_dir = Path(job_id)
192
 
193
+ bundle_id = load_bundle_id(job_dir)
 
 
194
 
195
+ ipa_url = f"{BASE_URL}/download/{job_id}"
 
 
 
 
196
 
197
+ plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
 
 
 
 
198
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
199
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 
200
  <plist version="1.0">
201
  <dict>
202
+ <key>items</key>
203
+ <array>
204
+ <dict>
205
+ <key>assets</key>
206
+ <array>
207
+ <dict>
208
+ <key>kind</key>
209
+ <string>software-package</string>
210
+ <key>url</key>
211
+ <string>{ipa_url}</string>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  </dict>
213
+ </array>
214
+ <key>metadata</key>
215
+ <dict>
216
+ <key>bundle-identifier</key>
217
+ <string>{bundle_id}</string>
218
+ <key>bundle-version</key>
219
+ <string>1.0</string>
220
+ <key>kind</key>
221
+ <string>software</string>
222
+ <key>title</key>
223
+ <string>SimpleBit App</string>
224
+ </dict>
225
+ </dict>
226
+ </array>
227
+ </dict>
228
+ </plist>"""
229
 
230
+ plist_path = job_dir / "manifest.plist"
 
 
 
231
 
232
+ plist_path.write_text(plist_content)
233
 
234
+ return send_file(plist_path, mimetype="application/xml")
 
 
235
 
236
 
237
+ # -------------------------------------------------
238
+ # DOWNLOAD IPA (IMPORTANT FOR iOS OTA)
239
+ # -------------------------------------------------
240
  @app.route("/download/<job_id>")
241
+ def download(job_id):
242
 
243
+ ipa_path = Path(job_id) / "signed.ipa"
244
 
245
+ if not ipa_path.exists():
246
+ return jsonify({"error": "IPA not found"}), 404
247
 
248
+ response = send_file(ipa_path, mimetype="application/octet-stream")
249
+ response.headers["Content-Disposition"] = "inline; filename=signed.ipa"
250
+ return response
251
 
 
 
 
 
 
 
 
 
 
252
 
253
+ # -------------------------------------------------
254
+ # HEALTH
255
+ # -------------------------------------------------
 
 
 
 
 
 
 
256
  @app.route("/health")
257
  def health():
 
258
  return jsonify({
259
+ "ok": True,
260
+ "zsign": shutil.which("zsign") is not None
261
  })
262
 
263
 
 
 
 
264
  if __name__ == "__main__":
265
+ app.run(host="0.0.0.0", port=7860, debug=True)