diff --git a/example/lib/src/sip_ua_helper.dart b/example/lib/src/sip_ua_helper.dart index 95ca6f3c..736aa6fc 100644 --- a/example/lib/src/sip_ua_helper.dart +++ b/example/lib/src/sip_ua_helper.dart @@ -50,13 +50,13 @@ class SIPUAHelper extends EventEmitter { start(wsUrl, uri, [password, displayName, wsExtraHeaders]) async { if (this._ua != null) { - debugerror('UA instance already exist!'); - this._ua.start(); - return; + debugerror('UA instance already exist!, stopping UA and creating a new one...'); + this._ua.stop(); + } _settings = new Settings(); var socket = new WebSocketInterface(wsUrl, wsExtraHeaders); - _settings.sockets = [socket]; + _settings.sockets = [socket] ; _settings.uri = uri; _settings.password = password; _settings.display_name = displayName; @@ -254,9 +254,12 @@ class SIPUAHelper extends EventEmitter { } connect(uri, [voiceonly]) async { - if (_ua != null) { + if (_ua != null && _ua.isConnected()) { _session = _ua.call(uri, this.options(voiceonly)); return _session; + }else + { + logger.error("Not connected, you will need to register."); } return null; } diff --git a/lib/src/Config.dart b/lib/src/Config.dart index 6a9f6192..0568e9c5 100644 --- a/lib/src/Config.dart +++ b/lib/src/Config.dart @@ -1,3 +1,5 @@ +import 'package:sip_ua/sip_ua.dart'; + import 'Constants.dart'; import 'Utils.dart' as Utils; import 'Constants.dart' as DartSIP_C; @@ -43,7 +45,7 @@ class Settings { var registrar_server = null; // Connection options. - var sockets = null; + List sockets = null; var connection_recovery_max_interval = 30; var connection_recovery_min_interval = 2; @@ -64,18 +66,16 @@ var settings = new Settings(); // Configuration checks. class Checks { var mandatory = { - 'sockets': (src, dst) { - var sockets = src.sockets; + 'sockets': (Settings src, Settings dst) { + List sockets = src.sockets; /* Allow defining sockets parameter as: * Socket: socket * List of Socket: [socket1, socket2] * List of Objects: [{socket: socket1, weight:1}, {socket: Socket2, weight:0}] * List of Objects and Socket: [{socket: socket1}, socket2] */ - var _sockets = []; - if (sockets != null && Socket.isSocket(sockets)) { - _sockets.add({'socket': sockets}); - } else if (sockets is List && sockets.length > 0) { + List _sockets = []; + if (sockets is List && sockets.length > 0) { for (var socket in sockets) { if (Socket.isSocket(socket)) { _sockets.add(socket); diff --git a/lib/src/Dialog.dart b/lib/src/Dialog.dart index 0bbfc91e..6bfe3b03 100644 --- a/lib/src/Dialog.dart +++ b/lib/src/Dialog.dart @@ -1,11 +1,11 @@ +import 'package:sip_ua/src/transactions/transaction_base.dart'; + import '../sip_ua.dart'; import 'Constants.dart'; +import 'Dialog/RequestSender.dart'; +import 'Exceptions.dart' as Exceptions; import 'RTCSession.dart'; import 'SIPMessage.dart' as SIPMessage; -import 'Constants.dart' as DartSIP_C; -import 'Transactions.dart' as Transactions; -import 'Exceptions.dart' as Exceptions; -import 'Dialog/RequestSender.dart'; import 'Utils.dart' as Utils; import 'logger.dart'; @@ -244,11 +244,11 @@ class Dialog { this._uas_pending_reply = true; var stateChanged = () { if (request.server_transaction.state == - Transactions.C.STATUS_ACCEPTED || + TransactionState.ACCEPTED || request.server_transaction.state == - Transactions.C.STATUS_COMPLETED || + TransactionState.COMPLETED || request.server_transaction.state == - Transactions.C.STATUS_TERMINATED) { + TransactionState.TERMINATED) { this._uas_pending_reply = false; } }; @@ -259,7 +259,7 @@ class Dialog { if (request.hasHeader('contact')) { request.server_transaction.on('stateChanged', () { if (request.server_transaction.state == - Transactions.C.STATUS_ACCEPTED) { + TransactionState.ACCEPTED) { this._remote_target = request.parseHeader('contact').uri; } }); @@ -269,7 +269,7 @@ class Dialog { if (request.hasHeader('contact')) { request.server_transaction.on('stateChanged', () { if (request.server_transaction.state == - Transactions.C.STATUS_COMPLETED) { + TransactionState.COMPLETED) { this._remote_target = request.parseHeader('contact').uri; } }); diff --git a/lib/src/Dialog/RequestSender.dart b/lib/src/Dialog/RequestSender.dart index 1aa477b3..0ff556b6 100644 --- a/lib/src/Dialog/RequestSender.dart +++ b/lib/src/Dialog/RequestSender.dart @@ -1,11 +1,11 @@ +import 'package:sip_ua/src/transactions/transaction_base.dart'; + import '../../sip_ua.dart'; -import '../Constants.dart' as DartSIP_C; import '../Constants.dart'; import '../Dialog.dart'; -import '../SIPMessage.dart'; -import '../Transactions.dart' as Transactions; import '../RTCSession.dart' as RTCSession; import '../RequestSender.dart'; +import '../SIPMessage.dart'; import '../Timers.dart'; // Default event handlers. @@ -64,17 +64,17 @@ class DialogRequestSender { if ((this._request.method == SipMethod.INVITE || (this._request.method == SipMethod.UPDATE && this._request.body != null)) && request_sender.clientTransaction.state != - Transactions.C.STATUS_TERMINATED) { + TransactionState.TERMINATED) { this._dialog.uac_pending_reply = true; var stateChanged; stateChanged = () { if (request_sender.clientTransaction.state == - Transactions.C.STATUS_ACCEPTED || + TransactionState.ACCEPTED || request_sender.clientTransaction.state == - Transactions.C.STATUS_COMPLETED || + TransactionState.COMPLETED || request_sender.clientTransaction.state == - Transactions.C.STATUS_TERMINATED) { + TransactionState.TERMINATED) { request_sender.clientTransaction.remove('stateChanged', stateChanged); this._dialog.uac_pending_reply = false; } diff --git a/lib/src/Grammar.dart b/lib/src/Grammar.dart index 9e977ca0..b412de54 100644 --- a/lib/src/Grammar.dart +++ b/lib/src/Grammar.dart @@ -2,7 +2,7 @@ import 'package:sip_ua/src/grammar_parser.dart'; import "package:parser_error/parser_error.dart"; class Grammar { - static parse(input, startRule) { + static parse(String input,String startRule) { var parser = new GrammarParser(''); var result = parser.parse(input, startRule); if (!parser.success) { diff --git a/lib/src/Message.dart b/lib/src/Message.dart index 91fc7842..98195cbe 100644 --- a/lib/src/Message.dart +++ b/lib/src/Message.dart @@ -107,7 +107,7 @@ class Message extends EventEmitter { request.reply(200); } - this._close(); + this.close(); } /* @@ -187,7 +187,7 @@ class Message extends EventEmitter { this._failed('system', null, DartSIP_C.causes.CONNECTION_ERROR); } - _close() { + close() { this._closed = true; this._ua.destroyMessage(this); } @@ -214,7 +214,7 @@ class Message extends EventEmitter { _failed(originator, response, cause) { debug('MESSAGE failed'); - this._close(); + this.close(); debug('emit "failed"'); @@ -228,7 +228,7 @@ class Message extends EventEmitter { _succeeded(originator, response) { debug('MESSAGE succeeded'); - this._close(); + this.close(); debug('emit "succeeded"'); diff --git a/lib/src/Parser.dart b/lib/src/Parser.dart index 3eb64df1..8c401fb6 100644 --- a/lib/src/Parser.dart +++ b/lib/src/Parser.dart @@ -1,3 +1,6 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/SIPMessage.dart'; + import 'Grammar.dart'; import 'SIPMessage.dart' as SIPMessage; import 'logger.dart'; @@ -9,10 +12,10 @@ debugerror(error) => logger.error(error); /** * Parse SIP Message */ -parseMessage(data, ua) { - var message; - var bodyStart; - var headerEnd = data.indexOf('\r\n'); +IncomingMessage parseMessage(String data, UA ua) { + IncomingMessage message; + int bodyStart; + int headerEnd = data.indexOf('\r\n'); if (headerEnd == -1) { debugerror('parseMessage() | no CRLF found, not a SIP message'); @@ -20,7 +23,7 @@ parseMessage(data, ua) { } // Parse first line. Check if it is a Request or a Reply. - var firstLine = data.substring(0, headerEnd); + String firstLine = data.substring(0, headerEnd); var parsed; try{ parsed = Grammar.parse(firstLine, 'Request_Response'); @@ -36,9 +39,10 @@ parseMessage(data, ua) { return null; } else if (parsed.status_code == null) { - message = new SIPMessage.IncomingRequest(ua); - message.method = parsed.method; - message.ruri = parsed.uri; + IncomingRequest incomingRequest = new SIPMessage.IncomingRequest(ua); + incomingRequest.method = parsed.method; + incomingRequest.ruri = parsed.uri; + message = incomingRequest; } else { message = new SIPMessage.IncomingResponse(); message.status_code = parsed.status_code; @@ -97,13 +101,13 @@ parseMessage(data, ua) { /** * Extract and parse every header of a SIP message. */ -getHeader(data, headerStart) { +getHeader(String data, int headerStart) { // 'start' position of the header. - var start = headerStart; + int start = headerStart; // 'end' position of the header. - var end = 0; + int end = 0; // 'partial end' position of the header. - var partialEnd = 0; + int partialEnd = 0; // End of message. if (data.substring(start, start + 2).contains(new RegExp(r'(^\r\n)'))) { diff --git a/lib/src/RTCSession.dart b/lib/src/RTCSession.dart index 73394e99..e8e4c30b 100644 --- a/lib/src/RTCSession.dart +++ b/lib/src/RTCSession.dart @@ -1,26 +1,25 @@ import 'dart:async'; import 'package:events2/events2.dart'; -import 'package:sdp_transform/sdp_transform.dart' as sdp_transform; import 'package:flutter_webrtc/webrtc.dart'; +import 'package:sdp_transform/sdp_transform.dart' as sdp_transform; +import 'package:sip_ua/src/transactions/transaction_base.dart'; import '../sip_ua.dart'; import 'Constants.dart'; +import 'Constants.dart' as DartSIP_C; +import 'Dialog.dart'; +import 'Exceptions.dart' as Exceptions; import 'RTCSession/DTMF.dart' as RTCSession_DTMF; import 'RTCSession/Info.dart' as RTCSession_Info; import 'RTCSession/ReferNotifier.dart' as RTCSession_ReferNotifier; import 'RTCSession/ReferSubscriber.dart' as RTCSession_ReferSubscriber; - -import 'Constants.dart' as DartSIP_C; -import 'Exceptions.dart' as Exceptions; +import 'RequestSender.dart'; import 'SIPMessage.dart'; -import 'Transactions.dart' as Transactions; -import 'Utils.dart' as Utils; -import 'Timers.dart'; import 'SIPMessage.dart' as SIPMessage; -import 'Dialog.dart'; -import 'RequestSender.dart'; +import 'Timers.dart'; import 'URI.dart'; +import 'Utils.dart' as Utils; import 'logger.dart'; class C { @@ -566,7 +565,7 @@ class RTCSession extends EventEmitter { // Don't ask for video if the incoming offer has no video section. if (mediaStream == null && !peerHasVideoLine) { - mediaConstraints.video = false; + mediaConstraints['video'] = false; } // Create a new RTCPeerConnection instance. @@ -765,7 +764,7 @@ class RTCSession extends EventEmitter { if (this._status == C.STATUS_WAITING_FOR_ACK && this._direction == 'incoming' && this._request.server_transaction.state != - Transactions.C.STATUS_TERMINATED) { + TransactionState.TERMINATED) { /// Save the dialog for later restoration. Dialog dialog = this._dialog; @@ -781,7 +780,7 @@ class RTCSession extends EventEmitter { // .., or when the INVITE transaction times out this._request.server_transaction.on('stateChanged', () { if (this._request.server_transaction.state == - Transactions.C.STATUS_TERMINATED) { + TransactionState.TERMINATED) { this.sendRequest( SipMethod.BYE, {'extraHeaders': extraHeaders, 'body': body}); dialog.terminate(); @@ -2267,7 +2266,9 @@ class RTCSession extends EventEmitter { // Be ready for 200 with SDP after a 180/183 with SDP. // We created a SDP 'answer' for it, so check the current signaling state. if (this._connection.signalingState == - RTCSignalingState.RTCSignalingStateStable) { + RTCSignalingState.RTCSignalingStateStable || + this._connection.signalingState == + RTCSignalingState.RTCSignalingStateHaveLocalOffer) { try { var offer = await this._connection.createOffer(this._rtcOfferConstraints); @@ -2453,7 +2454,7 @@ class RTCSession extends EventEmitter { // Must have SDP answer. if (sdpOffer != null) { - if (!response.body) { + if (response.body !=null && response.body.trim().isNotEmpty) { onFailed(); return; } else if (response.getHeader('Content-Type') != 'application/sdp') { diff --git a/lib/src/RequestSender.dart b/lib/src/RequestSender.dart index bda61f57..b5ca95ee 100644 --- a/lib/src/RequestSender.dart +++ b/lib/src/RequestSender.dart @@ -1,10 +1,13 @@ +import 'package:sip_ua/src/transactions/ack_client.dart'; +import 'package:sip_ua/src/transactions/invite_client.dart'; +import 'package:sip_ua/src/transactions/non_invite_client.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; + import '../sip_ua.dart'; -import 'Constants.dart' as DartSIP_C; import 'Constants.dart'; import 'DigestAuthentication.dart'; -import 'Transactions.dart' as Transactions; import 'UA.dart' as UAC; import 'logger.dart'; @@ -24,7 +27,7 @@ class RequestSender { var _auth; var _challenged; var _staled; - var clientTransaction; + TransactionBase clientTransaction; final logger = new Logger('RequestSender'); debug(msg) => logger.debug(msg); debugerror(error) => logger.error(error); @@ -66,15 +69,15 @@ class RequestSender { switch (this._method) { case SipMethod.INVITE: - this.clientTransaction = new Transactions.InviteClientTransaction( + this.clientTransaction = new InviteClientTransaction( this._ua, this._ua.transport, this._request, eventHandlers); break; case SipMethod.ACK: - this.clientTransaction = new Transactions.AckClientTransaction( + this.clientTransaction = new AckClientTransaction( this._ua, this._ua.transport, this._request, eventHandlers); break; default: - this.clientTransaction = new Transactions.NonInviteClientTransaction( + this.clientTransaction = new NonInviteClientTransaction( this._ua, this._ua.transport, this._request, eventHandlers); } diff --git a/lib/src/SIPMessage.dart b/lib/src/SIPMessage.dart index c5e68516..fb8f54a7 100644 --- a/lib/src/SIPMessage.dart +++ b/lib/src/SIPMessage.dart @@ -323,7 +323,7 @@ class InitialOutgoingInviteRequest extends OutgoingRequest { } class IncomingMessage { - var data; + String data; var headers; SipMethod method; var via; @@ -334,7 +334,7 @@ class IncomingMessage { var from_tag; var to; var to_tag; - var body; + String body; var sdp; var status_code; var reason_phrase; @@ -409,7 +409,7 @@ class IncomingMessage { /** * Verify the existence of the given header. */ - hasHeader(name) { + bool hasHeader(name) { return this.headers.containsKey(Utils.headerize(name)); } diff --git a/lib/src/Transactions.dart b/lib/src/Transactions.dart deleted file mode 100644 index 4fd75929..00000000 --- a/lib/src/Transactions.dart +++ /dev/null @@ -1,692 +0,0 @@ -import 'package:events2/events2.dart'; -import '../sip_ua.dart'; -import 'Constants.dart' as DartSIP_C; -import 'Constants.dart'; -import 'SIPMessage.dart' as SIPMessage; -import 'Timers.dart'; -import 'Transport.dart'; -import 'Utils.dart'; -import 'logger.dart'; - -final nict_logger = new Logger('NonInviteClientTransaction'); -debugnict(msg) => nict_logger.debug(msg); -final ict_logger = new Logger('InviteClientTransaction'); -debugict(msg) => ict_logger.debug(msg); -final act_logger = new Logger('AckClientTransaction'); -debugact(msg) => act_logger.debug(msg); -final nist_logger = new Logger('NonInviteServerTransaction'); -debugnist(msg) => nist_logger.debug(msg); -final ist_logger = new Logger('InviteServerTransaction'); -debugist(msg) => ist_logger.debug(msg); - -class C { - // Transaction states. - static const STATUS_TRYING = 1; - static const STATUS_PROCEEDING = 2; - static const STATUS_CALLING = 3; - static const STATUS_ACCEPTED = 4; - static const STATUS_COMPLETED = 5; - static const STATUS_TERMINATED = 6; - static const STATUS_CONFIRMED = 7; - - // Transaction types. - static const NON_INVITE_CLIENT = 'nict'; - static const NON_INVITE_SERVER = 'nist'; - static const INVITE_CLIENT = 'ict'; - static const INVITE_SERVER = 'ist'; -} - -class NonInviteClientTransaction extends EventEmitter { - var type; - var id; - UA ua; - Transport transport; - var request; - var eventHandlers; - var F, K; - var state; - - NonInviteClientTransaction(UA ua, Transport transport, request, eventHandlers) { - this.type = C.NON_INVITE_CLIENT; - this.id = 'z9hG4bK${Math.floor(Math.random())}'; - this.ua = ua; - this.transport = transport; - this.request = request; - this.eventHandlers = eventHandlers; - - var via = 'SIP/2.0/${transport.via_transport}'; - - via += ' ${ua.configuration.via_host};branch=${this.id}'; - - this.request.setHeader('via', via); - - this.ua.newTransaction(this); - } - - stateChanged(state) { - this.state = state; - this.emit('stateChanged'); - } - - send() { - this.stateChanged(C.STATUS_TRYING); - this.F = setTimeout(() { - this.timer_F(); - }, Timers.TIMER_F); - - if (!this.transport.send(this.request)) { - this.onTransportError(); - } - } - - onTransportError() { - debugnict('transport error occurred, deleting transaction ${this.id}'); - clearTimeout(this.F); - clearTimeout(this.K); - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - this.eventHandlers['onTransportError'](); - } - - timer_F() { - debugnict('Timer F expired for transaction ${this.id}'); - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - this.eventHandlers['onRequestTimeout'](); - } - - timer_K() { - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - - receiveResponse(SIPMessage.IncomingResponse response) { - var status_code = response.status_code; - - if (status_code < 200) { - switch (this.state) { - case C.STATUS_TRYING: - case C.STATUS_PROCEEDING: - this.stateChanged(C.STATUS_PROCEEDING); - this.eventHandlers['onReceiveResponse'](response); - break; - } - } else { - switch (this.state) { - case C.STATUS_TRYING: - case C.STATUS_PROCEEDING: - this.stateChanged(C.STATUS_COMPLETED); - clearTimeout(this.F); - - if (status_code == 408) { - this.eventHandlers['onRequestTimeout'](); - } else { - this.eventHandlers['onReceiveResponse'](response); - } - - this.K = setTimeout(() { - this.timer_K(); - }, Timers.TIMER_K); - break; - case C.STATUS_COMPLETED: - break; - } - } - } -} - -class InviteClientTransaction extends EventEmitter { - var type; - var id; - UA ua; - Transport transport; - var request; - var eventHandlers; - var state; - var B, D, M; - - InviteClientTransaction(UA ua, Transport transport, request, eventHandlers) { - this.type = C.INVITE_CLIENT; - this.id = 'z9hG4bK${Math.floor(Math.random() * 10000000)}'; - this.ua = ua; - this.transport = transport; - this.request = request; - this.eventHandlers = eventHandlers; - request.transaction = this; - - var via = 'SIP/2.0/${transport.via_transport}'; - - via += ' ${ua.configuration.via_host};branch=${this.id}'; - - this.request.setHeader('via', via); - - this.ua.newTransaction(this); - } - - stateChanged(state) { - this.state = state; - this.emit('stateChanged'); - } - - send() { - this.stateChanged(C.STATUS_CALLING); - this.B = setTimeout(() { - this.timer_B(); - }, Timers.TIMER_B); - - if (!this.transport.send(this.request)) { - this.onTransportError(); - } - } - - onTransportError() { - clearTimeout(this.B); - clearTimeout(this.D); - clearTimeout(this.M); - - if (this.state != C.STATUS_ACCEPTED) { - debugict('transport error occurred, deleting transaction ${this.id}'); - this.eventHandlers['onTransportError'](); - } - - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - - // RFC 6026 7.2. - timer_M() { - debugict('Timer M expired for transaction ${this.id}'); - - if (this.state == C.STATUS_ACCEPTED) { - clearTimeout(this.B); - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - } - - // RFC 3261 17.1.1. - timer_B() { - debugict('Timer B expired for transaction ${this.id}'); - if (this.state == C.STATUS_CALLING) { - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - this.eventHandlers['onRequestTimeout'](); - } - } - - timer_D() { - debugict('Timer D expired for transaction ${this.id}'); - clearTimeout(this.B); - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - - sendACK(response) { - var ack = new SIPMessage.OutgoingRequest( - SipMethod.ACK, this.request.ruri, this.ua, { - 'route_set': this.request.getHeaders('route'), - 'call_id': this.request.getHeader('call-id'), - 'cseq': this.request.cseq - }); - - ack.setHeader('from', this.request.getHeader('from')); - ack.setHeader('via', this.request.getHeader('via')); - ack.setHeader('to', response.getHeader('to')); - - this.D = setTimeout(() { - this.timer_D(); - }, Timers.TIMER_D); - - this.transport.send(ack); - } - - cancel(reason) { - // Send only if a provisional response (>100) has been received. - if (this.state != C.STATUS_PROCEEDING) { - return; - } - - var cancel = new SIPMessage.OutgoingRequest( - SipMethod.CANCEL, this.request.ruri, this.ua, { - 'route_set': this.request.getHeaders('route'), - 'call_id': this.request.getHeader('call-id'), - 'cseq': this.request.cseq - }); - - cancel.setHeader('from', this.request.getHeader('from')); - cancel.setHeader('via', this.request.getHeader('via')); - cancel.setHeader('to', this.request.getHeader('to')); - - if (reason != null) { - cancel.setHeader('reason', reason); - } - - this.transport.send(cancel); - } - - receiveResponse(SIPMessage.IncomingResponse response) { - var status_code = response.status_code; - - if (status_code >= 100 && status_code <= 199) { - switch (this.state) { - case C.STATUS_CALLING: - this.stateChanged(C.STATUS_PROCEEDING); - this.eventHandlers['onReceiveResponse'](response); - break; - case C.STATUS_PROCEEDING: - this.eventHandlers['onReceiveResponse'](response); - break; - } - } else if (status_code >= 200 && status_code <= 299) { - switch (this.state) { - case C.STATUS_CALLING: - case C.STATUS_PROCEEDING: - this.stateChanged(C.STATUS_ACCEPTED); - this.M = setTimeout(() { - this.timer_M(); - }, Timers.TIMER_M); - this.eventHandlers['onReceiveResponse'](response); - break; - case C.STATUS_ACCEPTED: - this.eventHandlers['onReceiveResponse'](response); - break; - } - } else if (status_code >= 300 && status_code <= 699) { - switch (this.state) { - case C.STATUS_CALLING: - case C.STATUS_PROCEEDING: - this.stateChanged(C.STATUS_COMPLETED); - this.sendACK(response); - this.eventHandlers['onReceiveResponse'](response); - break; - case C.STATUS_COMPLETED: - this.sendACK(response); - break; - } - } - } -} - -class AckClientTransaction extends EventEmitter { - var id; - Transport transport; - var request; - var eventHandlers; - - AckClientTransaction(UA ua, Transport transport, request, eventHandlers) { - this.id = 'z9hG4bK${Math.floor(Math.random() * 10000000)}'; - this.transport = transport; - this.request = request; - this.eventHandlers = eventHandlers; - - var via = 'SIP/2.0/${transport.via_transport}'; - - via += ' ${ua.configuration.via_host};branch=${this.id}'; - - this.request.setHeader('via', via); - } - - send() { - if (!this.transport.send(this.request)) { - this.onTransportError(); - } - } - - onTransportError() { - debugact('transport error occurred for transaction ${this.id}'); - this.eventHandlers['onTransportError'](); - } -} - -class NonInviteServerTransaction extends EventEmitter { - var type; - var id; - UA ua; - Transport transport; - var request; - var last_response; - var state; - var transportError; - var J; - - NonInviteServerTransaction(UA ua, Transport transport, request) { - this.type = C.NON_INVITE_SERVER; - this.id = request.via_branch; - this.ua = ua; - this.transport = transport; - this.request = request; - this.last_response = ''; - request.server_transaction = this; - - this.state = C.STATUS_TRYING; - - ua.newTransaction(this); - } - - stateChanged(state) { - this.state = state; - this.emit('stateChanged'); - } - - timer_J() { - debugnist('Timer J expired for transaction ${this.id}'); - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - - onTransportError() { - if (this.transportError == null) { - this.transportError = true; - - debugnist('transport error occurred, deleting transaction ${this.id}'); - - clearTimeout(this.J); - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - } - - receiveResponse(status_code, response, onSuccess, onFailure) { - if (status_code == 100) { - /* RFC 4320 4.1 - * 'A SIP element MUST NOT - * send any provisional response with a - * Status-Code other than 100 to a non-INVITE request.' - */ - switch (this.state) { - case C.STATUS_TRYING: - this.stateChanged(C.STATUS_PROCEEDING); - if (!this.transport.send(response)) { - this.onTransportError(); - } - break; - case C.STATUS_PROCEEDING: - this.last_response = response; - if (!this.transport.send(response)) { - this.onTransportError(); - if (onFailure != null) { - onFailure(); - } - } else if (onSuccess != null) { - onSuccess(); - } - break; - } - } else if (status_code >= 200 && status_code <= 699) { - switch (this.state) { - case C.STATUS_TRYING: - case C.STATUS_PROCEEDING: - this.stateChanged(C.STATUS_COMPLETED); - this.last_response = response; - this.J = setTimeout(() { - this.timer_J(); - }, Timers.TIMER_J); - if (!this.transport.send(response)) { - this.onTransportError(); - if (onFailure != null) { - onFailure(); - } - } else if (onSuccess != null) { - onSuccess(); - } - break; - case C.STATUS_COMPLETED: - break; - } - } - } -} - -class InviteServerTransaction extends EventEmitter { - var type; - var id; - UA ua; - Transport transport; - var request; - var last_response; - var state; - var resendProvisionalTimer; - var transportError; - var L, H, I; - - InviteServerTransaction(UA ua, Transport transport, request) { - this.type = C.INVITE_SERVER; - this.id = request.via_branch; - this.ua = ua; - this.transport = transport; - this.request = request; - this.last_response = ''; - request.server_transaction = this; - - this.state = C.STATUS_PROCEEDING; - - ua.newTransaction(this); - - this.resendProvisionalTimer = null; - - request.reply(100); - } - - stateChanged(state) { - this.state = state; - this.emit('stateChanged'); - } - - timer_H() { - debugist('Timer H expired for transaction ${this.id}'); - - if (this.state == C.STATUS_COMPLETED) { - debugist('ACK not received, dialog will be terminated'); - } - - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - - timer_I() { - this.stateChanged(C.STATUS_TERMINATED); - } - - // RFC 6026 7.1. - timer_L() { - debugist('Timer L expired for transaction ${this.id}'); - - if (this.state == C.STATUS_ACCEPTED) { - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - } - - onTransportError() { - if (this.transportError == null) { - this.transportError = true; - - debugist('transport error occurred, deleting transaction ${this.id}'); - - if (this.resendProvisionalTimer != null) { - clearInterval(this.resendProvisionalTimer); - this.resendProvisionalTimer = null; - } - - clearTimeout(this.L); - clearTimeout(this.H); - clearTimeout(this.I); - - this.stateChanged(C.STATUS_TERMINATED); - this.ua.destroyTransaction(this); - } - } - - resend_provisional() { - if (!this.transport.send(this.last_response)) { - this.onTransportError(); - } - } - - // INVITE Server Transaction RFC 3261 17.2.1. - receiveResponse(status_code, response, onSuccess, onFailure) { - if (status_code >= 100 && status_code <= 199) { - switch (this.state) { - case C.STATUS_PROCEEDING: - if (!this.transport.send(response)) { - this.onTransportError(); - } - this.last_response = response; - break; - } - } - - if (status_code > 100 && - status_code <= 199 && - this.state == C.STATUS_PROCEEDING) { - // Trigger the resendProvisionalTimer only for the first non 100 provisional response. - if (this.resendProvisionalTimer == null) { - this.resendProvisionalTimer = setInterval(() { - this.resend_provisional(); - }, Timers.PROVISIONAL_RESPONSE_INTERVAL); - } - } else if (status_code >= 200 && status_code <= 299) { - if (this.state == C.STATUS_PROCEEDING) { - this.stateChanged(C.STATUS_ACCEPTED); - this.last_response = response; - this.L = setTimeout(() { - this.timer_L(); - }, Timers.TIMER_L); - - if (this.resendProvisionalTimer != null) { - clearInterval(this.resendProvisionalTimer); - this.resendProvisionalTimer = null; - } - } - /* falls through */ - if (this.state == C.STATUS_ACCEPTED) { - // Note that this point will be reached for proceeding this.state also. - if (!this.transport.send(response)) { - this.onTransportError(); - if (onFailure != null) { - onFailure(); - } - } else if (onSuccess != null) { - onSuccess(); - } - } - } else if (status_code >= 300 && status_code <= 699) { - switch (this.state) { - case C.STATUS_PROCEEDING: - if (this.resendProvisionalTimer != null) { - clearInterval(this.resendProvisionalTimer); - this.resendProvisionalTimer = null; - } - - if (!this.transport.send(response)) { - this.onTransportError(); - if (onFailure != null) { - onFailure(); - } - } else { - this.stateChanged(C.STATUS_COMPLETED); - this.H = setTimeout(() { - this.timer_H(); - }, Timers.TIMER_H); - if (onSuccess != null) { - onSuccess(); - } - } - break; - } - } - } -} - -/** - * INVITE: - * _true_ if retransmission - * _false_ new request - * - * ACK: - * _true_ ACK to non2xx response - * _false_ ACK must be passed to TU (accepted state) - * ACK to 2xx response - * - * CANCEL: - * _true_ no matching invite transaction - * _false_ matching invite transaction and no final response sent - * - * OTHER: - * _true_ retransmission - * _false_ new request - */ -checkTransaction(_transactions, request) { - var tr; - switch (request.method) { - case SipMethod.INVITE: - tr = _transactions['ist'][request.via_branch]; - if (tr != null) { - switch (tr.state) { - case C.STATUS_PROCEEDING: - tr.transport.send(tr.last_response); - break; - - // RFC 6026 7.1 Invite retransmission. - // Received while in C.STATUS_ACCEPTED state. Absorb it. - case C.STATUS_ACCEPTED: - break; - } - - return true; - } - break; - case SipMethod.ACK: - tr = _transactions['ist'][request.via_branch]; - - // RFC 6026 7.1. - if (tr != null) { - if (tr.state == C.STATUS_ACCEPTED) { - return false; - } else if (tr.state == C.STATUS_COMPLETED) { - tr.state = C.STATUS_CONFIRMED; - tr.I = setTimeout(() { - tr.timer_I(); - }, Timers.TIMER_I); - - return true; - } - } - // ACK to 2XX Response. - else { - return false; - } - break; - case SipMethod.CANCEL: - tr = _transactions['ist'][request.via_branch]; - if (tr != null) { - request.reply_sl(200); - if (tr.state == C.STATUS_PROCEEDING) { - return false; - } else { - return true; - } - } else { - request.reply_sl(481); - return true; - } - break; - default: - // Non-INVITE Server Transaction RFC 3261 17.2.2. - tr = _transactions['nist'][request.via_branch]; - if (tr != null) { - switch (tr.state) { - case C.STATUS_TRYING: - break; - case C.STATUS_PROCEEDING: - case C.STATUS_COMPLETED: - tr.transport.send(tr.last_response); - break; - } - - return true; - } - break; - } - return false; -} diff --git a/lib/src/Transport.dart b/lib/src/Transport.dart index 4f30d7d0..65b4efbd 100644 --- a/lib/src/Transport.dart +++ b/lib/src/Transport.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:sip_ua/src/WebSocketInterface.dart'; + import 'Socket.dart' as Socket; import 'Exceptions.dart' as Exceptions; import 'Utils.dart'; @@ -32,7 +36,7 @@ class C { */ class Transport { var status; - var socket; + WebSocketInterface socket; var sockets; var recovery_options; var recover_attempts; diff --git a/lib/src/UA.dart b/lib/src/UA.dart index 65c42023..f1dc21b4 100644 --- a/lib/src/UA.dart +++ b/lib/src/UA.dart @@ -1,4 +1,11 @@ import 'package:events2/events2.dart'; +import 'package:sip_ua/src/SIPMessage.dart'; +import 'package:sip_ua/src/transactions/Transactions.dart'; +import 'package:sip_ua/src/transactions/invite_client.dart'; +import 'package:sip_ua/src/transactions/invite_server.dart'; +import 'package:sip_ua/src/transactions/non_invite_client.dart'; +import 'package:sip_ua/src/transactions/non_invite_server.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; import 'Config.dart' as config; import 'Config.dart'; @@ -6,18 +13,17 @@ import 'Constants.dart' as DartSIP_C; import 'Constants.dart'; import 'Dialog.dart'; import 'Exceptions.dart' as Exceptions; -import 'Registrator.dart'; -import 'RTCSession.dart'; import 'Message.dart'; -import 'Transactions.dart' as Transactions; -import 'Transport.dart'; -import 'Utils.dart' as Utils; -import 'URI.dart'; import 'Parser.dart' as Parser; +import 'RTCSession.dart'; +import 'Registrator.dart'; import 'SIPMessage.dart' as SIPMessage; import 'Timers.dart'; -import 'sanityCheck.dart'; +import 'Transport.dart'; +import 'URI.dart'; +import 'Utils.dart' as Utils; import 'logger.dart'; +import 'sanityCheck.dart'; class C { // UA status codes. @@ -78,14 +84,14 @@ class UA extends EventEmitter { var _cache; Settings _configuration; var _dynConfiguration; - Map _dialogs; - var _applicants; - Map _sessions = {}; + Map _dialogs; + Set _applicants; + Map _sessions = {}; Transport _transport; Contact _contact; var _status; var _error; - var _transactions; + TransactionBag _transactions = TransactionBag(); var _data; var _closeTimer; var _registrator; @@ -110,7 +116,7 @@ class UA extends EventEmitter { this._contact = null; this._status = C.STATUS_INIT; this._error = null; - this._transactions = {'nist': {}, 'nict': {}, 'ist': {}, 'ict': {}}; + this._transactions = TransactionBag(); // Custom UA empty object for high level use. this._data = {}; @@ -143,7 +149,7 @@ class UA extends EventEmitter { Transport get transport => this._transport; - get transactions => this._transactions; + TransactionBag get transactions => this._transactions; // ============ // High Level API @@ -300,20 +306,15 @@ class UA extends EventEmitter { }); // Run _close_ on every applicant. - for (var applicant in this._applicants) { - if (this._applicants.containsKey(applicant)) - try { - this._applicants[applicant].close(); - } catch (error) {} + for (Message message in this._applicants) { + try { + message.close(); + } catch (error) {} } this._status = C.STATUS_USER_CLOSED; - var num_transactions = this._transactions['nict'].length + - this._transactions['nist'].length + - this._transactions['ict'].length + - this._transactions['ist'].length; - + var num_transactions = this._transactions.countTransactions(); if (num_transactions == 0 && num_sessions == 0) { this._transport.disconnect(); } else { @@ -401,16 +402,16 @@ class UA extends EventEmitter { /** * new Transaction */ - newTransaction(transaction) { - this._transactions[transaction.type][transaction.id] = transaction; + newTransaction(TransactionBase transaction) { + this._transactions.addTransaction(transaction); this.emit('newTransaction', {'transaction': transaction}); } /** * Transaction destroyed. */ - destroyTransaction(transaction) { - this._transactions[transaction.type].remove(transaction.id); + destroyTransaction(TransactionBase transaction) { + this._transactions.removeTransaction(transaction); this.emit('transactionDestroyed', {'transaction': transaction}); } @@ -424,29 +425,29 @@ class UA extends EventEmitter { /** * Dialog destroyed. */ - destroyDialog(dialog) { + destroyDialog(Dialog dialog) { this._dialogs.remove(dialog.id.toString()); } /** * new Message */ - newMessage(message, data) { - this._applicants[message] = message; + newMessage(Message message, data) { + this._applicants.add(message); this.emit('newMessage', data); } /** * Message destroyed. */ - destroyMessage(message) { + destroyMessage(Message message) { this._applicants.remove(message); } /** * new RTCSession */ - newRTCSession(session, data) { + newRTCSession(RTCSession session, data) { this._sessions[session.id] = session; this.emit('newRTCSession', data); } @@ -454,7 +455,7 @@ class UA extends EventEmitter { /** * RTCSession destroyed. */ - destroyRTCSession(session) { + destroyRTCSession(RTCSession session) { this._sessions.remove([session.id]); } @@ -508,19 +509,18 @@ class UA extends EventEmitter { } // Check transaction. - if (Transactions.checkTransaction(_transactions, request)) { + if (checkTransaction(_transactions, request)) { return; } // Create the server transaction. if (method == SipMethod.INVITE) { /* eslint-disable no-new */ - new Transactions.InviteServerTransaction(this, this._transport, request); + new InviteServerTransaction(this, this._transport, request); /* eslint-enable no-new */ } else if (method != SipMethod.ACK && method != SipMethod.CANCEL) { /* eslint-disable no-new */ - new Transactions.NonInviteServerTransaction( - this, this._transport, request); + new NonInviteServerTransaction(this, this._transport, request); /* eslint-enable no-new */ } @@ -845,12 +845,8 @@ class UA extends EventEmitter { // Transport disconnected event. onTransportDisconnect(data) { // Run _onTransportError_ callback on every client transaction using _transport_. - ['nict', 'ict', 'nist', 'ist'].forEach((type) { - if (this._transactions[type] != null) { - this._transactions[type].forEach((id, transaction) { - transaction.onTransportError(); - }); - } + this._transactions.removeAll().forEach((transaction) { + transaction.onTransportError(); }); this.emit('disconnected', data); @@ -867,9 +863,9 @@ class UA extends EventEmitter { // Transport data event. onTransportData(data) { var transport = data['transport']; - var message = data['message']; + String messageData = data['message']; - message = Parser.parseMessage(message, this); + IncomingMessage message = Parser.parseMessage(messageData, this); if (message == null) { return; @@ -894,11 +890,11 @@ class UA extends EventEmitter { * in order to be discarded there. */ - var transaction; - switch (message.method) { case SipMethod.INVITE: - transaction = this._transactions['ict'][message.via_branch]; + InviteClientTransaction transaction = this + ._transactions + .getTransaction(InviteClientTransaction, message.via_branch); if (transaction != null) { transaction.receiveResponse(message); } @@ -907,7 +903,9 @@ class UA extends EventEmitter { // Just in case ;-). break; default: - transaction = this._transactions['nict'][message.via_branch]; + NonInviteClientTransaction transaction = this + ._transactions + .getTransaction(NonInviteClientTransaction, message.via_branch); if (transaction != null) { transaction.receiveResponse(message); } diff --git a/lib/src/WebSocketInterface.dart b/lib/src/WebSocketInterface.dart index ff7939c5..71e7a748 100644 --- a/lib/src/WebSocketInterface.dart +++ b/lib/src/WebSocketInterface.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:sip_ua/src/Timers.dart'; + import 'Socket.dart'; import 'logger.dart'; import 'Grammar.dart'; @@ -81,6 +84,7 @@ class WebSocketInterface implements Socket { _connected = false; this._onClose(true, this._ws.closeCode, this._ws.closeReason); }); + _closed = false; _connected = true; this._onOpen(); } catch (e) { @@ -98,7 +102,9 @@ class WebSocketInterface implements Socket { this._connected = false; this._onClose(true, 0, "Client send disconnect"); try { - this._ws.close(); + if (this._ws != null) { + this._ws.close(); + } } catch (error) { debugerror('close() | error closing the WebSocket: ' + error); } @@ -111,7 +117,17 @@ class WebSocketInterface implements Socket { throw 'transport closed'; } try { - this._ws.add(message); + // temporary diagnostic message add to the end of every SIP message sent + var now = new DateTime.now(); + String tmp = message + + "\nSIP message generated and sent at: ${now}\n"; // + ("A" * 4096); + + this._ws.add(tmp); + setTimeout(() { + // extra message to wake asterisk up + this._ws.add(""); + }, 100); + return true; } catch (error) { logger.failure('send() | error sending message: ' + error.toString()); @@ -124,7 +140,7 @@ class WebSocketInterface implements Socket { } isConnecting() { - return false; // TODO: this._ws && this._ws.readyState == this._ws.CONNECTING; + return this._ws != null && this._ws.readyState == WebSocket.connecting; } /** diff --git a/lib/src/sanityCheck.dart b/lib/src/sanityCheck.dart index 090f73f5..b557bfda 100644 --- a/lib/src/sanityCheck.dart +++ b/lib/src/sanityCheck.dart @@ -1,3 +1,9 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/SIPMessage.dart'; +import 'package:sip_ua/src/Transport.dart'; +import 'package:sip_ua/src/transactions/invite_server.dart'; +import 'package:sip_ua/src/transactions/non_invite_server.dart'; + import 'Constants.dart' as DartSIP_C; import 'Constants.dart'; import 'SIPMessage.dart' as SIPMessage; @@ -22,11 +28,11 @@ const requests = [ const responses = [rfc3261_8_1_3_3, rfc3261_18_3_response]; // local variables. -var message; -var ua; -var transport; +IncomingMessage message; +UA ua; +Transport transport; -sanityCheck(m, u, t) { +sanityCheck(IncomingMessage m, UA u, Transport t) { message = m; ua = u; transport = t; @@ -123,12 +129,12 @@ rfc3261_8_2_2_2() { // If the branch matches the key of any IST then assume it is a retransmission // and ignore the INVITE. // TODO: we should reply the last response. - if (ua.transactions['ist'][message.via_branch] != null) { + if (ua.transactions.getTransaction(InviteServerTransaction,message.via_branch) != null) { return false; } // Otherwise check whether it is a merged request. else { - ua.transactions['ist'].forEach((transaction, tr) { + ua.transactions.getAll(InviteServerTransaction).forEach(( tr) { if (tr.request.from_tag == fromTag && tr.request.call_id == call_id && tr.request.cseq == cseq) { @@ -145,13 +151,13 @@ rfc3261_8_2_2_2() { // If the branch matches the key of any NIST then assume it is a retransmission // and ignore the request. // TODO: we should reply the last response. - else if (ua.transactions['nist'][message.via_branch] != null) { + else if (ua.transactions.getTransaction(NonInviteServerTransaction,message.via_branch) != null) { return false; } // Otherwise check whether it is a merged request. else { - ua.transactions['nist'].forEach((transaction, tr) { + ua.transactions.getAll(NonInviteServerTransaction).forEach(( tr) { if (tr.request.from_tag == fromTag && tr.request.call_id == call_id && tr.request.cseq == cseq) { diff --git a/lib/src/transactions/Transactions.dart b/lib/src/transactions/Transactions.dart new file mode 100644 index 00000000..55e022a8 --- /dev/null +++ b/lib/src/transactions/Transactions.dart @@ -0,0 +1,153 @@ +import 'package:events2/events2.dart'; +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Constants.dart'; +import 'package:sip_ua/src/Timers.dart'; +import 'package:sip_ua/src/transactions/invite_server.dart'; +import 'package:sip_ua/src/transactions/non_invite_server.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; + +class TransactionBag { + Map transactions = {}; + + int countTransactions() { + return transactions.length; + } + + String _buildKey(Type type, String id) { + return type.toString() + ':' + id; + } + + addTransaction(TransactionBase transaction) { + String key = _buildKey(transaction.runtimeType, transaction.id); + transactions[key] = transaction; + } + + removeTransaction(TransactionBase transaction) { + String key = _buildKey(transaction.runtimeType, transaction.id); + transactions.remove(key); + } + + List getAll(Type type) { + List results = []; + + transactions.values.forEach((transaction) { + if (transaction.runtimeType == type) { + results.add(transaction as T); + } + }); + + return results; + } + + T getTransaction(Type type, String id) { + String key = _buildKey(type, id); + return transactions[key] as T; + } + + List removeAll() { + List list = []; + list.addAll(transactions.values); + transactions.clear(); + return list; + } +} + +/** + * INVITE: + * _true_ if retransmission + * _false_ new request + * + * ACK: + * _true_ ACK to non2xx response + * _false_ ACK must be passed to TU (accepted state) + * ACK to 2xx response + * + * CANCEL: + * _true_ no matching invite transaction + * _false_ matching invite transaction and no final response sent + * + * OTHER: + * _true_ retransmission + * _false_ new request + */ +checkTransaction(TransactionBag _transactions, request) { + switch (request.method) { + case SipMethod.INVITE: + InviteServerTransaction tr = _transactions.getTransaction( + InviteServerTransaction, request.via_branch); + if (tr != null) { + switch (tr.state) { + case TransactionState.PROCEEDING: + tr.transport.send(tr.last_response); + break; + + // RFC 6026 7.1 Invite retransmission. + // Received while in TransactionState.ACCEPTED state. Absorb it. + case TransactionState.ACCEPTED: + break; + default: + break; + } + + return true; + } + break; + case SipMethod.ACK: + InviteServerTransaction tr = _transactions.getTransaction( + InviteServerTransaction, request.via_branch); + + // RFC 6026 7.1. + if (tr != null) { + if (tr.state == TransactionState.ACCEPTED) { + return false; + } else if (tr.state == TransactionState.COMPLETED) { + tr.state = TransactionState.CONFIRMED; + tr.I = setTimeout(() { + tr.timer_I(); + }, Timers.TIMER_I); + + return true; + } + } + // ACK to 2XX Response. + else { + return false; + } + break; + case SipMethod.CANCEL: + InviteServerTransaction tr = _transactions.getTransaction( + InviteServerTransaction, request.via_branch); + if (tr != null) { + request.reply_sl(200); + if (tr.state == TransactionState.PROCEEDING) { + return false; + } else { + return true; + } + } else { + request.reply_sl(481); + return true; + } + break; + default: + // Non-INVITE Server Transaction RFC 3261 17.2.2. + NonInviteServerTransaction tr = _transactions.getTransaction( + NonInviteServerTransaction, request.via_branch); + if (tr != null) { + switch (tr.state) { + case TransactionState.TRYING: + break; + case TransactionState.PROCEEDING: + case TransactionState.COMPLETED: + tr.transport.send(tr.last_response); + break; + default: + break; + } + + return true; + } + break; + } + return false; +} diff --git a/lib/src/transactions/ack_client.dart b/lib/src/transactions/ack_client.dart new file mode 100644 index 00000000..e1cf19aa --- /dev/null +++ b/lib/src/transactions/ack_client.dart @@ -0,0 +1,37 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Transport.dart'; +import 'package:sip_ua/src/Utils.dart'; +import 'package:sip_ua/src/transactions/Transactions.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; + +final act_logger = new Logger('AckClientTransaction'); +debugact(msg) => act_logger.debug(msg); + +class AckClientTransaction extends TransactionBase { + var eventHandlers; + + AckClientTransaction(UA ua, Transport transport, request, eventHandlers) { + this.id = 'z9hG4bK${Math.floor(Math.random() * 10000000)}'; + this.transport = transport; + this.request = request; + this.eventHandlers = eventHandlers; + + var via = 'SIP/2.0/${transport.via_transport}'; + + via += ' ${ua.configuration.via_host};branch=${this.id}'; + + this.request.setHeader('via', via); + } + + send() { + if (!this.transport.send(this.request)) { + this.onTransportError(); + } + } + + onTransportError() { + debugact('transport error occurred for transaction ${this.id}'); + this.eventHandlers['onTransportError'](); + } +} + diff --git a/lib/src/transactions/invite_client.dart b/lib/src/transactions/invite_client.dart new file mode 100644 index 00000000..ec915ec9 --- /dev/null +++ b/lib/src/transactions/invite_client.dart @@ -0,0 +1,186 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Constants.dart'; +import 'package:sip_ua/src/Timers.dart'; + +import 'package:sip_ua/src/Transport.dart'; +import 'package:sip_ua/src/Utils.dart'; +import 'package:sip_ua/src/transactions/Transactions.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; +import '../SIPMessage.dart' as SIPMessage; + + +final ict_logger = new Logger('InviteClientTransaction'); +debugict(msg) => ict_logger.debug(msg); + +class InviteClientTransaction extends TransactionBase { + var eventHandlers; + + var B, D, M; + + InviteClientTransaction(UA ua, Transport transport, request, eventHandlers) { + this.id = 'z9hG4bK${Math.floor(Math.random() * 10000000)}'; + this.ua = ua; + this.transport = transport; + this.request = request; + this.eventHandlers = eventHandlers; + request.transaction = this; + + var via = 'SIP/2.0/${transport.via_transport}'; + + via += ' ${ua.configuration.via_host};branch=${this.id}'; + + this.request.setHeader('via', via); + + this.ua.newTransaction(this); + } + + stateChanged(TransactionState state) { + this.state = state; + this.emit('stateChanged'); + } + + send() { + this.stateChanged(TransactionState.CALLING); + this.B = setTimeout(() { + this.timer_B(); + }, Timers.TIMER_B); + + if (!this.transport.send(this.request)) { + this.onTransportError(); + } + } + + onTransportError() { + clearTimeout(this.B); + clearTimeout(this.D); + clearTimeout(this.M); + + if (this.state != TransactionState.ACCEPTED) { + debugict('transport error occurred, deleting transaction ${this.id}'); + this.eventHandlers['onTransportError'](); + } + + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + + // RFC 6026 7.2. + timer_M() { + debugict('Timer M expired for transaction ${this.id}'); + + if (this.state == TransactionState.ACCEPTED) { + clearTimeout(this.B); + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + } + + // RFC 3261 17.1.1. + timer_B() { + debugict('Timer B expired for transaction ${this.id}'); + if (this.state == TransactionState.CALLING) { + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + this.eventHandlers['onRequestTimeout'](); + } + } + + timer_D() { + debugict('Timer D expired for transaction ${this.id}'); + clearTimeout(this.B); + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + + sendACK(response) { + var ack = new SIPMessage.OutgoingRequest( + SipMethod.ACK, this.request.ruri, this.ua, { + 'route_set': this.request.getHeaders('route'), + 'call_id': this.request.getHeader('call-id'), + 'cseq': this.request.cseq + }); + + ack.setHeader('from', this.request.getHeader('from')); + ack.setHeader('via', this.request.getHeader('via')); + ack.setHeader('to', response.getHeader('to')); + + this.D = setTimeout(() { + this.timer_D(); + }, Timers.TIMER_D); + + this.transport.send(ack); + } + + cancel(reason) { + // Send only if a provisional response (>100) has been received. + if (this.state != TransactionState.PROCEEDING) { + return; + } + + var cancel = new SIPMessage.OutgoingRequest( + SipMethod.CANCEL, this.request.ruri, this.ua, { + 'route_set': this.request.getHeaders('route'), + 'call_id': this.request.getHeader('call-id'), + 'cseq': this.request.cseq + }); + + cancel.setHeader('from', this.request.getHeader('from')); + cancel.setHeader('via', this.request.getHeader('via')); + cancel.setHeader('to', this.request.getHeader('to')); + + if (reason != null) { + cancel.setHeader('reason', reason); + } + + this.transport.send(cancel); + } + + receiveResponse(SIPMessage.IncomingMessage response) { + var status_code = response.status_code; + + if (status_code >= 100 && status_code <= 199) { + switch (this.state) { + case TransactionState.CALLING: + this.stateChanged(TransactionState.PROCEEDING); + this.eventHandlers['onReceiveResponse'](response); + break; + case TransactionState.PROCEEDING: + this.eventHandlers['onReceiveResponse'](response); + break; + default: + break; + } + } else if (status_code >= 200 && status_code <= 299) { + switch (this.state) { + case TransactionState.CALLING: + case TransactionState.PROCEEDING: + this.stateChanged(TransactionState.ACCEPTED); + this.M = setTimeout(() { + this.timer_M(); + }, Timers.TIMER_M); + this.eventHandlers['onReceiveResponse'](response); + break; + case TransactionState.ACCEPTED: + this.eventHandlers['onReceiveResponse'](response); + break; + default: + break; + } + } else if (status_code >= 300 && status_code <= 699) { + switch (this.state) { + case TransactionState.CALLING: + case TransactionState.PROCEEDING: + this.stateChanged(TransactionState.COMPLETED); + this.sendACK(response); + this.eventHandlers['onReceiveResponse'](response); + break; + case TransactionState.COMPLETED: + this.sendACK(response); + break; + default: + break; + } + } + } +} + diff --git a/lib/src/transactions/invite_server.dart b/lib/src/transactions/invite_server.dart new file mode 100644 index 00000000..d2593f2e --- /dev/null +++ b/lib/src/transactions/invite_server.dart @@ -0,0 +1,170 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Timers.dart'; +import 'package:sip_ua/src/Transport.dart'; +import 'package:sip_ua/src/transactions/Transactions.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; + +final ist_logger = new Logger('InviteServerTransaction'); +debugist(msg) => ist_logger.debug(msg); + +class InviteServerTransaction extends TransactionBase { + var resendProvisionalTimer; + var transportError; + var L, H, I; + + InviteServerTransaction(UA ua, Transport transport, request) { + this.id = request.via_branch; + this.ua = ua; + this.transport = transport; + this.request = request; + this.last_response = ''; + request.server_transaction = this; + + this.state = TransactionState.PROCEEDING; + + ua.newTransaction(this); + + this.resendProvisionalTimer = null; + + request.reply(100); + } + + stateChanged(state) { + this.state = state; + this.emit('stateChanged'); + } + + timer_H() { + debugist('Timer H expired for transaction ${this.id}'); + + if (this.state == TransactionState.COMPLETED) { + debugist('ACK not received, dialog will be terminated'); + } + + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + + timer_I() { + this.stateChanged(TransactionState.TERMINATED); + } + + // RFC 6026 7.1. + timer_L() { + debugist('Timer L expired for transaction ${this.id}'); + + if (this.state == TransactionState.ACCEPTED) { + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + } + + onTransportError() { + if (this.transportError == null) { + this.transportError = true; + + debugist('transport error occurred, deleting transaction ${this.id}'); + + if (this.resendProvisionalTimer != null) { + clearInterval(this.resendProvisionalTimer); + this.resendProvisionalTimer = null; + } + + clearTimeout(this.L); + clearTimeout(this.H); + clearTimeout(this.I); + + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + } + + resend_provisional() { + if (!this.transport.send(this.last_response)) { + this.onTransportError(); + } + } + + // INVITE Server Transaction RFC 3261 17.2.1. + receiveResponse(status_code, response, onSuccess, onFailure) { + if (status_code >= 100 && status_code <= 199) { + switch (this.state) { + case TransactionState.PROCEEDING: + if (!this.transport.send(response)) { + this.onTransportError(); + } + this.last_response = response; + break; + default: + break; + } + } + + if (status_code > 100 && + status_code <= 199 && + this.state == TransactionState.PROCEEDING) { + // Trigger the resendProvisionalTimer only for the first non 100 provisional response. + if (this.resendProvisionalTimer == null) { + this.resendProvisionalTimer = setInterval(() { + this.resend_provisional(); + }, Timers.PROVISIONAL_RESPONSE_INTERVAL); + } + } else if (status_code >= 200 && status_code <= 299) { + if (this.state == TransactionState.PROCEEDING) { + this.stateChanged(TransactionState.ACCEPTED); + this.last_response = response; + this.L = setTimeout(() { + this.timer_L(); + }, Timers.TIMER_L); + + if (this.resendProvisionalTimer != null) { + clearInterval(this.resendProvisionalTimer); + this.resendProvisionalTimer = null; + } + } + /* falls through */ + if (this.state == TransactionState.ACCEPTED) { + // Note that this point will be reached for proceeding this.state also. + if (!this.transport.send(response)) { + this.onTransportError(); + if (onFailure != null) { + onFailure(); + } + } else if (onSuccess != null) { + onSuccess(); + } + } + } else if (status_code >= 300 && status_code <= 699) { + switch (this.state) { + case TransactionState.PROCEEDING: + if (this.resendProvisionalTimer != null) { + clearInterval(this.resendProvisionalTimer); + this.resendProvisionalTimer = null; + } + + if (!this.transport.send(response)) { + this.onTransportError(); + if (onFailure != null) { + onFailure(); + } + } else { + this.stateChanged(TransactionState.COMPLETED); + this.H = setTimeout(() { + this.timer_H(); + }, Timers.TIMER_H); + if (onSuccess != null) { + onSuccess(); + } + } + break; + default: + break; + } + } + } + + @override + void send() { + throw Exception("Not Implemented"); + } +} diff --git a/lib/src/transactions/non_invite_client.dart b/lib/src/transactions/non_invite_client.dart new file mode 100644 index 00000000..5f9952cc --- /dev/null +++ b/lib/src/transactions/non_invite_client.dart @@ -0,0 +1,108 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Timers.dart'; +import 'package:sip_ua/src/Transport.dart'; +import 'package:sip_ua/src/Utils.dart'; +import 'package:sip_ua/src/transactions/Transactions.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; + +import '../SIPMessage.dart' as SIPMessage; + +final nict_logger = new Logger('NonInviteClientTransaction'); +debugnict(msg) => nict_logger.debug(msg); + +class NonInviteClientTransaction extends TransactionBase { + var eventHandlers; + var F, K; + + NonInviteClientTransaction( + UA ua, Transport transport, request, eventHandlers) { + this.id = 'z9hG4bK${Math.floor(Math.random())}'; + this.ua = ua; + this.transport = transport; + this.request = request; + this.eventHandlers = eventHandlers; + + var via = 'SIP/2.0/${transport.via_transport}'; + + via += ' ${ua.configuration.via_host};branch=${this.id}'; + + this.request.setHeader('via', via); + + this.ua.newTransaction(this); + } + + stateChanged(state) { + this.state = state; + this.emit('stateChanged'); + } + + send() { + this.stateChanged(TransactionState.TRYING); + this.F = setTimeout(() { + this.timer_F(); + }, Timers.TIMER_F); + + if (!this.transport.send(this.request)) { + this.onTransportError(); + } + } + + onTransportError() { + debugnict('transport error occurred, deleting transaction ${this.id}'); + clearTimeout(this.F); + clearTimeout(this.K); + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + this.eventHandlers['onTransportError'](); + } + + timer_F() { + debugnict('Timer F expired for transaction ${this.id}'); + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + this.eventHandlers['onRequestTimeout'](); + } + + timer_K() { + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + + receiveResponse(SIPMessage.IncomingResponse response) { + var status_code = response.status_code; + + if (status_code < 200) { + switch (this.state) { + case TransactionState.TRYING: + case TransactionState.PROCEEDING: + this.stateChanged(TransactionState.PROCEEDING); + this.eventHandlers['onReceiveResponse'](response); + break; + default: + break; + } + } else { + switch (this.state) { + case TransactionState.TRYING: + case TransactionState.PROCEEDING: + this.stateChanged(TransactionState.COMPLETED); + clearTimeout(this.F); + + if (status_code == 408) { + this.eventHandlers['onRequestTimeout'](); + } else { + this.eventHandlers['onReceiveResponse'](response); + } + + this.K = setTimeout(() { + this.timer_K(); + }, Timers.TIMER_K); + break; + case TransactionState.COMPLETED: + break; + default: + break; + } + } + } +} diff --git a/lib/src/transactions/non_invite_server.dart b/lib/src/transactions/non_invite_server.dart new file mode 100644 index 00000000..70620313 --- /dev/null +++ b/lib/src/transactions/non_invite_server.dart @@ -0,0 +1,109 @@ +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Timers.dart'; + +import 'package:sip_ua/src/Transport.dart'; +import 'package:sip_ua/src/transactions/Transactions.dart'; +import 'package:sip_ua/src/transactions/transaction_base.dart'; + +final nist_logger = new Logger('NonInviteServerTransaction'); +debugnist(msg) => nist_logger.debug(msg); + +class NonInviteServerTransaction extends TransactionBase { + var transportError; + var J; + + NonInviteServerTransaction(UA ua, Transport transport, request) { + this.id = request.via_branch; + this.ua = ua; + this.transport = transport; + this.request = request; + this.last_response = ''; + request.server_transaction = this; + + this.state = TransactionState.TRYING; + + ua.newTransaction(this); + } + + stateChanged(state) { + this.state = state; + this.emit('stateChanged'); + } + + timer_J() { + debugnist('Timer J expired for transaction ${this.id}'); + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + + onTransportError() { + if (this.transportError == null) { + this.transportError = true; + + debugnist('transport error occurred, deleting transaction ${this.id}'); + + clearTimeout(this.J); + this.stateChanged(TransactionState.TERMINATED); + this.ua.destroyTransaction(this); + } + } + + receiveResponse(status_code, response, onSuccess, onFailure) { + if (status_code == 100) { + /* RFC 4320 4.1 + * 'A SIP element MUST NOT + * send any provisional response with a + * Status-Code other than 100 to a non-INVITE request.' + */ + switch (this.state) { + case TransactionState.TRYING: + this.stateChanged(TransactionState.PROCEEDING); + if (!this.transport.send(response)) { + this.onTransportError(); + } + break; + case TransactionState.PROCEEDING: + this.last_response = response; + if (!this.transport.send(response)) { + this.onTransportError(); + if (onFailure != null) { + onFailure(); + } + } else if (onSuccess != null) { + onSuccess(); + } + break; + default: + break; + } + } else if (status_code >= 200 && status_code <= 699) { + switch (this.state) { + case TransactionState.TRYING: + case TransactionState.PROCEEDING: + this.stateChanged(TransactionState.COMPLETED); + this.last_response = response; + this.J = setTimeout(() { + this.timer_J(); + }, Timers.TIMER_J); + if (!this.transport.send(response)) { + this.onTransportError(); + if (onFailure != null) { + onFailure(); + } + } else if (onSuccess != null) { + onSuccess(); + } + break; + case TransactionState.COMPLETED: + break; + default: + break; + } + } + } + + @override + void send() { + throw Exception("Not Implemented"); + } +} diff --git a/lib/src/transactions/transaction_base.dart b/lib/src/transactions/transaction_base.dart new file mode 100644 index 00000000..d796ebeb --- /dev/null +++ b/lib/src/transactions/transaction_base.dart @@ -0,0 +1,26 @@ +import 'package:events2/events2.dart'; +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/Transport.dart'; + +enum TransactionState { + // Transaction states. + TRYING, + PROCEEDING, + CALLING, + ACCEPTED, + COMPLETED, + TERMINATED, + CONFIRMED +} + +abstract class TransactionBase extends EventEmitter { + String id; + UA ua; + Transport transport; + TransactionState state; + var last_response; + var request; + void onTransportError(); + + void send(); +}