How We Bypassed Review Guidelines and Launched a Server on the Phone

Hello, Habr. My name is Anton Loginov, I am an iOS developer at FINCH.



Recently, we faced the problem of using web-based interfaces for gambling. In the next update of the AppStore Review Guidelines, colleagues from Cupertino again tightened the rules. More specifically, now Apple can redirect the application if any of the web-based interfaces will be classified as a real money gambling.



One of our applications is 90% gambling, and the remaining 10% is used to advertise these games. Some of them work through webView, so we needed to protect ourselves from the redirect in any way.



What could be done:



  1. Take these games outside of the main application.

    In other words, just postpone the inevitable.
  2. Use a certain container for games that can be painlessly updated.

    Sounds good → tried to penetrate → killed a few days to study React and React Native → realized that “skiing doesn’t go” → it doesn’t sound so good.



    This is a very expensive solution, because there was little time, and the games would have to be rewritten from scratch. It's all about internal routing - it is entirely tied to urlPathComponents

  3. Implement the game natively.

    Long, expensive, and again long. In the future, they would have to be supported on an ongoing basis, but we did not have such opportunities.
  4. Simulate the behavior of a server that would give a locally lying site with games.

    It sounds crazy, but this is the option I have chosen. This is fast, since minimal modifications to the legacy of games are required.

    The estimated disadvantages include: increasing the size of the assembly due to the locally lying site, increasing the load on the device by starting the server.


I did not find any article on Habré which would describe how to start the server on the phone. Having decided that the case is quite rare and interesting, I decided to talk about it here on Habré.



Training



While our brave frontman tried to reduce the size of games by 16 times (80 Mb -> 5 Mb) and changed the internal paths to relative, I decided on the library by selecting GCDWebServer. This is a lightweight framework with which you can raise an HTTP server in a few lines of code.



After choosing the library, there came a long hours of studying and understanding how the server works under the hood, what happens at what point in time, how to configure the server so that it does not waste system resources. Our server learned to catch transitions, process them, and I learned to work with the server on the other side of the barricades.



Customization



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





Start



Actually, we prescribe the parameters for starting our server and run:



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





Proxies



image



The module internally communicates with the API, but uses its own baseURL for this. In our case, localhost. Therefore, it was necessary to teach the server to determine those requests that should go to the API, and change their baseURL.



 // MARK: -     ,        Safari
      
      





Based on the above, it was necessary to configure handlers for specific tasks:





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 } }
      
      





Conclusion



It was fun and nervous. Firstly, I had never done anything like this before. Secondly, until recently we did not understand how our ideas will affect the parent application.



It took us about 32 hours to implement: 8 to optimize the site size, 24 to design and write this functionality.



While writing an article, I came to the conclusion that a more popular way to use this technology is to develop a native without waiting for the backend to be ready.



Well, to summarize the advantages of the chosen approach:





Thanks for your attention. If you had a similar experience, then tell us about it in the comments.



All Articles