dns settings now actually work

This commit is contained in:
Seiike 2025-03-26 20:23:12 +01:00
parent ec6c8c94eb
commit 11bcb663d2
11 changed files with 325 additions and 125 deletions

View file

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Sora may requires access to your device's camera.</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

View file

@ -13,6 +13,10 @@ struct SoraApp: App {
@StateObject private var moduleManager = ModuleManager()
@StateObject private var librarykManager = LibraryManager()
init() {
registerCustomDNSGlobally()
}
var body: some Scene {
WindowGroup {
ContentView()

View file

@ -45,7 +45,7 @@ extension JSContext {
request.setValue(value, forHTTPHeaderField: key)
}
}
let task = URLSession.cloudflareCustom.dataTask(with: request) { data, _, error in
let task = URLSession.customDNS.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])
@ -111,7 +111,7 @@ extension JSContext {
}
}
let task = URLSession.cloudflareCustom.downloadTask(with: request) { tempFileURL, response, error in
let task = URLSession.customDNS.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])

View file

@ -5,55 +5,9 @@
// Created by Francesco on 05/01/25.
//
import Network
import Foundation
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"]
}
}
}
extension URLSession {
private static let dnsSelectorKey = "CustomDNSProvider"
static var currentDNSProvider: DNSProvider {
get {
guard let savedProviderRawValue = UserDefaults.standard.string(forKey: dnsSelectorKey) else {
UserDefaults.standard.set(DNSProvider.cloudflare.rawValue, forKey: dnsSelectorKey)
return .cloudflare
}
return DNSProvider(rawValue: savedProviderRawValue) ?? .cloudflare
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: dnsSelectorKey)
}
}
static let userAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
@ -80,33 +34,21 @@ extension URLSession {
"Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0"
]
static var randomUserAgent: String {
static let randomUserAgent: String = {
userAgents.randomElement() ?? userAgents[0]
}
}()
static var custom: URLSession {
static let custom: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"User-Agent": randomUserAgent
]
configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent]
return URLSession(configuration: configuration)
}
}()
static var cloudflareCustom: URLSession {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"User-Agent": randomUserAgent
]
let dnsServers = currentDNSProvider.servers
let dnsSettings: [AnyHashable: Any] = [
"DNSSettings": [
"ServerAddresses": dnsServers
]
]
configuration.connectionProxyDictionary = dnsSettings
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)
}()
}

View file

@ -36,7 +36,7 @@ class JSController: ObservableObject {
return
}
URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in
URLSession.customDNS.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.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in
URLSession.customDNS.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.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in
URLSession.customDNS.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.cloudflareCustom.dataTask(with: url) { data, response, error in
let task = URLSession.customDNS.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)) }

View file

@ -29,7 +29,7 @@ class VTTSubtitlesLoader: ObservableObject {
let format = determineSubtitleFormat(from: url)
URLSession.cloudflareCustom.dataTask(with: url) { data, _, error in
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data,
let content = String(data: data, encoding: .utf8),
error == nil else { return }

View file

@ -154,7 +154,7 @@ struct ModuleAdditionSettingsView: View {
return
}
do {
let (data, _) = try await URLSession.cloudflareCustom.data(from: url)
let (data, _) = try await URLSession.customDNS.data(from: url)
let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data)
await MainActor.run {
self.moduleMetadata = metadata

View file

@ -46,14 +46,14 @@ class ModuleManager: ObservableObject {
throw NSError(domain: "Module already exists", code: -1)
}
let (metadataData, _) = try await URLSession.cloudflareCustom.data(from: url)
let (metadataData, _) = try await URLSession.customDNS.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.cloudflareCustom.data(from: scriptUrl)
let (scriptData, _) = try await URLSession.customDNS.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.cloudflareCustom.data(from: URL(string: module.metadataUrl)!)
let (metadataData, _) = try await URLSession.customDNS.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.cloudflareCustom.data(from: scriptUrl)
let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl)
guard let jsContent = String(data: scriptData, encoding: .utf8) else {
throw NSError(domain: "Invalid script encoding", code: -1)
}

View file

@ -0,0 +1,248 @@
//
// CustomDNS.swift
// Sora
//
// Created by Seiike on 26/03/25.
//
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 dnsServerIP: String {
return DNSProvider.current.servers.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..<qdCount {
while offset < data.count && data[offset] != 0 {
let length = Int(data[offset])
offset += 1 + length
}
offset += 1
offset += 4
}
for _ in 0..<anCount {
if offset < data.count {
let nameByte = data[offset]
if nameByte & 0xC0 == 0xC0 {
offset += 2
} else {
while offset < data.count && data[offset] != 0 {
let length = Int(data[offset])
offset += 1 + length
}
offset += 1
}
}
guard let type = readUInt16(), let _ = readUInt16() else { break }
let _ = readUInt32()
guard let dataLen = readUInt16() else { break }
if type == 1 && dataLen == 4 {
guard offset + 4 <= data.count else { break }
let ipBytes = data[offset..<offset+4]
let ip = ipBytes.map { String($0) }.joined(separator: ".")
ips.append(ip)
}
offset += Int(dataLen)
}
return ips
}
func resolve(host: String, completion: @escaping (Result<[String], Error>) -> 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
}

View file

@ -122,6 +122,10 @@
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
1EBA87D92D94653B00CABC28 /* NetworkDns */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NetworkDns; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
133D7C672D2BE2500075467E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -294,6 +298,7 @@
133D7C882D2BE2640075467E /* Modules */,
1399FAD12D3AB33D00E97C31 /* Logger */,
13D842532D45266900EBBFA6 /* Drops */,
1EBA87D92D94653B00CABC28 /* NetworkDns */,
);
path = Utils;
sourceTree = "<group>";
@ -452,6 +457,9 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
1EBA87D92D94653B00CABC28 /* NetworkDns */,
);
name = Sulfur;
packageProductDependencies = (
133D7C962D2BE2AF0075467E /* Kingfisher */,
@ -701,13 +709,13 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
DEVELOPMENT_TEAM = 399LMK6Q2Y;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Sora/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Sora;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
INFOPLIST_KEY_NSCameraUsageDescription = "Sora may requires access to your device's camera.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -750,6 +758,7 @@
INFOPLIST_FILE = Sora/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Sora;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
INFOPLIST_KEY_NSCameraUsageDescription = "Sora may requires access to your device's camera.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;

View file

@ -1,43 +1,42 @@
{
"object": {
"pins": [
{
"package": "Drops",
"repositoryURL": "https://github.com/omaralbeik/Drops.git",
"state": {
"branch": "main",
"revision": "5824681795286c36bdc4a493081a63e64e2a064e",
"version": null
}
},
{
"package": "FFmpeg-iOS-Lame",
"repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Lame",
"state": {
"branch": "main",
"revision": "1808fa5a1263c5e216646cd8421fc7dcb70520cc",
"version": null
}
},
{
"package": "FFmpeg-iOS-Support",
"repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Support",
"state": {
"branch": null,
"revision": "be3bd9149ac53760e8725652eee99c405b2be47a",
"version": "0.0.2"
}
},
{
"package": "Kingfisher",
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e",
"version": "7.9.1"
}
"originHash" : "28f2c123747ea3d0aee96430294eb72e7254bafd504c83303b2a2e02f270f26f",
"pins" : [
{
"identity" : "drops",
"kind" : "remoteSourceControl",
"location" : "https://github.com/omaralbeik/Drops.git",
"state" : {
"branch" : "main",
"revision" : "5824681795286c36bdc4a493081a63e64e2a064e"
}
]
},
"version": 1
},
{
"identity" : "ffmpeg-ios-lame",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kewlbear/FFmpeg-iOS-Lame",
"state" : {
"branch" : "main",
"revision" : "1808fa5a1263c5e216646cd8421fc7dcb70520cc"
}
},
{
"identity" : "ffmpeg-ios-support",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kewlbear/FFmpeg-iOS-Support",
"state" : {
"revision" : "be3bd9149ac53760e8725652eee99c405b2be47a",
"version" : "0.0.2"
}
},
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher.git",
"state" : {
"revision" : "b6f62758f21a8c03cd64f4009c037cfa580a256e",
"version" : "7.9.1"
}
}
],
"version" : 3
}