Add support for SASL PLAIN

This commit is contained in:
Simon Ser 2020-06-12 18:17:49 +02:00
parent 6787f90bdf
commit 69f0658b1e
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
2 changed files with 109 additions and 5 deletions

View File

@ -4,11 +4,14 @@ var server = {
realname: null, realname: null,
nick: null, nick: null,
pass: null, pass: null,
saslPlain: null,
autojoin: [], autojoin: [],
}; };
var ws = null; var ws = null;
var registered = false;
var availableCaps = {}; var availableCaps = {};
var enabledCaps = {};
var buffers = {}; var buffers = {};
var activeBuffer = null; var activeBuffer = null;
@ -188,7 +191,7 @@ function addAvailableCaps(s) {
l.forEach(function(s) { l.forEach(function(s) {
var parts = s.split("="); var parts = s.split("=");
var k = parts[0]; var k = parts[0];
var v = null; var v = "";
if (parts.length > 1) { if (parts.length > 1) {
v = parts[1]; v = parts[1];
} }
@ -204,20 +207,74 @@ function handleCap(msg) {
addAvailableCaps(args[args.length - 1]); addAvailableCaps(args[args.length - 1]);
if (args[0] != "*") { if (args[0] != "*") {
console.log("Available server caps:", availableCaps); console.log("Available server caps:", availableCaps);
var reqCaps = [];
var saslCap = availableCaps["sasl"];
var supportsSaslPlain = (saslCap !== undefined);
if (saslCap.length > 0) {
supportsSaslPlain = saslCap.split(",").includes("PLAIN");
}
var capEnd = true;
if (server.saslPlain && supportsSaslPlain) {
// CAP END is deferred after authentication finishes
reqCaps.push("sasl");
capEnd = false;
}
if (reqCaps.length > 0) {
sendMessage({ command: "CAP", params: ["REQ"].concat(reqCaps) });
}
if (!registered && capEnd) {
sendMessage({ command: "CAP", params: ["END"] }); sendMessage({ command: "CAP", params: ["END"] });
} }
}
break; break;
case "NEW": case "NEW":
addAvailableCaps(args[0]); addAvailableCaps(args[0]);
console.log("Server added available caps:", args[0]); console.log("Server added available caps:", args[0]);
break; break;
case "DEL": case "DEL":
args[0].split(" ").forEach(function(k) { args[0].split(" ").forEach(function(cap) {
delete availableCaps[k]; delete availableCaps[cap];
delete enabledCaps[cap];
}); });
console.log("Server removed available caps:", args[0]); console.log("Server removed available caps:", args[0]);
break; break;
case "ACK":
console.log("Server ack'ed caps:", args[0]);
args[0].split(" ").forEach(function(cap) {
enabledCaps[cap] = true;
if (cap == "sasl" && server.saslPlain) {
console.log("Starting SASL PLAIN authentication");
sendMessage({ command: "AUTHENTICATE", params: ["PLAIN"] });
} }
});
break;
case "NAK":
console.log("Server nak'ed caps:", args[0]);
if (!registered) {
sendMessage({ command: "CAP", params: ["END"] });
}
break;
}
}
function handleAuthenticate(msg) {
var challengeStr = msg.params[0];
// For now only PLAIN is supported
if (challengeStr != "+") {
console.error("Expected an empty challenge, got:", challengeStr);
sendMessage({ command: "AUTHENTICATE", params: ["*"] });
return;
}
var respStr = btoa("\0" + server.saslPlain.username + "\0" + server.saslPlain.password);
sendMessage({ command: "AUTHENTICATE", params: [respStr] });
} }
function connect() { function connect() {
@ -249,7 +306,14 @@ function connect() {
switch (msg.command) { switch (msg.command) {
case RPL_WELCOME: case RPL_WELCOME:
if (server.saslPlain && availableCaps["sasl"] === undefined) {
console.error("Server doesn't support SASL PLAIN");
disconnect();
return;
}
console.log("Registration complete"); console.log("Registration complete");
registered = true;
connectElt.style.display = "none"; connectElt.style.display = "none";
if (server.autojoin.length > 0) { if (server.autojoin.length > 0) {
@ -292,6 +356,29 @@ function connect() {
case "CAP": case "CAP":
handleCap(msg); handleCap(msg);
break; break;
case "AUTHENTICATE":
handleAuthenticate(msg);
break;
case RPL_LOGGEDIN:
console.log("Logged in");
break;
case RPL_LOGGEDOUT:
console.log("Logged out");
break;
case RPL_SASLSUCCESS:
console.log("SASL authentication success");
if (!registered) {
sendMessage({ command: "CAP", params: ["END"] });
}
break;
case ERR_NICKLOCKED:
case ERR_SASLFAIL:
case ERR_SASLTOOLONG:
case ERR_SASLABORTED:
case ERR_SASLALREADY:
console.error("SASL error:", msg);
disconnect();
break;
case "NOTICE": case "NOTICE":
case "PRIVMSG": case "PRIVMSG":
var target = msg.params[0]; var target = msg.params[0];
@ -369,6 +456,7 @@ function connect() {
function disconnect() { function disconnect() {
ws.close(1000); ws.close(1000);
registered = false;
} }
function sendMessage(msg) { function sendMessage(msg) {
@ -470,10 +558,17 @@ connectFormElt.onsubmit = function(event) {
server.url = connectFormElt.elements.url.value; server.url = connectFormElt.elements.url.value;
server.nick = connectFormElt.elements.nick.value; server.nick = connectFormElt.elements.nick.value;
server.pass = connectFormElt.elements.password.value;
server.username = connectFormElt.elements.username.value || server.nick; server.username = connectFormElt.elements.username.value || server.nick;
server.realname = connectFormElt.elements.realname.value || server.nick; server.realname = connectFormElt.elements.realname.value || server.nick;
server.saslPlain = null;
if (connectFormElt.elements.password.value) {
server.saslPlain = {
username: server.username,
password: connectFormElt.elements.password.value,
};
}
server.autojoin = []; server.autojoin = [];
connectFormElt.elements.autojoin.value.split(",").forEach(function(ch) { connectFormElt.elements.autojoin.value.split(",").forEach(function(ch) {
ch = ch.trim(); ch = ch.trim();

View File

@ -3,6 +3,15 @@ const RPL_TOPIC = "332";
const RPL_NAMREPLY = "353"; const RPL_NAMREPLY = "353";
const RPL_ENDOFNAMES = "366"; const RPL_ENDOFNAMES = "366";
const ERR_PASSWDMISMATCH = "464"; const ERR_PASSWDMISMATCH = "464";
// https://ircv3.net/specs/extensions/sasl-3.1
const RPL_LOGGEDIN = "900";
const RPL_LOGGEDOUT = "901";
const ERR_NICKLOCKED = "902";
const RPL_SASLSUCCESS = "903";
const ERR_SASLFAIL = "904";
const ERR_SASLTOOLONG = "905";
const ERR_SASLABORTED = "906";
const ERR_SASLALREADY = "907";
function parsePrefix(s) { function parsePrefix(s) {
var prefix = { var prefix = {