feat(irc): SASL PLAIN login and IRCv3 server-time
Registration now opens with CAP LS 302 (servers without IRCv3 ignore it and register as before). From the offered capabilities (multi-line LS lists included) the client requests server-time and, when the profile enables it, sasl; then AUTHENTICATE PLAIN with the account (defaulting to the nick) and a stored SASL password (irc-sasl-password), base64 in 400-byte chunks, and CAP END on success or failure — failures are shown on the Server tab and the connection carries on (NickServ still works). server-time tags stamp events with the server's clock, so replayed history shows real times. Tested against the fake IRC server: multi-line LS, REQ/ACK, the PLAIN exchange, 903, CAP END, and a tagged message's timestamp.
This commit is contained in:
@@ -168,6 +168,7 @@ Use Nexus Terminal when you need to:
|
|||||||
|
|
||||||
### IRC capabilities
|
### IRC capabilities
|
||||||
|
|
||||||
|
- IRCv3 capability negotiation with SASL PLAIN login and server-time timestamps.
|
||||||
- Nick, real name, TLS and an optional server password.
|
- Nick, real name, TLS and an optional server password.
|
||||||
- NickServ identification on connect.
|
- NickServ identification on connect.
|
||||||
- Auto-join rooms.
|
- Auto-join rooms.
|
||||||
|
|||||||
+2
-1
@@ -180,8 +180,9 @@ The viewer works like the VNC one: click the desktop to give it keyboard focus,
|
|||||||
### IRC
|
### IRC
|
||||||
|
|
||||||
- **IRC**: nick, real name, **Use TLS** (port 6697 for most TLS networks) and an optional server password.
|
- **IRC**: nick, real name, **Use TLS** (port 6697 for most TLS networks) and an optional server password.
|
||||||
|
- **SASL**: log in during registration with SASL PLAIN (account defaults to your nick), which most networks now expect; use TLS or an SSH tunnel so the password isn't sent in the clear. If the network doesn't offer SASL or rejects the login, the Server tab says so and the connection carries on.
|
||||||
- **NickServ**: identify on connect, with an optional account name and password.
|
- **NickServ**: identify on connect, with an optional account name and password.
|
||||||
- **Rooms**: rooms to join automatically, one per line or comma-separated.
|
- **Rooms**: rooms to join automatically, one per line or comma-separated. Networks that support IRCv3 server-time stamp messages with the time they were sent, so replayed history shows its real times.
|
||||||
- **SSH Tunnel** and **Reconnection** settings.
|
- **SSH Tunnel** and **Reconnection** settings.
|
||||||
|
|
||||||
The IRC view has a **Server** tab and a tab per room or conversation, with a dot on tabs that have unread messages. Type messages or `/commands` in the composer (`/join`, `/part`, `/msg`, `/me`, `/notice`, `/nick`, `/topic`, `/list`, `/names`, `/whois`, `/who`, `/raw`, `/quit`, `/help`). Press **Tab** to complete a nick, or click a nick to mention it. The side panel lists joined rooms, a room browser with a filter (click a room to join it), and the users in the current room.
|
The IRC view has a **Server** tab and a tab per room or conversation, with a dot on tabs that have unread messages. Type messages or `/commands` in the composer (`/join`, `/part`, `/msg`, `/me`, `/notice`, `/nick`, `/topic`, `/list`, `/names`, `/whois`, `/who`, `/raw`, `/quit`, `/help`). Press **Tab** to complete a nick, or click a nick to mention it. The side panel lists joined rooms, a room browser with a filter (click a room to join it), and the users in the current room.
|
||||||
|
|||||||
@@ -812,6 +812,19 @@ impl ProfileEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
section(ui, "SASL", |ui| {
|
||||||
|
check(ui, "Log In with SASL", &mut c.sasl, "Authenticates during registration (SASL PLAIN), as most networks now expect");
|
||||||
|
if c.sasl {
|
||||||
|
text(ui, "SASL Account", &mut c.sasl_account, "Defaults to your nick");
|
||||||
|
mode(ui, "SASL Password Storage", &mut c.sasl_mode);
|
||||||
|
if c.sasl_mode == SecretMode::Saved {
|
||||||
|
secret(ui, "SASL Password", secrets, SecretKind::IrcSaslPassword, "Leave blank to keep the saved password");
|
||||||
|
}
|
||||||
|
if !c.use_tls && !c.ssh_tunnel {
|
||||||
|
widgets::hint(ui, "Without TLS or an SSH tunnel the password crosses the network in the clear.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
section(ui, "NickServ", |ui| {
|
section(ui, "NickServ", |ui| {
|
||||||
check(ui, "Identify on Connect", &mut c.nick_serv_identify, "");
|
check(ui, "Identify on Connect", &mut c.nick_serv_identify, "");
|
||||||
if c.nick_serv_identify {
|
if c.nick_serv_identify {
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ pub struct Config {
|
|||||||
pub nickserv_account: String,
|
pub nickserv_account: String,
|
||||||
pub nickserv_password: String,
|
pub nickserv_password: String,
|
||||||
|
|
||||||
|
/// SASL PLAIN login during registration (IRCv3); empty account disables it.
|
||||||
|
pub sasl_account: String,
|
||||||
|
pub sasl_password: String,
|
||||||
|
|
||||||
/// Provides the transport instead of a direct TCP connect (e.g. an SSH tunnel). TLS, when
|
/// Provides the transport instead of a direct TCP connect (e.g. an SSH tunnel). TLS, when
|
||||||
/// enabled, is layered on top of it.
|
/// enabled, is layered on top of it.
|
||||||
pub connector: Option<Connector>,
|
pub connector: Option<Connector>,
|
||||||
@@ -78,6 +82,8 @@ impl Default for Config {
|
|||||||
nickserv_identify: false,
|
nickserv_identify: false,
|
||||||
nickserv_account: String::new(),
|
nickserv_account: String::new(),
|
||||||
nickserv_password: String::new(),
|
nickserv_password: String::new(),
|
||||||
|
sasl_account: String::new(),
|
||||||
|
sasl_password: String::new(),
|
||||||
connector: None,
|
connector: None,
|
||||||
connect_timeout: Duration::ZERO,
|
connect_timeout: Duration::ZERO,
|
||||||
on_event: Arc::new(|_| {}),
|
on_event: Arc::new(|_| {}),
|
||||||
@@ -139,6 +145,8 @@ struct Channel {
|
|||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
nick: String,
|
nick: String,
|
||||||
|
/// Capabilities from a multi-line CAP LS, gathered until the last line.
|
||||||
|
caps_offered: Vec<String>,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
closing: bool,
|
closing: bool,
|
||||||
list_rooms: Vec<RoomInfo>,
|
list_rooms: Vec<RoomInfo>,
|
||||||
@@ -153,6 +161,9 @@ pub struct Client {
|
|||||||
cfg: Config,
|
cfg: Config,
|
||||||
state: Mutex<State>,
|
state: Mutex<State>,
|
||||||
out: Mutex<Option<Sender<Out>>>,
|
out: Mutex<Option<Sender<Out>>>,
|
||||||
|
/// The server-time of the line being handled (ms since the epoch; 0 = none), stamped on the
|
||||||
|
/// events it produces.
|
||||||
|
line_time: std::sync::atomic::AtomicI64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A parsed server line.
|
/// A parsed server line.
|
||||||
@@ -160,6 +171,8 @@ struct Message {
|
|||||||
source: String,
|
source: String,
|
||||||
command: String,
|
command: String,
|
||||||
params: Vec<String>,
|
params: Vec<String>,
|
||||||
|
/// IRCv3 `@time=` tag, in ms since the epoch.
|
||||||
|
time: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
@@ -187,6 +200,7 @@ impl Client {
|
|||||||
cfg,
|
cfg,
|
||||||
state: Mutex::new(State {
|
state: Mutex::new(State {
|
||||||
nick,
|
nick,
|
||||||
|
caps_offered: Vec::new(),
|
||||||
connected: false,
|
connected: false,
|
||||||
closing: false,
|
closing: false,
|
||||||
list_rooms: Vec::new(),
|
list_rooms: Vec::new(),
|
||||||
@@ -196,6 +210,7 @@ impl Client {
|
|||||||
last_pong: Instant::now(),
|
last_pong: Instant::now(),
|
||||||
}),
|
}),
|
||||||
out: Mutex::new(None),
|
out: Mutex::new(None),
|
||||||
|
line_time: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +561,8 @@ impl Client {
|
|||||||
stream.write_all(b"\r\n")?;
|
stream.write_all(b"\r\n")?;
|
||||||
Ok(stream.flush()?)
|
Ok(stream.flush()?)
|
||||||
};
|
};
|
||||||
|
// IRCv3 capability negotiation; servers without it ignore the command and register as usual.
|
||||||
|
write(&mut stream, "CAP LS 302")?;
|
||||||
if !self.cfg.server_pass.is_empty() {
|
if !self.cfg.server_pass.is_empty() {
|
||||||
write(&mut stream, &encode("PASS", &[&self.cfg.server_pass]))?;
|
write(&mut stream, &encode("PASS", &[&self.cfg.server_pass]))?;
|
||||||
}
|
}
|
||||||
@@ -589,7 +606,9 @@ impl Client {
|
|||||||
let raw: Vec<u8> = buf.drain(..=pos).collect();
|
let raw: Vec<u8> = buf.drain(..=pos).collect();
|
||||||
let line = String::from_utf8_lossy(&raw);
|
let line = String::from_utf8_lossy(&raw);
|
||||||
if let Some(msg) = parse_line(line.trim_end_matches(['\r', '\n'])) {
|
if let Some(msg) = parse_line(line.trim_end_matches(['\r', '\n'])) {
|
||||||
|
self.line_time.store(msg.time.unwrap_or(0), std::sync::atomic::Ordering::Relaxed);
|
||||||
self.handle_message(&msg);
|
self.handle_message(&msg);
|
||||||
|
self.line_time.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if buf.len() > MAX_LINE_BYTES {
|
if buf.len() > MAX_LINE_BYTES {
|
||||||
@@ -612,6 +631,27 @@ impl Client {
|
|||||||
|
|
||||||
fn handle_message(&self, m: &Message) {
|
fn handle_message(&self, m: &Message) {
|
||||||
match m.command.as_str() {
|
match m.command.as_str() {
|
||||||
|
"CAP" => self.handle_cap(m),
|
||||||
|
"AUTHENTICATE" if m.param(0) == "+" => {
|
||||||
|
// SASL PLAIN: authzid NUL authcid NUL password, base64, in 400-byte chunks.
|
||||||
|
use base64::Engine;
|
||||||
|
let blob = base64::engine::general_purpose::STANDARD.encode(format!("{0}\0{0}\0{1}", self.cfg.sasl_account, self.cfg.sasl_password));
|
||||||
|
for chunk in blob.as_bytes().chunks(400) {
|
||||||
|
let _ = self.send("AUTHENTICATE", &[std::str::from_utf8(chunk).unwrap_or("")]);
|
||||||
|
}
|
||||||
|
if blob.len() % 400 == 0 {
|
||||||
|
let _ = self.send("AUTHENTICATE", &["+"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 900 logged in, 903 success; 902/904–907 failures. Either way registration goes on.
|
||||||
|
"903" => {
|
||||||
|
self.emit(Event { kind: "notice".into(), text: format!("SASL: logged in as {}", self.cfg.sasl_account), ..Default::default() });
|
||||||
|
let _ = self.send("CAP", &["END"]);
|
||||||
|
}
|
||||||
|
"902" | "904" | "905" | "906" | "907" => {
|
||||||
|
self.emit(Event { kind: "error".into(), text: format!("SASL login failed: {}", m.last()), ..Default::default() });
|
||||||
|
let _ = self.send("CAP", &["END"]);
|
||||||
|
}
|
||||||
"001" => self.handle_welcome(m),
|
"001" => self.handle_welcome(m),
|
||||||
"433" | "436" | "437" => {
|
"433" | "436" | "437" => {
|
||||||
let mut st = self.st();
|
let mut st = self.st();
|
||||||
@@ -736,6 +776,52 @@ impl Client {
|
|||||||
self.emit(Event { kind: "notice".into(), target, from: m.source.clone(), text: m.last().into(), ..Default::default() });
|
self.emit(Event { kind: "notice".into(), target, from: m.source.clone(), text: m.last().into(), ..Default::default() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CAP LS / ACK / NAK: ask for server-time (and sasl when configured), then authenticate or
|
||||||
|
/// end negotiation.
|
||||||
|
fn handle_cap(&self, m: &Message) {
|
||||||
|
let sub = m.param(1).to_ascii_uppercase();
|
||||||
|
let caps: Vec<&str> = m.last().split_whitespace().map(|c| c.split('=').next().unwrap_or(c)).collect();
|
||||||
|
let want_sasl = !self.cfg.sasl_account.is_empty();
|
||||||
|
match sub.as_str() {
|
||||||
|
// A multi-line LS list ("CAP * LS * :…") continues; act on the last line.
|
||||||
|
"LS" if m.params.len() >= 4 && m.param(2) == "*" => {
|
||||||
|
self.st().caps_offered.extend(caps.iter().map(|c| c.to_string()));
|
||||||
|
}
|
||||||
|
"LS" => {
|
||||||
|
let offered: Vec<String> = {
|
||||||
|
let mut st = self.st();
|
||||||
|
st.caps_offered.extend(caps.iter().map(|c| c.to_string()));
|
||||||
|
std::mem::take(&mut st.caps_offered)
|
||||||
|
};
|
||||||
|
let mut req = vec![];
|
||||||
|
if offered.iter().any(|c| c == "server-time") {
|
||||||
|
req.push("server-time");
|
||||||
|
}
|
||||||
|
if want_sasl && offered.iter().any(|c| c == "sasl") {
|
||||||
|
req.push("sasl");
|
||||||
|
} else if want_sasl {
|
||||||
|
self.emit(Event { kind: "error".into(), text: "SASL: the server doesn't offer it".into(), ..Default::default() });
|
||||||
|
}
|
||||||
|
if req.is_empty() {
|
||||||
|
let _ = self.send("CAP", &["END"]);
|
||||||
|
} else {
|
||||||
|
let _ = self.send("CAP", &["REQ", &req.join(" ")]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ACK" => {
|
||||||
|
if want_sasl && caps.contains(&"sasl") {
|
||||||
|
let _ = self.send("AUTHENTICATE", &["PLAIN"]);
|
||||||
|
} else {
|
||||||
|
let _ = self.send("CAP", &["END"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"NAK" => {
|
||||||
|
let _ = self.send("CAP", &["END"]);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_join(&self, m: &Message) {
|
fn handle_join(&self, m: &Message) {
|
||||||
let room = m.param(0).to_string();
|
let room = m.param(0).to_string();
|
||||||
if room.is_empty() {
|
if room.is_empty() {
|
||||||
@@ -931,7 +1017,8 @@ impl Client {
|
|||||||
|
|
||||||
fn emit(&self, mut event: Event) {
|
fn emit(&self, mut event: Event) {
|
||||||
if event.timestamp == 0 {
|
if event.timestamp == 0 {
|
||||||
event.timestamp = chrono::Utc::now().timestamp_millis();
|
let t = self.line_time.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
event.timestamp = if t > 0 { t } else { chrono::Utc::now().timestamp_millis() };
|
||||||
}
|
}
|
||||||
if event.target.is_empty() {
|
if event.target.is_empty() {
|
||||||
event.target = SERVER_TARGET.into();
|
event.target = SERVER_TARGET.into();
|
||||||
@@ -1004,8 +1091,11 @@ fn encode(command: &str, params: &[&str]) -> String {
|
|||||||
|
|
||||||
fn parse_line(line: &str) -> Option<Message> {
|
fn parse_line(line: &str) -> Option<Message> {
|
||||||
let mut rest = line.trim_start();
|
let mut rest = line.trim_start();
|
||||||
if rest.starts_with('@') {
|
let mut time = None;
|
||||||
rest = rest.split_once(' ')?.1.trim_start();
|
if let Some(tagged) = rest.strip_prefix('@') {
|
||||||
|
let (tags, tail) = tagged.split_once(' ')?;
|
||||||
|
time = tags.split(';').find_map(|t| t.strip_prefix("time=")).and_then(|v| chrono::DateTime::parse_from_rfc3339(v).ok()).map(|d| d.timestamp_millis());
|
||||||
|
rest = tail.trim_start();
|
||||||
}
|
}
|
||||||
let mut source = String::new();
|
let mut source = String::new();
|
||||||
if let Some(prefixed) = rest.strip_prefix(':') {
|
if let Some(prefixed) = rest.strip_prefix(':') {
|
||||||
@@ -1021,7 +1111,7 @@ fn parse_line(line: &str) -> Option<Message> {
|
|||||||
let command = words.next()?.to_uppercase();
|
let command = words.next()?.to_uppercase();
|
||||||
let mut params: Vec<String> = words.map(String::from).collect();
|
let mut params: Vec<String> = words.map(String::from).collect();
|
||||||
params.extend(trailing.map(String::from));
|
params.extend(trailing.map(String::from));
|
||||||
Some(Message { source, command, params })
|
Some(Message { source, command, params, time })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A CTCP request in a PRIVMSG: `(TAG, args)`, as girc's `DecodeCTCP`.
|
/// A CTCP request in a PRIVMSG: `(TAG, args)`, as girc's `DecodeCTCP`.
|
||||||
@@ -1204,6 +1294,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiates_server_time_and_sasl() {
|
||||||
|
let (client, listener, rx, _) = harness(Config { sasl_account: "acct".into(), sasl_password: "s3cret".into(), ..Default::default() });
|
||||||
|
let c = client.clone();
|
||||||
|
let connecting = std::thread::spawn(move || c.connect());
|
||||||
|
let mut srv = accept(&listener);
|
||||||
|
assert_eq!(srv.line(), "CAP LS 302");
|
||||||
|
assert_eq!(srv.line(), "NICK me");
|
||||||
|
assert_eq!(srv.line(), "USER me * * me");
|
||||||
|
// A two-line LS list; only the last line triggers the request.
|
||||||
|
srv.send(":srv CAP * LS * :multi-prefix server-time");
|
||||||
|
srv.send(":srv CAP * LS :sasl=PLAIN,EXTERNAL away-notify");
|
||||||
|
assert_eq!(srv.line(), "CAP REQ :server-time sasl");
|
||||||
|
srv.send(":srv CAP me ACK :server-time sasl");
|
||||||
|
assert_eq!(srv.line(), "AUTHENTICATE PLAIN");
|
||||||
|
srv.send("AUTHENTICATE +");
|
||||||
|
use base64::Engine;
|
||||||
|
assert_eq!(srv.line(), format!("AUTHENTICATE {}", base64::engine::general_purpose::STANDARD.encode("acct\0acct\0s3cret")));
|
||||||
|
srv.send(":srv 903 me :SASL authentication successful");
|
||||||
|
assert_eq!(srv.line(), "CAP END");
|
||||||
|
assert!(wait_for(&rx, |e| e.text.starts_with("SASL: logged in")).text.contains("acct"));
|
||||||
|
srv.send(":srv 001 me :Welcome");
|
||||||
|
connecting.join().unwrap().unwrap();
|
||||||
|
// server-time stamps events with the server's clock, not ours.
|
||||||
|
srv.send("@time=2024-05-01T12:00:00.250Z :bob!u@h PRIVMSG me :old news");
|
||||||
|
let e = wait_for(&rx, |e| e.kind == "message" || e.kind == "privmsg" || e.text == "old news");
|
||||||
|
assert_eq!(e.timestamp, 1714564800250);
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registers_tracks_rooms_and_handles_input() {
|
fn registers_tracks_rooms_and_handles_input() {
|
||||||
let (client, listener, rx, closes) = harness(Config {
|
let (client, listener, rx, closes) = harness(Config {
|
||||||
@@ -1217,6 +1337,7 @@ mod tests {
|
|||||||
let c = client.clone();
|
let c = client.clone();
|
||||||
let connecting = std::thread::spawn(move || c.connect());
|
let connecting = std::thread::spawn(move || c.connect());
|
||||||
let mut srv = accept(&listener);
|
let mut srv = accept(&listener);
|
||||||
|
assert_eq!(srv.line(), "CAP LS 302");
|
||||||
assert_eq!(srv.line(), "PASS pw");
|
assert_eq!(srv.line(), "PASS pw");
|
||||||
assert_eq!(srv.line(), "NICK me");
|
assert_eq!(srv.line(), "NICK me");
|
||||||
assert_eq!(srv.line(), "USER me * * me");
|
assert_eq!(srv.line(), "USER me * * me");
|
||||||
|
|||||||
@@ -466,6 +466,11 @@ pub struct IrcConfig {
|
|||||||
pub nick_serv_identify: bool,
|
pub nick_serv_identify: bool,
|
||||||
pub nick_serv_account: String,
|
pub nick_serv_account: String,
|
||||||
pub nick_serv_mode: SecretMode,
|
pub nick_serv_mode: SecretMode,
|
||||||
|
/// Log in with SASL PLAIN during registration (needs the network to offer it).
|
||||||
|
pub sasl: bool,
|
||||||
|
/// SASL account (defaults to the nick).
|
||||||
|
pub sasl_account: String,
|
||||||
|
pub sasl_mode: SecretMode,
|
||||||
#[serde(deserialize_with = "nd")]
|
#[serde(deserialize_with = "nd")]
|
||||||
pub auto_join_rooms: Vec<String>,
|
pub auto_join_rooms: Vec<String>,
|
||||||
pub ssh_tunnel: bool,
|
pub ssh_tunnel: bool,
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ pub enum SecretKind {
|
|||||||
RdpSshPassword,
|
RdpSshPassword,
|
||||||
RdpSshPassphrase,
|
RdpSshPassphrase,
|
||||||
RdpGatewayPassword,
|
RdpGatewayPassword,
|
||||||
|
IrcSaslPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SecretKind {
|
impl SecretKind {
|
||||||
/// Every kind, so deleting a profile can purge them all from the keychain. A new variant
|
/// Every kind, so deleting a profile can purge them all from the keychain. A new variant
|
||||||
/// must be added here as well as to `as_str`.
|
/// must be added here as well as to `as_str`.
|
||||||
pub const ALL: [SecretKind; 16] = [
|
pub const ALL: [SecretKind; 17] = [
|
||||||
Self::Password,
|
Self::Password,
|
||||||
Self::Passphrase,
|
Self::Passphrase,
|
||||||
Self::Totp,
|
Self::Totp,
|
||||||
@@ -57,6 +58,7 @@ impl SecretKind {
|
|||||||
Self::RdpSshPassword,
|
Self::RdpSshPassword,
|
||||||
Self::RdpSshPassphrase,
|
Self::RdpSshPassphrase,
|
||||||
Self::RdpGatewayPassword,
|
Self::RdpGatewayPassword,
|
||||||
|
Self::IrcSaslPassword,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// The storage name, shared with the Go app.
|
/// The storage name, shared with the Go app.
|
||||||
@@ -78,6 +80,7 @@ impl SecretKind {
|
|||||||
Self::RdpSshPassword => "rdp-ssh-password",
|
Self::RdpSshPassword => "rdp-ssh-password",
|
||||||
Self::RdpSshPassphrase => "rdp-ssh-passphrase",
|
Self::RdpSshPassphrase => "rdp-ssh-passphrase",
|
||||||
Self::RdpGatewayPassword => "rdp-gateway-password",
|
Self::RdpGatewayPassword => "rdp-gateway-password",
|
||||||
|
Self::IrcSaslPassword => "irc-sasl-password",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ impl Manager {
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
let (sasl_account, sasl_password) = if ic.sasl {
|
||||||
|
let account = if ic.sasl_account.trim().is_empty() { ic.nick.clone() } else { ic.sasl_account.trim().to_string() };
|
||||||
|
let pw = self.secret_fn(id, &p.id, SecretKind::IrcSaslPassword, ic.sasl_mode, prompt.clone())().map_err(|e| format!("SASL password: {e}"))?;
|
||||||
|
(account, pw.to_string())
|
||||||
|
} else {
|
||||||
|
(String::new(), String::new())
|
||||||
|
};
|
||||||
let mut rooms = ic.auto_join_rooms.clone();
|
let mut rooms = ic.auto_join_rooms.clone();
|
||||||
if !p.id.is_empty() {
|
if !p.id.is_empty() {
|
||||||
let last = self.store.irc_last_rooms(&p.id).map_err(|e| format!("loading remembered IRC rooms: {e}"))?;
|
let last = self.store.irc_last_rooms(&p.id).map_err(|e| format!("loading remembered IRC rooms: {e}"))?;
|
||||||
@@ -206,6 +213,8 @@ impl Manager {
|
|||||||
nickserv_identify: ic.nick_serv_identify,
|
nickserv_identify: ic.nick_serv_identify,
|
||||||
nickserv_account: ic.nick_serv_account.clone(),
|
nickserv_account: ic.nick_serv_account.clone(),
|
||||||
nickserv_password: ns_pass,
|
nickserv_password: ns_pass,
|
||||||
|
sasl_account,
|
||||||
|
sasl_password,
|
||||||
connector,
|
connector,
|
||||||
connect_timeout: Duration::from_secs(p.connect_timeout.max(1) as u64),
|
connect_timeout: Duration::from_secs(p.connect_timeout.max(1) as u64),
|
||||||
on_event,
|
on_event,
|
||||||
|
|||||||
Reference in New Issue
Block a user