Added bytestream ALPN
This commit is contained in:
@@ -52,6 +52,9 @@ pub struct MusicDhtConfig {
|
||||
/// network find each other knowing nothing but the network id. `None`
|
||||
/// disables it; peers are then connected via tickets only.
|
||||
pub rendezvous: Option<RendezvousConfig>,
|
||||
/// Auxiliary ALPN protocols on which this peer accepts raw byte streams
|
||||
/// (see [`crate::MusicDhtService::stream_acceptor`]).
|
||||
pub stream_protocols: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MusicDhtConfig {
|
||||
@@ -76,6 +79,7 @@ pub struct MusicDhtConfigBuilder {
|
||||
transport_timeout: Option<Duration>,
|
||||
dial_timeout: Option<Duration>,
|
||||
rendezvous: Option<RendezvousConfig>,
|
||||
stream_protocols: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MusicDhtConfigBuilder {
|
||||
@@ -133,6 +137,15 @@ impl MusicDhtConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Declares an auxiliary ALPN protocol on which this peer accepts raw
|
||||
/// byte streams (see [`crate::MusicDhtService::stream_acceptor`]). May
|
||||
/// be called multiple times, once per protocol. The ALPNs are validated
|
||||
/// by the underlying network engine on service start.
|
||||
pub fn stream_protocol(mut self, alpn: impl Into<Vec<u8>>) -> Self {
|
||||
self.stream_protocols.push(alpn.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Validates and builds the configuration.
|
||||
pub fn build(self) -> Result<MusicDhtConfig> {
|
||||
let data_dir = self
|
||||
@@ -157,6 +170,7 @@ impl MusicDhtConfigBuilder {
|
||||
transport_timeout: self.transport_timeout.unwrap_or(DEFAULT_TRANSPORT_TIMEOUT),
|
||||
dial_timeout: self.dial_timeout.unwrap_or(DEFAULT_DIAL_TIMEOUT),
|
||||
rendezvous: self.rendezvous,
|
||||
stream_protocols: self.stream_protocols,
|
||||
};
|
||||
for (name, value) in [
|
||||
("republish_interval", config.republish_interval),
|
||||
|
||||
@@ -105,4 +105,7 @@ pub use service::{
|
||||
};
|
||||
|
||||
// Re-exported types from the transport layer that appear in this API.
|
||||
pub use federation_net::{EndpointId, NetworkId, PeerTicket, RendezvousConfig};
|
||||
pub use federation_net::{
|
||||
ByteStream, EndpointAddr, EndpointId, NetworkId, PeerTicket, RecvStream, RendezvousConfig,
|
||||
SendStream, StreamAcceptor,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use federation_net::{EndpointId, NetworkConfig, NetworkEngine, PeerTicket, SchemaId};
|
||||
use federation_net::{
|
||||
ByteStream, EndpointAddr, EndpointId, NetworkConfig, NetworkEngine, PeerTicket, SchemaId,
|
||||
StreamAcceptor,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::info;
|
||||
@@ -164,6 +167,9 @@ impl MusicDhtService {
|
||||
if let Some(rendezvous) = config.rendezvous.clone() {
|
||||
engine_builder = engine_builder.rendezvous(rendezvous);
|
||||
}
|
||||
for alpn in &config.stream_protocols {
|
||||
engine_builder = engine_builder.stream_protocol(alpn.clone());
|
||||
}
|
||||
let engine_config = engine_builder
|
||||
.build()
|
||||
.map_err(|err| MusicDhtError::Network(err.to_string()))?;
|
||||
@@ -241,6 +247,39 @@ impl MusicDhtService {
|
||||
self.node.engine.is_connected(peer)
|
||||
}
|
||||
|
||||
/// Takes the acceptor of incoming raw byte streams for the stream
|
||||
/// protocol `alpn` declared in the configuration.
|
||||
///
|
||||
/// The acceptor for a protocol can be taken exactly once; see
|
||||
/// [`federation_net::NetworkEngine::stream_acceptor`].
|
||||
pub fn stream_acceptor(&self, alpn: &[u8]) -> Result<StreamAcceptor> {
|
||||
self.node.ensure_running()?;
|
||||
self.node.engine.stream_acceptor(alpn).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Opens a raw byte stream to `peer` on the auxiliary ALPN `alpn`.
|
||||
///
|
||||
/// The peer is dialed using the address from its stored DHT contact
|
||||
/// when one is known (which is the case for every peer that appears in
|
||||
/// search results), falling back to whatever the transport itself knows
|
||||
/// about the peer. See [`federation_net::NetworkEngine::open_stream`].
|
||||
pub async fn open_stream(&self, peer: EndpointId, alpn: &[u8]) -> Result<ByteStream> {
|
||||
self.node.ensure_running()?;
|
||||
let contact_addr = self
|
||||
.node
|
||||
.known_contacts()
|
||||
.into_iter()
|
||||
.find(|contact| contact.peer_id == peer)
|
||||
.and_then(|contact| contact.ticket.parse::<PeerTicket>().ok())
|
||||
.map(|ticket| ticket.endpoint_addr);
|
||||
let addr: EndpointAddr = contact_addr.unwrap_or_else(|| peer.into());
|
||||
self.node
|
||||
.engine
|
||||
.open_stream(addr, alpn)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Synchronizes the published library with `specs`: the desired set of
|
||||
/// items this peer wants to share.
|
||||
///
|
||||
|
||||
@@ -187,3 +187,72 @@ async fn library_sync_and_distributed_search() {
|
||||
.await
|
||||
.expect("test timed out");
|
||||
}
|
||||
|
||||
/// Two nodes exchange raw bytes over an auxiliary stream protocol: the
|
||||
/// requester finds an item through the DHT and then opens a byte stream to
|
||||
/// its owner (the flow a file-transfer application follows).
|
||||
#[tokio::test]
|
||||
async fn byte_stream_to_item_owner() {
|
||||
let _net = NET_LOCK.lock().await;
|
||||
tokio::time::timeout(TEST_TIMEOUT, async {
|
||||
const BLOB_ALPN: &[u8] = b"music-test/blob/1";
|
||||
|
||||
let dir_a = tempfile::tempdir().expect("tempdir");
|
||||
let dir_b = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
// Node A owns the library and serves byte streams.
|
||||
let config_a = MusicDhtConfig::builder()
|
||||
.data_dir(dir_a.path())
|
||||
.network_id(NetworkId::from_name("stream-test-net"))
|
||||
.request_timeout(Duration::from_secs(5))
|
||||
.lookup_timeout(Duration::from_secs(10))
|
||||
.stream_protocol(BLOB_ALPN)
|
||||
.build()
|
||||
.expect("valid config");
|
||||
let (node_a, _events_a) = MusicDhtService::start(config_a)
|
||||
.await
|
||||
.expect("service starts");
|
||||
let mut acceptor = node_a.stream_acceptor(BLOB_ALPN).expect("acceptor");
|
||||
let payload = b"pretend this is FLAC".to_vec();
|
||||
let served = payload.clone();
|
||||
let serve_task = tokio::spawn(async move {
|
||||
let mut stream = acceptor.accept().await.expect("incoming stream");
|
||||
stream.send.write_all(&served).await.expect("write");
|
||||
stream.send.finish().expect("finish");
|
||||
let _ = stream.send.stopped().await;
|
||||
stream.peer_id
|
||||
});
|
||||
node_a
|
||||
.sync_library(vec![spec("track:1", ItemKind::Track, "Teardrop", &["Massive Attack"])])
|
||||
.await
|
||||
.expect("sync");
|
||||
|
||||
// Node B joins via ticket and finds the track and its owner.
|
||||
let (node_b, _events_b) = start(dir_b.path(), "stream-test-net").await;
|
||||
let ticket = node_a.ticket().await.expect("ticket");
|
||||
node_b.connect(ticket).await.expect("connect");
|
||||
let owner = search_until("the track by name", &node_b, "teardrop", |items| {
|
||||
!items.is_empty()
|
||||
})
|
||||
.await
|
||||
.pop()
|
||||
.expect("found item")
|
||||
.owner;
|
||||
assert_eq!(owner, node_a.endpoint_id());
|
||||
|
||||
// B streams the bytes from the owner.
|
||||
let mut stream = node_b.open_stream(owner, BLOB_ALPN).await.expect("open stream");
|
||||
let mut received = Vec::new();
|
||||
let mut chunk = [0u8; 1024];
|
||||
while let Some(n) = stream.recv.read(&mut chunk).await.expect("read") {
|
||||
received.extend_from_slice(&chunk[..n]);
|
||||
}
|
||||
assert_eq!(received, payload);
|
||||
assert_eq!(serve_task.await.expect("serve task"), node_b.endpoint_id());
|
||||
|
||||
node_a.shutdown().await.expect("shutdown a");
|
||||
node_b.shutdown().await.expect("shutdown b");
|
||||
})
|
||||
.await
|
||||
.expect("test timed out");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user