###########################################################################
#
# 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.
#
# Then, when the expression completes without being aborted,
# the server will build the packet based on the attributes
# on the REPLY list, so at first also putting in the original request
# authenticator as the response authenticator. It then signs 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 showing all operators, 
# with contexts, precedence, association and auto-conversion properties.  
# Also read this to understand that Attribute = Attribute references two
# different attributes, and how REQ: and REP: influence that.
#
# The && and || operators do short-circuit boolean evaluation as they do
# in C, Perl and shell scripts - that's how conditional subexpressions
# are implemented. 'and' and 'or' are synonyms.

# First, look up the client's secret by packet's source IP address; 
# log an error and drop the request if not found
#
# Set the first 'str' instance to IP-Source (type conversion is automatic), 
# call Clients and evaluate the returned 'str' instance as a boolean 
# (false if nonexistant or empty).

Clientsfile(str = IP-Source), 
str or (
  Log-Line = "Request received on " . IP-Dest . ":" . UDP-Dest . 
  	     " from unknown client " . IP-Source . " identified as NAS " . 
	     (NAS-Identifier or NAS-IP-Address) . " for user " . User-Name,
  abort
), 

# Save returned string attribute in REP:Secret and delete the 'str' 
# attribute used as parameter for the Clientsfile call and the attributes 
# that were returned. Note that the 'del' operator has the REQUEST list 
# as default for lowercase attributes; it's a 'write' operator after all.

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

# Now we can create legitimate responses, initialise the reply list

RAD-Code = Access-Reject, 
RAD-Identifier = RAD-Identifier, 
RAD-Authenticator = "" . RAD-Authenticator,	# we need a copy
moveall Proxy-State,

# Create the start of a log line. We add to it as we proceed below. 
# The response type and RADIUS code are added by the server.

Log-Line = "from " . IP-Dest . ":" . UDP-Dest . " for request from NAS " . 
	   (NAS-Identifier or NAS-IP-Address) .
	   (NAS-Port exists and (" port " . NAS-Port)) .
	   " via " . IP-Source . " for " . User-Name .
	   (Calling-Station-Id and (" CLI " . Calling-Station-Id)),

# Add NAS-dependent data

Nasesfile(str = (NAS-Identifier or NAS-IP-Address)), 
del str, del REP:int,

# Add realm-dependent data. Also used to find local realms, i.e. realms
# that should be stripped before we process things further. I strongly
# believe in the home server stripping, not the upstream proxy, so that
# the home server can distinguish among multiple services.

str = (User-Name beforefirst "/" or User-Name afterlast "@"),
REQ:str and (
  Realmsfile 0, del REP:int,
  local-realm and REQ:User-Name := (User-Name afterfirst "/" or
				    User-Name beforelast "@" or
				    User-Name)
),
del str,

# We now take one of two separate paths that later join again, 
# depending on whether we received an accounting request or not.

RAD-Code != Accounting-Request and (

  # This is for authentication. Decrypt PAP password, if any; 
  # set CHAP-Challenge by copying it from the request authenticator 
  # if we're doing CHAP and it wasn't already there

  User-Password and (
    REQ:User-Password := (md5 (REP:Secret . RAD-Authenticator) ^ User-Password
    			  . "\x00") beforefirst "\x00",
    Log-Line := REP:Log-Line . " [" . User-Password . "]" 
  ),
  CHAP-Password and (
    CHAP-Challenge or (REQ:CHAP-Challenge = RAD-Authenticator)
  ),

  # Check for hardcoded (backdoor) users. Just a few examples 
  # that must of course be commented out when in production. This may
  # also be useful to allow a telecommuting administrator in when the
  # real backend database is down.
  #
  #(User-Name == "evb" and User-Password == "pingping" or 
  # User-Name == "emile" and User-Password == "pingping" or 
  # User-Name == "evbergen" and User-Password == "pingping") and (
  #
  #  Reply-Message := "You rang, milord?\n",
  #  Service-Type = Administrative,
  #  accept
  #),
  #
  #User-Name == "backdoor" and User-Password == "user" and (
  #
  #  Reply-Message := "Welcome, backdoor user.\n",
  #  Service-Type = Framed,
  #  Framed-Protocol = PPP,
  #  accept
  #),
  #
  #User-Name == "staff" and User-Password == "member" and (
  #
  #  Reply-Message := "Hi, staff member. You're coming in on" . 
  #		      " NAS " . (NAS-Identifier or NAS-IP-Address) . 
  #		      " port " . NAS-Port . ".\nEnjoy.\n",
  #  Service-Type = Administrative,
  #  accept
  #),

  # Find user in users file and get attributes from it. Reject right 
  # here if REP:auth-type is Reject, and accept without checking 
  # passwords if REP:auth-type is Accept.

  Usersfile(str = User-Name), 
  del str, del REP:int,

  auth-type == Reject and reject,
  auth-type == Accept and accept,

  # Handle both supported authentication types (either PAP or CHAP) 
  # if the users file gave us a cleartext password

  clear-password and (

    # See if we're doing PAP

    User-Password exists and (

      # PAP: check and done

      User-Password == clear-password and accept,
      Reply-Message = "PAP authentication failed. Access denied.",
      reject
    ),

    # See if we're doing CHAP

    CHAP-Password exists and (

      # CHAP: check and done

      16 lastof CHAP-Password == md5 (1 firstof CHAP-Password .
				      clear-password . CHAP-Challenge) 
      and accept,
      Reply-Message = "CHAP authentication failed. Access denied.",
      reject
    ),

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

    reject
  ),

  # 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 exists and User-Password exists and (
    32 lastof REP:md5-hex-password == 
        hex md5 (User-Password . 4 firstof REP:md5-hex-password) 
    and accept,
    Reply-Message = "MD5-Hex authentication failed. Access denied.",
    reject
  ),

  # Add other authentication schemes here.

1) or (

  # Handle accounting. First verify request authenticator. Note that 
  # REP:RAD-Authenticator contains a saved copy of REQ:RAD-Authenticator

  REQ:Acct-Authenticator = Mismatch,
  REQ:RAD-Authenticator pokedwith "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
  REP:RAD-Authenticator == md5 (RAD-Packet . REP:Secret) 
  and REQ:Acct-Authenticator := Verified,

  # Next, create unique database key based on NAS, timestamp and session ID.
  # No single NAS will reuse the same session ID in the same second.

  REQ:Record-Unique-Key = hex (NAS-IP-Address toraw . Timestamp toraw) .
			  Acct-Session-Id,

  # Extend summary log line
  Log-Line := REP:Log-Line . " signature " . (Acct-Authenticator and "Verified" or "MISMATCH" ) . " key " . Record-Unique-Key,

  # Always log it; drop request if that fails
  Acctlogger(str = Timestamp as "%c"),
  int or abort,
  del REP:int
),

# If we weren't able yet to answer an authentication request, and for
# all accounting requests, we continue here.
#
# If somehow we obtained one or more Target-Server attributes, proxy.

REP:Target-Server exists and (

  # for such things you'd want negative ACLs, but we don't have them yet.
  delall REQ:RAD-Identifier,
  delall REQ:RAD-Length,
  delall REQ:RAD-Authenticator,
  delall REQ:RAD-Attributes,

  # strip realm before proxying if asked to
  strip-realm and REQ:User-Name := (User-Name afterfirst "/" or
				    User-Name beforelast "@" or
				    User-Name),

  # proxy; drop request if radclient gave us a real error
  Log-Line := REP:Log-Line . " proxied as " . User-Name . " to " . 
  	      REP:Target-Server,
  Radiusclient(moveall REP:Target-Server),
  Log-Line := REP:Log-Line . " resulting in " . REP:RAD-Code . " (" . int . ")",
  int < 64 or abort,
  del REP:int,

  # for such things you'd want negative ACLs, but we don't have them yet.
  del F:RAD-Code,
  del RAD-Identifier,
  del RAD-Length,
  del RAD-Authenticator,
  del RAD-Attributes,

  # We rely on the home server and our receive ACL to keep inappropriate 
  # attributes from rejects and accounting responses. If you want to be 
  # really sure that we follow our own dictionary in this respect, uncomment 
  # the following two lines.
  #REP:RAD-Code == Accounting-Response and acctresp,
  #REP:RAD-Code == Access-Reject and reject,

  halt
),

# Are you still here? We don't really know what to do,
# but these should be some sensible default actions for 
# access requests and accounting requests.

RAD-Code == Accounting-Request and acctresp,
RAD-Code == Access-Request and reject,
abort

