+2
crates/atproto-client/Cargo.toml
+2
crates/atproto-client/Cargo.toml
+2
-2
crates/atproto-client/src/bin/atproto-client-app-password.rs
+2
-2
crates/atproto-client/src/bin/atproto-client-app-password.rs
···
11
11
use atproto_identity::{
12
12
config::{CertificateBundles, DnsNameservers, default_env, optional_env, version},
13
13
plc,
14
-
resolve::{create_resolver, resolve_subject},
14
+
resolve::{HickoryDnsResolver, resolve_subject},
15
15
web,
16
16
};
17
17
use clap::Parser;
···
197
197
client_builder = client_builder.user_agent(user_agent);
198
198
let http_client = client_builder.build()?;
199
199
200
-
let dns_resolver = create_resolver(dns_nameservers.as_ref());
200
+
let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
201
201
202
202
println!("Resolving subject: {}", subject);
203
203
+2
-2
crates/atproto-client/src/bin/atproto-client-auth.rs
+2
-2
crates/atproto-client/src/bin/atproto-client-auth.rs
···
8
8
use atproto_identity::{
9
9
config::{CertificateBundles, DnsNameservers, default_env, optional_env, version},
10
10
plc,
11
-
resolve::{create_resolver, resolve_subject},
11
+
resolve::{HickoryDnsResolver, resolve_subject},
12
12
web,
13
13
};
14
14
use clap::{Parser, Subcommand};
···
98
98
client_builder = client_builder.user_agent(user_agent);
99
99
let http_client = client_builder.build()?;
100
100
101
-
let dns_resolver = create_resolver(dns_nameservers.as_ref());
101
+
let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
102
102
103
103
match args.command {
104
104
Commands::Login {
+2
-2
crates/atproto-client/src/bin/atproto-client-dpop.rs
+2
-2
crates/atproto-client/src/bin/atproto-client-dpop.rs
···
10
10
config::{CertificateBundles, DnsNameservers, default_env, optional_env, version},
11
11
key::identify_key,
12
12
plc,
13
-
resolve::{create_resolver, resolve_subject},
13
+
resolve::{HickoryDnsResolver, resolve_subject},
14
14
web,
15
15
};
16
16
use clap::Parser;
···
210
210
client_builder = client_builder.user_agent(user_agent);
211
211
let http_client = client_builder.build()?;
212
212
213
-
let dns_resolver = create_resolver(dns_nameservers.as_ref());
213
+
let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
214
214
215
215
println!("Resolving subject: {}", subject);
216
216
+4
-3
crates/atproto-identity/Cargo.toml
+4
-3
crates/atproto-identity/Cargo.toml
···
19
19
test = false
20
20
bench = false
21
21
doc = true
22
-
required-features = ["clap"]
22
+
required-features = ["clap", "hickory-dns"]
23
23
24
24
[[bin]]
25
25
name = "atproto-identity-sign"
···
45
45
[dependencies]
46
46
anyhow.workspace = true
47
47
ecdsa.workspace = true
48
-
hickory-resolver.workspace = true
48
+
hickory-resolver = { workspace = true, optional = true }
49
49
k256 = { workspace = true, features = ["jwk"] }
50
50
multibase.workspace = true
51
51
p256 = { workspace = true, features = ["jwk"] }
···
69
69
zeroize = { workspace = true, optional = true }
70
70
71
71
[features]
72
-
default = ["lru", "axum"]
72
+
default = ["lru", "axum", "hickory-dns"]
73
73
lru = ["dep:lru"]
74
74
axum = ["dep:axum", "dep:http"]
75
75
clap = ["dep:clap"]
76
76
zeroize = ["dep:zeroize"]
77
+
hickory-dns = ["dep:hickory-resolver"]
77
78
78
79
[lints]
79
80
workspace = true
+2
-2
crates/atproto-identity/src/bin/atproto-identity-resolve.rs
+2
-2
crates/atproto-identity/src/bin/atproto-identity-resolve.rs
···
6
6
use atproto_identity::{
7
7
config::{CertificateBundles, DnsNameservers, default_env, optional_env, version},
8
8
plc::query as plc_query,
9
-
resolve::{InputType, create_resolver, parse_input, resolve_subject},
9
+
resolve::{InputType, HickoryDnsResolver, parse_input, resolve_subject},
10
10
web::query as web_query,
11
11
};
12
12
use clap::Parser;
···
70
70
client_builder = client_builder.user_agent(user_agent);
71
71
let http_client = client_builder.build()?;
72
72
73
-
let dns_resolver = create_resolver(dns_nameservers.as_ref());
73
+
let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
74
74
75
75
for subject in args.subjects {
76
76
let resolved_did = resolve_subject(&http_client, &dns_resolver, &subject).await;
+6
crates/atproto-identity/src/errors.rs
+6
crates/atproto-identity/src/errors.rs
···
95
95
ConflictingDIDsFound,
96
96
97
97
/// Occurs when DNS TXT record lookup fails
98
+
#[cfg(feature = "hickory-dns")]
98
99
#[error("error-atproto-identity-resolve-4 DNS resolution failed: {error:?}")]
99
100
DNSResolutionFailed {
100
101
/// The underlying DNS resolution error
101
102
error: hickory_resolver::ResolveError,
102
103
},
104
+
105
+
/// Occurs when DNS TXT record lookup fails (generic version for when hickory-dns is not enabled)
106
+
#[cfg(not(feature = "hickory-dns"))]
107
+
#[error("error-atproto-identity-resolve-4 DNS resolution failed")]
108
+
DNSResolutionFailed,
103
109
104
110
/// Occurs when HTTP request to .well-known/atproto-did endpoint fails
105
111
#[error("error-atproto-identity-resolve-5 HTTP resolution failed: {error:?}")]
+71
-29
crates/atproto-identity/src/resolve.rs
+71
-29
crates/atproto-identity/src/resolve.rs
···
19
19
//! 4. For DIDs: return the identifier directly
20
20
21
21
use anyhow::Result;
22
+
#[cfg(feature = "hickory-dns")]
22
23
use hickory_resolver::{
23
24
Resolver, TokioResolver,
24
25
config::{NameServerConfigGroup, ResolverConfig},
···
37
38
use crate::validation::{is_valid_did_method_plc, is_valid_handle};
38
39
use crate::web::query as web_query;
39
40
41
+
/// Trait for DNS resolution operations.
42
+
/// Provides async DNS TXT record lookups for handle resolution.
43
+
#[async_trait::async_trait]
44
+
pub trait DnsResolver: Send + Sync {
45
+
/// Resolves TXT records for a given domain name.
46
+
/// Returns a vector of strings representing the TXT record values.
47
+
async fn resolve_txt(&self, domain: &str) -> Result<Vec<String>, ResolveError>;
48
+
}
49
+
50
+
/// Hickory DNS implementation of the DnsResolver trait.
51
+
/// Wraps hickory_resolver::TokioResolver for TXT record resolution.
52
+
#[cfg(feature = "hickory-dns")]
53
+
#[derive(Clone)]
54
+
pub struct HickoryDnsResolver {
55
+
resolver: TokioResolver,
56
+
}
57
+
58
+
#[cfg(feature = "hickory-dns")]
59
+
impl HickoryDnsResolver {
60
+
/// Creates a new HickoryDnsResolver with the given TokioResolver.
61
+
pub fn new(resolver: TokioResolver) -> Self {
62
+
Self { resolver }
63
+
}
64
+
65
+
/// Creates a DNS resolver with custom or system nameservers.
66
+
/// Uses custom nameservers if provided, otherwise system defaults.
67
+
pub fn create_resolver(nameservers: &[std::net::IpAddr]) -> Self {
68
+
// Initialize the DNS resolver with custom nameservers if configured
69
+
let tokio_resolver = if !nameservers.is_empty() {
70
+
tracing::debug!("Using custom DNS nameservers: {:?}", nameservers);
71
+
let nameserver_group = NameServerConfigGroup::from_ips_clear(nameservers, 53, true);
72
+
let resolver_config = ResolverConfig::from_parts(None, vec![], nameserver_group);
73
+
Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default()).build()
74
+
} else {
75
+
tracing::debug!("Using system default DNS nameservers");
76
+
Resolver::builder_tokio().unwrap().build()
77
+
};
78
+
Self::new(tokio_resolver)
79
+
}
80
+
}
81
+
82
+
#[cfg(feature = "hickory-dns")]
83
+
#[async_trait::async_trait]
84
+
impl DnsResolver for HickoryDnsResolver {
85
+
async fn resolve_txt(&self, domain: &str) -> Result<Vec<String>, ResolveError> {
86
+
let lookup = self.resolver
87
+
.txt_lookup(domain)
88
+
.instrument(tracing::info_span!("txt_lookup"))
89
+
.await
90
+
.map_err(|error| ResolveError::DNSResolutionFailed { error })?;
91
+
92
+
Ok(lookup
93
+
.iter()
94
+
.map(|record| record.to_string())
95
+
.collect())
96
+
}
97
+
}
98
+
40
99
/// Type of input identifier for resolution.
41
100
/// Distinguishes between handles and different DID methods.
42
101
pub enum InputType {
···
51
110
/// Resolves a handle to DID using DNS TXT records.
52
111
/// Looks up _atproto.{handle} TXT record for DID value.
53
112
#[instrument(skip(dns_resolver), err)]
54
-
pub async fn resolve_handle_dns(
55
-
dns_resolver: &TokioResolver,
113
+
pub async fn resolve_handle_dns<R: DnsResolver + ?Sized>(
114
+
dns_resolver: &R,
56
115
lookup_dns: &str,
57
116
) -> Result<String, ResolveError> {
58
-
let lookup = dns_resolver
59
-
.txt_lookup(&format!("_atproto.{}", lookup_dns))
60
-
.instrument(tracing::info_span!("txt_lookup"))
61
-
.await
62
-
.map_err(|error| ResolveError::DNSResolutionFailed { error })?;
117
+
let txt_records = dns_resolver
118
+
.resolve_txt(&format!("_atproto.{}", lookup_dns))
119
+
.await?;
63
120
64
-
let dids = lookup
121
+
let dids = txt_records
65
122
.iter()
66
123
.filter_map(|record| {
67
124
record
68
-
.to_string()
69
125
.strip_prefix("did=")
70
126
.map(|did| did.to_string())
71
127
})
···
136
192
/// Resolves a handle to DID using both DNS and HTTP methods.
137
193
/// Returns DID if both methods agree, or error if conflicting.
138
194
#[instrument(skip(http_client, dns_resolver), err)]
139
-
pub async fn resolve_handle(
195
+
pub async fn resolve_handle<R: DnsResolver + ?Sized>(
140
196
http_client: &reqwest::Client,
141
-
dns_resolver: &TokioResolver,
197
+
dns_resolver: &R,
142
198
handle: &str,
143
199
) -> Result<String, ResolveError> {
144
200
let trimmed = {
···
174
230
/// Resolves any subject (handle or DID) to a canonical DID.
175
231
/// Handles all supported identifier formats automatically.
176
232
#[instrument(skip(http_client, dns_resolver), err)]
177
-
pub async fn resolve_subject(
233
+
pub async fn resolve_subject<R: DnsResolver + ?Sized>(
178
234
http_client: &reqwest::Client,
179
-
dns_resolver: &TokioResolver,
235
+
dns_resolver: &R,
180
236
subject: &str,
181
237
) -> Result<String, ResolveError> {
182
238
match parse_input(subject)? {
···
185
241
}
186
242
}
187
243
188
-
/// Creates a DNS resolver with custom or system nameservers.
189
-
/// Uses custom nameservers if provided, otherwise system defaults.
190
-
pub fn create_resolver(nameservers: &[std::net::IpAddr]) -> TokioResolver {
191
-
// Initialize the DNS resolver with custom nameservers if configured
192
-
if !nameservers.is_empty() {
193
-
tracing::debug!("Using custom DNS nameservers: {:?}", nameservers);
194
-
let nameserver_group = NameServerConfigGroup::from_ips_clear(nameservers, 53, true);
195
-
let resolver_config = ResolverConfig::from_parts(None, vec![], nameserver_group);
196
-
Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default()).build()
197
-
} else {
198
-
tracing::debug!("Using system default DNS nameservers");
199
-
Resolver::builder_tokio().unwrap().build()
200
-
}
201
-
}
202
244
203
245
/// Core identity resolution components for AT Protocol subjects.
204
246
///
···
206
248
/// handles and DIDs to their corresponding DID documents.
207
249
pub struct InnerIdentityResolver {
208
250
/// DNS resolver for handle-to-DID resolution via TXT records.
209
-
pub dns_resolver: TokioResolver,
251
+
pub dns_resolver: Box<dyn DnsResolver>,
210
252
/// HTTP client for DID document retrieval and well-known endpoint queries.
211
253
pub http_client: Client,
212
254
/// Hostname of the PLC directory server for `did:plc` resolution.
···
235
277
/// Takes a handle or DID, resolves it to a canonical DID, then retrieves
236
278
/// the corresponding DID document from the appropriate source (PLC directory or web).
237
279
pub async fn resolve(&self, subject: &str) -> Result<Document> {
238
-
let resolved_did = resolve_subject(&self.http_client, &self.dns_resolver, subject).await?;
280
+
let resolved_did = resolve_subject(&self.http_client, &*self.dns_resolver, subject).await?;
239
281
240
282
match parse_input(&resolved_did) {
241
283
Ok(InputType::Plc(did)) => plc_query(&self.http_client, &self.plc_hostname, &did)
+2
crates/atproto-jetstream/Cargo.toml
+2
crates/atproto-jetstream/Cargo.toml
+2
crates/atproto-oauth-aip/Cargo.toml
+2
crates/atproto-oauth-aip/Cargo.toml
+2
crates/atproto-oauth-axum/Cargo.toml
+2
crates/atproto-oauth-axum/Cargo.toml
···
50
50
zeroize = { workspace = true, optional = true }
51
51
52
52
[features]
53
+
default = ["hickory-dns"]
53
54
clap = ["dep:clap", "dep:rpassword", "dep:secrecy"]
54
55
zeroize = ["dep:zeroize", "atproto-identity/zeroize", "atproto-oauth/zeroize"]
56
+
hickory-dns = ["atproto-identity/hickory-dns", "atproto-oauth/hickory-dns"]
55
57
56
58
[lints]
57
59
workspace = true
+18
-7
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
+18
-7
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
···
32
32
config::{CertificateBundles, DnsNameservers, default_env, optional_env, require_env, version},
33
33
key::{KeyData, KeyProvider, KeyType, generate_key, identify_key, to_public},
34
34
plc,
35
-
resolve::{create_resolver, resolve_subject},
35
+
resolve::{resolve_subject},
36
36
storage::DidDocumentStorage,
37
37
storage_lru::LruDidDocumentStorage,
38
38
web,
39
39
};
40
+
41
+
#[cfg(feature = "hickory-dns")]
42
+
use atproto_identity::resolve::HickoryDnsResolver;
40
43
use atproto_oauth::{
41
44
pkce,
42
45
resources::pds_resources,
···
50
53
use axum::{Router, extract::FromRef, routing::get};
51
54
use chrono::{Duration, Utc};
52
55
use clap::{Parser, Subcommand};
53
-
use hickory_resolver::TokioResolver;
54
56
use rand::distributions::{Alphanumeric, DistString};
55
57
use rpassword::read_password;
56
58
use secrecy::{ExposeSecret, SecretString};
···
91
93
92
94
pub struct InnerWebContext {
93
95
pub http_client: reqwest::Client,
94
-
pub dns_resolver: TokioResolver,
96
+
#[cfg(feature = "hickory-dns")]
97
+
pub dns_resolver: HickoryDnsResolver,
98
+
#[cfg(not(feature = "hickory-dns"))]
99
+
pub dns_resolver: Box<dyn atproto_identity::resolve::DnsResolver>,
95
100
pub oauth_client_config: OAuthClientConfig,
96
101
pub oauth_storage: Arc<dyn OAuthRequestStorage + Send + Sync>,
97
102
pub document_storage: Arc<dyn DidDocumentStorage + Send + Sync>,
···
243
248
client_builder = client_builder.user_agent(user_agent);
244
249
let http_client = client_builder.build()?;
245
250
246
-
let dns_resolver = create_resolver(dns_nameservers.as_ref());
251
+
let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
247
252
248
253
let external_base = require_env("EXTERNAL_BASE")?;
249
254
let port = default_env("PORT", "8080");
···
356
361
#[allow(clippy::too_many_arguments)]
357
362
async fn handle_login_command(
358
363
http_client: &reqwest::Client,
359
-
dns_resolver: &TokioResolver,
364
+
#[cfg(feature = "hickory-dns")]
365
+
dns_resolver: &HickoryDnsResolver,
366
+
#[cfg(not(feature = "hickory-dns"))]
367
+
dns_resolver: &dyn atproto_identity::resolve::DnsResolver,
360
368
private_signing_key: &str,
361
369
subject: &str,
362
370
external_base: &str,
···
442
450
http_client,
443
451
&oauth_client,
444
452
&dpop_key,
445
-
subject,
453
+
Some(subject),
446
454
&authorization_server,
447
455
&oauth_request_state,
448
456
)
···
494
502
#[allow(clippy::too_many_arguments)]
495
503
async fn handle_refresh_command(
496
504
http_client: &reqwest::Client,
497
-
dns_resolver: &TokioResolver,
505
+
#[cfg(feature = "hickory-dns")]
506
+
dns_resolver: &HickoryDnsResolver,
507
+
#[cfg(not(feature = "hickory-dns"))]
508
+
dns_resolver: &dyn atproto_identity::resolve::DnsResolver,
498
509
private_signing_key: &str,
499
510
subject: &str,
500
511
external_base: &str,
+2
-1
crates/atproto-oauth/Cargo.toml
+2
-1
crates/atproto-oauth/Cargo.toml
···
47
47
zeroize = { workspace = true, optional = true }
48
48
49
49
[features]
50
-
default = ["lru", "axum"]
50
+
default = ["lru", "axum", "hickory-dns"]
51
51
lru = ["dep:lru"]
52
52
axum = ["dep:axum", "dep:http"]
53
53
zeroize = ["dep:zeroize", "atproto-identity/zeroize"]
54
+
hickory-dns = ["atproto-identity/hickory-dns"]
54
55
55
56
[lints]
56
57
workspace = true
+2
crates/atproto-record/Cargo.toml
+2
crates/atproto-record/Cargo.toml
+2
crates/atproto-xrpcs-helloworld/Cargo.toml
+2
crates/atproto-xrpcs-helloworld/Cargo.toml
+3
-3
crates/atproto-xrpcs-helloworld/src/main.rs
+3
-3
crates/atproto-xrpcs-helloworld/src/main.rs
···
6
6
axum::state::DidDocumentStorageExtractor,
7
7
config::{CertificateBundles, DnsNameservers, default_env, optional_env, require_env, version},
8
8
key::{KeyData, KeyProvider, identify_key, to_public},
9
-
resolve::{IdentityResolver, InnerIdentityResolver, create_resolver},
9
+
resolve::{IdentityResolver, InnerIdentityResolver, HickoryDnsResolver},
10
10
storage::DidDocumentStorage,
11
11
storage_lru::LruDidDocumentStorage,
12
12
};
···
197
197
client_builder = client_builder.user_agent(user_agent);
198
198
let http_client = client_builder.build()?;
199
199
200
-
let dns_resolver = create_resolver(dns_nameservers.as_ref());
200
+
let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
201
201
202
202
let external_base = require_env("EXTERNAL_BASE")?;
203
203
let port = default_env("PORT", "8080");
···
233
233
let service_did = ServiceDID(service_did);
234
234
235
235
let identity_resolver = IdentityResolver(Arc::new(InnerIdentityResolver {
236
-
dns_resolver,
236
+
dns_resolver: Box::new(dns_resolver),
237
237
http_client: http_client.clone(),
238
238
plc_hostname,
239
239
}));