From 2eb07aebf19e5b7ff3b9e84a8ee7b2379e4a2e09 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:23:55 +0100 Subject: [PATCH] =?UTF-8?q?yeah=20fuck=20@Seeike=20not=20the=20ISPs=20?= =?UTF-8?q?=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/SoraApp.swift | 4 - .../JavaScriptCore+Extensions.swift | 17 +- Sora/Utils/Extensions/URLSession.swift | 10 +- Sora/Utils/JSLoader/JSController.swift | 8 +- .../Modules/ModuleAdditionSettingsView.swift | 2 +- Sora/Utils/Modules/ModuleManager.swift | 8 +- Sora/Utils/NetworkDNS/CustomDNS.swift | 265 ------------------ .../SettingsViewGeneral.swift | 20 -- Sulfur.xcodeproj/project.pbxproj | 12 - 9 files changed, 13 insertions(+), 333 deletions(-) delete mode 100644 Sora/Utils/NetworkDNS/CustomDNS.swift diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 34ababb..4073e20 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -13,10 +13,6 @@ struct SoraApp: App { @StateObject private var moduleManager = ModuleManager() @StateObject private var librarykManager = LibraryManager() - init() { - registerCustomDNSGlobally() - } - var body: some Scene { WindowGroup { ContentView() diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index d30569b..eda6f2a 100644 --- a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -11,13 +11,11 @@ extension JSContext { func setupConsoleLogging() { let consoleObject = JSValue(newObjectIn: self) - // Set up console.log let consoleLogFunction: @convention(block) (String) -> Void = { message in Logger.shared.log(message, type: "Debug") } consoleObject?.setObject(consoleLogFunction, forKeyedSubscript: "log" as NSString) - // Set up console.error let consoleErrorFunction: @convention(block) (String) -> Void = { message in Logger.shared.log(message, type: "Error") } @@ -25,7 +23,6 @@ extension JSContext { self.setObject(consoleObject, forKeyedSubscript: "console" as NSString) - // Global log function let logFunction: @convention(block) (String) -> Void = { message in Logger.shared.log("JavaScript log: \(message)", type: "Debug") } @@ -45,7 +42,7 @@ extension JSContext { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.customDNS.dataTask(with: request) { data, _, error in + let task = URLSession.custom.dataTask(with: request) { data, _, error in if let error = error { Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: "Error") reject.call(withArguments: [error.localizedDescription]) @@ -91,27 +88,24 @@ extension JSContext { Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil")", type: "Debug") - // Ensure no body for GET requests if httpMethod == "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" { Logger.shared.log("GET request must not have a body", type: "Error") reject.call(withArguments: ["GET request must not have a body"]) return } - // Set the body for non-GET requests if httpMethod != "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" { request.httpBody = body.data(using: .utf8) } - // Set headers if let headers = headers { for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.customDNS.downloadTask(with: request) { tempFileURL, response, error in + let task = URLSession.custom.downloadTask(with: request) { tempFileURL, response, error in if let error = error { Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error") reject.call(withArguments: [error.localizedDescription]) @@ -127,8 +121,7 @@ extension JSContext { do { let data = try Data(contentsOf: tempFileURL) - // Check response size before processing - if data.count > 10_000_000 { // Example: 10MB limit + if data.count > 10_000_000 { Logger.shared.log("Response exceeds maximum size", type: "Error") reject.call(withArguments: ["Response exceeds maximum size"]) return @@ -203,7 +196,6 @@ extension JSContext { } func setupBase64Functions() { - // btoa function: converts binary string to base64-encoded ASCII string let btoaFunction: @convention(block) (String) -> String? = { data in guard let data = data.data(using: .utf8) else { Logger.shared.log("btoa: Failed to encode input as UTF-8", type: "Error") @@ -212,7 +204,6 @@ extension JSContext { return data.base64EncodedString() } - // atob function: decodes base64-encoded ASCII string to binary string let atobFunction: @convention(block) (String) -> String? = { base64String in guard let data = Data(base64Encoded: base64String) else { Logger.shared.log("atob: Invalid base64 input", type: "Error") @@ -222,12 +213,10 @@ extension JSContext { return String(data: data, encoding: .utf8) } - // Add the functions to the JavaScript context self.setObject(btoaFunction, forKeyedSubscript: "btoa" as NSString) self.setObject(atobFunction, forKeyedSubscript: "atob" as NSString) } - // Helper method to set up all JavaScript functionality func setupJavaScriptEnvironment() { setupConsoleLogging() setupNativeFetch() diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 50a32e1..544b084 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -43,12 +43,4 @@ extension URLSession { configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent] return URLSession(configuration: configuration) }() - - static let customDNS: URLSession = { - let config = URLSessionConfiguration.default - var protocols = config.protocolClasses ?? [] - protocols.insert(CustomURLProtocol.self, at: 0) - config.protocolClasses = protocols - return URLSession(configuration: config, delegate: InsecureSessionDelegate(), delegateQueue: nil) - }() -} \ No newline at end of file +} diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index 8edbda2..a0a220b 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -36,7 +36,7 @@ class JSController: ObservableObject { return } - URLSession.customDNS.dataTask(with: url) { [weak self] data, _, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -77,7 +77,7 @@ class JSController: ObservableObject { return } - URLSession.customDNS.dataTask(with: url) { [weak self] data, _, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -130,7 +130,7 @@ class JSController: ObservableObject { return } - URLSession.customDNS.dataTask(with: url) { [weak self] data, _, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -430,7 +430,7 @@ class JSController: ObservableObject { func fetchStreamUrlJSSecond(episodeUrl: String, softsub: Bool = false, completion: @escaping ((stream: String?, subtitles: String?)) -> Void) { let url = URL(string: episodeUrl)! - let task = URLSession.customDNS.dataTask(with: url) { data, response, error in + let task = URLSession.custom.dataTask(with: url) { data, response, error in if let error = error { Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error") DispatchQueue.main.async { completion((nil, nil)) } diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index c7f6d96..b2580e2 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -154,7 +154,7 @@ struct ModuleAdditionSettingsView: View { return } do { - let (data, _) = try await URLSession.customDNS.data(from: url) + let (data, _) = try await URLSession.custom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data) await MainActor.run { self.moduleMetadata = metadata diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index 9bf815c..d13d8cf 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -46,14 +46,14 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Module already exists", code: -1) } - let (metadataData, _) = try await URLSession.customDNS.data(from: url) + let (metadataData, _) = try await URLSession.custom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) guard let scriptUrl = URL(string: metadata.scriptUrl) else { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } @@ -94,7 +94,7 @@ class ModuleManager: ObservableObject { func refreshModules() async { for (index, module) in modules.enumerated() { do { - let (metadataData, _) = try await URLSession.customDNS.data(from: URL(string: module.metadataUrl)!) + let (metadataData, _) = try await URLSession.custom.data(from: URL(string: module.metadataUrl)!) let newMetadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) if newMetadata.version != module.metadata.version { @@ -102,7 +102,7 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } diff --git a/Sora/Utils/NetworkDNS/CustomDNS.swift b/Sora/Utils/NetworkDNS/CustomDNS.swift deleted file mode 100644 index 98c7bc4..0000000 --- a/Sora/Utils/NetworkDNS/CustomDNS.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// CustomDNS.swift -// Sora -// -// Created by Seiike on 26/03/25. -// -// fuck region restrictions - -import Foundation -import Network - -enum DNSProvider: String, CaseIterable, Hashable { - case cloudflare = "Cloudflare" - case google = "Google" - case openDNS = "OpenDNS" - case quad9 = "Quad9" - case adGuard = "AdGuard" - case cleanbrowsing = "CleanBrowsing" - case controld = "ControlD" - - var servers: [String] { - switch self { - case .cloudflare: - return ["1.1.1.1", "1.0.0.1"] - case .google: - return ["8.8.8.8", "8.8.4.4"] - case .openDNS: - return ["208.67.222.222", "208.67.220.220"] - case .quad9: - return ["9.9.9.9", "149.112.112.112"] - case .adGuard: - return ["94.140.14.14", "94.140.15.15"] - case .cleanbrowsing: - return ["185.228.168.168", "185.228.169.168"] - case .controld: - return ["76.76.2.0", "76.76.10.0"] - } - } - - static var current: DNSProvider { - get { - let raw = UserDefaults.standard.string(forKey: "SelectedDNSProvider") ?? DNSProvider.cloudflare.rawValue - return DNSProvider(rawValue: raw) ?? .cloudflare - } - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "SelectedDNSProvider") - } - } -} - -class CustomDNSResolver { - var dnsServers: [String] { - if let provider = UserDefaults.standard.string(forKey: "CustomDNSProvider"), - provider == "Custom" { - let primary = UserDefaults.standard.string(forKey: "customPrimaryDNS") ?? "" - let secondary = UserDefaults.standard.string(forKey: "customSecondaryDNS") ?? "" - var servers = [String]() - if !primary.isEmpty { servers.append(primary) } - if !secondary.isEmpty { servers.append(secondary) } - if !servers.isEmpty { - return servers - } - } - return DNSProvider.current.servers - } - - var dnsServerIP: String { - return dnsServers.first ?? "1.1.1.1" - } - - func buildDNSQuery(for host: String) -> (Data, UInt16) { - var data = Data() - let queryID = UInt16.random(in: 0...UInt16.max) - data.append(UInt8(queryID >> 8)) - data.append(UInt8(queryID & 0xFF)) - data.append(contentsOf: [0x01, 0x00]) - data.append(contentsOf: [0x00, 0x01]) - data.append(contentsOf: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - let labels = host.split(separator: ".") - for label in labels { - if let labelData = label.data(using: .utf8) { - data.append(UInt8(labelData.count)) - data.append(labelData) - } - } - data.append(0) - data.append(contentsOf: [0x00, 0x01]) - data.append(contentsOf: [0x00, 0x01]) - return (data, queryID) - } - - func parseDNSResponse(_ data: Data, queryID: UInt16) -> [String] { - var ips = [String]() - var offset = 0 - func readUInt16() -> UInt16? { - guard offset + 2 <= data.count else { return nil } - let value = (UInt16(data[offset]) << 8) | UInt16(data[offset+1]) - offset += 2 - return value - } - func readUInt32() -> UInt32? { - guard offset + 4 <= data.count else { return nil } - let value = (UInt32(data[offset]) << 24) | (UInt32(data[offset+1]) << 16) | (UInt32(data[offset+2]) << 8) | UInt32(data[offset+3]) - offset += 4 - return value - } - guard data.count >= 12 else { return [] } - let responseID = (UInt16(data[0]) << 8) | UInt16(data[1]) - if responseID != queryID { return [] } - offset = 2 - offset += 2 - guard let qdCount = readUInt16() else { return [] } - guard let anCount = readUInt16() else { return [] } - offset += 4 - for _ in 0..) -> Void) { - let dnsServer = self.dnsServerIP - guard let port = NWEndpoint.Port(rawValue: 53) else { - completion(.failure(NSError(domain: "CustomDNS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid port"]))) - return - } - let connection = NWConnection(host: NWEndpoint.Host(dnsServer), port: port, using: .udp) - connection.stateUpdateHandler = { newState in - switch newState { - case .ready: - let (queryData, queryID) = self.buildDNSQuery(for: host) - connection.send(content: queryData, completion: .contentProcessed({ error in - if let error = error { - completion(.failure(error)) - connection.cancel() - } else { - connection.receive(minimumIncompleteLength: 1, maximumLength: 512) { content, _, _, error in - if let error = error { - completion(.failure(error)) - } else if let content = content { - let ips = self.parseDNSResponse(content, queryID: queryID) - if !ips.isEmpty { - completion(.success(ips)) - } else { - completion(.failure(NSError(domain: "CustomDNS", code: 2, userInfo: [NSLocalizedDescriptionKey: "No A records found"]))) - } - } - connection.cancel() - } - } - })) - case .failed(let error): - completion(.failure(error)) - connection.cancel() - default: - break - } - } - connection.start(queue: DispatchQueue.global()) - } -} - - -class CustomURLProtocol: URLProtocol { - static let resolver = CustomDNSResolver() - override class func canInit(with request: URLRequest) -> Bool { - return URLProtocol.property(forKey: "Handled", in: request) == nil - } - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - override func startLoading() { - guard let url = request.url, let host = url.host else { - client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -1, userInfo: nil)) - return - } - CustomURLProtocol.resolver.resolve(host: host) { result in - switch result { - case .success(let ips): - guard let ip = ips.first, - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -2, userInfo: nil)) - return - } - components.host = ip - guard let ipURL = components.url else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -3, userInfo: nil)) - return - } - guard let mutableRequest = (self.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -4, userInfo: nil)) - return - } - mutableRequest.url = ipURL - mutableRequest.setValue(host, forHTTPHeaderField: "Host") - URLProtocol.setProperty(true, forKey: "Handled", in: mutableRequest) - let finalRequest = mutableRequest as URLRequest - let session = URLSession.customDNS - let task = session.dataTask(with: finalRequest) { data, response, error in - if let data = data, let response = response { - self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - self.client?.urlProtocol(self, didLoad: data) - self.client?.urlProtocolDidFinishLoading(self) - } else if let error = error { - self.client?.urlProtocol(self, didFailWithError: error) - } - } - task.resume() - case .failure(let error): - self.client?.urlProtocol(self, didFailWithError: error) - } - } - } - override func stopLoading() {} -} - -class InsecureSessionDelegate: NSObject, URLSessionDelegate { - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust { - let credential = URLCredential(trust: serverTrust) - completionHandler(.useCredential, credential) - } else { - completionHandler(.performDefaultHandling, nil) - } - } -} - -func registerCustomDNSGlobally() { - let config = URLSessionConfiguration.default - var protocols = config.protocolClasses ?? [] - protocols.insert(CustomURLProtocol.self, at: 0) - config.protocolClasses = protocols - URLSessionConfiguration.default.protocolClasses = protocols - URLSessionConfiguration.ephemeral.protocolClasses = protocols - URLSessionConfiguration.background(withIdentifier: "CustomDNSBackground").protocolClasses = protocols -} diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index a3414dc..154d1d2 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -101,26 +101,6 @@ struct SettingsViewGeneral: View { .tint(.accentColor) } - Section(header: Text("Network"), footer: Text("Try between some of the providers in case something is not loading if it should be, as it might be the fault of your ISP.")){ - HStack { - Text("DNS service") - Spacer() - Menu(customDNSProvider) { - ForEach(customDNSProviderList, id: \.self) { provider in - Button(action: { customDNSProvider = provider }) { - Text(provider) - } - } - } - } - if customDNSProvider == "Custom" { - TextField("Primary DNS", text: $customPrimaryDNS) - .keyboardType(.numbersAndPunctuation) - TextField("Secondary DNS", text: $customSecondaryDNS) - .keyboardType(.numbersAndPunctuation) - } - } - Section(header: Text("Advanced"), footer: Text("Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time.")) { Toggle("Enable Analytics", isOn: $analyticsEnabled) .tint(.accentColor) diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index d9c74ca..ad75420 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; - 1ED156202D949B1A00C11BCD /* CustomDNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; /* End PBXBuildFile section */ @@ -120,7 +119,6 @@ 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; - 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNS.swift; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -289,7 +287,6 @@ 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, - 1ED1561E2D949AFB00C11BCD /* NetworkDNS */, 13DC0C442D302C6A00D0F966 /* MediaPlayer */, 133D7C862D2BE2640075467E /* Extensions */, 1327FBA52D758CEA00FC6689 /* Analytics */, @@ -440,14 +437,6 @@ path = Components; sourceTree = ""; }; - 1ED1561E2D949AFB00C11BCD /* NetworkDNS */ = { - isa = PBXGroup; - children = ( - 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */, - ); - path = NetworkDNS; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -573,7 +562,6 @@ 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, - 1ED156202D949B1A00C11BCD /* CustomDNS.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */,