Untitled
unknown
swift
3 years ago
17 kB
7
Indexable
// // APIHandler.swift // GameBridge // // Created by Julien Moreau-Mathis on 26/06/2020. // Copyright © 2020 FDJ-GS. All rights reserved. // import Foundation import GCDWebServer class APIWebServerHandler : WebServerHandler { /** Defines the regular expression used to determine if the current handler is the one to use. */ var pathRegex: String = #"^\/(api|push)"# /** Defines the HTTP method the current handler will handle. */ var methods: Set<String> = Set(["GET", "POST", "PUT"]) private weak var _gameBridge: GameBridgeViewController? /** Inits the handler. - Parameter gameBridge defines the reference to the game bridge. */ init(wwithGameBridge gameBridge: GameBridgeViewController) { self._gameBridge = gameBridge } /** Defines the function called on the catched request is of the current mehod and satisties the current regular expression. - Parameter request defines the request create by GCDWebServer. - Parameter completion defines the callback that is used when the handler finished handling the request. */ func handle(request: GCDWebServerRequest, completion: @escaping GCDWebServerCompletionBlock) -> Void { // Check options guard let options = _gameBridge?.options else { _gameBridge?.options?.hostErrorHandler?.onError(withError: .requestForGameNotHandled( path: request.path, debugDescription: "No options available in GameBridgeViewController instance. Can't handle API WebServer request." )) return completion(GCDWebServerDataResponse(statusCode: 418)) } // Check server location guard let serverLocation = options.serverLocation else { _gameBridge?.options?.hostErrorHandler?.onError(withError: .requestForGameNotHandled( path: request.path, debugDescription: "Server location is mandatory for API requests in options. Can't handle API WebServer request." )) return completion(GCDWebServerDataResponse(statusCode: 418)) } let path = request.path let regexApi = try! NSRegularExpression(pattern: #"^\/api\/([^\/]+)\/(.+)"#) if let match = regexApi.firstMatch(in: path, range: NSRange(path.startIndex..., in: path)) { let apiPath = String(path[Range(match.range(at: 2), in: path)!]) return _handleApiRequest(withRequest: request, andPath: "\(serverLocation)/\(apiPath)", thatIsPush: false, completion: completion) } let pushRegex = try! NSRegularExpression(pattern: #"^\/push\/([^\/]+)\/(.+)"#) if pushRegex.firstMatch(in: path, range: NSRange(path.startIndex..., in: path)) != nil { // Check push server location guard let pushServerLocation = options.pushServerLocation else { _gameBridge?.options?.hostErrorHandler?.onError(withError: .requestForGameNotHandled(path: request.path, debugDescription: "Push Server Location not set in GameBridgeOptions")) return completion(GCDWebServerDataResponse(statusCode: 404)) } return _handleApiRequest(withRequest: request, andPath: "\(pushServerLocation)\(path)", thatIsPush: true, completion: completion) } let regexApigw = try! NSRegularExpression(pattern: #"^\/apigw\/([^\/]+)\/(.+)"#) if regexApigw.firstMatch(in: path, range: NSRange(path.startIndex..., in: path)) != nil { let apiPath = "\(serverLocation.scheme!)://\(serverLocation.host!)\(path)" return _handleApiRequest(withRequest: request, andPath: apiPath, thatIsPush: false, completion: completion) } } var cookiesHeader = "" /** Handles the given Api request. */ private func _handleApiRequest(withRequest request: GCDWebServerRequest, andPath path: String, thatIsPush isPush: Bool, completion: @escaping GCDWebServerCompletionBlock) { // Create Url var urlString = path if let query = request.query, query.count > 0 { urlString += "?" for (index, element) in query.enumerated() { urlString += ((index > 0 ? "&" : "") + "\(element.key)=\(element.value)") } } guard let escapedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return completion(nil) } guard let url = URL(string: escapedUrlString) else { return } // Create request var outRequest = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData) outRequest.httpMethod = request.method // Copy headers for (headerName, headerValue) in request.headers { outRequest.addValue(headerValue, forHTTPHeaderField: headerName) } outRequest.setValue(url.host, forHTTPHeaderField: "host") // Cookies if let cookies = HTTPCookieStorage.shared.cookies(for: url) { for cookie in cookies { if cookie.domain != url.host { continue } outRequest.setValue("\(cookie.name)=\(cookie.value)", forHTTPHeaderField: "Cookie") } } else { print("No cookies found in shared cookies storage.") } // Body if request.hasBody(), let dataRequest = request as? GCDWebServerDataRequest { outRequest.httpBody = dataRequest.data } // Is Push? if (isPush) { let response = GCDWebServerStreamedResponse(contentType: "text/event-stream", asyncStreamBlock: { (completion: @escaping GCDWebServerBodyReaderCompletionBlock) in let session = URLSession(configuration: URLSessionConfiguration.default, delegate: SSERequestDelegate(withCompletion: completion), delegateQueue: nil) session.dataTask(with: outRequest).resume() }) completion(response) } else { let session = URLSession(configuration: URLSessionConfiguration.default, delegate: ApiRequestDelegate(), delegateQueue: nil) if !cookiesHeader.isEmpty { outRequest.addValue(String(cookiesHeader.split(separator: ";").first!), forHTTPHeaderField: "cookie") } let task = session.dataTask(with: outRequest, completionHandler: { (data, response, error) in // Error? if let error = error { self._gameBridge?._checkNotifyRequestError(withStatusCode: 500, andPath: escapedUrlString) let response = GCDWebServerDataResponse(html: "<html><head><title>Oopsie</title></head><body>\(error.localizedDescription)</body></html>")! response.statusCode = 500 return completion(response) } // Response? if let httpResponse = response as? HTTPURLResponse { // Get content type or apply one by default let contentType = httpResponse.allHeaderFields["content-type"] as? String ?? httpResponse.allHeaderFields["Content-Type"] as? String ?? "application/octet-stream" // Build response var gcdResponse: GCDWebServerDataResponse var responseData: Data? if let data = data { responseData = data // Game configuration for GDK2? Replace modules by original ones if request.path.hasPrefix("/api/itf/itf/catalog/game/config/") { responseData = self._handleCatchedGameConfiguration(data, contentType) ?? data } if request.path.hasPrefix("/api/itf/itf/catalog-unauthenticated/lotteries/") && request.path.hasSuffix("/config") { responseData = self._handleCatchedGameConfiguration(data, contentType) ?? data } gcdResponse = GCDWebServerDataResponse(data: responseData ?? data, contentType: contentType) gcdResponse.statusCode = httpResponse.statusCode } else { gcdResponse = GCDWebServerDataResponse(statusCode: httpResponse.statusCode) } // Headers for (headerName, headerValue) in httpResponse.allHeaderFields { if let name = headerName as? String, let value = headerValue as? String { print("____ NAME: \(name), \(value)") if name == "Set-Cookie" { if let url = httpResponse.url, let allHeaderFields = httpResponse.allHeaderFields as? [String : String] { let cookieHeaderField = ["Set-Cookie": "key=value"] // Or ["Set-Cookie": "key=value, key2=value2"] for multiple cookies let cookies = HTTPCookie.cookies(withResponseHeaderFields: cookieHeaderField, for: url) self.cookiesHeader = value // HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: nil) // NotificationCenter.default.post(name: NSNotification.Name("CustomeNotificationName"), object: cookies) } } gcdResponse.setValue(value, forAdditionalHeader: name) } } // The response might be encoded but when we re-emit the response our data body is already deflated // So we need to remove the encoding header telling the client to deflate the data response body gcdResponse.setValue(nil, forAdditionalHeader: "Content-Encoding") // Reset the content-length here if let data = responseData { gcdResponse.setValue(String(data.count), forAdditionalHeader: "Content-Length") } print("____ response: \(gcdResponse)") // Answer! completion(gcdResponse) // Notify in case or error self._gameBridge?._checkNotifyRequestError(withStatusCode: httpResponse.statusCode, andPath: escapedUrlString) } }) task.resume() } } /** Called on a GDK2 game.conf.json file has been catched (via ITF request). */ private func _handleCatchedGameConfiguration(_ data: Data, _ contentType: String) -> Data? { // Keep catched game configuration let decoder = JSONDecoder() _gameBridge?.catchedGameConfiguration = try? decoder.decode(CatchedGameConfiguration.self, from: data) // Take some data from original game configuration if exists // and transform the response. guard var originalGameConfiguration = _gameBridge?.catchedOriginalGameConfiguration else { return nil } guard let catchedGameConfigurationDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return nil } originalGameConfiguration["betMode"] = catchedGameConfigurationDictionary["betMode"] originalGameConfiguration["currency"] = catchedGameConfigurationDictionary["currency"] originalGameConfiguration["demo"] = catchedGameConfigurationDictionary["demo"] originalGameConfiguration["locale"] = catchedGameConfigurationDictionary["locale"] // GDK 3 specific originalGameConfiguration["selectedBehaviour"] = catchedGameConfigurationDictionary["selectedBehaviour"] originalGameConfiguration["stakes"] = catchedGameConfigurationDictionary["stakes"] originalGameConfiguration["selectedLocale"] = catchedGameConfigurationDictionary["selectedLocale"] originalGameConfiguration["selectedTheme"] = catchedGameConfigurationDictionary["selectedTheme"] originalGameConfiguration["selectedBetMode"] = catchedGameConfigurationDictionary["selectedBetMode"] originalGameConfiguration["lotteryCode"] = catchedGameConfigurationDictionary["lotteryCode"] originalGameConfiguration["jackpot"] = catchedGameConfigurationDictionary["jackpot"] originalGameConfiguration["lotteryGameCode"] = catchedGameConfigurationDictionary["lotteryGameCode"] originalGameConfiguration["phygital"] = catchedGameConfigurationDictionary["phygital"] let catchedGame = catchedGameConfigurationDictionary["game"] as? [String: Any?] let catchedTheme = catchedGame?["theme"] as? String var game = originalGameConfiguration["game"] as? [String: Any?] if game != nil && catchedTheme != nil { game!["theme"] = catchedTheme } originalGameConfiguration["game"] = game // Return the modified game.conf.json result return try? JSONSerialization.data(withJSONObject: originalGameConfiguration, options: []) } } /** Defines the delegate for catched requests in the Api handler. */ internal class ApiRequestDelegate : NSObject, URLSessionDelegate, URLSessionDataDelegate { /** Requests credentials from the delegate in response to a session-level authentication request from the remote server. */ func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { if challenge.previousFailureCount > 0 { completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } else if let serverTrust = challenge.protectionSpace.serverTrust { completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust)) } else { print("Unknown state error") } } } /** Defines the delegate for catched SSE requests in the Api handler. */ internal class SSERequestDelegate : NSObject, URLSessionDataDelegate { private let _completion: GCDWebServerBodyReaderCompletionBlock /** Inits the SSE request delegate. */ init(withCompletion completion: @escaping GCDWebServerBodyReaderCompletionBlock) { self._completion = completion } /** Requests credentials from the delegate in response to a session-level authentication request from the remote server. */ func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { if challenge.previousFailureCount > 0 { completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } else if let serverTrust = challenge.protectionSpace.serverTrust { completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust)) } else { // Handle error? } } /** Called on data has been received from server. */ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { _completion(data, nil) } /** Called on an error occured. */ func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { _completion(nil, error) } /** Allow request? */ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { print(">>>> response: \(response)") completionHandler(URLSession.ResponseDisposition.allow) } /** Called on an error occured. */ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { _completion(nil, error) } }
Editor is loading...