563 lines
18 KiB
Python
563 lines
18 KiB
Python
|
|
"""
|
|||
|
|
MontanaSign — Сервис подписи iOS приложений
|
|||
|
|
efir.org/install
|
|||
|
|
|
|||
|
|
Модель как LazyShop:
|
|||
|
|
1. Пользователь регистрирует UDID
|
|||
|
|
2. Мы выдаём сертификат (или подписываем своим)
|
|||
|
|
3. .ipa скачивается и устанавливается
|
|||
|
|
4. Apple не контролирует
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from flask import Flask, request, jsonify, send_file, render_template_string
|
|||
|
|
from flask_cors import CORS
|
|||
|
|
import os
|
|||
|
|
import subprocess
|
|||
|
|
import hashlib
|
|||
|
|
import json
|
|||
|
|
import sqlite3
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
import secrets
|
|||
|
|
|
|||
|
|
app = Flask(__name__)
|
|||
|
|
CORS(app)
|
|||
|
|
|
|||
|
|
# Конфигурация
|
|||
|
|
UPLOAD_FOLDER = '/var/montana/ipa'
|
|||
|
|
SIGNED_FOLDER = '/var/montana/signed'
|
|||
|
|
CERTS_FOLDER = '/var/montana/certs'
|
|||
|
|
DATABASE = '/var/montana/montanasign.db'
|
|||
|
|
|
|||
|
|
# IPA файлы Montana (беззнаковые)
|
|||
|
|
MONTANA_APPS = {
|
|||
|
|
'wallet': {
|
|||
|
|
'name': 'Montana Wallet',
|
|||
|
|
'bundle_id': 'network.montana.wallet',
|
|||
|
|
'version': '1.0.0',
|
|||
|
|
'ipa': 'MontanaWallet.ipa',
|
|||
|
|
'icon': 'wallet_icon.png',
|
|||
|
|
'description': 'Кошелёк Ɉ — баланс и переводы'
|
|||
|
|
},
|
|||
|
|
'junona': {
|
|||
|
|
'name': 'Junona AI',
|
|||
|
|
'bundle_id': 'network.montana.junona',
|
|||
|
|
'version': '1.0.0',
|
|||
|
|
'ipa': 'JunonaAI.ipa',
|
|||
|
|
'icon': 'junona_icon.png',
|
|||
|
|
'description': 'Чат с ИИ Montana Protocol'
|
|||
|
|
},
|
|||
|
|
'contracts': {
|
|||
|
|
'name': 'Montana Contracts',
|
|||
|
|
'bundle_id': 'network.montana.contracts',
|
|||
|
|
'version': '1.0.0',
|
|||
|
|
'ipa': 'MontanaContracts.ipa',
|
|||
|
|
'icon': 'contracts_icon.png',
|
|||
|
|
'description': 'Контракты Bitcoin Pizza Style'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def init_db():
|
|||
|
|
"""Инициализация базы данных"""
|
|||
|
|
conn = sqlite3.connect(DATABASE)
|
|||
|
|
c = conn.cursor()
|
|||
|
|
|
|||
|
|
c.execute('''
|
|||
|
|
CREATE TABLE IF NOT EXISTS devices (
|
|||
|
|
udid TEXT PRIMARY KEY,
|
|||
|
|
user_id TEXT,
|
|||
|
|
device_name TEXT,
|
|||
|
|
model TEXT,
|
|||
|
|
registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
expires_at TIMESTAMP,
|
|||
|
|
certificate_id TEXT
|
|||
|
|
)
|
|||
|
|
''')
|
|||
|
|
|
|||
|
|
c.execute('''
|
|||
|
|
CREATE TABLE IF NOT EXISTS certificates (
|
|||
|
|
cert_id TEXT PRIMARY KEY,
|
|||
|
|
udid TEXT,
|
|||
|
|
cert_data BLOB,
|
|||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
expires_at TIMESTAMP,
|
|||
|
|
revoked BOOLEAN DEFAULT FALSE
|
|||
|
|
)
|
|||
|
|
''')
|
|||
|
|
|
|||
|
|
c.execute('''
|
|||
|
|
CREATE TABLE IF NOT EXISTS installations (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
udid TEXT,
|
|||
|
|
app_id TEXT,
|
|||
|
|
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
version TEXT
|
|||
|
|
)
|
|||
|
|
''')
|
|||
|
|
|
|||
|
|
conn.commit()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
# LANDING PAGE
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
LANDING_HTML = '''
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="ru">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Montana Install — Установка без App Store</title>
|
|||
|
|
<style>
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
body {
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
|
|||
|
|
background: #0F0F1A;
|
|||
|
|
color: white;
|
|||
|
|
min-height: 100vh;
|
|||
|
|
}
|
|||
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|||
|
|
.header {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 40px 0;
|
|||
|
|
}
|
|||
|
|
.logo { font-size: 64px; margin-bottom: 16px; }
|
|||
|
|
h1 { font-size: 28px; margin-bottom: 8px; }
|
|||
|
|
.subtitle { color: #888; font-size: 16px; }
|
|||
|
|
|
|||
|
|
.apps { margin-top: 32px; }
|
|||
|
|
.app-card {
|
|||
|
|
background: #1A1A2E;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
padding: 20px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
.app-icon {
|
|||
|
|
width: 60px;
|
|||
|
|
height: 60px;
|
|||
|
|
background: #4A90D9;
|
|||
|
|
border-radius: 14px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 28px;
|
|||
|
|
}
|
|||
|
|
.app-info { flex: 1; }
|
|||
|
|
.app-name { font-weight: 600; font-size: 18px; }
|
|||
|
|
.app-desc { color: #888; font-size: 14px; margin-top: 4px; }
|
|||
|
|
.install-btn {
|
|||
|
|
background: #4A90D9;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 12px 24px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
.install-btn:disabled {
|
|||
|
|
background: #333;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.step {
|
|||
|
|
background: #1A1A2E;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
padding: 24px;
|
|||
|
|
margin-top: 32px;
|
|||
|
|
}
|
|||
|
|
.step-number {
|
|||
|
|
background: #4A90D9;
|
|||
|
|
width: 32px;
|
|||
|
|
height: 32px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-right: 12px;
|
|||
|
|
}
|
|||
|
|
.step h3 { display: inline; font-size: 18px; }
|
|||
|
|
.step p { margin-top: 12px; color: #888; line-height: 1.6; }
|
|||
|
|
|
|||
|
|
.udid-input {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
border: 1px solid #333;
|
|||
|
|
background: #0F0F1A;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 16px;
|
|||
|
|
margin-top: 16px;
|
|||
|
|
font-family: monospace;
|
|||
|
|
}
|
|||
|
|
.udid-input:focus { border-color: #4A90D9; outline: none; }
|
|||
|
|
|
|||
|
|
.register-btn {
|
|||
|
|
width: 100%;
|
|||
|
|
background: #4A90D9;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 16px;
|
|||
|
|
margin-top: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 40px 0;
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status { padding: 12px; border-radius: 8px; margin-top: 16px; }
|
|||
|
|
.status.success { background: rgba(16, 185, 129, 0.2); color: #10B981; }
|
|||
|
|
.status.error { background: rgba(239, 68, 68, 0.2); color: #EF4444; }
|
|||
|
|
|
|||
|
|
.hidden { display: none; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="header">
|
|||
|
|
<div class="logo">Ɉ</div>
|
|||
|
|
<h1>Montana Install</h1>
|
|||
|
|
<p class="subtitle">Установка без App Store. Apple не контролирует.</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="step">
|
|||
|
|
<span class="step-number">1</span>
|
|||
|
|
<h3>Зарегистрируй устройство</h3>
|
|||
|
|
<p>Открой Настройки → Основные → Об этом устройстве → UDID<br>
|
|||
|
|
Или используй <a href="udid://" style="color: #4A90D9">get.udid.io</a></p>
|
|||
|
|
<input type="text" class="udid-input" id="udid" placeholder="00000000-0000000000000000">
|
|||
|
|
<button class="register-btn" onclick="registerDevice()">Зарегистрировать</button>
|
|||
|
|
<div id="register-status" class="status hidden"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="apps" id="apps-section">
|
|||
|
|
<div class="app-card">
|
|||
|
|
<div class="app-icon">💰</div>
|
|||
|
|
<div class="app-info">
|
|||
|
|
<div class="app-name">Montana Wallet</div>
|
|||
|
|
<div class="app-desc">Кошелёк Ɉ — баланс и переводы</div>
|
|||
|
|
</div>
|
|||
|
|
<button class="install-btn" onclick="installApp('wallet')" id="btn-wallet" disabled>Установить</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="app-card">
|
|||
|
|
<div class="app-icon">🤖</div>
|
|||
|
|
<div class="app-info">
|
|||
|
|
<div class="app-name">Junona AI</div>
|
|||
|
|
<div class="app-desc">Чат с ИИ Montana Protocol</div>
|
|||
|
|
</div>
|
|||
|
|
<button class="install-btn" onclick="installApp('junona')" id="btn-junona" disabled>Установить</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="app-card">
|
|||
|
|
<div class="app-icon">📄</div>
|
|||
|
|
<div class="app-info">
|
|||
|
|
<div class="app-name">Montana Contracts</div>
|
|||
|
|
<div class="app-desc">Контракты Bitcoin Pizza Style</div>
|
|||
|
|
</div>
|
|||
|
|
<button class="install-btn" onclick="installApp('contracts')" id="btn-contracts" disabled>Установить</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="step">
|
|||
|
|
<span class="step-number">2</span>
|
|||
|
|
<h3>После установки</h3>
|
|||
|
|
<p>Настройки → Основные → VPN и управление устройством → Доверять сертификату Montana</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="footer">
|
|||
|
|
<p>Время — единственная реальная валюта</p>
|
|||
|
|
<p style="margin-top: 8px;">Apple не может удалить время. Ɉ</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
let registeredUDID = localStorage.getItem('montana_udid');
|
|||
|
|
|
|||
|
|
if (registeredUDID) {
|
|||
|
|
document.getElementById('udid').value = registeredUDID;
|
|||
|
|
enableInstallButtons();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function registerDevice() {
|
|||
|
|
const udid = document.getElementById('udid').value.trim();
|
|||
|
|
const status = document.getElementById('register-status');
|
|||
|
|
|
|||
|
|
if (!udid || udid.length < 20) {
|
|||
|
|
status.className = 'status error';
|
|||
|
|
status.textContent = 'Неверный формат UDID';
|
|||
|
|
status.classList.remove('hidden');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/register', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ udid })
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
if (data.success) {
|
|||
|
|
localStorage.setItem('montana_udid', udid);
|
|||
|
|
registeredUDID = udid;
|
|||
|
|
status.className = 'status success';
|
|||
|
|
status.textContent = 'Устройство зарегистрировано! Можешь устанавливать.';
|
|||
|
|
enableInstallButtons();
|
|||
|
|
} else {
|
|||
|
|
status.className = 'status error';
|
|||
|
|
status.textContent = data.error || 'Ошибка регистрации';
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
status.className = 'status error';
|
|||
|
|
status.textContent = 'Ошибка соединения';
|
|||
|
|
}
|
|||
|
|
status.classList.remove('hidden');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function enableInstallButtons() {
|
|||
|
|
document.querySelectorAll('.install-btn').forEach(btn => {
|
|||
|
|
btn.disabled = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function installApp(appId) {
|
|||
|
|
if (!registeredUDID) {
|
|||
|
|
alert('Сначала зарегистрируй устройство');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Открываем manifest для установки
|
|||
|
|
window.location.href = `itms-services://?action=download-manifest&url=${encodeURIComponent(
|
|||
|
|
window.location.origin + '/api/manifest/' + appId + '?udid=' + registeredUDID
|
|||
|
|
)}`;
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
'''
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/')
|
|||
|
|
def landing():
|
|||
|
|
return render_template_string(LANDING_HTML)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
# API
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@app.route('/api/register', methods=['POST'])
|
|||
|
|
def register_device():
|
|||
|
|
"""Регистрация устройства по UDID"""
|
|||
|
|
data = request.json
|
|||
|
|
udid = data.get('udid', '').strip()
|
|||
|
|
|
|||
|
|
if not udid or len(udid) < 20:
|
|||
|
|
return jsonify({'success': False, 'error': 'Invalid UDID'})
|
|||
|
|
|
|||
|
|
conn = sqlite3.connect(DATABASE)
|
|||
|
|
c = conn.cursor()
|
|||
|
|
|
|||
|
|
# Проверяем существует ли
|
|||
|
|
c.execute('SELECT * FROM devices WHERE udid = ?', (udid,))
|
|||
|
|
existing = c.fetchone()
|
|||
|
|
|
|||
|
|
if existing:
|
|||
|
|
conn.close()
|
|||
|
|
return jsonify({'success': True, 'message': 'Already registered'})
|
|||
|
|
|
|||
|
|
# Регистрируем
|
|||
|
|
expires_at = datetime.now() + timedelta(days=365)
|
|||
|
|
cert_id = secrets.token_hex(16)
|
|||
|
|
|
|||
|
|
c.execute('''
|
|||
|
|
INSERT INTO devices (udid, expires_at, certificate_id)
|
|||
|
|
VALUES (?, ?, ?)
|
|||
|
|
''', (udid, expires_at, cert_id))
|
|||
|
|
|
|||
|
|
conn.commit()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
'success': True,
|
|||
|
|
'message': 'Device registered',
|
|||
|
|
'expires': expires_at.isoformat()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/api/manifest/<app_id>')
|
|||
|
|
def get_manifest(app_id):
|
|||
|
|
"""Генерация manifest.plist для установки"""
|
|||
|
|
udid = request.args.get('udid')
|
|||
|
|
|
|||
|
|
if app_id not in MONTANA_APPS:
|
|||
|
|
return 'App not found', 404
|
|||
|
|
|
|||
|
|
app = MONTANA_APPS[app_id]
|
|||
|
|
|
|||
|
|
# Генерируем manifest
|
|||
|
|
manifest = 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>{request.host_url}api/download/{app_id}?udid={udid}</string>
|
|||
|
|
</dict>
|
|||
|
|
<dict>
|
|||
|
|
<key>kind</key>
|
|||
|
|
<string>display-image</string>
|
|||
|
|
<key>url</key>
|
|||
|
|
<string>{request.host_url}static/icons/{app['icon']}</string>
|
|||
|
|
</dict>
|
|||
|
|
</array>
|
|||
|
|
<key>metadata</key>
|
|||
|
|
<dict>
|
|||
|
|
<key>bundle-identifier</key>
|
|||
|
|
<string>{app['bundle_id']}</string>
|
|||
|
|
<key>bundle-version</key>
|
|||
|
|
<string>{app['version']}</string>
|
|||
|
|
<key>kind</key>
|
|||
|
|
<string>software</string>
|
|||
|
|
<key>title</key>
|
|||
|
|
<string>{app['name']}</string>
|
|||
|
|
</dict>
|
|||
|
|
</dict>
|
|||
|
|
</array>
|
|||
|
|
</dict>
|
|||
|
|
</plist>'''
|
|||
|
|
|
|||
|
|
return manifest, 200, {'Content-Type': 'application/xml'}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/api/download/<app_id>')
|
|||
|
|
def download_ipa(app_id):
|
|||
|
|
"""Скачивание подписанного IPA"""
|
|||
|
|
udid = request.args.get('udid')
|
|||
|
|
|
|||
|
|
if app_id not in MONTANA_APPS:
|
|||
|
|
return 'App not found', 404
|
|||
|
|
|
|||
|
|
app = MONTANA_APPS[app_id]
|
|||
|
|
|
|||
|
|
# Путь к подписанному IPA для этого UDID
|
|||
|
|
signed_path = os.path.join(SIGNED_FOLDER, udid, app['ipa'])
|
|||
|
|
|
|||
|
|
# Если ещё не подписан — подписываем
|
|||
|
|
if not os.path.exists(signed_path):
|
|||
|
|
sign_ipa_for_udid(app_id, udid)
|
|||
|
|
|
|||
|
|
if os.path.exists(signed_path):
|
|||
|
|
# Логируем установку
|
|||
|
|
conn = sqlite3.connect(DATABASE)
|
|||
|
|
c = conn.cursor()
|
|||
|
|
c.execute('''
|
|||
|
|
INSERT INTO installations (udid, app_id, version)
|
|||
|
|
VALUES (?, ?, ?)
|
|||
|
|
''', (udid, app_id, app['version']))
|
|||
|
|
conn.commit()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
return send_file(signed_path, as_attachment=True, download_name=app['ipa'])
|
|||
|
|
|
|||
|
|
return 'Signing failed', 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sign_ipa_for_udid(app_id: str, udid: str):
|
|||
|
|
"""Подписать IPA для конкретного UDID"""
|
|||
|
|
app = MONTANA_APPS[app_id]
|
|||
|
|
|
|||
|
|
unsigned_path = os.path.join(UPLOAD_FOLDER, app['ipa'])
|
|||
|
|
output_dir = os.path.join(SIGNED_FOLDER, udid)
|
|||
|
|
os.makedirs(output_dir, exist_ok=True)
|
|||
|
|
output_path = os.path.join(output_dir, app['ipa'])
|
|||
|
|
|
|||
|
|
# Используем zsign или ldid для подписи
|
|||
|
|
# zsign -k cert.p12 -m profile.mobileprovision -o output.ipa input.ipa
|
|||
|
|
|
|||
|
|
cert_path = os.path.join(CERTS_FOLDER, 'montana.p12')
|
|||
|
|
profile_path = os.path.join(CERTS_FOLDER, f'{udid}.mobileprovision')
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
subprocess.run([
|
|||
|
|
'zsign',
|
|||
|
|
'-k', cert_path,
|
|||
|
|
'-m', profile_path,
|
|||
|
|
'-o', output_path,
|
|||
|
|
unsigned_path
|
|||
|
|
], check=True)
|
|||
|
|
return True
|
|||
|
|
except subprocess.CalledProcessError as e:
|
|||
|
|
print(f"Signing failed: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/api/apps')
|
|||
|
|
def list_apps():
|
|||
|
|
"""Список приложений"""
|
|||
|
|
return jsonify(MONTANA_APPS)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/api/stats')
|
|||
|
|
def stats():
|
|||
|
|
"""Статистика"""
|
|||
|
|
conn = sqlite3.connect(DATABASE)
|
|||
|
|
c = conn.cursor()
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM devices')
|
|||
|
|
devices = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM installations')
|
|||
|
|
installs = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
'registered_devices': devices,
|
|||
|
|
'total_installations': installs
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
# MAIN
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
|||
|
|
os.makedirs(SIGNED_FOLDER, exist_ok=True)
|
|||
|
|
os.makedirs(CERTS_FOLDER, exist_ok=True)
|
|||
|
|
|
|||
|
|
init_db()
|
|||
|
|
|
|||
|
|
print("🏔 MontanaSign — iOS Distribution Service")
|
|||
|
|
print(" efir.org/install")
|
|||
|
|
print("")
|
|||
|
|
print(" Модель: LazyShop-style")
|
|||
|
|
print(" 1. Пользователь регистрирует UDID")
|
|||
|
|
print(" 2. Мы подписываем IPA")
|
|||
|
|
print(" 3. Apple не контролирует")
|
|||
|
|
print("")
|
|||
|
|
|
|||
|
|
app.run(host='0.0.0.0', port=8080, debug=True)
|