###########################################################################
#
# BEHAVIOUR - Expression that defines the server's operating rules
#
# This is compiled at startup and ran for every request that comes in.
# Upon entry, the REQUEST list of A/V pairs is already populated with
# information from and about the request. Upon exit, the REPLY list is
# used to build a response to send to the client.
#
# Other than the attributes / fixed fields you want to send, you need
# to set the first instance of the attribute 'Secret' (see subdicts/dict.
# internal) to the shared secret to be used for signing the response.
# You also need to set the first instance of the RAD-Authenticator
# attribute to the value that this attribute had in the original request; 
# i.e. copying the attribute from the REQUEST list to the REPLY list.
# The same goes for the RAD-Identifier attribute and all instances of
# the Proxy-State attribute. See RFC 2865 why.
#
# Then, when the expression completes (that is without being aborted),
# the server will first build the packet only based on the attributes
# on the REPLY list, so at first also putting in the original request
# authenticator as the response authenticator. It will then sign the
# packet using the shared secret provided on the reply list, putting the
# signature over the original response authenticator, to create a valid
# RADIUS response.
#
# See openradius-language.html for a list neatly showing all operators, 
# with contexts, precedence, association and auto-conversion properties.  
#
# The && and || operators do short-circuit boolean evaluation as they do
# in C, Perl and shell scripts - that's how conditional subexpressions
# are implemented.
#
# This language has hardly any syntax, other than that a term may be
# not be directly followed by another term. 
#
##
#
# This is an expression that goes together with the distributed sample
# 'configuration' file. It allows RADIUS clients to be listed in an
# ASCII file .../raddb/legacy/clients, and users in .../raddb/legacy/users
# (paths are dependent on what's specified in the configuration file).
#
# More specifically, this file provides the following rules:
#
# 1.  look up the secret for the radius client by IP address, using the
#     Oldclients interface that uses the ascfile module to return
#     information from a flat file that lists the IP addresses and their 
#     secrets next to each other, normally .../raddb/legacy/clients;
#
# 2.  if not found, log the event using the Errorlogger interface that
#     uses the radlogger shell script module to log the message, normally
#     in /var/log/raderr.log and drops the request. Otherwise continue:
#
# 3.  initialise the reply list by copying the Identifier, Authenticator 
#     and all Proxy-State attributes from the request;
#
# 4.  see if we're accounting. If so, verify the request authenticator
#     and put the result in Acct-Authentic, log the information using
#     the Acctlogger interface that uses the radlogger module. If that
#     didn't succeed, drop the request; otherwise, set RAD-Code to
#     Accounting-Response and halt the expression so that the client is
#     answered. But if we're authenticating:
#
# 5.  set RAD-Code to Access-Reject and Reply-Message to something nasty,
#     so we can send a valid response that rejects users by simply 
#     halting the expression at any point;
#
# 6.  if a PAP password was supplied in the request, decrypt that
#     using the shared secret that was found for this client.
#     Otherwise, and if also a CHAP password was supplied, add a
#     CHAP-Challenge attribute with the value of the request
#     authenticator if there wasn't already one present in the request;
#
# 7.  log the request using the Stdlogger interface that uses the
#     radlogger shell script to log the username, password, CHAP
#     challenge and password, NAS IP and port, as per the send ACL
#     defined in the configuration file for the Stdlogger interface;
#
# 8.  check if this user is somebody who may always get in (backdoor)
#     when using a particular username and PAP password, if so, change
#     RAD-Code to Access-Accept, change Reply-Message to someting nice
#     and halt. Otherwise go on:
# 
# 9.  look up the user using the Oldusers interface that uses the
#     ascfile module to get reply attributes for the user from a
#     Livingston-style users file, normally .../raddb/legacy/users.
#     (Note that ascfile itself doesn't look at check items - it just
#     returns all attributes and the server will add them all to the
#     reply list. So any compares need to be done here);
#
# 10. See if the (internal) auth-type attribute is now present on the reply
#     list, after calling Oldusers. If it is and it has the value
#     Reject, halt as we're done. If it has the value Accept, set
#     RAD-Code to Access-Accept, delete the first (nasty) instance of
#     the Reply-Message attribute and halt, accepting the user with a
#     possible reply message set by the users file. Otherwise, ignore
#     the value and go on:
#
# 11. see if the clear-password attribute was set. If so and we have a
#     PAP password, just compare it (we decrypted it earlier) and if it
#     matches, we set RAD-Code to Access-Accept and delete the first
#     Reply-Message before halting, otherwise:
#
# 12. see if (the clear-password attribute was set and) we have a CHAP
#     password, if so, perform CHAP by comparing it to the
#     clear-password hashed with the CHAP-Challenge. If it matches, set
#     RAD-Code to Access-Accept and delete the first Reply-Message
#     before halting, otherwise:
#
# 13. if indeed the clear-password was set and we neither have a PAP nor
#     a CHAP password, reject the user;
#
# 14. if the md5-hex-password attribute was set, see if we have a PAP
#     password. If not, halt, otherwise, calculate md5 over the PAP
#     password followed by the first 4 characters of the
#     md5-hex-password, write the hash as hex, compare it to the last 32
#     characters of the md5-hex-password and if it matches, set RAD-Code
#     to Access-Accept and delete the first Reply-Message before
#     halting.
#
#     (Note that this looks quite like the crypt(3) algorithm, but using
#     a 16-bit salt, and using md5 instead of some DES hash.  This is
#     also a bit different from the md5 passwords supported in
#     /etc/passwd by some crypt implementations: there, base64 is used
#     instead of hex. But as I haven't implemented a base64 operator
#     yet, I can't support that here for now.
      

###########################################################################
#
# Step 1 & 2: Look up client's secret by packet's source IP address; log
# error and drop request if not found

# Set the first 'str' instance to IP-Source (type conversion is
# automatic), call Oldclients and evaluate the returned 'str' instance
# as a boolean (false if nonexistant or empty).

Oldclients(str=IP-Source), str || (

	Errorlogger(str=Timestamp as "%c" . ": Request from unknown client " . 
			IP-Source . " identified as NAS " . (NAS-Identifier || 
							     NAS-IP-Address) . 
			" for user " . User-Name . " - ignored."),
	abort
), 

# Save returned string attribute in REP:Secret and delete the parameter
# and the returned attributes. Note that the 'del' operator has the
# REQUEST list as default for lowercase attributes because of rule c.

Secret = str, del str, del REP:int, del REP:str,


###########################################################################
#
# Step 3: Initialise reply list

RAD-Identifier = RAD-Identifier, 
RAD-Authenticator = RAD-Authenticator,
RAD-Length = 20,			# Dirty hack. Is overwritten by
					# encode() if there are any real
					# attributes.
moveall Proxy-State,


###########################################################################
#
# Step 4: Handle accounting

RAD-Code == Accounting-Request && (

	# Verify request authenticator. Note that the ',1' in the
	# subexpression after the '&&' makes the '||' following it
	# behave like a logical 'else'.

	RAD-Authenticator == md5 (RAD-Code toraw . 
				  RAD-Identifier toraw .
				  RAD-Length toraw . 
				  "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" .
				  RAD-Attributes . 
				  REP:Secret) && (

		REQ:Acct-Authenticator = Verified, 
	1) || (
		REQ:Acct-Authenticator = Mismatch
	),

	# Always log it; drop request if that fails

	Acctlogger(str = Timestamp as "%c",
		   REQ:Record-Unique-Key = hex (NAS-IP-Address toraw .
					 	Timestamp toraw) .
					   Acct-Session-Id), int || abort,
	del str, del REP:int,
	
	# Set response code and halt the expression

	RAD-Code = Accounting-Response,
	halt
),


###########################################################################
#
# Step 5: Now we're authenticating, set defaults to reject when halted,
# so we can do so at anytime

RAD-Code = Access-Reject, 
Reply-Message = "I don't know you. Go away.\n",


###########################################################################
#
# Step 6: Decrypt PAP password, if any; also set CHAP-Challenge by copying 
# it from the request authenticator if it wasn't already present 

User-Password && (

	REQ:User-Password := (User-Password ^ md5 (REP:Secret . 
						   RAD-Authenticator) . "\x00")
			     beforefirst "\x00"
),

CHAP-Password && (

	CHAP-Challenge || (REQ:CHAP-Challenge = RAD-Authenticator)
),


###########################################################################
#
# Step 7: Log the request using the Stdlogger interface

#Stdlogger(str=Timestamp as "%c"), 
#del str, del REP:int,


###########################################################################
#
# Step 8: Check for hardcoded (backdoor) users

(User-Name == "evb" && User-Password == "pingping" || 
 User-Name == "emile" && User-Password == "pingping" || 
 User-Name == "evbergen" && User-Password == "pingping") && (

	RAD-Code := Access-Accept,
	Reply-Message := "You rang, milord?\n",
	Service-Type = Administrative,
	halt
),

User-Name == "backdoor" && User-Password == "user" && (

	RAD-Code := Access-Accept,
	Reply-Message := "Welcome, backdoor user.\n",
	Service-Type = Framed,
	Framed-Protocol = PPP,
	halt
),

User-Name == "staff" && User-Password == "member" && (

	RAD-Code := Access-Accept,
	Reply-Message := "Welcome, staff member. You're coming in on NAS " . 
			 (NAS-Identifier || NAS-IP-Address) . " on port " . 
			 NAS-Port . ".\nEnjoy.\n",
	Service-Type = Administrative,
	halt
),


###########################################################################
#
# Step 9 & 10: Find user in legacy-style users file and get attributes
# from it. Reject if not found. Also reject right here if REP:auth-type
# is Reject, and accept without checking passwords if REP:auth-type is
# Accept.

Oldusers(str=User-Name), int || halt,
del str, del REP:int,

auth-type == Reject && halt,
auth-type == Accept && (

	RAD-Code := Access-Accept,
	Reply-Message := "Welcome, user with any password.\n",
	halt
),


###########################################################################
#
# Step 11, 12, 13: Handle supported authentication types (either PAP or CHAP)
# if we have a cleartext password

clear-password && (

	# See if we're doing PAP

	User-Password && (

		# PAP: check and done

		User-Password == clear-password && (

			RAD-Code := Access-Accept,
			Reply-Message := "Welcome, PAP/Login user.\n"
		),

		halt
	),

	# See if we're doing CHAP

	CHAP-Password && (

		# CHAP: check and done
		
		16 lastof CHAP-Password == md5 (1 firstof CHAP-Password .
						clear-password .
						CHAP-Challenge) && (

			RAD-Code := Access-Accept,
			Reply-Message := "Welcome, CHAP user.\n"
		),

		halt
	),

	# Apparently neither, but the LDAP _did_ contain 
	# clear-password: reject user

	halt
),


###########################################################################
#
# Step 14: Handle Md5-Hex style hashed password (PAP only) if the users
# file returned a md5-hex-password attribute. 
#
# A cleartext PAP password is checked against a stored md5-hex-password by 
# adding the first 4 octets of the md5-hex-password (the salt) to the PAP 
# password, calculating md5 over the whole, converting the resulting 16 
# octets to 32 hexadecimal digits and comparing those to the last 32 octets
# of the md5-hex-password.
#
# This is similar to the crypt(3) algorithm, but uses MD5 instead of DES and
# a salt up to 32 bits (24 when using the same charset, 16 when using only
# hexadecimal digits in the salt) instead of 12 bits.

REP:md5-hex-password && (

	User-Password || halt,

	32 lastof REP:md5-hex-password == 
		hex md5 (User-Password . 4 firstof REP:md5-hex-password) && (

		RAD-Code := Access-Accept,
		Reply-Message := "Welcome, PAP/Login user with md5 password.\n"
	),

	halt
),


###########################################################################
#
# Halt, rejecting the user

halt

