#
# OpenSIPS residential configuration script
#     by OpenSIPS Solutions <team@opensips-solutions.com>
#
# This script was generated via "make menuconfig", from
#   the "Residential" scenario.
# You can enable / disable more features / functionalities by
#   re-generating the scenario with different options.#
#
# Please refer to the Core CookBook at:
#      https://opensips.org/Resources/DocsCookbooks
# for a explanation of possible statements, functions and parameters.
#


####### Global Parameters #########

debug_mode=no
log_stdout = yes
log_level=3
xlog_level=3
stderror_enabled=no
syslog_enabled=yes
syslog_facility=LOG_LOCAL0

udp_workers=4

socket=udp:enp0s3:5060
socket=udp:127.0.0.1:5060

####### Modules Section ########

#set module path
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"

#### SIGNALING module
loadmodule "signaling.so"

#### StateLess module
loadmodule "sl.so"

#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timeout", 5)
modparam("tm", "fr_inv_timeout", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)

#### Record Route Module
loadmodule "rr.so"
modparam("rr", "append_fromtag", 0)

#### MAX ForWarD module
loadmodule "maxfwd.so"

#### SIP MSG OPerationS module
loadmodule "sipmsgops.so"

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

#### USeR LOCation module
loadmodule "usrloc.so"
modparam("usrloc", "nat_bflag", "NAT")
modparam("usrloc", "working_mode_preset", "single-instance-no-db")

#### REGISTRAR module
loadmodule "registrar.so"
modparam("registrar", "tcp_persistent_flag", "TCP_PERSISTENT")
#modparam("registrar", "max_contacts", 10)

loadmodule "proto_udp.so"

#### Dialog module
loadmodule "dialog.so"

#### Exec module
loadmodule "exec.so"

#### JSON module
loadmodule "json.so"

#### Diameter module
loadmodule "aaa_diameter.so"
modparam("aaa_diameter", "fd_log_level", 0)
modparam("aaa_diameter", "realm", "diameter.test")
modparam("aaa_diameter", "peer_identity", "server")
modparam("aaa_diameter", "aaa_url", "diameter:/etc/freeDiameter/freeDiameter.conf;extra-avps-file:/etc/opensips/dictionary.opensips")

####### Routing Logic ########

# main request routing logic

route{

	if (!mf_process_maxfwd_header(10)) {
		send_reply(483,"Too Many Hops");
		exit;
	}

	if (has_totag()) {

		# handle hop-by-hop ACK (no routing required)
		if ( is_method("ACK") && t_check_trans() ) {
			t_relay();
			exit;
		}

		# sequential request within a dialog should
		# take the path determined by record-routing
		if ( !loose_route() ) {
			# we do record-routing for all our traffic, so we should not
			# receive any sequential requests without Route hdr.
			send_reply(404,"Not here");
			exit;
		}

		# route it out to whatever destination was set by loose_route()
		# in $du (destination URI).
		route(relay);
		exit;
	}

	# CANCEL processing
	if (is_method("CANCEL")) {
		if (t_check_trans())
			t_relay();
		exit;
	}

	# absorb retransmissions, but do not create transaction
	t_check_trans();

	if ( !(is_method("REGISTER")  ) ) {
		
		if (is_myself("$fd")) {
					
		} else {
			# if caller is not local, then called number must be local
			
			if (!is_myself("$rd")) {
				send_reply(403,"Relay Forbidden");
				exit;
			}
		}

	}

	# preloaded route checking
	if (loose_route()) {
		xlog("L_ERR",
			"Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
		if (!is_method("ACK"))
			send_reply(403,"Preload Route denied");
		exit;
	}

	# record routing
	if (!is_method("REGISTER|MESSAGE"))
		record_route();

	# account only INVITEs
	if (is_method("INVITE")) {
		
		create_dialog(); 

		# populate dialog variables to use them inside handlers
		$dlg_val(setuptime) = $Ts;
		$dlg_val(callid) = $ci;
		$dlg_val(originhost) = $(ou{uri.host});
		$dlg_val(originrealm) = $fd;
		$dlg_val(account) = $fU;
		$dlg_val(destination) = $rU;

		# use the unix2ntp helper script to convert unix time to 32bit format 
		# for the Event-Timestamp AVP
		# can this be done directly inside the cfg file?
		exec("/usr/local/bin/unix2ntp", "$dlg_val(setuptime)", $var(setuptime32bit), , );

		# Send blocking CCR INITIAL_REQUEST and retrieve MaxUsage
		$var(payload) = "[
			{ \"Session-Id\": \"" + $dlg_val(callid) + "\" },
			{ \"Origin-Host\": \"client.diameter.test\" },
			{ \"Origin-Realm\": \"diameter.test\" },
			{ \"Destination-Realm\": \"diameter.test\" },
			{ \"Service-Context-Id\": \"voice@OsipsDiamCCR\" },
			{ \"CC-Request-Type\": 1 },
			{ \"CC-Request-Number\": 0 },
			{ \"Event-Timestamp\": \"" + $var(setuptime32bit) + "\" },
			{ \"User-Name\": \"" + $dlg_val(account) + "\" },
			{ \"Subscription-Id\": [
				{ \"Subscription-Id-Type\": 0 },  
				{ \"Subscription-Id-Data\": \"" + $dlg_val(account) + "\" }
			]},
			{ \"Requested-Service-Unit\": [
				{\"CC-Time\": 10800}
			]},
			{ \"Service-Information\": [
				{\"IMS-Information\": [
					{ \"Calling-Party-Address\": \"" + $dlg_val(account) + "\"  },
					{ \"Called-Party-Address\": \"" + $dlg_val(destination) + "\"  }
				]}
			]}
		]";

		# send the diameter request
		dm_send_request(4, 272, $var(payload), $var(rpl_avps));

		# handle the diameter reply
		$json(rpl) := $var(rpl_avps);
		if ($rc != 1) {
			xlog("L_NOTICE", "failed to send diameter request: rc=$rc\n");
			send_reply(403, "Forbidden");
			exit;
		}

		# check Result-Code coming from diameter server
		# is there a way to index directly by the name of the key without knowing the index first?
		if ($json(rpl[5]/Result-Code) != 2001) {
			xlog("L_NOTICE", "failed to authorize session (Result-Code=$json(rpl[5]/Result-Code))\n");
			send_reply(403, "Forbidden");
			exit;
		}
		
		# set dialog timeout based on the received Granted-Service-Unit AVP
		$DLG_timeout = $json(rpl[6]/Granted-Service-Unit[0]/CC-Time);

		# dialog handlers
		# only send async requests due to issue detailed at the bottom of the cfg file
		dlg_on_answer("handle_answer");
		dlg_on_hangup("handle_hangup");
		dlg_on_timeout("handle_timeout");
	}

	if (!is_myself("$rd")) {
		append_hf("P-hint: outbound\r\n"); 
		
		route(relay);
	}

	# requests for my domain
	
	if (is_method("PUBLISH|SUBSCRIBE")) {
		send_reply(503, "Service Unavailable");
		exit;
	}

	if (is_method("REGISTER")) {
		# store the registration and generate a SIP reply
		if (!save("location"))
			xlog("failed to register AoR $tu\n");

		exit;
	}

	if ($rU==NULL) {
		# request with no Username in RURI
		send_reply(484,"Address Incomplete");
		exit;
	}

	# do lookup with method filtering
	if (!lookup("location","method-filtering")) {
		t_reply(404, "Not Found");
		exit;
	}

	route(relay);
}

route[handle_answer] {
	$dlg_val(answertime) = $Ts;
	exec("/usr/local/bin/unix2ntp", "$dlg_val(answertime)", $dlg_val(answertime32bit), , );
	$var(payload) = "[
		{ \"Session-Id\": \"" + $dlg_val(callid) + "\" },
		{ \"Origin-Host\": \"client.diameter.test\" },
		{ \"Origin-Realm\": \"diameter.test\" },
		{ \"Destination-Realm\": \"diameter.test\" },
		{ \"Service-Context-Id\": \"voice@OsipsDiamCCR\" },
		{ \"CC-Request-Type\": 2 },
		{ \"CC-Request-Number\": 1 },
		{ \"Event-Timestamp\": \"" + $dlg_val(answertime32bit) + "\" },
		{ \"User-Name\": \"" + $dlg_val(account) + "\" },
		{ \"Subscription-Id\": [
        	{ \"Subscription-Id-Type\": 0 },  
        	{ \"Subscription-Id-Data\": \"" + $dlg_val(account) + "\" }
    	]},
		{ \"Requested-Service-Unit\": [
			{\"CC-Time\": 0}
		]},
		{ \"Service-Information\": [
			{\"IMS-Information\": [
				{ \"Calling-Party-Address\": \"" + $dlg_val(account) + "\"  },
				{ \"Called-Party-Address\": \"" + $dlg_val(destination) + "\"  }
			]}
		]}
	]";

	async(dm_send_request(4, 272, $var(payload), $var(rpl_avps)), dm_reply);
	
}

route[handle_hangup] {
	$var(duration1) = $Ts - $dlg_val(answertime);
	$var(payload) = "[
		{ \"Session-Id\": \"" + $dlg_val(callid) + "\" },
		{ \"Origin-Host\": \"client.diameter.test\" },
		{ \"Origin-Realm\": \"diameter.test\" },
		{ \"Destination-Realm\": \"diameter.test\" },
		{ \"Service-Context-Id\": \"voice@OsipsDiamCCR\" },
		{ \"CC-Request-Type\": 3 },
		{ \"CC-Request-Number\": 2 },
		{ \"Termination-Cause\": 1 },
		{ \"Event-Timestamp\": \"" + $dlg_val(answertime32bit) + "\" },
		{ \"User-Name\": \"" + $dlg_val(account) + "\" },
		{ \"Subscription-Id\": [
        	{ \"Subscription-Id-Type\": 0 },  
        	{ \"Subscription-Id-Data\": \"" + $dlg_val(account) + "\" }
    	]},
		{ \"Used-Service-Unit\": [
			{\"CC-Time\": " + $var(duration1) + " }
		]},
		{ \"Service-Information\": [
			{\"IMS-Information\": [
				{ \"Calling-Party-Address\": \"" + $dlg_val(account) + "\"  },
				{ \"Called-Party-Address\": \"" + $dlg_val(destination) + "\"  }
			]}
		]}
	]";

	async(dm_send_request(4, 272, $var(payload), $var(rpl_avps)), dm_reply);
}

# should be almost identical to handle_hangup. Sends Termination-Cause 6 (DIAMETER_AUTH_EXPIRED) instead of 1 (DIAMETER_LOGOUT)
route[handle_timeout] {
	$var(duration2) = $Ts - $dlg_val(answertime);
	$var(payload) = "[
		{ \"Session-Id\": \"" + $dlg_val(callid) + "\" },
		{ \"Origin-Host\": \"client.diameter.test\" },
		{ \"Origin-Realm\": \"diameter.test\" },
		{ \"Destination-Realm\": \"diameter.test\" },
		{ \"Service-Context-Id\": \"voice@OsipsDiamCCR\" },
		{ \"CC-Request-Type\": 3 },
		{ \"CC-Request-Number\": 2 },
		{ \"Termination-Cause\": 6 },
		{ \"Event-Timestamp\": \"" + $dlg_val(answertime32bit) + "\" },
		{ \"User-Name\": \"" + $dlg_val(account) + "\" },
		{ \"Subscription-Id\": [
        	{ \"Subscription-Id-Type\": 0 },  
        	{ \"Subscription-Id-Data\": \"" + $dlg_val(account) + "\" }
    	]},
		{ \"Used-Service-Unit\": [
			{\"CC-Time\": " + $var(duration2) + " }
		]},
		{ \"Service-Information\": [
			{\"IMS-Information\": [
				{ \"Calling-Party-Address\": \"" + $dlg_val(account) + "\"  },
				{ \"Called-Party-Address\": \"" + $dlg_val(destination) + "\"  }
			]}
		]}
	]";

	async(dm_send_request(4, 272, $var(payload), $var(rpl_avps)), dm_reply);
}

# async dm reply handler
route[dm_reply] {
	$json(rpl) := $var(rpl_avps);
	if ($rc != 1 || $json(rpl[5]/Result-Code) != 2001) {
		xlog("L_NOTICE", "rc=$rc\nResult-Code=$json(rpl[5]/Result-Code)\nCC-Request-Type=$json(rpl[3]/CC-Request-Type)\n");
		exit;
	}
}

route[relay] {
	# for INVITEs enable some additional helper routes
	if (is_method("INVITE")) {
		t_on_branch("per_branch_ops");
		t_on_reply("handle_nat");
		t_on_failure("missed_call");
	}

	if (!t_relay()) {
		send_reply(500,"Internal Error");
	}
	exit;
}

branch_route[per_branch_ops] {
	xlog("new branch at $ru\n");
}

onreply_route[handle_nat] {
	xlog("incoming reply\n");
}

failure_route[missed_call] {
	if (t_was_cancelled()) {
		exit;
	}

	# uncomment the following lines if you want to block client 
	# redirect based on 3xx replies.
	##if (t_check_status("3[0-9][0-9]")) {
	##t_reply(404,"Not found");
	##	exit;
	##}
}



# Tried to add Time-Stamps as an extra field to keep track of SetupTime/AnswerTime and set Event-Timestamp to the time of the event itself. Then the route started to behave weirdly. It was sending only the authorize diameter request and then, when the call was supposed to hangup, it sent the authorize request again.
#
# { \"Time-Stamps\": [
# 	{\"SIP-Request-Timestamp\": " + $dlg_val(setuptime) + " },
# 	{\"SIP-Response-Timestamp\": \"" + $dlg_val(answertime32bit) + "\" }
# ]}

# Had to use async diameter requests, because otherwise it would not do anything on hangup/timeout. It would be stuck in an infinite loop with these (still has them, but it works because it's async):
#
# Apr 30 21:37:15 [26704] DBG:tm:timer_routine: timer routine:3,tl=0x7fd0bb345400 next=(nil), timeout=125
# Apr 30 21:37:15 [26704] DBG:tm:delete_handler: removing 0x7fd0bb345350 
# Apr 30 21:37:15 [26704] DBG:tm:delete_cell: delete_cell 0x7fd0bb345350: can't delete -- still reffed (-1)
# Apr 30 21:37:15 [26704] DBG:tm:set_timer: relative timeout is 2
# Apr 30 21:37:15 [26704] DBG:tm:insert_timer_unsafe: [3]: 0x7fd0bb345400 (127)
# Apr 30 21:37:15 [26704] DBG:tm:delete_handler: done
# Apr 30 21:37:18 [26704] DBG:tm:timer_routine: timer routine:3,tl=0x7fd0bb345400 next=(nil), timeout=127
# Apr 30 21:37:18 [26704] DBG:tm:delete_handler: removing 0x7fd0bb345350 
# Apr 30 21:37:18 [26704] DBG:tm:delete_cell: delete_cell 0x7fd0bb345350: can't delete -- still reffed (-1)
# Apr 30 21:37:18 [26704] DBG:tm:set_timer: relative timeout is 2
# Apr 30 21:37:18 [26704] DBG:tm:insert_timer_unsafe: [3]: 0x7fd0bb345400 (129)
# Apr 30 21:37:18 [26704] DBG:tm:delete_handler: done
# Apr 30 21:37:20 [26704] DBG:tm:timer_routine: timer routine:3,tl=0x7fd0bb345400 next=(nil), timeout=129
# Apr 30 21:37:20 [26704] DBG:tm:delete_handler: removing 0x7fd0bb345350 
# Apr 30 21:37:20 [26704] DBG:tm:delete_cell: delete_cell 0x7fd0bb345350: can't delete -- still reffed (-1)
# Apr 30 21:37:20 [26704] DBG:tm:set_timer: relative timeout is 2
# Apr 30 21:37:20 [26704] DBG:tm:insert_timer_unsafe: [3]: 0x7fd0bb345400 (131)
# Apr 30 21:37:20 [26704] DBG:tm:delete_handler: done
