///|
/// Registration and CAP negotiation for one connection. Socket and timeout ownership stays with the host.
pub struct Session {
caps : Capabilities
desired : Array[String]
pending : Map[String, Bool]
mut state : String
mut requested : Bool
mut auth_data : String
mut auth_state : String
}
///|
pub fn Session::new(
desired : Array[String],
plain? : SaslPlain? = None,
) -> Session raise IrcError {
if desired.length() > 64 {
raise Invalid("too many desired capabilities")
}
for name in desired {
ignore(capability_token(name, false, false))
if name == "sasl" && plain is None {
raise Invalid("SASL credentials required")
}
}
let wanted = desired.copy()
if plain is Some(_) && !wanted.contains("sasl") {
wanted.push("sasl")
}
{
caps: Capabilities::new(),
desired: wanted,
pending: Map([]),
state: "new",
requested: false,
auth_data: match plain {
Some(p) => p.payload
None => ""
},
auth_state: if plain is Some(_) {
"required"
} else {
"none"
},
}
}
///|
pub fn Session::status(self : Session) -> String {
self.state
}
///|
pub fn Session::enabled(self : Session, name : String) -> Bool {
self.caps.enabled(name)
}
///|
pub fn Session::start(
self : Session,
nick : String,
user : String,
realname : String,
server_password? : String? = None,
) -> Array[Message] raise IrcError {
if self.state != "new" {
raise Invalid("session already started")
}
for value in [nick, user] {
if value.is_empty() ||
value.has_prefix(":") ||
value.to_array().iter().any(c => c.to_int() <= 32) {
raise Invalid("invalid registration identifier")
}
}
let messages = []
if server_password is Some(password) {
if password.is_empty() {
raise Invalid("empty server password")
}
messages.push(Message::{
tags: [],
prefix: None,
command: "PASS",
params: [password],
})
}
let registration = [
Message::{ tags: [], prefix: None, command: "CAP", params: ["LS", "302"], },
Message::{ tags: [], prefix: None, command: "NICK", params: [nick], },
Message::{
tags: [],
prefix: None,
command: "USER",
params: [user, "0", "*", realname],
},
]
for message in registration {
messages.push(message)
}
for message in messages {
ignore(message.encode())
}
self.state = "negotiating"
messages
}
///|
pub fn Session::receive(
self : Session,
message : Message,
) -> Array[Message] raise IrcError {
if self.state == "new" || self.state == "failed" {
raise Invalid("session is not receiving")
}
errdefer {
self.state = "failed"
self.auth_data = ""
}
if message.command == "ERROR" ||
["432", "433", "436", "464", "465"].contains(message.command) {
raise Invalid("server rejected session: " + message.command)
}
if message.command == "PING" {
return [message.pong()]
}
if ["902", "904", "905", "906", "907"].contains(message.command) &&
self.auth_state != "none" {
raise Invalid("SASL authentication rejected: " + message.command)
}
if message.command == "AUTHENTICATE" {
if self.auth_state != "challenge" || message.params != ["+"] {
raise Invalid("unexpected SASL challenge")
}
let out = []
let mut at = 0
while at < self.auth_data.length() {
let end = (at + 400).min(self.auth_data.length())
out.push(auth_message(self.auth_data[at:end].to_owned()))
at = end
}
if self.auth_data.length() % 400 == 0 {
out.push(auth_message("+"))
}
self.auth_data = ""
self.auth_state = "result"
return out
}
if message.command == "903" {
if self.auth_state != "result" {
raise Invalid("unexpected SASL success")
}
self.auth_state = "success"
return self.finish_negotiation()
}
if message.command == "001" {
if self.auth_state != "none" && self.auth_state != "success" {
raise Invalid("registration before required authentication")
}
self.state = "registered"
return []
}
// Legacy servers may reject CAP; NICK/USER were already sent.
if self.state == "negotiating" &&
message.command == "421" &&
message.params.contains("CAP") {
if self.auth_state != "none" {
raise Invalid("server does not support required SASL")
}
self.state = "registering"
return []
}
if message.command != "CAP" {
return []
}
self.caps.observe(message)
let kind = message.params[1]
if (self.auth_state == "challenge" || self.auth_state == "result") &&
!self.caps.enabled("sasl") {
raise Invalid("SASL capability revoked during authentication")
}
let out = []
if kind == "LS" && self.caps.listing_complete() && !self.requested {
if self.auth_state == "required" {
if !self.caps.available("sasl") {
raise Invalid("SASL unavailable")
}
if self.caps.value("sasl") is Some(mechanisms) {
if !mechanisms.split(",").any(x => x == "PLAIN") {
raise Invalid("SASL PLAIN unavailable")
}
}
}
self.requested = true
for name in self.desired {
if self.caps.available(name) && !self.pending.contains(name) {
out.push(self.caps.request([name]))
self.pending[name] = true
}
}
}
if kind == "ACK" || kind == "NAK" || kind == "DEL" {
let tokens = message.params[message.params.length() - 1].split(" ")
for token in tokens {
let name = if token.has_prefix("-") {
token[1:].to_owned()
} else {
token.to_owned()
}
if kind != "ACK" || self.caps.enabled(name) {
self.pending.remove(name)
}
}
// A final ACK may commit capabilities from earlier continuation lines.
for name in self.desired {
if self.caps.enabled(name) {
self.pending.remove(name)
}
}
}
if kind == "NEW" && self.state == "registered" {
for name in self.desired {
if self.caps.available(name) &&
!self.caps.enabled(name) &&
!self.pending.contains(name) {
out.push(self.caps.request([name]))
self.pending[name] = true
}
}
}
if self.auth_state == "required" &&
self.requested &&
!self.pending.contains("sasl") {
if !self.caps.enabled("sasl") {
raise Invalid("required SASL capability rejected")
}
self.auth_state = "challenge"
out.push(auth_message("PLAIN"))
}
for message in self.finish_negotiation() {
out.push(message)
}
out
}
///|
fn Session::finish_negotiation(self : Session) -> Array[Message] {
if self.state == "negotiating" &&
self.requested &&
self.pending.is_empty() &&
(self.auth_state == "none" || self.auth_state == "success") {
self.state = "registering"
[Message::{ tags: [], prefix: None, command: "CAP", params: ["END"], }]
} else {
[]
}
}