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

package sasl_auth;

## SASL authentication script for EPIC5.
## Written by zlonix@efnet, public domain.
##
## Version: 1.0 (January, 2014)
##  - Initial roll-out
##
## Version: 1.1 (March, 2014)
##  - Perl is not required anymore
##
## Version: 1.2 (April, 2014)
##  - Fixed bug with '\' in password (rb skered)
##
## Version: 1.3 (September, 2014)
##  - Fixed bug with reconnection (rb skered)

## This script implements SASL authentication, primarily used on
## Freenode network. Currently only PLAIN method is supported.
##
## !!! WARNING  !!! WARNING !!! WARNING !!! WARNING !!! WARNING !!!
##    Never forget that your password may be intercepted, which
##    is also very easy with the only currently implemented
##    method. Because of that consider using SSL connection, and
##    do not use same password for IRC and anything important,
##    it can be easily read by root user on your server.
##
##    DUE TO UNKNOWN REASONS THIS SCRIPT DOESN'T WORK WITH
##    'FLOODPROT' SCRIPT FROM DEFAULT EPIC DISTRIBUTION.
## !!! WARNING  !!! WARNING !!! WARNING !!! WARNING !!! WARNING !!!
##
## All rules are controlled with 'sasl_auth' alias, which take three or
## four arguments: server (you may use asterisks like a pattern, best
## match wins if there is a tie), method of the authentication
## (currently unchecked on input and always resolved to 'PLAIN'), user
## name and optional password.  If you do not provide the password it
## will be prompted upon connection. Prompted password is not saved in
## any variable.
##
## Be aware that there is some difference between 'server' definition
## across hooks and EPIC itself (server to which you issue /server
## command may be different to which you're actually connecting).  You
## may define your server as 'irc.freenode.net' but by DNS round-robin
## mechanism be connected to 'hobana.freenode.net', for example, and
## hooks won't be thrown.  If in any doubt - use wildcards, or define
## your preferred servers in ircII.servers file under your $IRCLIB to
## escape from DNS round-robin, if desired.
##
## Examples:
##   sasl_auth *.freenode.net plain MyNick MyPassowrd
##   sasl_auth *.other.net garbage TestNick
##
## Put those into your ~/.ircrc (or such) file after loading
## sasl_auth.
##
## Take special considiration if your credentials contain special chars
## like '\' or '$', because, if, for example, you're using PF loader to
## load your .ircrc (or whatever script you use with sasl_auth) - those
## will be evaluated, and password "\test$var" may end up just as
## "test".
## 
## If you specify server under /sasl_auth command, but upon connect it
## won't ACK our request (essentialy meaning it doesn't support
## capabilities) or NAK it script will disconnect your from this server.
##
## Limitation and bugs:
##  - only one and insecure method (freenode disabled blowfish support,
##    so, this may never be implemented);
##  - in case of a long base64 string for authentication it may
##    not fit to the maximum message length and authentication
##    will be failed, it can happen if you have very long
##    password;

alias sasl_auth (server, method, user, password, void) {
	if (@server && @method && @user) {
		if (findw($server $sasl_auth.servers) == -1) {
			@ push(sasl_auth.servers $server);
			@ push(sasl_auth.methods $method);
			@ push(sasl_auth.users $user);
			if (@password) {
				@ push(sasl_auth.passwords $password);
				@ push(sasl_auth.interactive 0);
			} else {
				@ push(sasl_auth.passwords INTERACTIVE);
				@ push(sasl_auth.interactive 1);
			};
		};
	};

};

alias sasl_auth.plain (nick, password, void) {
#	echo nick  - $nick;
#	echo pass  - $password;
#	echo xform - $xform("-CTCP +B64" $^\nick\\0$^\nick\\0$^\password);
#	echo xform - $xform("-b64 +ctcp" $xform("-CTCP +B64" $^\nick\\0$^\nick\\0$^\password));
	return $xform("-CTCP +B64" $^\nick\\0$^\nick\\0$^\password);
};

alias sasl_auth.setstate (server, state, void) {
	@ :enc = encode($server);

	if (state != [null]) {
		assign sasl_auth.status.$enc $state;
	} else {
		assign -sasl_auth.status.$enc;
	};
};

alias sasl_auth.getstate (server, void) {
	@ :enc = encode($server);

	return $sasl_auth.status[$enc];
};

on #-server_established 100 '\\\\[$$sasl_auth.servers\\\\] %' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);
	
	if (state == []) {
		sasl_auth.setstate $server req_sent;

		quote CAP REQ :sasl;
	} else {
		xecho -b sasl_auth: server_established: server $server is in a wrong state [$state] (should be <empty>);
		sasl_auth.setstate $server null;
	};
};

on ^odd_server_stuff '\\\\[$$sasl_auth.servers\\\\] CAP % ACK *sasl*' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);

	if (state == [req_sent]) {
		sasl_auth.setstate $server meth_sent;
		@ :idx = rmatch($server $sasl_auth.servers)-1;

		quote AUTHENTICATE $toupper($word($idx $sasl_auth.methods));
	} else {
		xecho -b sasl_auth: odd_server_stuff: CAP ACK: server $server \(real: $0\) is in a wrong state [$state] (should be "req_sent");
		sasl_auth.setstate $server null;
	};

};

alias sasl_auth.sendauth (auth, void) {
	quote AUTHENTICATE $auth;
};

on ^odd_server_stuff '\* AUTHENTICATE \+' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);

	if (state == [meth_sent]) {
		sasl_auth.setstate $server auth_sent;
		@ :idx = rmatch($server $sasl_auth.servers)-1;
		@ :method = word($idx $sasl_auth.methods);
		@ :nick = word($idx $sasl_auth.users);
		@ :pass = word($idx $sasl_auth.passwords);
		@ :interactive = word($idx $sasl_auth.interactive);
		
		if (interactive == 1) {
			input -noecho "SASL password: " {
				## local variables of the hook are not accessible here
				@ :pass = *0;
				@ :idx = rmatch($servername() $sasl_auth.servers)-1;
				@ :nick = word($idx $sasl_auth.users);
				@ :auth = sasl_auth.plain($nick $pass);
				sasl_auth.sendauth $auth;
			};
		} else {
			@ :auth = sasl_auth.plain($nick $pass);
			sasl_auth.sendauth $auth;
		};
	} else {
		xecho -b sasl_auth: odd_server_stuff: AUTHENTICATE: server $server \(real: $0\) is in a wrong state [$state] (should be "meth_sent");
		sasl_auth.setstate $server null;
	};
};

on -901 '\\\\[$$sasl_auth.servers\\\\] *' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);

	if (state == [auth_sent]) {
		sasl_auth.setstate $server null;
		disconnect;
	} else {
		xecho -b sasl_auth: 901: server $server \(real: $0\) is in a wrong state [$state] (should be "auth_sent");
		sasl_auth.setstate $server null;
	};
};

on -903 '\\\\[$$sasl_auth.servers\\\\] *' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);

	if (state == [auth_sent]) {
		sasl_auth.setstate $server null;
		quote CAP END;
	} else {
		xecho -b sasl_auth: 903: server $server \(real: $0\) is in a wrong state [$state] (should be "auth_sent");
		sasl_auth.setstate $server null;
	};
};

on #-001 100 '\\\\[$$sasl_auth.servers\\\\] *' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);
	
	if (state != []) {
		xecho -b sasl_auth: 001: server $server \(real: $0\) in a state [$state] and do not support capabilities (001 received without ACK/NAK);
		xecho -b sasl_auth: odd_server_stuff: CAP NAK: disconnecting from $server \(real: $0\);
		disconnect;
	};

	sasl_auth.setstate $server null;
};

on ^odd_server_stuff '\\\\[$$sasl_auth.servers\\\\] CAP * NAK *sasl*' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);
	
	xecho -b sasl_auth: odd_server_stuff: CAP NAK: server $server \(real: $0\) in a state [$state] and do not support proposed SASL capability (:sasl), NAK received;
	xecho -b sasl_auth: odd_server_stuff: CAP NAK: disconnecting from $server \(real: $0\);

	sasl_auth.setstate $server null;
	disconnect;
};

on #-server_lost 100 '% \\\\[$$sasl_auth.servers\\\\] *' {
	@ :server = servername();
	@ :state = sasl_auth.getstate($server);
	
	if (state != []) {
		xecho -b sasl_auth: server_lost: server $server \(real: $1\) in a wrong state [$state] (should be <empty>);
	};
	sasl_auth.setstate $server null;
};
