├── .gitignore ├── .gitmodules ├── README.md ├── config.json.example ├── ipas └── .gitkeep ├── main.py └── templates ├── index.html ├── olderversions.html └── searchresults.html /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | __pycache__/ 3 | downloaded/ 4 | *.ipa 5 | ssl/ 6 | libipatoolpy.py -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ipatool-py"] 2 | path = ipatoolpy 3 | url = https://github.com/NyaMisty/ipatool-py.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPADown 2 | Web frontend for [ipatool-py](https://github.com/nyaMisty/ipatool-py/) 3 | 4 | ## Features 5 | * Can downgrade iOS apps 6 | * Works jailed, on the latest iOS version 7 | 8 | ## How to setup 9 | 1. Follow the guide on how to setup iTunes for older app versions in the ipatool-py repo. 10 | 2. Generate some self-signed ssl certs or get legit ones and copy them to the "ssl" folder. 11 | * ssl/private.key && ssl/public.crt 12 | 3. Fill in config.json, using the example at config.json.example. 13 | 4. Run the main.py, and visit the webpage at your ip 14 | 15 | ## Disclaimer 16 | This script was supposed to be just a private thing, not meant for the public to see. After showing it to some people, they requested for it to be publicised. The code quality is shit, and I know that, because it was supposed to be only by me. 17 | There's probably lots of vulnerabilities in this, which is why I don't recommend hosting a public instance, but it works fine for local use. 18 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "appleid": "appleid@email.com", 3 | "password": "password" 4 | } -------------------------------------------------------------------------------- /ipas/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mineek/IPADown-Public/d386d76ccc26d07179bbdc1bede10f9255caa1ac/ipas/.gitkeep -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from flask import Flask, request, jsonify, send_file, render_template 4 | # make sure ipatoolpy can import reqs.* modules by adding the path to sys.path 5 | import sys 6 | sys.path.append(os.path.join(os.path.dirname(__file__), 'ipatoolpy')) 7 | from ipatoolpy.reqs.itunes import * 8 | from ipatoolpy.reqs.store import * 9 | # I don't know anymore, I cannot do python correctly, why does ipatoolpy not use if __name__ == '__main__'? 10 | if not os.path.exists('libipatoolpy.py'): 11 | with open('ipatoolpy/main.py', 'r') as f: 12 | mainpy = f.read() 13 | search = """def main(): 14 | tool = IPATool() 15 | 16 | tool.tool_main() 17 | 18 | main()""" 19 | mainpy = mainpy.replace(search, "") 20 | with open('libipatoolpy.py', 'w') as f2: 21 | f2.write(mainpy) 22 | from libipatoolpy import IPATool 23 | import argparse 24 | import logging 25 | from rich.logging import RichHandler 26 | from rich.console import Console 27 | import rich 28 | rich.get_console().file = sys.stderr 29 | 30 | logging_handler = RichHandler(rich_tracebacks=True) 31 | logging.basicConfig( 32 | level="INFO", 33 | format="%(message)s", 34 | datefmt="[%X]", 35 | handlers=[logging_handler] 36 | ) 37 | logging.getLogger('urllib3').setLevel(logging.WARNING) 38 | logger = logging.getLogger('main') 39 | 40 | app = Flask(__name__) 41 | configjson = None 42 | with open('config.json') as f: 43 | configjson = json.load(f) 44 | appleidemail = configjson['appleid'] 45 | appleidpass = configjson['password'] 46 | 47 | @app.route('/download', methods=['POST']) 48 | def download(): 49 | ipaTool = IPATool() 50 | ipaTool.appId = request.form['appId'] 51 | args = { 52 | "appleid": appleidemail, 53 | "password": appleidpass, 54 | "appId": ipaTool.appId, 55 | "purchase": True, 56 | "output_dir": "ipas", 57 | "downloadAllVersion": False, 58 | "appVerId": None, 59 | "itunes_server": "http://127.0.0.1:9000", 60 | "session_dir": None, 61 | "log_level": "info", 62 | "out_json": False 63 | } 64 | args = argparse.Namespace(**args) 65 | ipaTool.handleDownload(args) 66 | return send_file(ipaTool.jsonOut['downloadedIPA'], as_attachment=True) 67 | 68 | @app.route('/downloadOlder', methods=['POST']) 69 | def downloadOlder(): 70 | ipaTool = IPATool() 71 | ipaTool.appId = request.form['appId'] 72 | ipaTool.appVerId = request.form['appVerId'] 73 | args = { 74 | "appleid": appleidemail, 75 | "password": appleidpass, 76 | "appId": ipaTool.appId, 77 | "purchase": True, 78 | "output_dir": "ipas", 79 | "downloadAllVersion": False, 80 | "appVerId": ipaTool.appVerId, 81 | "itunes_server": "http://127.0.0.1:9000", 82 | "session_dir": None, 83 | "log_level": "info", 84 | "out_json": False 85 | } 86 | args = argparse.Namespace(**args) 87 | ipaTool.handleDownload(args) 88 | return send_file(ipaTool.jsonOut['downloadedIPA'], as_attachment=True) 89 | 90 | @app.route('/olderVersions', methods=['POST']) 91 | def olderVersions(): 92 | ipaTool = IPATool() 93 | ipaTool.appId = request.form['appId'] 94 | args = { 95 | "appleid": appleidemail, 96 | "password": appleidpass, 97 | "appId": ipaTool.appId, 98 | "purchase": True, 99 | "output_dir": "ipas", 100 | "downloadAllVersion": False, 101 | "appVerId": None, 102 | "itunes_server": "http://127.0.0.1:9000", 103 | "session_dir": None, 104 | "log_level": "info", 105 | "out_json": True 106 | } 107 | args = argparse.Namespace(**args) 108 | ipaTool.handleHistoryVersion(args) 109 | return jsonify(ipaTool.jsonOut) 110 | 111 | # Websocket server for log 112 | from flask_socketio import SocketIO 113 | socketio = SocketIO(app, cors_allowed_origins="*") 114 | 115 | def logViaWS(msg): 116 | # don't log web requests 117 | if 'HTTP' in msg: 118 | return 119 | socketio.emit('log', msg) 120 | 121 | @app.route('/ws', methods=['GET']) 122 | def ws(): 123 | return socketio.handle_request(request) 124 | 125 | socketio.on_event('connect', lambda: logViaWS('Hello from server!')) 126 | socketio.on_event('disconnect', lambda: print('Lost connection to client :(')) 127 | 128 | # pipe log to socketio 129 | wsHandler = logging.StreamHandler() 130 | wsHandler.setFormatter(logging.Formatter('%(message)s')) 131 | wsHandler.emit = lambda record: logViaWS(record.getMessage()) 132 | logging.getLogger().addHandler(wsHandler) 133 | 134 | # Start the web server 135 | ssl = False 136 | if os.path.exists('ssl/private.key') and os.path.exists('ssl/public.crt'): 137 | ssl = ('ssl/public.crt', 'ssl/private.key') 138 | import threading 139 | #threading.Thread(target=socketio.run, args=(app, '0.0.0.0', 9001)).start() 140 | if ssl: 141 | threading.Thread(target=lambda: socketio.run(app, '0.0.0.0', 9001, ssl_context=ssl)).start() 142 | else: 143 | threading.Thread(target=socketio.run, args=(app, '0.0.0.0', 9001)).start() 144 | 145 | # Detect our local IP address 146 | baseIP = None 147 | try: 148 | import socket 149 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 150 | s.connect(("8.8.8.8", 80)) 151 | baseIP = s.getsockname()[0] 152 | s.close() 153 | except: 154 | print("Failed to get local IP address, exiting...") 155 | exit(1) 156 | 157 | print(f"Local IP: {baseIP}") 158 | 159 | useHTTPS = False 160 | if ssl: 161 | useHTTPS = True 162 | baseIP = f"https://{baseIP}" if useHTTPS else f"http://{baseIP}" 163 | 164 | @app.route('/', methods=['GET']) 165 | def index(): 166 | return render_template('index.html', baseIP=baseIP) 167 | 168 | @app.route('/search/', methods=['GET']) 169 | def search(query): 170 | ipaTool = IPATool() 171 | args = { 172 | "appleid": appleidemail, 173 | "password": appleidpass, 174 | "bundle_id": query, 175 | "output_dir": "ipas", 176 | "downloadAllVersion": False, 177 | "itunes_server": "http://127.0.0.1:9000", 178 | "session_dir": None, 179 | "log_level": "info", 180 | "out_json": True, 181 | "country": "us", 182 | "appId": None, 183 | "get_verid": False, 184 | "purchase": False 185 | } 186 | args = argparse.Namespace(**args) 187 | ipaTool.handleLookup(args) 188 | apps = ipaTool.jsonOut 189 | print(apps) 190 | return render_template('searchresults.html', query=query, app=apps) 191 | 192 | @app.route('/olderVersions/', methods=['GET']) 193 | def olderVersionsPage(appId): 194 | ipaTool = IPATool() 195 | args = { 196 | "appleid": appleidemail, 197 | "password": appleidpass, 198 | "appId": appId, 199 | "purchase": True, 200 | "output_dir": "ipas", 201 | "downloadAllVersion": False, 202 | "appVerId": None, 203 | "itunes_server": "http://127.0.0.1:9000", 204 | "session_dir": None, 205 | "log_level": "info", 206 | "out_json": True 207 | } 208 | args = argparse.Namespace(**args) 209 | ipaTool.handleHistoryVersion(args) 210 | print(ipaTool.jsonOut) 211 | return render_template('olderversions.html', appId=appId, appVerIds=ipaTool.jsonOut['appVerIds']) 212 | 213 | # for OTA itms-services 214 | @app.route('/ota//', methods=['GET']) 215 | def ota(appId, appVerId): 216 | # look for the app in 'ipas' folder 217 | ipas = os.listdir('ipas') 218 | appName = None 219 | for ipa in ipas: 220 | if appId in ipa: 221 | appName = ipa 222 | break 223 | if not appName: 224 | return "App not found", 404 225 | # lookup app info 226 | ipaTool = IPATool() 227 | ipaTool.appId = appId 228 | args = { 229 | "appleid": appleidemail, 230 | "password": appleidpass, 231 | "appId": ipaTool.appId, 232 | "purchase": True, 233 | "output_dir": "ipas", 234 | "downloadAllVersion": False, 235 | "appVerId": None, 236 | "itunes_server": "http://127.0.0.1:9000", 237 | "session_dir": None, 238 | "log_level": "info", 239 | "out_json": True, 240 | "bundle_id": None, 241 | "country": "us", 242 | "get_verid": False 243 | } 244 | args = argparse.Namespace(**args) 245 | ipaTool.handleLookup(args) 246 | appInfo = ipaTool.jsonOut 247 | # create manifest 248 | manifest = f""" 249 | 250 | 251 | 252 | items 253 | 254 | 255 | assets 256 | 257 | 258 | kind 259 | software-package 260 | url 261 | {baseIP}/ipas/{appName} 262 | 263 | 264 | metadata 265 | 266 | bundle-identifier 267 | {appInfo['bundleId']} 268 | bundle-version 269 | {appVerId} 270 | kind 271 | software 272 | title 273 | MineekIPA 274 | 275 | 276 | 277 | 278 | 279 | """ 280 | return manifest, 200, {'Content-Type': 'application/xml'} 281 | 282 | @app.route('/ipas/', methods=['GET']) 283 | def ipas(ipa): 284 | if not os.path.exists(f'ipas/{ipa}'): 285 | return "IPA not found", 404 286 | return send_file(f'ipas/{ipa}', as_attachment=True) 287 | 288 | if __name__ == '__main__': 289 | if ssl: 290 | app.run(host='0.0.0.0', port=443, ssl_context=ssl) 291 | else: 292 | app.run(host='0.0.0.0', port=80) -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IPATool 5 | 6 | 7 | 8 |

IPATool

9 | 10 |
11 | 91 |

Search by Bundle ID

92 |
93 | 94 | 95 | 96 |
97 |
98 |

Get older versions

99 |
100 | 101 | 102 | 103 |
104 |
105 |

Download IPA (latest version)

106 |
107 | 108 | 109 | 110 |
111 |
112 |

Download IPA (older version)

113 |
114 | 115 | 116 | 117 | 118 | 119 |
120 |
121 |

Get older versions (old)

122 |
123 | 124 | 125 | 126 |
127 | 128 | -------------------------------------------------------------------------------- /templates/olderversions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IPATool 5 | 6 | 7 |

IPATool

8 |

Back to home

9 | 10 | {% for appVerId in appVerIds %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 |
App Version IDDownload
{{ appVerId }}
21 | 43 | 44 | -------------------------------------------------------------------------------- /templates/searchresults.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IPATool - Search Results for {{ query }} 5 | 6 | 7 |

IPATool

8 |

Search results for '{{ query }}':

9 |

Back to home

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
App NameApp IDBundle IDDownload
{{ app.name }}{{ app.appId }}{{ app.bundleId }}
24 | 50 | 51 | --------------------------------------------------------------------------------