Safari сдаётся, а мы — нет: как iPhone 3GS научился читать DTF
У меня есть iPhone 3GS. Телефон 2009 года, iOS 6, последняя, которая на него встала. Захотелось открыть на нём DTF. Спойлер: получилось. Но по дороге выяснилось, что этот телефон в принципе не может зайти на DTF — и это оказалось самой интересной частью
Стена
Первое, что я сделал — открыл DTF в Safari прямо на устройстве.
Safari не удаётся открыть страницу, так как не удалось установить безопасное соединение с сервером.
Формулировка важна. Не «сертификату нельзя доверять», а «не удалось установить соединение» — то есть разговор оборвался на самом первом рукопожатии, до проверки сертификатов дело не дошло вообще
Замерил, что сервер вообще принимает
Три приговора сразу:
- Только TLS 1.2. Старые версии протокола сервер не принимает.
- Только AES-GCM. Этого режима шифрования в системной криптографии iOS 6 нет — он появился позже.
- Сертификат от Let's Encrypt. Его корня в хранилище 2012 года не существовало: сам Let's Encrypt появился в 2015-м.
Любого из трёх пунктов хватило бы. Системная сеть iOS 6 к DTF не подключится никогда — ни в браузере, ни в приложении
Отдельный забавный момент. Сначала я проверял через Opera Mini, и там DTF открывался. Радость была недолгой: в адресной строке значилось global-4-lvs-lamport.ope… — адрес сервера Opera. Opera Mini не ходит на сайты сама, она отправляет адрес на свои серверы, те открывают страницу и присылают телефону готовый результат. То есть с DTF разговаривала Opera, а не мой телефон. Проверка не засчитана
Решение — своя сеть целиком
Раз системному стеку доверять нельзя — не используем его вообще. Внутри приложения работает своя криптография: голый сокет, mbedTLS, свой HTTP и корневые сертификаты, вкомпилированные прямо в бинарник. Ни одной системной сетевой функции
Первым делом я собрал не приложение, а пробу — крошечную программу, которая делает один запрос и отчитывается по каждому этапу отдельно. Чтобы в случае провала сразу знать, обо что именно споткнулись
Вот что она напечатала на 3GS:
Телефон, который полчаса назад не мог открыть DTF даже в браузере, установил современное шифрование и скачал 324 килобайта живой ленты. Стена пробита
Сборка без Mac
Отдельная история — чем всё это компилировать. У меня Windows и нет ни Mac, ни Xcode.
Оказалось, не нужны. Сборка идёт на бесплатной Linux-машине GitHub:
- компилятор — clang с линковщиком ld64, собранный под Linux
- SDK — iPhoneOS 9.3 из публичного репозитория theos, с флагом -miphoneos-version-min=6.0
- подпись — ldid -S, обычная «фейковая» для джейлбрейка
Итоговая команда выглядит буднично:
Но чтобы она заработала, пришлось обойти три грабли, которых нет ни в одной инструкции.
Грабля первая: времени не существует
mbedTLS хочет монотонные часы через clock_gettime. Эта функция появилась в iOS только в 10-й версии — при цели 6.0 код просто не компилируется.
Лечится подменой собственной реализацией:
Грабля вторая: библиотека-призрак
Линковщик падал с загадочным:
Системная libSystem ссылается на liblaunch, но в публичном SDK этой библиотеки нет. Описание-заглушку линковщик игнорирует — ему нужен настоящий файл. Собираем пустышку с правильным именем:
Грабля третья: memcpy не существует
Дальше вылезло восемь неразрешённых символов: memcpy, memset, memmove, memcmp, memchr, strcmp, strncmp, strchr. Самые базовые функции языка. При этом сокеты, ввод-вывод и работа с памятью слинковались прекрасно — SDK просто не экспортирует именно эти для armv7.
Пишем свои. Тут важен флаг -fno-builtin, иначе компилятор «оптимизирует» тело memcpy в вызов… memcpy, и получится бесконечная рекурсия:
Само приложение
Написано на Objective-C с ручным управлением памятью — автоматическое (ARC) для цели iOS 6 современные компиляторы уже не поддерживают, нужной библиотеки в комплекте больше нет.
Интерфейс — стандартный UIKit тех лет. Скевоморфизм тут получается почти даром: глянцевые панели, выпуклые кнопки и утопленные таблицы UIKit 2012 года рисует сам, достаточно задать цвет. Сверху я добавил градиентные ячейки и гравированный текст с белой подсветкой.
Статья поста рендерится как HTML в веб-просмотре. Это сильно экономит силы: перенос текста, заголовки, списки и цитаты получаются бесплатно, вместо ручного расчёта высоты каждого абзаца. Но есть подвох: у веб-просмотра нет работающей сети — та же стена. Поэтому картинки скачиваю я сам и встраиваю прямо в страницу.
Именно это встраивание и подарило самый эффектный баг.
Как я уронил телефон мегабайтом сердечек
Реакции на DTF — картинки. Я встраивал каждую в текст страницы. На посте со 138 комментариями, у каждого из которых свои реакции, одна и та же иконка повторялась сотни раз, и страница разрасталась за мегабайт.
На устройстве с 256 МБ оперативной памяти это выглядело так: заходишь в пост, на долю секунды видишь реакции во весь экран (стили не успевали примениться), и приложение вылетает.
Дальше я потратил три подхода на починку вслепую — и все три мимо. Пробовал ссылаться на файлы вместо встраивания, менял стили, переставлял порядок загрузки. Не работало ничего, а гадать по скриншотам «а вдруг теперь?» — путь в никуда.
Тогда я сделал экран диагностики: пусть телефон сам расскажет, что у него происходит. Он проверяет каждое звено и рисует картинку двумя способами сразу — тегом и фоном.
Ответ пришёл за один запуск:
Оказалось: движок рисует картинки обоими способами, картинки скачиваются и ложатся на диск. Виноват был не телефон, а мой способ вывода. Перевёл реакции на обычный тег с ограничением в 70 иконок на страницу — и всё встало на место.
Вывод, который стоил трёх итераций: когда чинишь вслепую, потрать один заход на инструмент, который покажет факты. Дальше починка занимает минуты.
Ещё один невидимый баг
Вход в аккаунт «не работал»: жмёшь кнопку — ничего. Ни индикатора, ни ошибки.
Я долго чинил сеть. А проблема была в арифметике: на экране 3.5 дюйма с нижней панелью остаётся 367 точек по высоте, а я разместил надпись о статусе на 348, а крутилку — на 410. Вход честно шёл и честно сообщал о результате — за нижним краем экрана.
Заодно выяснилось, что сервер на успешный вход отвечает {"message":"logined"}, но имя поля с токеном я не угадал. Теперь приложение не гадает, а обходит весь ответ и пробует все длинные строки, похожие на токен.
Скорость
Полное шифрованное рукопожатие на процессоре 600 МГц — самая дорогая операция во всём приложении. А один экран делает несколько запросов подряд.
Решается переиспользованием сессии: приложение запоминает установленное соединение с сервером, и следующие пропускают обмен ключами и проверку цепочки сертификатов.
Замер выше показывает результат: 2.0 секунды → 0.8. Втрое.
Ещё два ускорения:
- Картинки просим в JPEG. По умолчанию DTF отдаёт WebP, а его iOS 6 декодировать не умеет вообще — достаточно дописать -/format/jpeg/ к адресу.
- Кэш на диске. Второй заход в пост открывается мгновенно
Что в итоге
Работает: четыре ленты с бесконечной подгрузкой, посты со всеми типами блоков, комментарии ветками с аватарками и реакциями, свои реакции и ответы, личные сообщения с отправкой, поиск, уведомления, закладки, блоги с баннером и счётчиками, вход по почте
Пока нет: видео и гифок, картинок при публикации, мини-игр, черновиков, написание постов.
Весь пакет — 780 килобайт. Внутри — полноценный TLS и 50 иконок реакций.
Телефон 2009 года, операционная система 2012-го, сервер 2026-го. Между ними полтора десятилетия и криптография, которой на момент выпуска этого iPhone ещё не придумали
Скачать можно будет (ха-ха, как будто кто то собрался ставить дтф на свой старенький айфончик) вскоре в телеграм канале Survival on iOS 6
На самом деле приложение я написал не сколько для себя, а просто потому что люблю пердолинг, а пердолиться мне пришлось много, и по приколу, потому что подписка на клауд у меня есть, а токены тратить не на что 😭 так что развлекаем себя по максимуму !
Щас планирую сделать рабочие клиенты для клауда и чат гпт, а потом может быть сделаю клиент дтф для часов самсунг на веар ос, чтобы уведомления приходили, можно было ленту поскролить или с комментов быстро покекать
а и наверное соберу маленькое приложение, которое будет присылать уведомление на часы, когда клод закончил писать код, а ты в это время где то в другом месте находишься (в другой комнате например или на кухне готовишь и всё такое)