use nexora_proto::Http1KeepAlive;
use super::*;
impl WebSocketClient {
pub(super) fn on_completion(
&mut self,
completion: Completion,
submissions: &mut SubmissionContext<'_>,
) -> Result<()> {
let subsystem = completion.operation_id.subsystem_id();
if subsystem == self.config.dns_subsystem {
return self
.dns
.dispatch(completion, submissions)
.map_err(Error::from);
}
if subsystem != self.config.net_subsystem {
return Ok(());
}
submissions.claim_completion(completion)?;
let (tag, raw) = decode_op(completion.operation_id.payload());
if !(TAG_CONNECT..=TAG_PING_TIMEOUT).contains(&tag) {
return Ok(());
}
let id = WebSocketSessionId::new(raw);
let Some(mut owned) = self.sessions.remove(&id) else {
return Ok(());
};
owned.inflight = owned.inflight.saturating_sub(1);
match tag {
TAG_CONNECT => self.on_connect(&mut owned, completion.result),
TAG_RECV => self.on_recv(&mut owned, completion.result),
TAG_SEND => self.on_send(&mut owned, completion.result),
TAG_CLOSE => {
owned.close_op = None;
owned.fd = -1;
owned.state = State::Closed;
}
TAG_CONNECT_TIMEOUT | TAG_PING_TIMEOUT => {
self.on_timer(&mut owned, tag, completion.result)
}
_ => {}
}
if owned.state != State::Closed || owned.inflight != 0 {
self.sessions.insert(id, owned);
} else {
self.handlers.remove(&id);
}
Ok(())
}
fn on_connect(&mut self, session: &mut Session, result: i32) {
session.connect_op = None;
session.peer = None;
if let Some(op) = session.timer_op.take() {
session.cancels.push(op);
}
if session.teardown {
return;
}
if result < 0 {
return self.fail(session, Error::Io(io_error(result)));
}
if !session.uri.scheme().is_tls() {
return self.start_upgrade(session);
}
let Some(config) = self.config.tls_config.clone() else {
return self.fail(
session,
invalid("a wss URL requires a TLS client configuration"),
);
};
let host = session.uri.host().to_owned();
match TlsSession::new_client(&config, &host) {
Ok(tls) => {
session.tls = Some(tls);
session.state = State::Handshaking;
self.drain_tls(session);
}
Err(error) => self.fail(session, Error::Proto(error)),
}
}
fn on_recv(&mut self, session: &mut Session, result: i32) {
session.recv_op = None;
if session.teardown {
return;
}
if result <= 0 {
let error = if result == 0 {
Error::ConnectionClosed
} else {
Error::Io(io_error(result))
};
return self.fail(session, error);
}
let read = result as usize;
if read > session.rx.len() {
return self.fail(session, invalid("receive completion exceeds the buffer"));
}
let mut failure = None;
if let Some(tls) = session.tls.as_mut() {
if let Err(error) = tls.feed_ciphertext(&session.rx[..read]) {
failure = Some(error);
} else {
loop {
match tls.read_plaintext(&mut session.plain) {
Ok(0) => break,
Ok(_) => {}
Err(error) => {
failure = Some(error);
break;
}
}
}
}
} else {
session.plain.extend_from_slice(&session.rx[..read]);
}
if let Some(error) = failure {
return self.fail(session, Error::Proto(error));
}
self.drain_tls(session);
if session.state == State::Handshaking
&& session
.tls
.as_ref()
.is_some_and(|tls| !tls.is_handshaking())
{
self.finish_handshake(session);
if session.teardown {
return;
}
}
self.advance(session);
}
fn on_send(&mut self, session: &mut Session, result: i32) {
session.send_op = None;
if result < 0 {
if !session.teardown {
self.fail(session, Error::Io(io_error(result)));
}
return;
}
if result == 0 {
return self.fail(session, Error::ConnectionClosed);
}
let sent = (result as usize).min(session.wire.len());
session.wire.advance(sent);
if session.wire.is_empty() && !session.deferred.is_empty() {
let deferred = std::mem::take(&mut session.deferred);
session.wire.extend_from_slice(&deferred);
}
}
fn on_timer(&mut self, session: &mut Session, tag: u64, result: i32) {
drop(session.timer.take());
session.timer_op = None;
if session.teardown || result != -libc::ETIME {
return;
}
match tag {
TAG_CONNECT_TIMEOUT if session.state == State::Connecting => {
if let Some(op) = session.connect_op.take() {
session.cancels.push(op);
}
self.fail(session, Error::Timeout);
}
TAG_PING_TIMEOUT if session.state == State::Open && !session.closing => {
if let Err(error) = session.queue_message(WebSocketMessage::Ping(Bytes::new())) {
self.fail(session, Error::Proto(error));
}
}
_ => {}
}
}
fn finish_handshake(&mut self, session: &mut Session) {
let alpn = session.tls.as_ref().and_then(|tls| tls.alpn_protocol());
if alpn == Some(AlpnProtocol::Http2) {
return self.fail(
session,
Error::Proto(ProtoError::Tls(String::from(
"the peer selected ALPN h2; a WebSocket upgrade requires http/1.1",
))),
);
}
self.start_upgrade(session);
}
fn start_upgrade(&mut self, session: &mut Session) {
let request =
WebSocketHandshake::build_upgrade_request(&session.uri, &session.key, &session.extra);
let mut bytes = BytesMut::with_capacity(256);
let mut http = Http1ClientCodec::default();
let encoded = http
.encode_request(&request, &mut bytes)
.and_then(|()| session.queue_app(&bytes));
if let Err(error) = encoded {
return self.fail(session, Error::Proto(error));
}
session.state = State::Upgrading;
}
fn advance(&mut self, session: &mut Session) {
if session.state == State::Upgrading {
let mut http = Http1ClientCodec::default();
match http.decode_response(&mut session.plain, Method::Get) {
Ok(Some((response, Http1KeepAlive::Upgrade))) => {
if let Err(error) =
WebSocketHandshake::validate_upgrade_response(&response, &session.key)
{
return self.fail(session, Error::Proto(error));
}
session.state = State::Open;
if let Some(callback) = session.on_open.take() {
let mut ctx = client_context(&self.inbox);
callback(&mut ctx, session.id, Ok(()));
}
}
Ok(Some(_)) => {
return self.fail(
session,
Error::Proto(ProtoError::WebSocketProtocol(String::from(
"the server did not switch protocols",
))),
);
}
Ok(None) => {}
Err(error) => return self.fail(session, Error::Proto(error)),
}
}
if session.state != State::Open || session.closing {
return;
}
loop {
match session.websocket.decode_message(&mut session.plain) {
Ok(None) => return,
Ok(Some(message)) => {
if !self.dispatch(session, message) {
return;
}
}
Err(error) => return self.fail(session, Error::Proto(error)),
}
if session.state != State::Open || session.teardown {
return;
}
}
}
fn dispatch(&mut self, session: &mut Session, message: WebSocketMessage) -> bool {
match message {
WebSocketMessage::Text(text) => self.emit(session, WebSocketEvent::Text(text)),
WebSocketMessage::Binary(data) => self.emit(session, WebSocketEvent::Binary(data)),
WebSocketMessage::Ping(data) => {
if let Err(error) = session.queue_message(WebSocketMessage::Pong(data.clone())) {
self.fail(session, Error::Proto(error));
return false;
}
self.emit(session, WebSocketEvent::Ping(data));
}
WebSocketMessage::Pong(data) => self.emit(session, WebSocketEvent::Pong(data)),
WebSocketMessage::Close(close) => {
let echo = WebSocketCloseFrame {
code: close
.as_ref()
.map_or(WebSocketCloseCode::Normal, |frame| frame.code),
reason: String::new(),
};
let reply = !session.closing;
session.closing = true;
self.emit(session, WebSocketEvent::Close(close));
if reply
&& let Err(error) = session.queue_message(WebSocketMessage::Close(Some(echo)))
{
self.fail(session, Error::Proto(error));
}
return false;
}
}
true
}
fn emit(&mut self, session: &mut Session, event: WebSocketEvent) {
if matches!(event, WebSocketEvent::Close(_)) {
if session.close_event_sent {
return;
}
session.close_event_sent = true;
}
let id = session.id;
let Some(handler) = self.handlers.get_mut(&id) else {
return;
};
let mut ctx = client_context(&self.inbox);
let mut view = WebSocketSession { id };
handler(&mut ctx, &mut view, event);
}
pub(super) fn fail(&mut self, session: &mut Session, error: Error) {
if session.teardown {
return;
}
match session.on_open.take() {
Some(callback) => {
let mut ctx = client_context(&self.inbox);
callback(&mut ctx, session.id, Err(error));
}
None => self.emit(session, WebSocketEvent::Close(None)),
}
if let Some(lookup) = session.lookup.take() {
self.dns_waiters.remove(&lookup);
}
session.teardown = true;
session.closing = true;
}
pub(super) fn pump(&mut self, proactor: &mut Proactor) -> Result<()> {
let mut sessions = std::mem::take(&mut self.sessions);
let mut failure: Option<Error> = None;
for (id, mut session) in sessions.drain() {
if let Err(error) = self.pump_session(&mut session, proactor)
&& failure.is_none()
{
failure = Some(error);
}
if session.state != State::Closed || session.inflight != 0 {
self.sessions.insert(id, session);
} else {
self.handlers.remove(&id);
}
}
match failure {
Some(error) => Err(error),
None => Ok(()),
}
}
fn pump_session(&mut self, session: &mut Session, proactor: &mut Proactor) -> Result<()> {
if session.state == State::Closed {
return Ok(());
}
self.stage_cancels(session, proactor)?;
if session.teardown {
return self.pump_close(session, proactor);
}
if session.send_op.is_none() && !session.wire.is_empty() {
self.stage_send(session, proactor)?;
}
let framed = matches!(
session.state,
State::Handshaking | State::Upgrading | State::Open
);
if framed && !session.closing && session.recv_op.is_none() {
self.stage_recv(session, proactor)?;
}
let keepalive = self
.config
.ping_interval
.is_some_and(|interval| !interval.is_zero());
if session.state == State::Open
&& keepalive
&& !session.closing
&& session.timer.is_none()
&& session.timer_op.is_none()
{
self.stage_ping_timer(session, proactor)?;
}
if session.closing
&& session.send_op.is_none()
&& session.wire.is_empty()
&& session.deferred.is_empty()
{
session.teardown = true;
self.pump_close(session, proactor)?;
}
Ok(())
}
fn stage_cancels(&mut self, session: &mut Session, proactor: &mut Proactor) -> Result<()> {
let targets = std::mem::take(&mut session.cancels);
for target in targets {
match stage_cancel(proactor, self.config.net_subsystem, target) {
Ok(()) => session.inflight += 1,
Err(error) => {
session.cancels.push(target);
return Err(error);
}
}
}
Ok(())
}
fn pump_close(&mut self, session: &mut Session, proactor: &mut Proactor) -> Result<()> {
let mut targets: Vec<OperationId> = [
session.recv_op.take(),
session.send_op.take(),
session.connect_op.take(),
session.timer_op.take(),
]
.into_iter()
.flatten()
.collect();
targets.append(&mut session.cancels);
for target in targets {
match stage_cancel(proactor, self.config.net_subsystem, target) {
Ok(()) => session.inflight += 1,
Err(error) => {
session.cancels.push(target);
return Err(error);
}
}
}
if session.inflight != 0 {
return Ok(());
}
if session.fd < 0 || session.close_op.is_some() {
if session.close_op.is_none() {
session.state = State::Closed;
}
return Ok(());
}
let fd = session.fd;
let op = operation_id(self.config.net_subsystem, TAG_CLOSE, session.id);
proactor.prepare_sqe_with(op, |sqe| {
unsafe { net::prep_close(sqe, fd) }
})?;
session.close_op = Some(op);
session.inflight += 1;
session.fd = -1;
Ok(())
}
fn stage_recv(&mut self, session: &mut Session, proactor: &mut Proactor) -> Result<()> {
let fd = session.fd;
if fd < 0 {
return Ok(());
}
let op = operation_id(self.config.net_subsystem, TAG_RECV, session.id);
let ptr = session.rx.as_mut_ptr();
let len = session.rx.len();
proactor.prepare_sqe_with(op, |sqe| {
unsafe { net::prep_recv(sqe, fd, ptr, len, 0) }
})?;
session.recv_op = Some(op);
session.inflight += 1;
Ok(())
}
fn stage_send(&mut self, session: &mut Session, proactor: &mut Proactor) -> Result<()> {
let fd = session.fd;
if fd < 0 || session.wire.is_empty() {
return Ok(());
}
let op = operation_id(self.config.net_subsystem, TAG_SEND, session.id);
let ptr = session.wire.as_slice().as_ptr();
let len = session.wire.len();
proactor.prepare_sqe_with(op, |sqe| {
unsafe { net::prep_send(sqe, fd, ptr, len, libc::MSG_NOSIGNAL) }
})?;
session.send_op = Some(op);
session.inflight += 1;
Ok(())
}
fn stage_ping_timer(&mut self, session: &mut Session, proactor: &mut Proactor) -> Result<()> {
let Some(interval) = self
.config
.ping_interval
.filter(|interval| !interval.is_zero())
else {
return Ok(());
};
let timeout = Timeout::new(interval)?;
let op = operation_id(self.config.net_subsystem, TAG_PING_TIMEOUT, session.id);
proactor.prepare_sqe_with(op, |sqe| {
unsafe { timeout.prepare(sqe) }
})?;
session.timer = Some(timeout);
session.timer_op = Some(op);
session.inflight += 1;
Ok(())
}
fn drain_tls(&mut self, session: &mut Session) {
let outcome = {
let Session {
tls,
wire,
deferred,
send_op,
..
} = &mut *session;
tls.as_mut()
.map(|tls| drain_tls(tls, wire, deferred, send_op.is_some()))
};
if let Some(Err(error)) = outcome {
self.fail(session, Error::Proto(error));
}
}
pub(super) fn apply_dns(&mut self, proactor: &mut Proactor) -> Result<usize> {
let mut applied = 0;
loop {
let popped = self
.dns_outcomes
.try_borrow_mut()
.ok()
.and_then(|mut queue| queue.pop_front());
let Some((lookup, addrs, error)) = popped else {
break;
};
applied += 1;
let Some(id) = self.dns_waiters.get(&lookup).copied() else {
continue;
};
let Some(mut owned) = self.sessions.remove(&id) else {
self.dns_waiters.remove(&lookup);
continue;
};
if owned.lookup != Some(lookup) {
self.dns_waiters.remove(&lookup);
self.sessions.insert(id, owned);
continue;
}
owned.dns_left = owned.dns_left.saturating_sub(1);
if let Some(error) = error
&& owned.dns_reason.is_none()
{
owned.dns_reason = Some(error);
}
if let Some(addr) = addrs.first().copied() {
owned.lookup = None;
owned.state = State::Connecting;
self.sessions.insert(id, owned);
self.dns_waiters.remove(&lookup);
if let Err(error) = self.begin_connect(proactor, id, addr)
&& let Some(mut failed) = self.sessions.remove(&id)
{
self.fail(&mut failed, error);
self.sessions.insert(id, failed);
}
} else if owned.dns_left == 0 {
owned.lookup = None;
let host = owned.uri.host().to_owned();
let reason = owned
.dns_reason
.take()
.unwrap_or_else(|| String::from("no records"));
self.fail(&mut owned, Error::DnsResolutionFailed { host, reason });
self.dns_waiters.remove(&lookup);
self.sessions.insert(id, owned);
} else {
self.sessions.insert(id, owned);
}
}
Ok(applied)
}
}
pub(super) enum PeerAddr {
V4(libc::sockaddr_in),
V6(libc::sockaddr_in6),
}
impl PeerAddr {
pub(super) fn new(addr: SocketAddr) -> Self {
match addr {
SocketAddr::V4(v4) => Self::V4(libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: v4.port().to_be(),
sin_addr: libc::in_addr {
s_addr: u32::from_ne_bytes(v4.ip().octets()),
},
sin_zero: [0; 8],
}),
SocketAddr::V6(v6) => Self::V6(libc::sockaddr_in6 {
sin6_family: libc::AF_INET6 as libc::sa_family_t,
sin6_port: v6.port().to_be(),
sin6_flowinfo: v6.flowinfo(),
sin6_addr: libc::in6_addr {
s6_addr: v6.ip().octets(),
},
sin6_scope_id: v6.scope_id(),
}),
}
}
pub(super) fn raw(&self) -> (*const libc::sockaddr, libc::socklen_t) {
match self {
Self::V4(value) => (
std::ptr::from_ref(value).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
),
Self::V6(value) => (
std::ptr::from_ref(value).cast(),
std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
),
}
}
}
pub(super) fn client_context(inbox: &SubsystemInbox) -> ClientContext {
inbox.context()
}
pub(super) fn push_wire(
wire: &mut BytesMut,
deferred: &mut BytesMut,
in_flight: bool,
bytes: &[u8],
) {
if in_flight {
deferred.extend_from_slice(bytes);
} else {
wire.extend_from_slice(bytes);
}
}
pub(super) fn drain_tls(
tls: &mut TlsSession,
wire: &mut BytesMut,
deferred: &mut BytesMut,
in_flight: bool,
) -> std::result::Result<(), ProtoError> {
let mut chunk = BytesMut::with_capacity(TLS_CHUNK_SIZE);
tls.take_ciphertext(&mut chunk)?;
if !chunk.is_empty() {
push_wire(wire, deferred, in_flight, &chunk);
}
Ok(())
}
pub(super) fn decode_op(payload: OperationPayload) -> (u64, u64) {
let raw = payload.raw();
(raw >> TAG_SHIFT, raw & ID_MASK)
}
pub(super) fn operation_id(
subsystem: SubsystemId,
tag: u64,
id: WebSocketSessionId,
) -> OperationId {
OperationId::pack(
subsystem,
OperationPayload::new_unchecked((tag << TAG_SHIFT) | (id.raw() & ID_MASK)),
)
}
pub(super) fn stage_cancel(
proactor: &mut Proactor,
subsystem: SubsystemId,
target: OperationId,
) -> Result<()> {
let session = WebSocketSessionId::new(target.payload().raw() & ID_MASK);
let op = operation_id(subsystem, TAG_CANCEL, session);
proactor.prepare_sqe_with(op, |sqe| {
unsafe { net::prep_cancel(sqe, target, 0) }
})?;
Ok(())
}
pub(super) fn open_socket(addr: &SocketAddr) -> Result<RawFd> {
let family = match addr {
SocketAddr::V4(_) => libc::AF_INET,
SocketAddr::V6(_) => libc::AF_INET6,
};
let fd = unsafe {
libc::socket(
family,
libc::SOCK_STREAM | libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
0,
)
};
if fd < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(fd)
}
pub(super) fn close_raw(fd: RawFd) {
if fd < 0 {
return;
}
unsafe { libc::close(fd) };
}
pub(super) fn io_error(result: i32) -> std::io::Error {
std::io::Error::from_raw_os_error(result.saturating_neg())
}
pub(super) fn invalid(reason: &str) -> Error {
Error::InvalidState(reason.to_owned())
}
pub(super) fn handshake_seed(id: WebSocketSessionId) -> u64 {
let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
hasher.write_u64(id.raw());
hasher.finish()
}
pub(super) fn system_servers_csv() -> String {
const FALLBACK: &str = "127.0.0.1";
let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") else {
return String::from(FALLBACK);
};
let mut servers: Vec<String> = Vec::new();
for line in contents.lines() {
let mut fields = line.split_whitespace();
if fields.next() != Some("nameserver") {
continue;
}
if let Some(server) = fields.next() {
servers.push(server.to_owned());
}
}
if servers.is_empty() {
String::from(FALLBACK)
} else {
servers.join(",")
}
}