Pull to refresh

Как мы обошли Review Guidelines и запустили сервер на телефоне

Reading time4 min
Views3.6K
Привет, Хабр. Меня зовут Антон Логинов, я iOS-разработчик в компании FINCH.

Недавно мы столкнулись с проблемой использования web-интерфейсов для азартных игр. В очередном обновлении AppStore Review Guidelines коллеги из Купертино опять ужесточили правила. Если конкретнее, то теперь Apple может зареджектить приложение, если какой-либо из web-интерфейсов будет классифицирован как азартная игра на реальные деньги.

Одно наше приложение на 90% состоит из азартных игр, а оставшиеся 10% используются, чтобы эти игры рекламировать. Часть из них работает через webView, поэтому нам нужно было любым образом обезопасить себя от реджекта.

Что можно было сделать:

  1. Вынести эти игры за пределы основного приложения.
    Если сказать другими словами — просто отсрочить неизбежное.
  2. Использовать некий контейнер для игр, который можно будет безболезненно обновлять.
    Звучит неплохо → попытался вникнуть → убил несколько дней на изучение React и React Native → понял, что «лыжи не едут» → звучит уже не так хорошо.

    Это очень дорогое решение, поскольку времени было мало, а игры пришлось бы переписывать с нуля. Все дело во внутреннем роутинге — он целиком завязан на urlPathComponents
  3. Реализовать игры нативно.
    Долго, дорого, и еще раз долго. В дальнейшем пришлось бы их поддерживать на постоянной основе, а у нас таких возможностей не было.
  4. Имитировать поведение сервера, который бы отдавал лежащий локально сайт с играми.
    Звучит безумно, но именно этот вариант я выбрал. Это быстро, так как требуются минимальные доработки легаси игр.
    К оценочным минусам можно отнести: увеличение размера сборки за счет лежащего локально сайта, увеличение нагрузки на устройство за счет запуска сервера.

Я не нашел ни одной статьи на Хабре, где описывалось бы как запустить сервер на телефоне. Решив, что кейс достаточно редкий и интересный, решил рассказать об этом здесь, на Хабре.

Подготовка


Пока наш бравый фронтмен пытался уменьшить размер игр в 16 раз (80 Mb -> 5 Mb) и менял внутренние пути на относительные, я определился с библиотекой, выбрав GCDWebServer. Это легковесный фреймворк, с помощью которого можно поднять HTTP-сервер в несколько строк кода.

После выбора библиотеки наступили долгие часы изучения и понимания того, как работает сервер под капотом, что в какой момент времени происходит, как настроить сервер так, чтобы он не тратил ресурсы системы впустую. Наш сервер учился ловить переходы, обрабатывать их, а я учился работать с сервером с другой стороны баррикад.

Настройка


func initWebServer() {

     // Инициализация
     let webServer = GCDWebServer()

    // В таких хендлерах обрабатываются обращения к серверу, например, этот будет обрабатывать все GET/POST запросы:

     webServer.addDefaultHandler(
          forMethod: HTTPMethod.get.rawValue,
          request: GCDWebServerDataRequest.self) { [weak self] request in
     
          return self?.handle(request: request)
     }
}

Старт


Собственно прописываем параметры для старта нашего сервера и запускаем:

do {
     try webServer.start(options: [GCDWebServerOption_BindToLocalhost: true,
                                                GCDWebServerOption_Port: 8080])
} catch {
     assertionFailure(error.localizedDescription)
     webServer.start(withPort: 8080, bonjourName: "PROJECT_NAME Web Server")
}

Прокси


image

Модуль внутри себя общается с API, но использует для этого свой собственный baseURL. В нашем случае — localhost. Следовательно, нужно было научить сервер определять те запросы, которые должны идти к API, и менять их baseURL.

// MARK: - в это время я узнал, насколько полезной может быть панель разработчика в Safari

Исходя из вышенаписанного, необходимо было сконфигурировать хендлеры для конкретных задач:

  • Отдать сайт. (Ну тут все просто);
  • Отдать какую-нибудь статику из Bundle. (Разобрали url запроса, поменяли baseUrl на bundleUrl, отдали контент (js/медиа);
  • Получить актуальные данные. (Разобрали url, поменяли baseUrl, запросили, отдали);
  • Отправить новые данные. (А POST запросы-то мы не обрабатывали, прикрутили, настроили, отправили);

Let's do it:

private func handle(request: GCDWebServerRequest) -> GCDWebServerResponse? {
        
        // 1) Отдаем сайт
        if request.url.pathComponents.contains(Endpoint.game.rawValue) {
            
            guard let indexURL = bundle.url(forResource: "index", withExtension: "html") else {
                return sendError(.noHTML(nil))
            }
            
            do {
                let data = try Data(contentsOf: indexURL)
                let htmlString = String(data: data, encoding: .utf8) ?? ""
                
                return GCDWebServerDataResponse(html: htmlString)
            
            } catch {
                return sendError(.noHTML(error))
            }
            
        // 2) Отдаем статику (js etc)   
        } else if request.url.pathComponents.contains(Endpoint.nstatic.rawValue) {
            
            guard let resoursePath = bundle.resourcePath else {
                return sendError(.noJS(nil))
            }
          
            let relativePath = request.url.pathComponents.joined(separator: "/")
            let absolutePath = resoursePath + relativePath.dropFirst()
            let staticURL = URL(fileURLWithPath: absolutePath)
            
            do {
                let data = try Data(contentsOf: staticURL)
                return GCDWebServerDataResponse(data: data, contentType: ContentType.js.description)
           
            } catch {
                return sendError(.noJS(error))
            }
        
        // 3) Делаем редирект для API
        } else if request.url.pathComponents.contains(Endpoint.api.rawValue) {
            var proxyRequest = request
            
            // Меняем url, дописываем хедеры, тело, и отправляем этот запрос
            let output = URLSession.shared.synchronousDataTask(with: proxyRequest)
            
           // Инициализируем ответ от сервера, используя данные полученные из запроса
           let response = GCDWebServerDataResponse(data: outputData, contentType: ContentType.url.description)
         
         // и возвращаем его пользователю
         return response
        }
    }

Заключение


Было весело и нервно. Во-первых, я никогда ничего подобного до этого не делал. Во-вторых, мы до последнего не понимали, как наши задумки повлияют на родительское приложение.

На реализацию у нас ушло около 32 часов: 8 на оптимизацию размера сайта, 24 на проектирование и написание данного функционала.

Пока писал статью, я пришел к выводу, что более популярным способом использования этой технологии является разработка нативки без ожидания готовности бэкенда.

Ну и подытожим плюсами выбранного подхода:

  • Экономия времени бэкенда за счет того, что сами даем ему модель данных
  • Возможность протестировать любое поведение сервера
  • Для перехода с Mock на реальное API нам просто нужно выключить конкретные хендлеры

Спасибо за внимание. Если у вас был подобный опыт, то расскажите о нем в комментариях.
Tags:
Hubs:
+11
Comments14

Articles