# Init reply list

RAD-Code = Access-Reject,
RAD-Identifier = RAD-Identifier, 
RAD-Authenticator = "" . RAD-Authenticator,	# get a copy

moveall Proxy-State,


# Look up secret and other attributes associated with the client

Sql(str = "select attribute, value from data where space=? and name=?",
    str = "str,IP-Source",
    str = "clients"), delall str, delall REP:int,

REP:Secret or (

    Sql(str = "insert into log (log_when, log_who, log_what) " .
	      "values (now(), ?, ?)", 
	str = "IP-Source,str",
	str = "Client unknown for request from NAS " . 
	      (NAS-Identifier or NAS-IP-Address) . " for user " . User-Name),
    Log-Line = IP-Source . ": " . REQ:str,
    abort
),


# Do accounting for stop records with on-line duplicate detection

RAD-Code == Accounting-Request and (

    # Hack against flooding by some overzealous NAS

    Acct-Status-Type == Accounting-On and acctresp,

    # Verify authenticator

    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)
    or (
	Sql(str = "insert into log (log_when, log_who, log_what) " .
		  "values (now(), ?, ?)", 
	    str = "IP-Source,str",
	    str = "Invalid signature on accounting request from NAS " .
		  (NAS-Identifier or NAS-IP-Address) .
		  " for user " . User-Name),
        Log-Line = IP-Source . ": " . REQ:str,
	acctresp
    ),

    # Perform timeslot end setting and Calling-Station-Id locking

    Acct-Status-Type == Start and (
	Log-Line = "Processed Start record for " . User-Name,
        Sql(str = "lock tables accounts write"), del str, del REP:int,
	Sql(str = "update accounts " .
		  "   set slotexp = unix_timestamp() + slotlength " .
		  " where space = ? " .
		  "   and name  = ? " .
		  "   and slotexp is null and slotlength is not null",
	    str = "str,str",
	    str = (User-Name beforefirst "/" or User-Name afterlast "@"), 
	    str = (User-Name afterfirst "/"  or User-Name beforelast "@" or User-Name)),
	int and Log-Line := REP:Log-Line . "; timeslot started",
	delall str, del REP:int,

	Sql(str = "update accounts " .
		  "   set callerlock = ? " .
		  " where space = ? " .
		  "   and name  = ? " .
		  "   and callerlock is not null " . 
		  "   and callerlock = ''",
	    str = "Calling-Station-Id,str,str",
	    str = (User-Name beforefirst "/" or User-Name afterlast "@"), 
	    str = (User-Name afterfirst "/"  or User-Name beforelast "@" or User-Name)),
	delall str, del REP:int,
	int and Log-Line := REP:Log-Line . "; locked to " . Calling-Station-Id,

        Sql(str = "unlock tables"), del str, del REP:int,
	acctresp
    ),

    # Only continue if Acct-Status-Type is Stop

    Acct-Status-Type == Stop or (
	Log-Line = "Received non-start/stop accounting record for " . User-Name,
	acctresp
    ),

    # Check and store the stop record in one transaction.  Note 1: if you use 
    # multiple prog= lines for connection pooling and/or load sharing, you 
    # *must* define a separate, *single* interface for accounting, because the 
    # next group of statements must use the same session!  

    Sql(str = "lock tables accounting write"), del str, del REP:int,
    # fixme: have radsql return error codes for lock and duplicate key errors!

    Sql(str = "select count(acct_id) as 'int' from accounting " .
	      " where acct_nas = ? " .
	      "   and acct_session_id = ? " .
	      "   and unix_timestamp(acct_timestamp)+120 >= unix_timestamp()",
	str = "str,Acct-Session-Id",
	str = (NAS-Identifier or NAS-IP-Address)), 
    delall str, del REP:int,

    # If count(acct_id) is non-zero, respond to duplicate and we're done

    int and (
	Sql(str = "unlock tables"), del str, del REP:int,
	Log-Line = "Ignored duplicate stop record for " . User-Name,
	acctresp
    ),

    Sql(str = "insert into accounting (acct_nas, acct_session_id, acct_timestamp, user_name, nas_ip_address, nas_port, service_type, framed_protocol, framed_ip_address, framed_ip_netmask, login_ip_host, login_service, login_tcp_port, class, called_station_id, calling_station_id, nas_identifier, nas_port_type, port_limit, acct_status_type, acct_delay_time, acct_input_octets, acct_output_octets, acct_session_time, acct_input_packets, acct_output_packets, acct_terminate_cause, acct_multi_session_id, acct_link_count) values (?, ?, now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
	str = "str,Acct-Session-Id,User-Name,NAS-IP-Address,NAS-Port,Service-Type,Framed-Protocol,Framed-IP-Address,Framed-IP-Netmask,Login-IP-Host,Login-Service,Login-TCP-Port,Class,Called-Station-Id,Calling-Station-Id,NAS-Identifier,NAS-Port-Type,Port-Limit,Acct-Status-Type,Acct-Delay-Time,Acct-Input-Octets,Acct-Output-Octets,Acct-Session-Time,Acct-Input-Packets,Acct-Output-Packets,Acct-Terminate-Cause,Acct-Multi-Session-Id,Acct-Link-Count",
	str = (NAS-Identifier or NAS-IP-Address)
    ), 
    delall str, delall int, delall REP:int,

    # Unlock tables, we're done with the accounting read-update

    Sql(str = "unlock tables"), del str, del REP:int,
    Log-Line = "Processed stop record for " . User-Name,

    # Deduct time from account if we we're keeping a balance, ie. secondsleft
    # is not NULL in db

    Acct-Session-Time and (
       Sql(str = "update accounts " .
		 "   set secondsleft = secondsleft - ? " .
		 " where space = ? " .
		 "   and name  = ? " .
		 "   and secondsleft is not null",
	   str = "Acct-Session-Time,str,str",
	   str = (User-Name beforefirst "/" or User-Name afterlast "@"), 
	   str = (User-Name afterfirst "/"  or User-Name beforelast "@" or User-Name)
       ),
       int and (Log-Line := REP:Log-Line . "; deducted " . Acct-Session-Time . " seconds"),
       delall str, delall int, delall REP:int
    ),

    # Respond to accounting request

    acctresp
),

# Temporarily allow everyone in

#REQ:User-Name := 'EVERYONE',

# Check account. Expired accounts are not even considered and are treated and
# logged exactly as a non-existing account. The timeslot expiry time and the
# current time are compared here, in the behaviour file, so that we can also
# obtain the number of seconds remaining.

Sql(str = "select password as 'clear-password', " .
	  "	  secondsleft as 'Session-Timeout', " .
	  "       unix_timestamp() as Timestamp, " .
	  "       slotexp as date, " .
	  "       slotlength as 'int', " .
	  "       callerlock as 'Calling-Station-Id', " .
	  "       profilename as str " .
	  "  from accounts " .
	  " where space = ? " .
	  "   and name  = ? " .
	  "   and (acctexp is null or " .
	  "	   unix_timestamp(acctexp) > unix_timestamp())",
    str = "str,str",
    str = (User-Name beforefirst "/" or User-Name afterlast "@"), 
    str = (User-Name afterfirst "/"  or User-Name beforelast "@" or User-Name)
),
delall str,
REQ:Timestamp := REP:Timestamp, del Timestamp,	# Move MySQL timestamp to REQ

# Log and reject if no records were returned (last REP:int)

int or (
    del REP:int,
    Sql(str = "insert into log (log_when, log_who, log_what) " .
	      "values (now(), ?, ?)", 
	str = "User-Name,str",
	str = "FAILED: account not found or expired, on NAS " .
	      (NAS-Identifier or NAS-IP-Address) . " via " . IP-Source),
    Reply-Message := REQ:str,
    Log-Line := User-Name . ": " . REQ:str,
    reject
),
del REP:int, 

# Check password (could be combined if fishing usernames is a problem)

clear-password and (

    User-Password and (
	REQ:User-Password := (User-Password ^ md5 (REP:Secret . RAD-Authenticator) . "\x00") beforefirst "\x00"
    ) and User-Password == clear-password or

    CHAP-Password and (
	CHAP-Challenge or REQ:CHAP-Challenge := RAD-Authenticator
    ) and 16 lastof CHAP-Password == md5 (1 firstof CHAP-Password . clear-password . CHAP-Challenge) or (

	Sql(str = "insert into log (log_when, log_who, log_what) " .
		  "values (now(), ?, ?)", 
	    str = "User-Name,str",
	    str = "FAILED: password mismatch for " . User-Password . 
		  " on NAS " . (NAS-Identifier or NAS-IP-Address) . 
		  " via " . IP-Source),
	Reply-Message := REQ:str,
        Log-Line := User-Name . ": " . REQ:str,
	reject
    )
),


# If we have a non-empty REP:Calling-Station-Id from the query above, then
# the received Calling-Station-Id must match, or else we reject 

no REP:Calling-Station-Id or Calling-Station-Id == REP:Calling-Station-Id or (
    Sql(str = "insert into log (log_when, log_who, log_what) " .
	      "values (now(), ?, ?)", 
	str = "User-Name,str",
	str = "FAILED: account is locked to '" . REP:Calling-Station-Id .
	      "', so access from '" . Calling-Station-Id . "' is denied" .
	      " on NAS " . (NAS-Identifier or NAS-IP-Address) . 
	      " via " . IP-Source),
    Reply-Message := REQ:str,
    Log-Line := User-Name . ": " . REQ:str,
    reject
),
del REP:Calling-Station-Id,

# Reject if there is no actual usable time left for this account, if we
# are keeping it (i.e. a Session-Timeout was retrieved from the account)

no REP:Session-Timeout exists or REP:Session-Timeout > 0 or (
    Sql(str = "insert into log (log_when, log_who, log_what) " .
	      "values (now(), ?, ?)", 
	str = "User-Name,str",
	str = "FAILED: no time left on account '" . User-Name . "'," .
	      " on NAS " . (NAS-Identifier or NAS-IP-Address) . 
	      " via " . IP-Source),
    Reply-Message := REQ:str,
    Log-Line = User-Name . ": " . REQ:str,
    reject
),

# If we have a timeslot end, calculate number of seconds left from now;
# reject if this number is zero or negative. Note the trick with replacing
# REP:int, which is initialised from slotlength by the query above: if
# we have a slotend already defined (in date), then we calculate int based
# on that, otherwise we use the slotlength, and failing that, we have no int.

date exists and (REP:int := date - Timestamp) <= 0 and (
    Sql(str = "insert into log (log_when, log_who, log_what) " .
	      "values (now(), ?, ?)", 
	str = "User-Name,str",
	str = "FAILED: timeslot ended at " . date as "%Y-%m-%d %H:%M:%S" . 
	      " for account '" . User-Name . "'," .
	      " on NAS " . (NAS-Identifier or NAS-IP-Address) . 
	      " via " . IP-Source),
    Reply-Message := REQ:str,
    Log-Line = User-Name . ": " . REQ:str,
    reject
),

# If we have a number of seconds remaining in the timeslot, and we haven't
# set a session end based on the number of actual usable seconds left or that
# lies beyond the number of seconds remaining in the timeslot, set the session
# end according to the timeslot end.
 
int exists 
and (no REP:Session-Timeout exists or REP:Session-Timeout > int) 
and Session-Timeout := int,

# Get profile data based on REP:str obtained by previous query

Sql(str = "select attribute, value from data where space=? and name=?",
    str = "str,str",
    str = "profiles",
    str = str), 
delall str, delall REP:int,

# Global defaults here

REP:Session-Timeout exists or Session-Timeout = 86400,

# Log succesful login and reply

Sql(str = "insert into log (log_when, log_who, log_what) " .
	  "values (now(), ?, ?)", 
    str = "User-Name,str",
    str = "Login Successful. Session ends in " . REP:Session-Timeout . 
    	  " seconds, on NAS " .
	  (NAS-Identifier or NAS-IP-Address) . " via " . IP-Source),
Reply-Message = REQ:str,
Log-Line = User-Name . " (" . Calling-Station-Id . "): " . REQ:str,
accept

# vim:sw=4:softtabstop=4:ts=8

