Untitled

 avatar
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...