diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index c3bfad6..eb1d1e5 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -40,20 +40,67 @@ public struct ATResolver { } public func didForDomain(_ name: String) async throws -> String? { - // I don't understand exactly why, but this triggers a timeout. When I do it with `dig` it returns right away... - if name.hasSuffix(".bsky.social") { + return await withTaskGroup(of: Optional.self) { group in + let provider = provider + group.addTask { + await Self.checkWellKnown(handle: name, provider: provider) + } + + group.addTask { + await Self.checkDNS(handle: name) + } + + let first = await group.next() + if let first { + return first + } + + return await group.next() ?? nil + } + } + + static func checkWellKnown(handle: String, provider: Provider) async -> String? { + do { + let dataResult = try await provider.data( + for: .init( + host: handle, + path: "/.well-known/atproto-did", + method: .get, + headers: ["Accept": "text/plain;charset=UTF-8"], + queryItems: [] + ) + ) + let result = String(data: dataResult, encoding: .utf8) + + if let result { + //workaround if we get erroneous 200 code but body return is e.g. + //"404 error" + guard result.hasPrefix("did:") else { + return nil + } + } + return result + } catch { return nil } - - let resolver = try AsyncDNSResolver() - - let txtRecords = try await resolver.queryTXT(name: "_atproto." + name) - - let didRecord = txtRecords.first { record in - record.txt.hasPrefix("did=") + } + + static func checkDNS(handle: String) async -> String? { + do { + // Only check Cloudflare and Google DNS servers + var dnsOptions = CAresDNSResolver.Options.default + dnsOptions.servers = ["1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4"] + let resolver = try AsyncDNSResolver(options: dnsOptions) + let txtRecords = try await resolver.queryTXT( + name: "_atproto." + handle + ) + let didRecord = txtRecords.first { record in + record.txt.hasPrefix("did=") + } + return didRecord?.txt.components(separatedBy: "=").last + } catch { + return nil } - - return didRecord?.txt.components(separatedBy: "=").last } public func didForHandle(_ handle: String) async throws -> String? { @@ -95,8 +142,6 @@ public struct ATResolver { } } -extension ATResolver: Sendable where Provider: Sendable {} - #if canImport(Foundation) import Foundation diff --git a/Sources/ATResolve/Networking.swift b/Sources/ATResolve/Networking.swift index 3b9f823..b59733c 100644 --- a/Sources/ATResolve/Networking.swift +++ b/Sources/ATResolve/Networking.swift @@ -18,6 +18,7 @@ extension URLSession: ResponseProviding { throw URLError(.badURL) } var urlRequest = URLRequest(url: url) + urlRequest.timeoutInterval = 3 urlRequest.httpMethod = request.method.rawValue for (key, value) in request.headers { urlRequest.addValue(value, forHTTPHeaderField: key) diff --git a/Sources/ATResolve/ResponseProviding.swift b/Sources/ATResolve/ResponseProviding.swift index 5688e6e..955d924 100644 --- a/Sources/ATResolve/ResponseProviding.swift +++ b/Sources/ATResolve/ResponseProviding.swift @@ -28,7 +28,7 @@ public struct Request: Sendable { public let queryItems: [(String, String?)] } -public protocol ResponseProviding { +public protocol ResponseProviding: Sendable { func data(for: Request) async throws -> Data } diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index a5f39b1..7f33b71 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -42,6 +42,27 @@ struct ATResolveTests { #expect(profile != nil) } + + @Test + func timedTestWellKnownTimeout() async throws { + // The /.well-known endpoint times out for @thisismissem.social + // It should time out after 3 seconds, so this test should be ~3 seconds + try await timedTest { + let resolver = ATResolver(provider: URLSession.shared) + let profile = try await resolver.resolveHandle("thisismissem.social") + #expect(profile?.did == "did:plc:5w4eqcxzw5jv5qfnmzxcakfy") + } + } + + @Test + func timedTestDNSTimeout() async throws { + // DNS should time out for any .bsky.social handle + try await timedTest { + let resolver = ATResolver(provider: URLSession.shared) + let profile = try await resolver.resolveHandle("cjrdev.bsky.social") + #expect(profile?.did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") + } + } @Test func decodeWithCustomProvider() async throws { struct CustomProvider: ResponseProviding { @@ -60,4 +81,11 @@ struct ATResolveTests { #expect(response.pds?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } + + private func timedTest(_ test: () async throws -> ()) async throws { + let start = CFAbsoluteTimeGetCurrent() + try await test() + let diff = CFAbsoluteTimeGetCurrent() - start + print("This test took \(diff) seconds") + } }