if (word(2 $loadinfo()) != [pf]) { load -pf $word(1 $loadinfo()); return; };

package sasl;

#
# Here's the plan
#
# 1. You load this script and create some /sasl rules
# 2. You connect to a server -- the best sasl rule is selected
# 3. EPIC does the dance -- 
#      CAP LS 302 / NICK / USER / CAP SASL / AUTHENTICATE / CAP END
# 4. You go on your merry way.
#
# This is a _Minimum Viable Product_.  I intentionally did not allow perfect
# to be the enemy of good.  If this script does not meet every one of your
# requirements, please engage with me on what you need to do.  Even better,
# send me your patches!
#

#
# This is based on the sasl_* scripts written by zlonix, without whom
# this wouldn't have been possible.
#

#
# We support three kinds of SASL.  
#
#	/sasl *.network.org EXTERNAL filename.pem
#	/sasl *.network.org ECDSA-NIST256P-CHALLENGE username ec2key.pem
#	/sasl *.network.org SCRAM-SHA-512 username password
#	/sasl *.network.org PLAIN username password
#	/sasl *.network.org -EXTERNAL
#	/sasl *.network.org -ECDSA-NIST256P-CHALLENGE
#	/sasl *.network.org -SCRAM-SHA-512
#	/sasl *.network.org -PLAIN
#

#
# THIS SCRIPT HAS OPINIONS...
#
# This script will only attempt one kind of SASL per server connection.
# It prefers EXTERNAL, then ECDSA-NIST256P-CHALLENGE, then SCRAM-SHA-512, 
# then PLAIN.  If that one attempt fails, EPIC will disconnect from the server.
# Because of how EPIC works, it will try the next IP address.
#
# If you are using EXTERNAL or PLAIN, then if necessary your server 
# will connect via TLS even if you didn't specify that.  If TLS does
# not work, then SASL won't be used.
# 
# If you are using ECDSA-NIST256P-CHALLENGE or SCRAM-SHA-512, then you can 
# connect with TLS or not.  EPIC will not force TLS if you use 
# ECDSA-NIST256P-CHALLENGE or SCRAM-SHA-512.
#

@sasl_auth.next_rule = 0;

# XXX TODO - Replace this with something better. :) 
for i from 0 to 100 {
	if (sasl_rules[$i][server] != []) {
		@sasl_auth.next_rule = i + 1;
	};
};

alias sasl (server, type, data) {
	if (server == []) {
		xecho -b SASL rules:;
		for (@:x = 0, x < sasl_auth.next_rule, @x++) {
			if (sasl_rules[$x][server] != []) {
				xecho -b SASL: $sasl_rules[$x][server] $sasl_rules[$x][type] $sasl_rules[$x][data];
			};
		};
	} elsif (left(1 $type) == [-]) {
		@:rtype = rest(1 $type);
		for (@x = 0, x < sasl_auth.next_rule, @x++) {
			if (sasl_rules[$x][server] == server) {
				if (sasl_rules[$x][type] == rtype) {
					@ sasl_rules[$x][server] = [];
					@ sasl_rules[$x][type] = [];
					@ sasl_rules[$x][data] = [];
				};
			};
		};
	} else {
		@ :rule = sasl_auth.next_rule++;
		@sasl_rules[$rule][server] = server;
		@sasl_rules[$rule][type] = type;
		@sasl_rules[$rule][data] = data;
	}
};

alias sasl_auth.which_rule {
	fe (EXTERNAL ECDSA-NIST256P-CHALLENGE SCRAM-SHA-512 PLAIN) t {
		for (@:x = 0, x < sasl_auth.next_rule, @x++) {
			if (sasl_rules[$x][type] == t) {
				if ( (serverctl(get $0 name) =~ sasl_rules[$x][server]) ||
				     (serverctl(get $0 itsname) =~ sasl_rules[$x][server]) ) {
					return $x
				};
			};
		};
	};
};

#
# When the server switches to POLICY, we decide what we want to do (if possible)
# Please remember at this point we don't have the server itsname yet.
# 
on #-server_state -100 "% % POLICY" {
	if ((:rule = sasl_auth.which_rule($0)) == []) {
		xecho -b SASL POLICY - You do not have a SASL policy for server $0;
		return;
	};
	# This is paranoia.
	@ scrambox(RESET);
	xecho -b SASL POLICY - I will be using SASL rule $rule for server $0;
	@ sasl_auth[$0][rule] = rule;

	if (sasl_rules[$rule][type] == [EXTERNAL]) {
		@serverctl(SET $0 SSL IRC-SSL);
		@serverctl(SET $0 PORT 6697);
		@serverctl(SET $0 CERT $sasl_rules[$rule][data]);
	} elsif (sasl_rules[$rule][type] == [ECDSA-NIST256P-CHALLENGE]) {
		# Nothing
	} elsif (sasl_rules[$rule][type] == [SCRAM-SHA-512]) {
		# Nothing
	} elsif (sasl_rules[$rule][type] == [PLAIN]) {
		@serverctl(SET $0 SSL IRC-SSL);
		@serverctl(SET $0 PORT 6697);
	};
};

# I do SASL here
# XXX I should be deciding *which* SASL to do here.
on #-cap -100 "% LS sasl *" {
	if ([$3-]) {
		@ sasl_auth[$servernum()][supported] = split(, $3-);
	};
	if (sasl_auth[$servernum()][rule] != []) {
		@serverctl(SET $servernum() CAP_HOLD 1);
		quote CAP REQ :sasl
	}
};

# You shall not pass!
on #-cap -100  "% NACK sasl" {
	xecho -b I'm sorry, server $servernum() says you can't do SASL;
	disconnect;
};

# You are now welcome to do SASL
on #-cap -100 "% ACK sasl" {
	if ((:rule = sasl_auth[$servernum()][rule]) == []) {
		xecho -b No SASL rule found for server $servernum();
		return;
	};

	# We stored the supported SASL types in $sasl_auth[$servernum()][supported]
	# TODO - Reconsider what to use if the server doesn't support our first choice

	# If the server says it supported the kind we want, then go with it.
	if (findw($sasl_rules[$rule][type] $sasl_auth[$servernum()][supported]) >= 0) {
		xecho -b Now beginning SASL $sasl_rules[$rule][type] negotiation;
		quote AUTHENTICATE $sasl_rules[$rule][type];
	} else {
		xecho -b The server supports '$sasl_auth[$servernum()][supported]', but i wanted '$sasl_rules[$rule][type]'.
		sasl_auth.failed;
	};
};

# That's fine, go ahead
on #-authenticate -100 "% +" {
	if ((:rule = sasl_auth[$servernum()][rule]) == []) {
		sasl_auth.failed;
	};

	if (sasl_rules[$rule][type] == [EXTERNAL]) {
		quote AUTHENTICATE +;
	} elsif (sasl_rules[$rule][type] == [ECDSA-NIST256P-CHALLENGE]) {
		@ :nick = word(0 $sasl_rules[$rule][data]);
		quote AUTHENTICATE $xform("-CTCP +B64" $^\nick\\0$^\nick\\0);
	} elsif (sasl_rules[$rule][type] == [SCRAM-SHA-512]) {
		@ :nick = word(0 $sasl_rules[$rule][data]);
		@ :pass = word(1 $sasl_rules[$rule][data]);
		@scrambox(RESET);
		@ :x = scrambox(BEGIN "$nick" "$pass");
		if (!x) {
			xecho -b Could not initialize scrambox;
			disconnect;
		} else {
			quote AUTHENTICATE $xform("+B64" n,,$x);
		}
	} elsif (sasl_rules[$rule][type] == [PLAIN]) {
		@ :nick = word(0 $sasl_rules[$rule][data]);
		@ :pass = word(1 $sasl_rules[$rule][data]);
		@ :x = xform("-CTCP +B64" $^\nick\\0$^\nick\\0$^\pass);
		quote AUTHENTICATE $x;
	};
};

on #-authenticate -100 "% *" {
	if ((:rule = sasl_auth[$servernum()][rule]) == []) {
		sasl_auth.failed;
	};

	if (sasl_rules[$rule][type] == [EXTERNAL]) {
		# Nothing -- this makes no sense
	} elsif (sasl_rules[$rule][type] == [ECDSA-NIST256P-CHALLENGE]) {
		@ :pemfile = word(1 $sasl_rules[$rule][data]);
		quote AUTHENTICATE $ecdsatool(sign $pemfile $1-);
	} elsif (sasl_rules[$rule][type] == [SCRAM-SHA-512]) {
		# SCRAM has multiple passes, but the scrambox handles that.
		@ :x = scrambox(RESPONSE $xform("-B64" $1-));
		if (!x) {
			xecho -b Could not authenticate scrambox from server;
			@scrambox(RESET);
			disconnect;
		} else {
			#echo x here is: $x;
			quote AUTHENTICATE $xform("+B64" $x);
		};
	} elsif (sasl_rules[$rule][type] == [PLAIN]) {
		# Nothing -- this makes no sense
	};
};


# Success!
on #-903 -100 * {
	@serverctl(SET $servernum() CAP_HOLD 0);
	if ((:rule = sasl_auth[$servernum()][rule]) == []) {
		sasl_auth.failed;
	};
	if (sasl_rules[$rule][type] == [SCRAM-SHA-512]) {
		@scrambox(RESET);
	};
};

# Failure
on #-904 -100 * {
	echo - Debug - SASL auth failed for >$*<;
	sasl_auth.failed;
};


alias sasl_auth.failed {
	xecho -b SASL Auth failed to server $servernum() - disconnecting.;
	disconnect;
};


