From c6915d1f911968613a780b546e8c2ce024692c6b Mon Sep 17 00:00:00 2001 From: Joseph Mattello Date: Mon, 7 Nov 2022 00:12:34 -0500 Subject: [PATCH] refs #103 redirects are working Signed-off-by: Joseph Mattello --- .../Operations/FetchSourceOperation.swift | 183 ++++++++++++------ AltStore/Services/NetworkService.swift | 31 ++- 2 files changed, 145 insertions(+), 69 deletions(-) diff --git a/AltStore/Operations/FetchSourceOperation.swift b/AltStore/Operations/FetchSourceOperation.swift index 250fbb6c..7f2caeba 100644 --- a/AltStore/Operations/FetchSourceOperation.swift +++ b/AltStore/Operations/FetchSourceOperation.swift @@ -12,6 +12,44 @@ import CoreData import AltStoreCore import Roxas +func matches(for regex: String, in text: String) -> [String] { + + do { + let regex = try NSRegularExpression(pattern: regex) + let results = regex.matches(in: text, + range: NSRange(text.startIndex..., in: text)) + return results.map { + String(text[Range($0.range, in: text)!]) + } + } catch let error { + print("invalid regex: \(error.localizedDescription)") + return [] + } +} + +func containsRedirect(_ response: URLResponse?, data: Data?) -> String? { + if let httpResponse = response as? HTTPURLResponse { + print("Request status code: \(httpResponse.statusCode)") + print("Request headers: \(httpResponse.allHeaderFields.debugDescription)") + + guard let data = data else { + print("Request error: missing data") + return nil + } + let rawHttp = String(decoding: data, as: UTF8.self) + let regex = "url=((https|http):\\/\\/[\\S]*)\">" + guard var redirectURL = matches(for: regex, in: rawHttp).first else { + return nil + } + redirectURL = redirectURL.replacingOccurrences(of: "url=", with: "") + redirectURL = redirectURL.replacingOccurrences(of: "\">", with: "") + print("redirectURL: \(redirectURL)") + return redirectURL + } else { + return nil + } +} + @objc(FetchSourceOperation) final class FetchSourceOperation: ResultOperation { @@ -34,68 +72,93 @@ final class FetchSourceOperation: ResultOperation override func main() { super.main() - - let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in - - let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext) - childContext.mergePolicy = NSOverwriteMergePolicy - childContext.perform { - do - { - let (data, _) = try Result((data, response), error).get() - - let decoder = AltStoreCore.JSONDecoder() - decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in - let container = try decoder.singleValueContainer() - let text = try container.decode(String.self) - - // Full ISO8601 Format. - self.dateFormatter.formatOptions = [.withFullDate, .withFullTime, .withTimeZone] - if let date = self.dateFormatter.date(from: text) - { - return date - } - - // Just date portion of ISO8601. - self.dateFormatter.formatOptions = [.withFullDate] - if let date = self.dateFormatter.date(from: text) - { - return date - } - - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.") - }) - - decoder.managedObjectContext = childContext - decoder.sourceURL = self.sourceURL - - let source = try decoder.decode(Source.self, from: data) - let identifier = source.identifier - - try childContext.save() - - self.managedObjectContext.perform { - if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), identifier), in: self.managedObjectContext) - { - self.finish(.success(source)) - } - else - { - self.finish(.failure(OperationError.noSources)) - } - } - } - catch - { - self.managedObjectContext.perform { - self.finish(.failure(error)) - } - } - } - } - + loadSource(self.sourceURL) + } + + private func loadSource(_ url: URL) { + let dataTask = createDataTask(with: url) self.progress.addChild(dataTask.progress, withPendingUnitCount: 1) dataTask.resume() } + + private func createDataTask(with url: URL) -> URLSessionDataTask { + let dataTask = self.session.dataTask(with: url) { (data, response, error) in + // Test code for http redirect HTML, though seems I got jekyll/sidestore to work without this now - @JoeMatt + if let error = error { + print("Request error: \(error)") + // TODO: Handle error + self.finish(.failure(error)) + return + } + + if let redirect = containsRedirect(response, data: data), let redirectURL = URL(string: redirect) { + DispatchQueue.main.async { + self.loadSource(redirectURL) + } + } else { + self.processJSON(data: data, response: response, error: error) + } + } + return dataTask + } + + private func processJSON(data: Data?, response: URLResponse?, error: Error?) { + let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext) + childContext.mergePolicy = NSOverwriteMergePolicy + childContext.perform { + do + { + let (data, _) = try Result((data, response), error).get() + + let decoder = AltStoreCore.JSONDecoder() + decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in + let container = try decoder.singleValueContainer() + let text = try container.decode(String.self) + + // Full ISO8601 Format. + self.dateFormatter.formatOptions = [.withFullDate, .withFullTime, .withTimeZone] + if let date = self.dateFormatter.date(from: text) + { + return date + } + + // Just date portion of ISO8601. + self.dateFormatter.formatOptions = [.withFullDate] + if let date = self.dateFormatter.date(from: text) + { + return date + } + + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.") + }) + + decoder.managedObjectContext = childContext + // Note: This may need to be response.url instead, to handle redirects @JoeMatt + decoder.sourceURL = self.sourceURL + + let source = try decoder.decode(Source.self, from: data) + let identifier = source.identifier + + try childContext.save() + + self.managedObjectContext.perform { + if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), identifier), in: self.managedObjectContext) + { + self.finish(.success(source)) + } + else + { + self.finish(.failure(OperationError.noSources)) + } + } + } + catch + { + self.managedObjectContext.perform { + self.finish(.failure(error)) + } + } + } + } } diff --git a/AltStore/Services/NetworkService.swift b/AltStore/Services/NetworkService.swift index 7ff19a95..06655434 100644 --- a/AltStore/Services/NetworkService.swift +++ b/AltStore/Services/NetworkService.swift @@ -24,7 +24,11 @@ public struct DefaultServices: Services { let AppServices = DefaultServices() -final public class AltNetworkDelegate: NSObject, URLSessionTaskDelegate { +final public class AltNetworkDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate { + public override init() { + super.init() + } + public struct Options: OptionSet { public let rawValue: Int public init(rawValue: Int) { @@ -37,7 +41,7 @@ final public class AltNetworkDelegate: NSObject, URLSessionTaskDelegate { var options: Options = .all -// func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { +// public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { // if options.contains(.redirect) { // completionHandler(request) // } else { @@ -55,27 +59,36 @@ final public class AltNetworkDelegate: NSObject, URLSessionTaskDelegate { } public final class AltNetworkService: NetworkService { - public let session: URLSession = { + let delegate = AltNetworkDelegate() + + lazy var delegateQueue: OperationQueue = { + let queue = OperationQueue.init() + queue.name = "com.sidestore.NetworkService.serialOperationQueue" + queue.maxConcurrentOperationCount = 1 + return queue + }() + + public lazy var session: URLSession = { let configuration: URLSessionConfiguration = URLSessionConfiguration.default configuration.httpShouldSetCookies = true configuration.httpShouldUsePipelining = true - let session = URLSession.init(configuration: configuration, delegate: AltNetworkDelegate(), delegateQueue: nil) + let session = URLSession.init(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) return session }() - public let sessionNoCache: URLSession = { + public lazy var sessionNoCache: URLSession = { let configuration: URLSessionConfiguration = URLSessionConfiguration.default configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - configuration.urlCache = nil - let session = URLSession.init(configuration: configuration, delegate: AltNetworkDelegate(), delegateQueue: nil) + configuration.urlCache = nil + let session = URLSession.init(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) return session }() static let backgroundSessionIdentifier = "SideStoreBackgroundSession" - public let backgroundSession: URLSession = { + public lazy var backgroundSession: URLSession = { let configuration: URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: AltNetworkService.backgroundSessionIdentifier) - let session = URLSession.init(configuration: configuration, delegate: AltNetworkDelegate(), delegateQueue: nil) + let session = URLSession.init(configuration: configuration, delegate: delegate, delegateQueue: nil) return session }() }