Untitled
unknown
swift
3 years ago
17 kB
10
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...