-
-
Notifications
You must be signed in to change notification settings - Fork 3
Handle->DID resolution via /.well-known/atproto-did endpoint #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0dee7a1
9c360c7
9252b85
5d74541
28b3010
2cbedc8
6a01c46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,20 +40,67 @@ public struct ATResolver<Provider: ResponseProviding> { | |
| } | ||
|
|
||
| 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<String>.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"] | ||
|
Comment on lines
+90
to
+92
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This resolves the DNS timeout issue, but we can also adjust There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could make this a variable on the resolver, and allow overriding it, but using these defaults — since they're major DNS server providers. We may want to provide IPv6 options here too. Another option is to not use AsyncDNSResolver and instead just use DNS over HTTPS: https://github.com/bluesky-social/atproto/blob/9dac8b0c600520ecb0066ac104787b27668dea47/packages/internal/handle-resolver/src/atproto-doh-handle-resolver.ts#L37 which would be somewhat more secure than using standard DNS (which is cleartext). That would also then allow this to be fully cancellable.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds like a really cool option. Perhaps that could be another thing in the chain of checks this system does? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, with a bit of reorganisation we could hsve different handle resolvers, DNS, HTTP and DNS over HTTPS, and then folks could choose the right methods for them? i suspect DoH would be superior here even though response times might be slightly higher than DNS
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds great to me! What I was trying to go for was a "all discrete options public API" so you can get the behaviors you need, if you have advanced requirements. And then also something pre-configured so there's an easy (but potentially suboptimal) thing for people that aren't interested in learning how it all works. |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could technically have issues, as some did methods can include query strings, so you probably want to just split on |
||
| } catch { | ||
| return nil | ||
| } | ||
|
|
||
| return didRecord?.txt.components(separatedBy: "=").last | ||
| } | ||
|
|
||
| public func didForHandle(_ handle: String) async throws -> String? { | ||
|
|
@@ -95,8 +142,6 @@ public struct ATResolver<Provider: ResponseProviding> { | |
| } | ||
| } | ||
|
|
||
| extension ATResolver: Sendable where Provider: Sendable {} | ||
|
|
||
| #if canImport(Foundation) | ||
| import Foundation | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ public struct Request: Sendable { | |
| public let queryItems: [(String, String?)] | ||
| } | ||
|
|
||
| public protocol ResponseProviding { | ||
| public protocol ResponseProviding: Sendable { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mark pointed out that you have a conditional conformance of ATResolve being Sendable if ResponseProviding is Sendable -- I'm making new problems for you by now requiring ResponseProviding to always be sendable. This is our best attempt at making the task groups work, but would defer to your expertise!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very generally speaking, Sendable protocols making the lives of the users of the protocol (that's us here) easier, while making the conformer's lives (that's the libraries clients) harder. In this case, however, I think it is pretty reasonable. So I think it's fine, we just need to remove the conditional conformance.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| func data(for: Request) async throws -> Data | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the
Providerhere would allow it, but you could set a maximum response body size to like 1kb, and stop reading the response after that, since anything larger and you know it's not a valid response (it's likely HTML)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be possible to make an appropriate provider function that does this, if the underlying request system allows it?