montana/iOS/Distribution/MontanaSign/server.py

563 lines
18 KiB
Python
Raw Normal View History

"""
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)