| module SIP_Emulation { |
| |
| /* SIP Emulation, runs on top of SIPmsg_PT. It multiplexes/demultiplexes |
| * the individual calls, so there can be separate TTCN-3 components handling |
| * each of the calls |
| * |
| * The SIP_Emulation.main() function processes SIP message from the SIPmsg |
| * socket via the SIPmsg_PT, and dispatches them to the per-connection components. |
| * |
| * Outbound SIP calls are initiated by sending a PDU_SIP_Request messages |
| * to the component running the SIP_Emulation.main() function. |
| * |
| * For each new inbound call, the SipOps.create_cb() is called. It can create |
| * or resolve a TTCN-3 component, and returns a component reference to which that inbound |
| * call is routed/dispatched. |
| * |
| * If a pre-existing component wants to register to handle a future inbound call, it can |
| * do so by registering an "expect" with the expected destination phone number. This is e.g. useful |
| * if you are simulating MNCC + SIP, and first trigger a connection from MNCC side in a |
| * component which then subsequently should also handle the SIP emulation. |
| * |
| * (C) 2018 by Harald Welte <laforge@gnumonks.org> |
| * All rights reserved. |
| * |
| * Released under the terms of GNU General Public License, Version 2 or |
| * (at your option) any later version. |
| * |
| * SPDX-License-Identifier: GPL-2.0-or-later |
| */ |
| |
| import from SIPmsg_Types all; |
| import from SIPmsg_PortType all; |
| |
| type component SIP_ConnHdlr { |
| /* ports towards SIP Emulation core / call dispatcher */ |
| port SIP_Conn_PT SIP; |
| port SIPEM_PROC_PT SIP_PROC; |
| } |
| |
| /* port between individual per-call components and this dispatcher */ |
| type port SIP_Conn_PT message { |
| inout PDU_SIP_Request, PDU_SIP_Response; |
| } with { extension "internal" }; |
| |
| /* represents a single SIP Call */ |
| type record CallData { |
| /* reference to the instance of the per-connection component */ |
| SIP_ConnHdlr comp_ref, |
| CallidString call_id |
| } |
| |
| type component SIP_Emulation_CT { |
| /* SIP test port on bottom side */ |
| port SIPmsg_PT SIP; |
| /* SIP port to the per-call clients */ |
| port SIP_Conn_PT CLIENT; |
| |
| var CallData SipCallTable[16]; |
| var ExpectData SipExpectTable[16]; |
| |
| /* procedure based port to register for incoming connections */ |
| port SIPEM_PROC_PT CLIENT_PROC; |
| }; |
| |
| template RequestLine tr_ReqLine(template Method method) := { |
| method := method, |
| requestUri := ?, |
| sipVersion := ? |
| } |
| |
| private template PDU_SIP_Request tr_SIP_REGISTER := { |
| requestLine := tr_ReqLine(REGISTER_E), |
| msgHeader := t_SIP_msgHeader_any, |
| messageBody := *, |
| payload := * |
| } |
| |
| private template PDU_SIP_Request tr_SIP_INVITE := { |
| requestLine := tr_ReqLine(INVITE_E), |
| msgHeader := t_SIP_msgHeader_any, |
| messageBody := *, |
| payload := * |
| } |
| |
| template SipUrl tr_SIP_Url(template charstring user_or_num, |
| template charstring host := *, |
| template integer portField := *) := { |
| scheme := "sip", |
| userInfo := { |
| userOrTelephoneSubscriber := user_or_num, |
| password := * |
| }, |
| hostPort := { |
| host := host, |
| portField := portField |
| }, |
| urlParameters := *, |
| headers := * |
| } |
| template (value) SipUrl ts_SIP_Url(charstring user_or_num, |
| template (omit) charstring host := omit, |
| template (omit) integer portField := omit) := { |
| scheme := "sip", |
| userInfo := { |
| userOrTelephoneSubscriber := user_or_num, |
| password := omit |
| }, |
| hostPort := { |
| host := host, |
| portField := portField |
| }, |
| urlParameters := omit, |
| headers := omit |
| } |
| |
| template Addr_Union tr_SIP_Addr(template SipUrl sip_url) := { |
| nameAddr := { |
| displayName := *, |
| addrSpec := sip_url |
| } |
| } |
| template (value) Addr_Union ts_SIP_Addr(template (value) SipUrl sip_url) := { |
| nameAddr := { |
| displayName := omit, |
| addrSpec := sip_url |
| } |
| } |
| |
| template To tr_SIP_To(template Addr_Union addr) := { |
| fieldName := TO_E, |
| addressField := addr, |
| toParams := * |
| } |
| template (value) To ts_SIP_To(template (value) Addr_Union addr) := { |
| fieldName := TO_E, |
| addressField := addr, |
| toParams := omit |
| } |
| |
| /* resolve component reference by connection ID */ |
| private function f_call_id_known(CallidString call_id) |
| runs on SIP_Emulation_CT return boolean { |
| var integer i; |
| for (i := 0; i < sizeof(SipCallTable); i := i+1) { |
| if (SipCallTable[i].call_id == call_id) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /* resolve component reference by connection ID */ |
| private function f_comp_by_call_id(CallidString call_id) |
| runs on SIP_Emulation_CT return SIP_ConnHdlr { |
| var integer i; |
| for (i := 0; i < sizeof(SipCallTable); i := i+1) { |
| if (SipCallTable[i].call_id == call_id) { |
| return SipCallTable[i].comp_ref; |
| } |
| } |
| setverdict(fail, "SIP Call table not found by SIP Call ID ", call_id); |
| mtc.stop; |
| } |
| |
| /* resolve connection ID by component reference */ |
| private function f_call_id_by_comp(SIP_ConnHdlr client) |
| runs on SIP_Emulation_CT return CallidString { |
| for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) { |
| if (SipCallTable[i].comp_ref == client) { |
| return SipCallTable[i].call_id; |
| } |
| } |
| setverdict(fail, "SIP Call table not found by component ", client); |
| mtc.stop; |
| } |
| |
| private function f_expect_table_init() |
| runs on SIP_Emulation_CT { |
| for (var integer i := 0; i < sizeof(SipExpectTable); i := i+1) { |
| SipExpectTable[i].sip_to := omit; |
| SipExpectTable[i].vc_conn := null; |
| } |
| } |
| |
| private function f_call_table_init() |
| runs on SIP_Emulation_CT { |
| for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) { |
| SipCallTable[i].comp_ref := null; |
| SipCallTable[i].call_id := ""; |
| } |
| } |
| |
| private function f_call_table_add(SIP_ConnHdlr comp_ref, CallidString call_id) |
| runs on SIP_Emulation_CT { |
| for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) { |
| if (SipCallTable[i].call_id == "") { |
| SipCallTable[i].comp_ref := comp_ref; |
| SipCallTable[i].call_id := call_id; |
| log("Added SIP Call Table entry [", i, "] for ", call_id, " at ", comp_ref); |
| return; |
| } |
| } |
| testcase.stop("SIP Call table full"); |
| } |
| |
| private function f_call_table_del(CallidString call_id) |
| runs on SIP_Emulation_CT { |
| for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) { |
| if (SipCallTable[i].call_id == call_id) { |
| SipCallTable[i].comp_ref := null; |
| SipCallTable[i].call_id := ""; |
| log("Deleted SIP Call Table entry [", i, "] for ", call_id); |
| return; |
| } |
| } |
| setverdict(fail, "SIP Call table attempt to delete non-existant ", call_id); |
| mtc.stop; |
| } |
| |
| /* call-back type, to be provided by specific implementation; called when new call connection |
| * arrives */ |
| type function SipCreateCallback(PDU_SIP_Request sip_req, charstring id) |
| runs on SIP_Emulation_CT return SIP_ConnHdlr; |
| |
| type record SipOps { |
| SipCreateCallback create_cb |
| }; |
| |
| function f_init_sip(inout SIP_Emulation_CT ct, charstring id := "SIP_EMU") { |
| var SipOps ops := { |
| create_cb := refers(SIP_Emulation.ExpectedCreateCallback) |
| }; |
| |
| ct := SIP_Emulation_CT.create(id) alive; |
| map(ct:SIP, system:SIP); |
| ct.start(SIP_Emulation.main(ops, id)); |
| } |
| |
| function main(SipOps ops, charstring id) |
| runs on SIP_Emulation_CT { |
| |
| f_expect_table_init(); |
| f_call_table_init(); |
| |
| while (true) { |
| var SIP_ConnHdlr vc_hdlr, vc_conn; |
| var PDU_SIP_Request sip_req; |
| var PDU_SIP_Response sip_resp; |
| var SipUrl sip_to; |
| |
| alt { |
| /* SIP INVITE was received on SIP socket/port */ |
| [] SIP.receive(tr_SIP_INVITE) -> value sip_req { |
| var CallidString call_id := sip_req.msgHeader.callId.callid; |
| if (f_call_id_known(call_id)) { |
| /* re-invite? */ |
| vc_conn := f_comp_by_call_id(call_id); |
| } else { |
| /* new INVITE: check expect */ |
| vc_conn := ops.create_cb.apply(sip_req, id); |
| f_call_table_add(vc_conn, call_id); |
| } |
| CLIENT.send(sip_req) to vc_conn; |
| } |
| /* other SIP request was received on SIP socket/port */ |
| [] SIP.receive(PDU_SIP_Request:?) -> value sip_req { |
| var CallidString call_id := sip_req.msgHeader.callId.callid; |
| if (f_call_id_known(call_id)) { |
| vc_conn := f_comp_by_call_id(call_id); |
| CLIENT.send(sip_req) to vc_conn; |
| } else { |
| setverdict(fail, "SIP Request for unknown call ", call_id); |
| mtc.stop; |
| } |
| } |
| /* SIP response was received on SIP socket/port */ |
| [] SIP.receive(PDU_SIP_Response:?) -> value sip_resp { |
| var CallidString call_id := sip_resp.msgHeader.callId.callid; |
| if (f_call_id_known(call_id)) { |
| vc_conn := f_comp_by_call_id(call_id); |
| CLIENT.send(sip_resp) to vc_conn; |
| } else { |
| setverdict(fail, "SIP Response for unknown call ", call_id); |
| mtc.stop; |
| } |
| } |
| |
| /* a ConnHdlr is sending us a SIP REGISTER: Forward to SIP port */ |
| [] CLIENT.receive(tr_SIP_REGISTER) -> value sip_req sender vc_conn { |
| var CallidString call_id := sip_req.msgHeader.callId.callid; |
| if (f_call_id_known(call_id)) { |
| /* re-register */ |
| vc_conn := f_comp_by_call_id(call_id); |
| } else { |
| /* new REGISTER: add to table */ |
| f_call_table_add(vc_conn, call_id); |
| } |
| SIP.send(sip_req); |
| } |
| |
| /* a ConnHdlr is sending us a SIP INVITE: Forward to SIP port */ |
| [] CLIENT.receive(tr_SIP_INVITE) -> value sip_req sender vc_conn { |
| var CallidString call_id := sip_req.msgHeader.callId.callid; |
| if (f_call_id_known(call_id)) { |
| /* re-invite? */ |
| vc_conn := f_comp_by_call_id(call_id); |
| } else { |
| /* new INVITE: add to table */ |
| f_call_table_add(vc_conn, call_id); |
| } |
| SIP.send(sip_req); |
| } |
| /* a ConnHdlr is sending us a SIP request: Forward to SIP port */ |
| [] CLIENT.receive(PDU_SIP_Request:?) -> value sip_req sender vc_conn { |
| SIP.send(sip_req); |
| } |
| /* a ConnHdlr is sending us a SIP request: Forward to SIP port */ |
| [] CLIENT.receive(PDU_SIP_Response:?) -> value sip_resp sender vc_conn { |
| SIP.send(sip_resp); |
| } |
| |
| [] CLIENT_PROC.getcall(SIPEM_register:{?,?}) -> param(sip_to, vc_hdlr) sender vc_conn { |
| f_create_expect(sip_to, vc_hdlr); |
| CLIENT_PROC.reply(SIPEM_register:{sip_to, vc_hdlr}) to vc_conn; |
| } |
| |
| } |
| } |
| } |
| |
| /*********************************************************************** |
| * "Expect" Handling (mapping for expected incoming SIP callds from IUT) |
| ***********************************************************************/ |
| |
| /* data about an expected future incoming connection */ |
| type record ExpectData { |
| /* SIP "To" (destination number) based on which we can match */ |
| SipUrl sip_to optional, |
| /* component reference registered for the connection */ |
| SIP_ConnHdlr vc_conn |
| } |
| |
| /* procedure based port to register for incoming calls */ |
| signature SIPEM_register(SipUrl sip_to, SIP_ConnHdlr vc_conn); |
| |
| type port SIPEM_PROC_PT procedure { |
| inout SIPEM_register; |
| } with { extension "internal" }; |
| |
| |
| /* CreateCallback that can be used as create_cb and will use the expect table */ |
| function ExpectedCreateCallback(PDU_SIP_Request sip_req, charstring id) |
| runs on SIP_Emulation_CT return SIP_ConnHdlr { |
| var SIP_ConnHdlr ret := null; |
| var SipUrl sip_to; |
| var integer i; |
| |
| if (sip_req.requestLine.method != INVITE_E) { |
| setverdict(fail, "SIP ExpectedCreateCallback needs INVITE"); |
| mtc.stop |
| return ret; |
| } |
| sip_to := sip_req.msgHeader.toField.addressField.nameAddr.addrSpec; |
| |
| for (i := 0; i < sizeof(SipExpectTable); i := i+1) { |
| if (not ispresent(SipExpectTable[i].sip_to)) { |
| continue; |
| } |
| /* build a template, use '*' for all 'omit' values */ |
| var template SipUrl t_exp := SipExpectTable[i].sip_to; |
| if (not ispresent(t_exp.hostPort.host)) { |
| t_exp.hostPort.host := *; |
| } |
| if (not ispresent(t_exp.hostPort.portField)) { |
| t_exp.hostPort.portField := *; |
| } else if (valueof(t_exp.hostPort.portField) == 5060) { |
| /* if the port number is 5060, it may be omitted */ |
| t_exp.hostPort.portField := 5060 ifpresent; |
| } |
| if (not ispresent(t_exp.urlParameters)) { |
| t_exp.urlParameters := *; |
| } |
| if (not ispresent(t_exp.headers)) { |
| t_exp.headers := *; |
| } |
| /* match against the constructed template */ |
| if (match(sip_to, t_exp)) { |
| ret := SipExpectTable[i].vc_conn; |
| /* release this entry to be used again */ |
| SipExpectTable[i].sip_to := omit; |
| SipExpectTable[i].vc_conn := null; |
| log("Found SipExpect[", i, "] for ", sip_to, " handled at ", ret); |
| return ret; |
| } |
| } |
| |
| setverdict(fail, "Couldn't find SipExpect for incoming call ", sip_to); |
| mtc.stop |
| return ret; |
| } |
| |
| /* server/emulation side function to create expect */ |
| private function f_create_expect(SipUrl sip_to, SIP_ConnHdlr hdlr) |
| runs on SIP_Emulation_CT { |
| var integer i; |
| for (i := 0; i < sizeof(SipExpectTable); i := i+1) { |
| if (not ispresent(SipExpectTable[i].sip_to)) { |
| SipExpectTable[i].sip_to := sip_to; |
| SipExpectTable[i].vc_conn := hdlr; |
| log("Created SipExpect[", i, "] for ", sip_to, " to be handled at ", hdlr); |
| return; |
| } |
| } |
| testcase.stop("No space left in SipExpectTable"); |
| } |
| |
| /* client/conn_hdlr side function to use procedure port to create expect in emulation */ |
| function f_create_sip_expect(SipUrl sip_to) runs on SIP_ConnHdlr { |
| SIP_PROC.call(SIPEM_register:{sip_to, self}) { |
| [] SIP_PROC.getreply(SIPEM_register:{?,?}) {}; |
| } |
| } |
| |
| |
| |
| } |