"""
UDID Collector через .mobileconfig профиль
Как работает:
1. Пользователь открывает efir.org/install
2. Нажимает "Зарегистрировать устройство"
3. iOS предлагает установить профиль
4. После установки профиля iOS отправляет данные на наш callback URL
5. Мы получаем UDID, модель, версию iOS
6. Редиректим пользователя на страницу с приложениями
"""
from flask import Flask, request, Response, redirect, render_template_string
import plistlib
import uuid
import sqlite3
from datetime import datetime
import os
app = Flask(__name__)
DATABASE = '/var/montana/montanasign.db'
CALLBACK_URL = 'https://install.efir.org/enroll/callback'
def generate_enrollment_profile(session_id: str) -> bytes:
"""
Генерация .mobileconfig профиля для сбора UDID
Профиль содержит:
- PayloadType: Profile Service
- URL: куда отправить данные устройства
"""
profile = {
'PayloadContent': {
'URL': f'{CALLBACK_URL}?session={session_id}',
'DeviceAttributes': [
'UDID',
'IMEI',
'ICCID',
'VERSION',
'PRODUCT',
'DEVICE_NAME',
'SERIAL',
'MAC_ADDRESS_EN0'
],
},
'PayloadOrganization': 'Montana Protocol',
'PayloadDisplayName': 'Montana Device Registration',
'PayloadDescription': 'Регистрация устройства для установки Montana apps',
'PayloadVersion': 1,
'PayloadUUID': str(uuid.uuid4()).upper(),
'PayloadIdentifier': f'network.montana.enroll.{session_id}',
'PayloadType': 'Profile Service',
}
return plistlib.dumps(profile)
# ═══════════════════════════════════════════════════════════════
# ENROLLMENT FLOW
# ═══════════════════════════════════════════════════════════════
ENROLL_PAGE = '''
Montana — Регистрация устройства
Ɉ
Регистрация устройства
Для установки приложений Montana нужно зарегистрировать твой iPhone
Зарегистрировать
1
Нажми кнопку выше
2
Разреши установку профиля в Настройках
3
Вернись сюда и выбери приложения
'''
SUCCESS_PAGE = '''
Montana — Готово!
✓
Устройство зарегистрировано!
Перенаправляю к приложениям...
'''
@app.route('/enroll')
def enroll_page():
"""Страница регистрации"""
return render_template_string(ENROLL_PAGE)
@app.route('/enroll/profile')
def enroll_profile():
"""
Отдаёт .mobileconfig профиль
iOS откроет диалог установки профиля
"""
# Создаём уникальную сессию
session_id = str(uuid.uuid4())
# Сохраняем сессию
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute('''
CREATE TABLE IF NOT EXISTS enrollment_sessions (
session_id TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed BOOLEAN DEFAULT FALSE,
udid TEXT
)
''')
c.execute('INSERT INTO enrollment_sessions (session_id) VALUES (?)', (session_id,))
conn.commit()
conn.close()
# Генерируем профиль
profile_data = generate_enrollment_profile(session_id)
return Response(
profile_data,
mimetype='application/x-apple-aspen-config',
headers={
'Content-Disposition': f'attachment; filename="montana-enroll.mobileconfig"'
}
)
@app.route('/enroll/callback', methods=['POST'])
def enroll_callback():
"""
Callback от iOS после установки профиля
iOS отправляет signed plist с данными устройства
"""
session_id = request.args.get('session')
if not session_id:
return 'Missing session', 400
# Парсим данные устройства из plist
try:
device_data = plistlib.loads(request.data)
except Exception as e:
print(f"Failed to parse device data: {e}")
return 'Invalid data', 400
# Извлекаем UDID и другие данные
udid = device_data.get('UDID', '')
product = device_data.get('PRODUCT', '')
version = device_data.get('VERSION', '')
device_name = device_data.get('DEVICE_NAME', '')
if not udid:
return 'Missing UDID', 400
# Сохраняем устройство
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
# Обновляем сессию
c.execute('''
UPDATE enrollment_sessions
SET completed = TRUE, udid = ?
WHERE session_id = ?
''', (udid, session_id))
# Добавляем устройство
c.execute('''
INSERT OR REPLACE INTO devices (udid, device_name, model, registered_at)
VALUES (?, ?, ?, ?)
''', (udid, device_name, product, datetime.now()))
conn.commit()
conn.close()
print(f"✅ Device enrolled: {udid} ({product}, iOS {version})")
# Отправляем конфигурацию для завершения
# Это удалит временный профиль и перенаправит пользователя
response_profile = {
'PayloadOrganization': 'Montana Protocol',
'PayloadDisplayName': 'Registration Complete',
'PayloadDescription': 'Устройство зарегистрировано',
'PayloadVersion': 1,
'PayloadUUID': str(uuid.uuid4()).upper(),
'PayloadIdentifier': 'network.montana.enrolled',
'PayloadType': 'Configuration',
'PayloadContent': [],
# Редирект после установки
'PayloadRemovalDisallowed': False,
}
return Response(
plistlib.dumps(response_profile),
mimetype='application/x-apple-aspen-config'
)
@app.route('/enroll/check/')
def check_enrollment(session_id):
"""Проверка статуса регистрации (для polling)"""
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute('SELECT completed, udid FROM enrollment_sessions WHERE session_id = ?', (session_id,))
result = c.fetchone()
conn.close()
if not result:
return {'enrolled': False, 'error': 'Session not found'}
completed, udid = result
return {'enrolled': bool(completed), 'udid': udid}
@app.route('/enroll/success')
def enroll_success():
"""Страница успешной регистрации"""
udid = request.args.get('udid', '')
return render_template_string(SUCCESS_PAGE, udid=udid)
# ═══════════════════════════════════════════════════════════════
# ALTERNATIVE: OTA Profile for UDID
# ═══════════════════════════════════════════════════════════════
def generate_ota_profile() -> bytes:
"""
Альтернативный способ через OTA enrollment
Работает как MDM lite
"""
profile = {
'PayloadContent': [
{
'PayloadType': 'com.apple.webClip.managed',
'PayloadVersion': 1,
'PayloadIdentifier': 'network.montana.webclip',
'PayloadUUID': str(uuid.uuid4()).upper(),
'PayloadDisplayName': 'Montana Apps',
'URL': 'https://install.efir.org/apps',
'Label': 'Montana',
'Icon': None, # Можно добавить base64 иконку
'IsRemovable': True,
'FullScreen': False,
}
],
'PayloadOrganization': 'Montana Protocol',
'PayloadDisplayName': 'Montana Protocol',
'PayloadDescription': 'Доступ к приложениям Montana без App Store',
'PayloadVersion': 1,
'PayloadUUID': str(uuid.uuid4()).upper(),
'PayloadIdentifier': 'network.montana.profile',
'PayloadType': 'Configuration',
'PayloadRemovalDisallowed': False,
}
return plistlib.dumps(profile)
if __name__ == '__main__':
os.makedirs('/var/montana', exist_ok=True)
app.run(host='0.0.0.0', port=8081, debug=True)