///|
pub struct SrflxGatherer {
local_candidate : IceCandidate
server : @transport.SocketAddress
outputs : @queue.Queue[@transport.OutboundDatagram]
events : @queue.Queue[SrflxEvent]
mut state : SrflxGatheringState
mut transaction_id : @stun.TransactionId?
mut request : Bytes?
mut deadline : @transport.Instant?
mut rto_milliseconds : Int64
mut retransmissions : Int
}
///|
fn srflx_after(
now : @transport.Instant,
milliseconds : Int64,
) -> @transport.Instant raise IceError {
let duration = @transport.Duration::milliseconds(milliseconds) catch {
error => raise Time(error)
}
now.checked_add(duration) catch {
error => raise Time(error)
}
}
///|
fn[T] srflx_stun(operation : () -> T raise @stun.StunError) -> T raise IceError {
operation() catch {
error => raise Stun(error)
}
}
///|
pub fn SrflxGatherer::new(
local_candidate~ : IceCandidate,
server~ : @transport.SocketAddress,
) -> SrflxGatherer raise IceError {
if local_candidate.candidate_type() != Host ||
local_candidate.protocol() != Udp ||
local_candidate.socket_address() is None {
raise InvalidCandidate(
"srflx gathering requires a resolved UDP host candidate",
)
}
{
local_candidate,
server,
outputs: Queue([]),
events: Queue([]),
state: SrflxNew,
transaction_id: None,
request: None,
deadline: None,
rto_milliseconds: 500L,
retransmissions: 0,
}
}
///|
pub fn SrflxGatherer::state(self : SrflxGatherer) -> SrflxGatheringState {
self.state
}
///|
fn SrflxGatherer::set_state(
self : SrflxGatherer,
state : SrflxGatheringState,
) -> Unit {
if self.state != state {
self.state = state
self.events.push(SrflxStateChanged(state))
}
}
///|
fn SrflxGatherer::context(self : SrflxGatherer) -> @transport.TransportContext {
{
local_address: self.local_candidate.socket_address().unwrap(),
peer: self.server,
ecn: None,
protocol: Udp,
}
}
///|
pub fn SrflxGatherer::handles_context(
self : SrflxGatherer,
context : @transport.TransportContext,
) -> Bool {
self.state == SrflxGathering && context == self.context()
}
///|
pub fn SrflxGatherer::start(
self : SrflxGatherer,
now : @transport.Instant,
) -> Unit raise IceError {
if self.state != SrflxNew {
raise InvalidState("srflx gatherer has already started")
}
let transaction_id = srflx_stun(() => @stun.TransactionId::random())
let request = srflx_stun(() => {
@stun.Message::new(class=Request, stun_method=Binding, transaction_id~).encode()
})
self.transaction_id = Some(transaction_id)
self.request = Some(request)
self.rto_milliseconds = 500L
self.retransmissions = 0
self.deadline = Some(srflx_after(now, self.rto_milliseconds))
self.outputs.push({ context: self.context(), payload: request, })
self.set_state(SrflxGathering)
}
///|
pub fn SrflxGatherer::poll_datagram(
self : SrflxGatherer,
) -> @transport.OutboundDatagram? {
self.outputs.pop()
}
///|
pub fn SrflxGatherer::poll_event(self : SrflxGatherer) -> SrflxEvent? {
self.events.pop()
}
///|
pub fn SrflxGatherer::poll_timeout(self : SrflxGatherer) -> @transport.Instant? {
self.deadline
}
///|
pub fn SrflxGatherer::handle_timeout(
self : SrflxGatherer,
now : @transport.Instant,
) -> Unit raise IceError {
guard self.state == SrflxGathering &&
self.deadline is Some(deadline) &&
deadline <= now else {
return
}
if self.retransmissions >= 7 {
self.deadline = None
self.set_state(SrflxFailed)
raise ChecklistFailed
}
guard self.request is Some(request) else {
raise InvalidState("srflx request is missing")
}
self.outputs.push({ context: self.context(), payload: request, })
self.retransmissions += 1
self.rto_milliseconds = if self.rto_milliseconds < 8000L {
self.rto_milliseconds * 2L
} else {
8000L
}
self.deadline = Some(srflx_after(now, self.rto_milliseconds))
}
///|
pub fn SrflxGatherer::handle_datagram(
self : SrflxGatherer,
datagram : @transport.InboundDatagram,
) -> Unit raise IceError {
if self.state != SrflxGathering || datagram.context != self.context() {
return
}
let message = srflx_stun(() => @stun.Message::decode(datagram.payload))
guard self.transaction_id is Some(transaction_id) &&
message.transaction_id() == transaction_id &&
message.stun_method() == Binding else {
return
}
if message.class() != SuccessResponse {
self.deadline = None
self.set_state(SrflxFailed)
raise ChecklistFailed
}
let mapped = match message.first_attribute(XorMappedAddress) {
Some(attribute) =>
srflx_stun(() => attribute.to_xor_address(transaction_id))
None =>
match message.first_attribute(MappedAddress) {
Some(attribute) => srflx_stun(() => attribute.to_address())
None => {
self.deadline = None
self.set_state(SrflxFailed)
raise InvalidCandidate("STUN response omitted its mapped address")
}
}
}
let related = self.local_candidate.socket_address().unwrap()
let candidate = IceCandidate::new(
foundation=self.local_candidate.foundation() + "-srflx",
component=self.local_candidate.component(),
protocol=Udp,
priority=IceCandidate::priority_value(
candidate_type=ServerReflexive,
local_preference=65534,
component=self.local_candidate.component(),
),
address=IpAddress(mapped.address()),
port=mapped.port(),
candidate_type=ServerReflexive,
related_address=IpAddress(related.address()),
related_port=related.port(),
)
self.deadline = None
self.events.push(ServerReflexiveCandidate(candidate))
self.set_state(SrflxComplete)
}
///|
pub fn SrflxGatherer::close(self : SrflxGatherer) -> Unit {
if self.state == SrflxClosed {
return
}
self.outputs.clear()
self.deadline = None
self.request = None
self.transaction_id = None
self.set_state(SrflxClosed)
}