--- /dev/null
+//!
+
+#[deny(missing_docs)]
+
+#[cfg(feature = "domain_resolver")]
+use std::net::SocketAddr;
+
+use std::sync::Mutex; // XXX: wont work without, should support no-std
+
+extern crate alloc;
+extern crate core;
+
+use core::str::FromStr;
+
+#[cfg(feature = "domain_resolver")]
+use alloc::sync::Arc;
+#[cfg(any(feature = "name_resolver", feature = "domain_resolver"))]
+use core::sync::atomic::{AtomicUsize, Ordering};
+
+use dnssec_prover::rr::Name;
+
+#[cfg(feature = "domain_resolver")]
+use dnssec_prover::query::build_txt_proof;
+
+use lightning::onion_message::packet::OnionMessageContents;
+use lightning::util::ser::{Readable, Writeable, Writer};
+use lightning::ln::msgs::DecodeError;
+use lightning::offers::offer::Offer;
+
+#[cfg(any(feature = "domain_resolver", feature = "name_resolver"))]
+use lightning::onion_message::messenger::{CustomOnionMessageHandler, PendingOnionMessage};
+
+#[cfg(feature = "name_resolver")]
+use lightning::blinded_path::BlindedPath;
+#[cfg(feature = "name_resolver")]
+use lightning::onion_message::messenger::Destination;
+
+#[derive(Debug)]
+pub enum ResolverMessages {
+ DNSSECQuery(DNSSECQuery),
+ DNSSECProof(DNSSECProof),
+ OfferRequest(OfferRequest),
+ OfferResponse(OfferResponse),
+}
+
+#[derive(Debug)]
+/// A Query for a proof of all TXT records at a given name.
+pub struct DNSSECQuery(pub Name);
+const DNSSEC_QUERY_TYPE: u64 = 65536;
+
+#[derive(Debug)]
+pub struct DNSSECProof {
+ pub name: Name,
+ pub proof: Vec<u8>,
+}
+const DNSSEC_PROOF_TYPE: u64 = 65538;
+
+#[derive(Debug)]
+pub struct OfferRequest {
+ pub user: String,
+ pub domain: String,
+}
+const OFFER_REQUEST_TYPE: u64 = 44; // XXX
+
+#[derive(Debug)]
+pub struct OfferResponse {
+ pub user: String,
+ pub domain: String,
+ pub offer: Offer,
+}
+const OFFER_RESPONSE_TYPE: u64 = 45; // XXX
+
+impl Writeable for ResolverMessages {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), lightning::io::Error> {
+ match self {
+ Self::DNSSECQuery(DNSSECQuery(q)) => {
+ (q.as_str().len() as u8).write(w)?;
+ w.write_all(&q.as_str().as_bytes())
+ },
+ Self::DNSSECProof(DNSSECProof { name, proof }) => {
+ (name.as_str().len() as u8).write(w)?;
+ w.write_all(&name.as_str().as_bytes())?;
+ proof.write(w)
+ },
+ Self::OfferRequest(OfferRequest { user, domain }) => {
+ (user.len() as u8).write(w)?;
+ w.write_all(&user.as_bytes())?;
+ (domain.len() as u8).write(w)?;
+ w.write_all(&domain.as_bytes())
+ },
+ Self::OfferResponse(OfferResponse { user, domain, offer }) => {
+ (user.len() as u8).write(w)?;
+ w.write_all(&user.as_bytes())?;
+ (domain.len() as u8).write(w)?;
+ w.write_all(&domain.as_bytes())?;
+ offer.to_string().write(w)
+ },
+ }
+ }
+}
+
+fn read_byte_len_ascii_string<R: lightning::io::Read>(buffer: &mut R)
+-> Result<String, DecodeError> {
+ let len: u8 = Readable::read(buffer)?;
+ let mut bytes = [0; 255];
+ buffer.read_exact(&mut bytes[..len as usize])?;
+ if bytes[..len as usize].iter().any(|b| *b < 0x20 || *b > 0x7e) {
+ // If the bytes are not entirely in the printable ASCII range, fail
+ return Err(DecodeError::InvalidValue);
+ }
+ let s = String::from_utf8(bytes[..len as usize].to_vec())
+ .map_err(|_| DecodeError::InvalidValue)?;
+ Ok(s)
+}
+
+impl ResolverMessages {
+ pub fn read_message<R: lightning::io::Read>(message_type: u64, buffer: &mut R)
+ -> Result<Option<ResolverMessages>, DecodeError> {
+ match message_type {
+ DNSSEC_QUERY_TYPE => {
+ let s = read_byte_len_ascii_string(buffer)?;
+ let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?;
+ Ok(Some(ResolverMessages::DNSSECQuery(DNSSECQuery(name))))
+ },
+ DNSSEC_PROOF_TYPE => {
+ let s = read_byte_len_ascii_string(buffer)?;
+ let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?;
+ let proof = Readable::read(buffer)?;
+ Ok(Some(ResolverMessages::DNSSECProof(DNSSECProof { name, proof })))
+ },
+ OFFER_REQUEST_TYPE => {
+ let user = read_byte_len_ascii_string(buffer)?;
+ let domain = read_byte_len_ascii_string(buffer)?;
+ Ok(Some(ResolverMessages::OfferRequest(OfferRequest { user, domain })))
+ },
+ OFFER_RESPONSE_TYPE => {
+ let user = read_byte_len_ascii_string(buffer)?;
+ let domain = read_byte_len_ascii_string(buffer)?;
+ let offer_string: String = Readable::read(buffer)?;
+ if !offer_string.is_ascii() { return Err(DecodeError::InvalidValue); }
+ if let Ok(offer) = Offer::from_str(&offer_string) {
+ Ok(Some(ResolverMessages::OfferResponse(OfferResponse { user, domain, offer })))
+ } else {
+ Err(DecodeError::InvalidValue)
+ }
+ },
+ _ => Ok(None),
+ }
+ }
+}
+
+impl OnionMessageContents for ResolverMessages {
+ fn tlv_type(&self) -> u64 {
+ match self {
+ ResolverMessages::DNSSECQuery(_) => DNSSEC_QUERY_TYPE,
+ ResolverMessages::DNSSECProof(_) => DNSSEC_PROOF_TYPE,
+ ResolverMessages::OfferRequest(_) => OFFER_REQUEST_TYPE,
+ ResolverMessages::OfferResponse(_) => OFFER_RESPONSE_TYPE,
+ }
+ }
+}
+
+#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
+const WE_REQUIRE_32_OR_64_BIT_USIZE: u8 = 424242;
+
+#[cfg(feature = "domain_resolver")]
+pub struct OMDomainResolver {
+ state: Arc<domain_resolver::OMResolverState>,
+}
+
+#[cfg(feature = "domain_resolver")]
+mod domain_resolver {
+ use super::*;
+
+ const MAX_PENDING_RESPONSES: usize = 1024;
+ pub(super) struct OMResolverState {
+ resolver: SocketAddr,
+ pending_msgs: Mutex<Vec<PendingOnionMessage<ResolverMessages>>>,
+ pending_query_count: AtomicUsize,
+ }
+
+ impl OMDomainResolver {
+ pub fn new(resolver: SocketAddr) -> Self {
+ Self { state: Arc::new(OMResolverState {
+ resolver,
+ pending_msgs: Mutex::new(Vec::new()),
+ pending_query_count: AtomicUsize::new(0),
+ }) }
+ }
+ }
+
+ impl CustomOnionMessageHandler for OMDomainResolver {
+ type CustomMessage = ResolverMessages;
+
+ fn handle_custom_message(&self, msg: ResolverMessages) -> Option<Self::CustomMessage> {
+ match msg {
+ ResolverMessages::DNSSECQuery(q) => {
+ if self.state.pending_query_count.fetch_add(1, Ordering::Relaxed) > MAX_PENDING_RESPONSES {
+ self.state.pending_query_count.fetch_sub(1, Ordering::Relaxed);
+ return None;
+ }
+ let us = Arc::clone(&self.state);
+ // TODO: Make this async after https://github.com/lightningdevkit/rust-lightning/issues/2882
+ //tokio::spawn(async move {
+ //if let Ok(proof) = build_txt_proof_async(us.resolver, &q.0).await {
+ if let Ok((proof, _ttl)) = build_txt_proof(us.resolver, &q.0) {
+ let contents = ResolverMessages::DNSSECProof(DNSSECProof {
+ name: q.0, proof,
+ });
+ us.pending_query_count.fetch_sub(1, Ordering::Relaxed);
+ return Some(contents);
+ /*let response = PendingOnionMessage {
+ contents,
+ destination: Destination::BlindedPath(reply_path),
+ reply_path: None,
+ };
+ us.pending_msgs.lock().unwrap().push(response);*/
+ } else { None }
+ //});
+ },
+ _ => None,
+ }
+ }
+ fn read_custom_message<R: std::io::Read>(&self, message_type: u64, buffer: &mut R)
+ -> Result<Option<ResolverMessages>, DecodeError> {
+ ResolverMessages::read_message(message_type, buffer)
+ }
+ fn release_pending_custom_messages(&self) -> Vec<PendingOnionMessage<ResolverMessages>> {
+ let mut res = Vec::new();
+ std::mem::swap(&mut res, &mut self.state.pending_msgs.lock().unwrap());
+ res
+ }
+ }
+}
+
+#[cfg(feature = "name_resolver")]
+pub struct OMNameResolver {
+ resolver: Destination,
+ reply_path: BlindedPath,
+ pending_msgs: Mutex<Vec<PendingOnionMessage<ResolverMessages>>>,
+ pending_resolves: Mutex<Vec<(u32, String, Box<dyn FnMut(Option<Offer>)>)>>,
+ latest_block_time: AtomicUsize,
+ latest_block_height: AtomicUsize,
+}
+
+#[cfg(feature = "name_resolver")]
+mod name_resolver {
+ use super::*;
+
+ use dnssec_prover::ser::parse_rr_stream;
+ use dnssec_prover::rr::RR;
+ use dnssec_prover::validation::verify_rr_stream;
+
+ use hex_conservative::FromHex;
+
+ impl OMNameResolver {
+ pub fn new(resolver: Destination, reply_path: BlindedPath, latest_block_time: u32, latest_block_height: u32) -> Self {
+ Self {
+ resolver,
+ reply_path,
+ pending_msgs: Mutex::new(Vec::new()),
+ pending_resolves: Mutex::new(Vec::new()),
+ latest_block_time: AtomicUsize::new(latest_block_time as usize),
+ latest_block_height: AtomicUsize::new(latest_block_height as usize),
+ }
+ }
+
+ pub fn set_block_time(&self, height: u32, time: u32) {
+ self.latest_block_time.store(time as usize, Ordering::Release);
+ self.latest_block_height.store(height as usize, Ordering::Release);
+ let mut resolves = self.pending_resolves.lock().unwrap();
+ resolves.retain_mut(|(res_height, _, callback)| {
+ if *res_height < height - 1 {
+ callback(None);
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ pub fn resolve_name(&self, name: String, resolution_callback: Box<dyn FnMut(Option<Offer>)>) -> Result<(), ()> {
+ if let Some((user, domain)) = name.split_once("@") {
+ let name_query =
+ Name::try_from(format!("{}.user._bitcoin-payment.{}.", user, domain))
+ .map(|q| ResolverMessages::DNSSECQuery(DNSSECQuery(q)));
+ if let Ok(q) = name_query {
+ let mut pending_msgs = self.pending_msgs.lock().unwrap();
+ let destination = self.resolver.clone();
+ let reply_path = Some(self.reply_path.clone());
+ pending_msgs.push(PendingOnionMessage {
+ contents: q, destination, reply_path,
+ });
+ } else {
+ return Err(());
+ }
+ let height = self.latest_block_height.load(Ordering::Acquire);
+ let mut pending_resolves = self.pending_resolves.lock().unwrap();
+ pending_resolves.push((height as u32, name, resolution_callback));
+ Ok(())
+ } else {
+ Err(())
+ }
+ }
+ }
+
+ impl CustomOnionMessageHandler for OMNameResolver {
+ type CustomMessage = ResolverMessages;
+
+ fn handle_custom_message(&self, msg: ResolverMessages) -> Option<Self::CustomMessage> {
+ match msg {
+ ResolverMessages::DNSSECQuery(_) => {},
+ ResolverMessages::OfferRequest(_) => {},
+ ResolverMessages::DNSSECProof(DNSSECProof { name: answer_name, proof }) => {
+ let parsed_rrs = parse_rr_stream(&proof);
+ let validated_rrs =
+ parsed_rrs.as_ref().and_then(|rrs| verify_rr_stream(rrs).map_err(|_| &()));
+ if let Ok(validated_rrs) = validated_rrs {
+ let block_time = self.latest_block_time.load(Ordering::Acquire) as u64;
+ if validated_rrs.valid_from > block_time + 60 * 2 {
+ return None;
+ }
+ if validated_rrs.expires < block_time - 60 * 2 {
+ return None;
+ }
+ let qname = answer_name.as_str();
+ if qname.len() <= "._bitcoin_payment.".len() { return None; }
+ let resolved_rrs = validated_rrs.resolve_name(&answer_name);
+ if resolved_rrs.is_empty() { return None; }
+
+ let mut pending_resolves = self.pending_resolves.lock().unwrap();
+ pending_resolves.retain_mut(|(_height, query, resolution_callback)| {
+ debug_assert_eq!(qname.chars().last().unwrap_or('X'), '.');
+ let (user, domain) = if let Some(r) = query.split_once("@") {
+ r
+ } else {
+ debug_assert!(false, "This should be checked before insertion");
+ return false;
+ };
+
+ let expected_len = user.len() + ".user._bitcoin-payment.".len() + domain.len() + 1;
+ if qname.len() == expected_len &&
+ qname.starts_with(user) &&
+ qname[user.len()..].starts_with(".user._bitcoin-payment.") &&
+ qname[..qname.len() - 1].ends_with(domain)
+ {
+ const URI_PREFIX: &str = "bitcoin:";
+ let mut candidate_records = resolved_rrs.iter()
+ .filter_map(|rr| if let RR::Txt(txt) = rr { Some(&txt.data) } else { None })
+ .filter_map(|data| if let Ok(s) = core::str::from_utf8(data) { Some(s) } else { None })
+ .filter(|data_string| data_string.len() > URI_PREFIX.len())
+ .filter(|data_string| data_string[..URI_PREFIX.len()].eq_ignore_ascii_case(URI_PREFIX));
+ let txt_record =
+ match (candidate_records.next(), candidate_records.next()) {
+ (Some(txt), None) => txt,
+ (_, _) => {
+ resolution_callback(None);
+ return false;
+ },
+ };
+ let (_onchain, params) = if let Some(split) = txt_record.split_once("?") {
+ split
+ } else {
+ resolution_callback(None);
+ return false;
+ };
+ for param in params.split("&") {
+ let (k, v) = if let Some(split) = param.split_once("=") {
+ split
+ } else {
+ continue;
+ };
+ if k.eq_ignore_ascii_case("b12") {
+ if let Ok(offer) = Offer::from_str(v) {
+ resolution_callback(Some(offer));
+ } else {
+ resolution_callback(None);
+ }
+ return false;
+ } else if k.eq_ignore_ascii_case("omlookup") {
+ let data_hex = Vec::from_hex(v).map_err(|_| ());
+ let request_path = data_hex
+ .and_then(|data| BlindedPath::read(&mut &data[..])
+ .map_err(|_| ()));
+ let request_path = if let Ok(path) = request_path {
+ path
+ } else {
+ resolution_callback(None);
+ return false;
+ };
+ let contents = ResolverMessages::OfferRequest(OfferRequest {
+ user: user.to_owned(),
+ domain: domain.to_owned(),
+ });
+ let response = PendingOnionMessage {
+ contents,
+ destination: Destination::BlindedPath(request_path),
+ reply_path: Some(self.reply_path.clone()),
+ };
+ self.pending_msgs.lock().unwrap().push(response);
+ return true;
+ }
+ }
+ resolution_callback(None);
+ false
+ } else {
+ true
+ }
+ });
+ }
+ },
+ ResolverMessages::OfferResponse(OfferResponse { user, domain, offer }) => {
+ let mut pending_resolves = self.pending_resolves.lock().unwrap();
+ pending_resolves.retain_mut(|(_height, query, resolution_callback)| {
+ let (query_user, query_domain) = if let Some(r) = query.split_once("@") {
+ r
+ } else {
+ debug_assert!(false, "This should be checked before insertion");
+ return false;
+ };
+ if query_user != user || query_domain != domain { return true; }
+ resolution_callback(Some(offer.clone()));
+ false
+ });
+ },
+ }
+ None
+ }
+ fn read_custom_message<R: lightning::io::Read>(&self, message_type: u64, buffer: &mut R)
+ -> Result<Option<ResolverMessages>, DecodeError> {
+ ResolverMessages::read_message(message_type, buffer)
+ }
+ fn release_pending_custom_messages(&self) -> Vec<PendingOnionMessage<ResolverMessages>> {
+ let mut res = Vec::new();
+ std::mem::swap(&mut res, &mut self.pending_msgs.lock().unwrap());
+ res
+ }
+ }
+}
+
+#[cfg(all(test, feature = "name_resolver", feature = "domain_resolver"))]
+mod test {
+ use super::*;
+
+ use secp256k1::{Secp256k1, PublicKey};
+
+ use lightning::sign::KeysManager;
+
+ use std::time::SystemTime;
+
+ #[test]
+ fn basic() {
+ let secp_ctx = Secp256k1::new();
+ let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
+
+ let resolver = OMDomainResolver::new("8.8.8.8:53".parse().unwrap());
+
+ let resolver_keys = KeysManager::new(&[0; 32], 42, 43);
+ let resolver_dest = Destination::Node(dummy_pk);
+ let payer_path =
+ BlindedPath::one_hop_for_message(dummy_pk, &resolver_keys, &secp_ctx).unwrap();
+ let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
+ let payer = OMNameResolver::new(resolver_dest.clone(), payer_path.clone(), now as u32, 1);
+
+ let resolved_offer = Arc::new(Mutex::new(None));
+ let offer_ref = Arc::clone(&resolved_offer);
+ let resolve_offer = Box::new(move |offer| *resolved_offer.lock().unwrap() = offer);
+ payer.resolve_name("matt@mattcorallo.com".to_owned(), resolve_offer).unwrap();
+ let mut msgs = payer.release_pending_custom_messages();
+ assert_eq!(msgs.len(), 1);
+
+ let msg = msgs.pop().unwrap();
+ assert_eq!(msg.destination, resolver_dest);
+ assert_eq!(msg.reply_path, Some(payer_path));
+ let response = resolver.handle_custom_message(msg.contents).unwrap();
+
+ assert!(payer.handle_custom_message(response).is_none());
+ offer_ref.lock().unwrap().take().unwrap();
+ }
+}