-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresolver.rb
More file actions
175 lines (138 loc) · 5.75 KB
/
resolver.rb
File metadata and controls
175 lines (138 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# frozen_string_literal: true
require 'net/http'
require 'resolv'
require_relative 'did'
require_relative 'document'
require_relative 'requests'
module DIDKit
#
# A class which manages resolving of handles to DIDs and DIDs to DID documents.
#
class Resolver
# These TLDs are not allowed in ATProto handles, so the resolver returns nil for them
# without trying to look them up.
RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test)
include Requests
# @return [String, Array<String>] custom DNS nameserver(s) to use for DNS TXT lookups
attr_accessor :nameserver
# @param options [Hash] resolver options
# @option options [String, Array<String>] :nameserver custom DNS nameserver(s) to use (IP or an array of IPs)
# @option options [Integer] :timeout request timeout in seconds (default: 15)
# @option options [Integer] :max_redirects maximum number of redirects to follow (default: 5)
def initialize(options = {})
@nameserver = options[:nameserver]
@request_options = options.slice(:timeout, :max_redirects)
end
# Resolve a handle into a DID. Looks up the given ATProto domain handle using the DNS TXT method
# and the HTTP .well-known method and returns a DID if one is assigned using either of the methods.
#
# If a DID string or a {DID} object is passed, it simply returns that DID, so you can use this
# method to pass it an input string from the user which can be a DID or handle, without having to
# check which one it is.
#
# @param handle [String, DID] a domain handle (may start with an `@`) or a DID string
# @return [DID, nil] resolved DID if found, nil otherwise
def resolve_handle(handle)
if handle.is_a?(DID) || handle =~ DID::GENERIC_REGEXP
return DID.new(handle)
end
domain = handle.gsub(/^@/, '')
return nil if RESERVED_DOMAINS.include?(domain.split('.').last)
if dns_did = resolve_handle_by_dns(domain)
DID.new(dns_did, :dns)
elsif http_did = resolve_handle_by_well_known(domain)
DID.new(http_did, :http)
else
nil
end
end
# Tries to resolve a handle into DID using the DNS TXT method.
#
# Checks the DNS records for a given domain for an entry `_atproto.#{domain}` whose value is
# a correct DID string.
#
# @param domain [String] a domain handle to look up
# @return [String, nil] resolved DID if found, nil otherwise
def resolve_handle_by_dns(domain)
dns_records = Resolv::DNS.open(resolv_options) do |d|
d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT)
end
if record = dns_records.first
if string = record.strings.first
return parse_did_from_dns(string)
end
end
nil
end
# Tries to resolve a handle into DID using the HTTP .well-known method.
#
# Checks the `/.well-known/atproto-did` endpoint on the given domain to see if it returns
# a text file that contains a correct DID string.
#
# @param domain [String] a domain handle to look up
# @return [String, nil] resolved DID if found, nil otherwise
def resolve_handle_by_well_known(domain)
url = "https://#{domain}/.well-known/atproto-did"
response = get_response(url, @request_options)
if response.is_a?(Net::HTTPSuccess) && (text = response.body)
return parse_did_from_well_known(text)
end
nil
rescue StandardError => e
nil
end
# Resolve a DID to a DID document.
#
# Looks up the DID document with the DID's identity details from an appropriate source, i.e. either
# [plc.directory](https://plc.directory) for did:plc DIDs, or the did:web's domain for did:web DIDs.
#
# @param did [String, DID] DID string or object
# @return [Document] resolved DID document
# @raise [APIError] if an incorrect response is returned
def resolve_did(did)
did = DID.new(did) if did.is_a?(String)
did.type == :plc ? resolve_did_plc(did) : resolve_did_web(did)
end
# Returns the first verified handle assigned to the given DID.
#
# Looks up the domain handles assigned to the DID in the DID document, checks if they are
# verified (i.e. assigned correctly to this DID using DNS TXT or .well-known) and returns
# the first handle that validates correctly, or nil if none matches.
#
# @param subject [String, DID, Document] a DID or its DID document
# @return [String, nil] verified handle domain, if found
def get_verified_handle(subject)
document = subject.is_a?(Document) ? subject : resolve_did(subject)
first_verified_handle(document.did, document.handles)
end
# Returns the first handle from the list that resolves back to the given DID.
#
# @param did [DID, String] DID to verify the handles against
# @param handles [Array<String>] handles to check
# @return [String, nil] a verified handle, if found
def first_verified_handle(did, handles)
handles.detect { |h| resolve_handle(h) == did.to_s }
end
private
def resolv_options
options = Resolv::DNS::Config.default_config_hash.dup
options[:nameserver] = nameserver if nameserver
options
end
def parse_did_from_dns(txt)
txt =~ /\Adid\=(did\:\w+\:.*)\z/ ? $1 : nil
end
def parse_did_from_well_known(text)
text = text.strip
text.lines.length == 1 && text =~ DID::GENERIC_REGEXP ? text : nil
end
def resolve_did_plc(did)
json = get_json("https://plc.directory/#{did}", content_type: /^application\/did\+ld\+json(;.+)?$/)
Document.new(did, json)
end
def resolve_did_web(did)
json = get_json("https://#{did.web_domain}/.well-known/did.json")
Document.new(did, json)
end
end
end