From 830777bbf673c3cee96b76aeaa3dabe44f9057f3 Mon Sep 17 00:00:00 2001 From: Perondas Date: Thu, 24 Mar 2022 15:29:31 +0100 Subject: [PATCH 01/10] Implemented subscribe support like here https://github.com/versatica/JsSIP/pull/711 --- lib/src/event_manager/internal_events.dart | 5 + lib/src/event_manager/notifier_events.dart | 18 + lib/src/event_manager/subscriber_events.dart | 23 + lib/src/notifier.dart | 318 ++++++++++++ lib/src/subscriber.dart | 519 +++++++++++++++++++ lib/src/ua.dart | 46 ++ 6 files changed, 929 insertions(+) create mode 100644 lib/src/event_manager/notifier_events.dart create mode 100644 lib/src/event_manager/subscriber_events.dart create mode 100644 lib/src/notifier.dart create mode 100644 lib/src/subscriber.dart diff --git a/lib/src/event_manager/internal_events.dart b/lib/src/event_manager/internal_events.dart index e9031101..31f8b4dc 100644 --- a/lib/src/event_manager/internal_events.dart +++ b/lib/src/event_manager/internal_events.dart @@ -161,3 +161,8 @@ class EventOnErrorResponse extends EventType { EventOnErrorResponse({this.response}); IncomingMessage? response; } + +class EventOnNewSubscribe extends EventType { + EventOnNewSubscribe({this.request}); + IncomingRequest? request; +} diff --git a/lib/src/event_manager/notifier_events.dart b/lib/src/event_manager/notifier_events.dart new file mode 100644 index 00000000..8d577359 --- /dev/null +++ b/lib/src/event_manager/notifier_events.dart @@ -0,0 +1,18 @@ +import 'package:sip_ua/src/sip_message.dart'; + +import 'events.dart'; + +class EventTerminated extends EventType { + EventTerminated(this.terminationCode, this.sendFinalNotify); + int terminationCode; + bool sendFinalNotify; +} + +class EventSubscribe extends EventType { + EventSubscribe( + this.isUnsubscribe, this.request, this.body, this.content_type); + bool isUnsubscribe; + IncomingRequest request; + String? body; + String content_type; +} diff --git a/lib/src/event_manager/subscriber_events.dart b/lib/src/event_manager/subscriber_events.dart new file mode 100644 index 00000000..747e3a66 --- /dev/null +++ b/lib/src/event_manager/subscriber_events.dart @@ -0,0 +1,23 @@ +import '../sip_message.dart'; +import 'events.dart'; + +class EventTerminated extends EventType { + EventTerminated(this.TerminationCode, [this.reason, this.retryAfter]); + int TerminationCode; + String? reason; + int? retryAfter; +} + +class EventPending extends EventType {} + +class EventActive extends EventType {} + +class EventNotify extends EventType { + EventNotify(this.isFinal, this.request, this.body, this.contentType); + bool isFinal; + IncomingRequest request; + String? body; + String contentType; +} + +class EventAccepted extends EventType {} diff --git a/lib/src/notifier.dart b/lib/src/notifier.dart new file mode 100644 index 00000000..01904275 --- /dev/null +++ b/lib/src/notifier.dart @@ -0,0 +1,318 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:sip_ua/src/constants.dart'; +import 'package:sip_ua/src/dialog.dart'; +import 'package:sip_ua/src/event_manager/notifier_events.dart'; +import 'package:sip_ua/src/exceptions.dart'; +import 'package:sip_ua/src/logger.dart'; +import 'package:sip_ua/src/sip_message.dart'; +import 'package:sip_ua/src/timers.dart'; +import 'package:sip_ua/src/ua.dart'; +import 'package:sip_ua/src/utils.dart'; + +import 'event_manager/event_manager.dart'; + +/** + * Termination codes. + */ +class C { + // Termination codes. + static const int NOTIFY_RESPONSE_TIMEOUT = 0; + static const int NOTIFY_TRANSPORT_ERROR = 1; + static const int NOTIFY_NON_OK_RESPONSE = 2; + static const int NOTIFY_FAILED_AUTHENTICATION = 3; + static const int SEND_FINAL_NOTIFY = 4; + static const int RECEIVE_UNSUBSCRIBE = 5; + static const int SUBSCRIPTION_EXPIRED = 6; + + // Notifier states + static const int STATE_PENDING = 0; + static const int STATE_ACTIVE = 1; + static const int STATE_TERMINATED = 2; +} + +class Notifier extends EventManager { + Notifier(this.ua, this._initialSubscribe, this._content_type, + [bool pending = false, + List? extraHeaders = null, + String? allowEvents = null]) { + logger.debug('new'); + if (!_initialSubscribe.hasHeader('contact')) { + throw TypeError('subscribe - no contact header'); + } + _state = pending ? C.STATE_PENDING : C.STATE_ACTIVE; + + _terminated_reason = null; + _terminated_retry_after = null; + + String eventName = _initialSubscribe.getHeader('event'); + + _expires = parseInt(_initialSubscribe.getHeader('expires'), 10)!; + _headers = cloneArray(extraHeaders); + _headers.add('Event: $eventName'); + + // Use contact from extraHeaders or create it. + String? c = _headers + .firstWhereOrNull((String header) => header.startsWith('Contact')); + if (c == null) { + _contact = 'Contact: ${ua.contact.toString()}'; + + _headers.add(_contact); + } else { + _contact = c; + } + + if (allowEvents != null) { + _headers.add('Allow-Events: $allowEvents'); + } + + _target = _initialSubscribe.from?.uri?.user; + + _initialSubscribe.to_tag = newTag(); + + // Create dialog for normal and fetch-subscribe. + Dialog dialog = Dialog(this, _initialSubscribe, 'UAS'); + + _dialog = dialog; + + if (_expires > 0) { + // Set expires timer and time-stamp. + _setExpiresTimer(); + } + } + + late int _state; + String? _terminated_reason; + num? _terminated_retry_after; + late Map data; + late int _expires; + late List _headers; + late String _contact; + String? _target; + + static C getC() { + return C(); + } + + UA ua; + final IncomingRequest _initialSubscribe; + final String _content_type; + DateTime? _expires_timestamp; + Timer? _expires_timer; + late Dialog? _dialog; + + /** + * Dialog callback. + * Called also for initial subscribe. + * Supported RFC 6665 4.4.3: initial fetch subscribe (with expires: 0). + */ + void receiveRequest(IncomingRequest request) { + if (request.method != SipMethod.NOTIFY) { + request.reply(405); + + return; + } + + if (request.hasHeader('expires')) { + _expires = parseInt(request.getHeader('expires'), 10)!; + } else { + // RFC 6665 3.1.1, default expires value. + _expires = 900; + + logger + .debug('missing Expires header field, default value set: $_expires'); + } + request.reply(200, null, ['Expires: $_expires', _contact]); + + String? body = request.body; + String content_type = request.getHeader('content-type'); + bool is_unsubscribe = _expires == 0; + + if (!is_unsubscribe) { + _setExpiresTimer(); + } + + logger.debug('emit "subscribe"'); + emit(EventSubscribe(is_unsubscribe, request, body, content_type)); + + if (is_unsubscribe) { + _dialogTerminated(C.RECEIVE_UNSUBSCRIBE); + } + } + + /** + * User API + */ + /** + * Please call after creating the Notifier instance and setting the event handlers. + */ + void start() { + logger.debug('start()'); + + receiveRequest(_initialSubscribe); + } + + /** + * Switch pending dialog state to active. + */ + void setActiveState() { + logger.debug('setActiveState()'); + + if (_state == C.STATE_PENDING) { + _state = C.STATE_ACTIVE; + } + } + + /** + * Send the initial and subsequent notify request. + * @param {string} body - notify request body. + */ + void notify([String? body = null]) { + logger.debug('notify()'); + + // Prevent send notify after final notify. + if (_dialog == null) { + logger.warn('final notify has sent'); + + return; + } + + String subs_state = _stateNumberToString(_state); + + if (_state != C.STATE_TERMINATED) { + num expires = Math.floor( + (_expires_timestamp!.subtract(DateTime.now())).millisecond / 1000); + + if (expires < 0) { + expires = 0; + } + + subs_state += ';expires=$expires'; + } else { + if (_terminated_reason != null) { + subs_state += ';reason=$_terminated_reason'; + } + if (_terminated_retry_after != null) { + subs_state += ';retry-after=$_terminated_retry_after'; + } + } + + ListSlice headers = _headers.slice(0); + + headers.add('Subscription-State: $subs_state'); + + if (body != null) { + headers.add('Content-Type: $_content_type'); + } + + _dialog!.sendRequest(SipMethod.NOTIFY, { + 'body': body, + 'extraHeaders': headers, + 'eventHandlers': { + 'onRequestTimeout': () { + _dialogTerminated(C.NOTIFY_RESPONSE_TIMEOUT); + }, + 'onTransportError': () { + _dialogTerminated(C.NOTIFY_TRANSPORT_ERROR); + }, + 'onErrorResponse': (IncomingResponse response) { + if (response.status_code == 401 || response.status_code == 407) { + _dialogTerminated(C.NOTIFY_FAILED_AUTHENTICATION); + } else { + _dialogTerminated(C.NOTIFY_NON_OK_RESPONSE); + } + }, + 'onDialogError': () { + _dialogTerminated(C.NOTIFY_NON_OK_RESPONSE); + } + } + }); + } + + /** + * Terminate. (Send the final NOTIFY request). + * + * @param {string} body - Notify message body. + * @param {string} reason - Set Subscription-State reason parameter. + * @param {number} retryAfter - Set Subscription-State retry-after parameter. + */ + void terminate( + [String? body = null, String? reason = null, num? retryAfter = null]) { + logger.debug('terminate()'); + + _state = C.STATE_TERMINATED; + _terminated_reason = reason; + _terminated_retry_after = retryAfter; + + notify(body); + + _dialogTerminated(C.SEND_FINAL_NOTIFY); + } + + /** + * Get dialog state. + */ + int get state { + return _state; + } + + /** + * Get dialog id. + */ + Id? get id { + return _dialog?.id; + } + + /** + * Private API + */ + void _dialogTerminated(int termination_code) { + if (_dialog == null) { + return; + } + + _state = C.STATE_TERMINATED; + clearTimeout(_expires_timer); + + if (_dialog != null) { + _dialog!.terminate(); + _dialog = null; + } + + bool send_final_notify = termination_code == C.SUBSCRIPTION_EXPIRED; + + logger.debug( + 'emit "terminated" code=$termination_code, send final notify=$send_final_notify'); + emit(EventTerminated(termination_code, send_final_notify)); + } + + void _setExpiresTimer() { + _expires_timestamp = + DateTime.now().add(Duration(milliseconds: _expires * 1000)); + + clearTimeout(_expires_timer); + _expires_timer = setTimeout(() { + if (_dialog == null) { + return; + } + + _terminated_reason = 'timeout'; + notify(); + _dialogTerminated(C.SUBSCRIPTION_EXPIRED); + }, _expires * 1000); + } + + String _stateNumberToString(int state) { + switch (state) { + case C.STATE_PENDING: + return 'pending'; + case C.STATE_ACTIVE: + return 'active'; + case C.STATE_TERMINATED: + return 'terminated'; + default: + throw TypeError('wrong state value'); + } + } +} diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart new file mode 100644 index 00000000..0be7b7cc --- /dev/null +++ b/lib/src/subscriber.dart @@ -0,0 +1,519 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:sip_ua/sip_ua.dart'; +import 'package:sip_ua/src/constants.dart'; +import 'package:sip_ua/src/dialog.dart'; +import 'package:sip_ua/src/event_manager/internal_events.dart'; +import 'package:sip_ua/src/event_manager/subscriber_events.dart'; +import 'package:sip_ua/src/exceptions.dart'; +import 'package:sip_ua/src/grammar.dart'; +import 'package:sip_ua/src/logger.dart'; +import 'package:sip_ua/src/request_sender.dart'; +import 'package:sip_ua/src/sip_message.dart'; +import 'package:sip_ua/src/timers.dart'; +import 'package:sip_ua/src/ua.dart'; +import 'package:sip_ua/src/utils.dart'; + +import 'event_manager/event_manager.dart'; + +/** + * Termination codes. + */ +class C { + // Termination codes. + static const int SUBSCRIBE_RESPONSE_TIMEOUT = 0; + static const int SUBSCRIBE_TRANSPORT_ERROR = 1; + static const int SUBSCRIBE_NON_OK_RESPONSE = 2; + static const int SUBSCRIBE_BAD_OK_RESPONSE = 3; + static const int SUBSCRIBE_FAILED_AUTHENTICATION = 4; + static const int UNSUBSCRIBE_TIMEOUT = 5; + static const int RECEIVE_FINAL_NOTIFY = 6; + static const int RECEIVE_BAD_NOTIFY = 7; + + // Subscriber states. + static const int STATE_PENDING = 0; + static const int STATE_ACTIVE = 1; + static const int STATE_TERMINATED = 2; + static const int STATE_INIT = 3; + static const int STATE_NOTIFY_WAIT = 4; +} + +class Subscriber extends EventManager { + Subscriber(this._ua, this._target, String eventName, String accept, + [int expires = 900, + String? contentType, + String? allowEvents, + Map requestParams = const {}, + List extraHeaders = const []]) { + logger.debug('new'); + + _expires = expires; + + // Used to subscribe with body. + _contentType = contentType; + + _params = requestParams; + + _params['from_tag'] = newTag(); + + _params['to_tag'] = null; + + _params['call_id'] = createRandomToken(20); + + if (_params['cseq'] == null) { + _params['cseq'] = Math.floor((Math.random() * 10000) + 1); + } + + _state = C.STATE_INIT; + + _dialog = null; + + _expires_timer = null; + + _expires_timestamp = null; + + _terminated = false; + + _unsubscribe_timeout_timer = null; + + dynamic parsed = Grammar.parse(eventName, 'Event'); + + if (parsed == -1) { + throw TypeError('eventName - wrong format'); + } + + _event_name = parsed.event; + // this._event_id = parsed.params && parsed.params.id; + _event_id = parsed.params && parsed.params.id; + + String eventValue = _event_name; + + if (_event_id != null) { + eventValue += ';id=$_event_id'; + } + + _headers = cloneArray(extraHeaders); + + _headers.addAll([ + 'Event: $eventValue', + 'Expires: $_expires', + 'Accept: $accept' + ]); + + if (!_headers.any((String element) => element.startsWith('Contact'))) { + String contact = 'Contact: ${_ua.contact.toString()}'; + + _headers.add(contact); + } + + if (allowEvents != null) { + _headers.add('Allow-Events: $allowEvents'); + } + + // To enqueue subscribes created before receive initial subscribe OK. + _queue = >[]; + } + + final UA _ua; + + final String _target; + + late int _expires; + + String? _contentType; + + late Map _params; + + late int _state; + + late Dialog? _dialog; + + DateTime? _expires_timestamp; + + Timer? _expires_timer; + + late bool _terminated; + + Timer? _unsubscribe_timeout_timer; + + late Map _data; + + late String _event_name; + + bool? _event_id; + + late List _headers; + + late List> _queue; + + /** + * Expose C object. + */ + static C getC() { + return C(); + } + + void onRequestTimeout() { + _dialogTerminated(C.SUBSCRIBE_RESPONSE_TIMEOUT); + } + + void onTransportError() { + _dialogTerminated(C.SUBSCRIBE_TRANSPORT_ERROR); + } + + /** + * Dialog callback. + */ + void receiveRequest(IncomingRequest request) { + if (request.method != SipMethod.NOTIFY) { + logger.warn('received non-NOTIFY request'); + request.reply(405); + + return; + } + + // RFC 6665 8.2.1. Check if event header matches. + dynamic eventHeader = request.parseHeader('Event'); + + if (!eventHeader) { + logger.warn('missed Event header'); + request.reply(400); + _dialogTerminated(C.RECEIVE_BAD_NOTIFY); + + return; + } + + dynamic eventName = eventHeader.event; + bool eventId = eventHeader.params && eventHeader.params.id; + + if (eventName != _event_name || eventId != _event_id) { + logger.warn('Event header does not match SUBSCRIBE'); + request.reply(489); + _dialogTerminated(C.RECEIVE_BAD_NOTIFY); + + return; + } + + // Process Subscription-State header. + dynamic subsState = request.parseHeader('subscription-state'); + + if (!subsState) { + logger.warn('missed Subscription-State header'); + request.reply(400); + _dialogTerminated(C.RECEIVE_BAD_NOTIFY); + + return; + } + + request.reply(200); + + int newState = _stateStringToNumber(subsState.state); + int prevState = _state; + + if (prevState != C.STATE_TERMINATED && newState != C.STATE_TERMINATED) { + _state = newState; + + if (subsState.expires != null) { + int expires = subsState.expires; + DateTime expiresTimestamp = + DateTime.now().add(Duration(milliseconds: expires * 1000)); + int maxTimeDeviation = 2000; + + // Expiration time is shorter and the difference is not too small. + if (_expires_timestamp!.difference(expiresTimestamp) > + maxTimeDeviation) { + logger.debug('update sending re-SUBSCRIBE time'); + + _scheduleSubscribe(expires); + } + } + } + + if (prevState != C.STATE_PENDING && newState == C.STATE_PENDING) { + logger.debug('emit "pending"'); + emit(EventPending()); + } else if (prevState != C.STATE_ACTIVE && newState == C.STATE_ACTIVE) { + logger.debug('emit "active"'); + emit(EventActive()); + } + + String? body = request.body; + + // Check if the notify is final. + bool isFinal = newState == C.STATE_TERMINATED; + + // Notify event fired only for notify with body. + if (body != null) { + dynamic contentType = request.getHeader('content-type'); + + logger.debug('emit "notify"'); + emit(EventNotify(isFinal, request, body, contentType)); + } + + if (isFinal) { + dynamic reason = subsState.reason; + dynamic retryAfter = null; + + if (subsState.params && subsState.params['retry-after'] != null) { + retryAfter = parseInt(subsState.params['retry-after'], 10); + } + + _dialogTerminated(C.RECEIVE_FINAL_NOTIFY, reason, retryAfter); + } + } + + /** + * User API + */ + + /** + * Send the initial (non-fetch) and subsequent subscribe. + * @param {string} body - subscribe request body. + */ + void subscribe([String? body]) { + logger.debug('subscribe()'); + + if (_state == C.STATE_INIT) { + _sendInitialSubscribe(body, _headers); + } else { + _sendSubsequentSubscribe(body, _headers); + } + } + + /** + * terminate. + * Send un-subscribe or fetch-subscribe (with Expires: 0). + * @param {string} body - un-subscribe request body + */ + void terminate([String? body]) { + logger.debug('terminate()'); + + // Prevent duplication un-subscribe sending. + if (_terminated) { + return; + } + _terminated = true; + + // Set header Expires: 0. + Iterable headers = _headers.map((String header) { + return header.startsWith('Expires') ? 'Expires: 0' : header; + }); + + if (_state == C.STATE_INIT) { + // fetch-subscribe - initial subscribe with Expires: 0. + _sendInitialSubscribe(body, headers); + } else { + _sendSubsequentSubscribe(body, headers); + } + + // Waiting for the final notify for a while. + int final_notify_timeout = 30000; + + _unsubscribe_timeout_timer = setTimeout(() { + _dialogTerminated(C.UNSUBSCRIBE_TIMEOUT); + }, final_notify_timeout); + } + + /** + * Private API. + */ + void _sendInitialSubscribe(String? body, List headers) { + if (body != null) { + if (_contentType == null) { + throw TypeError('content_type is undefined'); + } + + headers = headers.slice(0); + headers.add('Content-Type: $_contentType'); + } + + _state = C.STATE_NOTIFY_WAIT; + + OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, + _ua.normalizeTarget(_target), _ua, _params, headers, body); + + EventManager handler = EventManager(); + + handler.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { + _receiveSubscribeResponse(response); + }); + + handler.on(EventOnRequestTimeout(), () { + onRequestTimeout(); + }); + + handler.on(EventOnTransportError(), () { + onTransportError(); + }); + + RequestSender request_sender = RequestSender(_ua, request, handler); + + request_sender.send(); + } + + void _receiveSubscribeResponse(IncomingRequest response) { + if (response.status_code >= 200 && response.status_code < 300) { + // Create dialog + if (_dialog == null) { + try { + Dialog dialog = Dialog(this, response, 'UAC'); + _dialog = dialog; + } catch (e) { + logger.warn(e); + _dialogTerminated(C.SUBSCRIBE_BAD_OK_RESPONSE); + + return; + } + + logger.debug('emit "accepted"'); + emit(EventAccepted()); + + // Subsequent subscribes saved in the queue until dialog created. + for (Map sub in _queue) { + logger.debug('dequeue subscribe'); + + _sendSubsequentSubscribe(sub['body'], sub['headers']); + } + } + + // Check expires value. + dynamic expires_value = response.getHeader('expires'); + + if (expires_value != 0 && !expires_value) { + logger.warn('response without Expires header'); + + // RFC 6665 3.1.1 subscribe OK response must contain Expires header. + // Use workaround expires value. + expires_value = '900'; + } + + int? expires = parseInt(expires_value, 10); + + if (expires! > 0) { + _scheduleSubscribe(expires); + } + } else if (response.status_code == 401 || response.status_code == 407) { + _dialogTerminated(C.SUBSCRIBE_FAILED_AUTHENTICATION); + } else if (response.status_code >= 300) { + _dialogTerminated(C.SUBSCRIBE_NON_OK_RESPONSE); + } + } + + void _sendSubsequentSubscribe(Object? body, List headers) { + if (_state == C.STATE_TERMINATED) { + return; + } + + if (_dialog == null) { + logger.debug('enqueue subscribe'); + + _queue.add({'body': body, 'headers': headers.slice(0)}); + + return; + } + + if (body != null) { + if (_contentType == null) { + throw TypeError('content_type is undefined'); + } + + headers = headers.slice(0); + headers.add('Content-Type: $_contentType'); + } + + _dialog!.sendRequest(SipMethod.SUBSCRIBE, { + 'body': body, + 'extraHeaders': headers, + 'eventHandlers': { + 'onRequestTimeout': () { + onRequestTimeout(); + }, + 'onTransportError': () { + onTransportError(); + }, + 'onSuccessResponse': (IncomingResponse response) { + _receiveSubscribeResponse(response); + }, + 'onErrorResponse': (IncomingResponse response) { + _receiveSubscribeResponse(response); + }, + 'onDialogError': (IncomingResponse response) { + _receiveSubscribeResponse(response); + } + } + }); + } + + void _dialogTerminated(int terminationCode, + [String? reason, int? retryAfter]) { + // To prevent duplicate emit terminated event. + if (_state == C.STATE_TERMINATED) { + return; + } + + _state = C.STATE_TERMINATED; + + // Clear timers. + clearTimeout(_expires_timer); + clearTimeout(_unsubscribe_timeout_timer); + + if (_dialog != null) { + _dialog!.terminate(); + _dialog = null; + } + + logger.debug('emit "terminated" code=$terminationCode'); + emit(EventTerminated(terminationCode, reason, retryAfter)); + } + + void _scheduleSubscribe(int expires) { + /* + If the expires time is less than 140 seconds we do not support Chrome intensive timer throttling mode. + In this case, the re-subscribe is sent 5 seconds before the subscription expiration. + + When Chrome is in intensive timer throttling mode, in the worst case, + the timer will be 60 seconds late. + We give the server 10 seconds to make sure it will execute the command even if it is heavily loaded. + As a result, we order the time no later than 70 seconds before the subscription expiration. + Resulting time calculated as half time interval + (half interval - 70) * random. + + E.g. expires is 140, re-subscribe will be ordered to send in 70 seconds. + expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds. + */ + + num timeout = expires >= 140 + ? (expires * 1000 / 2) + + Math.floor(((expires / 2) - 70) * 1000 * Math.random()) + : (expires * 1000) - 5000; + + _expires_timestamp = + DateTime.now().add(Duration(milliseconds: expires * 1000)); + + logger.debug( + 'next SUBSCRIBE will be sent in ${Math.floor(timeout / 1000)} sec'); + + clearTimeout(_expires_timer); + _expires_timer = setTimeout(() { + _expires_timer = null; + _sendSubsequentSubscribe(null, _headers); + }, timeout); + } + + int _stateStringToNumber(String? strState) { + switch (strState) { + case 'pending': + return C.STATE_PENDING; + case 'active': + return C.STATE_ACTIVE; + case 'terminated': + return C.STATE_TERMINATED; + case 'init': + return C.STATE_INIT; + case 'notify_wait': + return C.STATE_NOTIFY_WAIT; + default: + throw TypeError('wrong state value'); + } + } +} diff --git a/lib/src/ua.dart b/lib/src/ua.dart index bf338acb..487c151e 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:sip_ua/src/data.dart'; +import 'package:sip_ua/src/subscriber.dart'; import 'config.dart' as config; import 'config.dart'; import 'constants.dart' as DartSIP_C; @@ -11,6 +12,7 @@ import 'event_manager/internal_events.dart'; import 'exceptions.dart' as Exceptions; import 'logger.dart'; import 'message.dart'; +import 'notifier.dart'; import 'parser.dart' as Parser; import 'registrator.dart'; import 'rtc_session.dart'; @@ -205,6 +207,41 @@ class UA extends EventManager { _registrator.unregister(all); } + /** + * Create subscriber instance + */ + Subscriber subscribe( + String target, + String eventName, + String accept, [ + int expires = 900, + String? contentType, + String? allowEvents, + Map requestParams = const {}, + List extraHeaders = const [], + ]) { + logger.debug('subscribe()'); + + return Subscriber(this, target, eventName, accept, expires, contentType, + allowEvents, requestParams, extraHeaders); + } + + /** + * Create notifier instance + */ + Notifier notify( + IncomingRequest subscribe, + String contentType, [ + bool pending = false, + List? extraHeaders = null, + String? allowEvents = null, + ]) { + logger.debug('notify()'); + + return Notifier( + this, subscribe, contentType, pending, extraHeaders, allowEvents); + } + /** * Get the Registrator instance. */ @@ -562,6 +599,12 @@ class UA extends EventManager { if (request.to_tag != null && !hasListeners(EventNewRTCSession())) { request.reply(405); + return; + } + } else if (method == SipMethod.SUBSCRIBE) { + if (listeners['newSubscribe']?.length == 0) { + request.reply(405); + return; } } @@ -622,6 +665,9 @@ class UA extends EventManager { emit(EventSipEvent(request: request)); request.reply(200); break; + case SipMethod.SUBSCRIBE: + emit(EventOnNewSubscribe(request: request)); + break; default: request.reply(405); break; From 01d8b2ba6cb6ce6d297313076dd471bc33fbdb32 Mon Sep 17 00:00:00 2001 From: Perondas Date: Sat, 26 Mar 2022 16:10:00 +0100 Subject: [PATCH 02/10] Fixed type Errors --- lib/src/notifier.dart | 9 +++++---- lib/src/subscriber.dart | 40 +++++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/src/notifier.dart b/lib/src/notifier.dart index 01904275..229b2859 100644 --- a/lib/src/notifier.dart +++ b/lib/src/notifier.dart @@ -6,6 +6,7 @@ import 'package:sip_ua/src/dialog.dart'; import 'package:sip_ua/src/event_manager/notifier_events.dart'; import 'package:sip_ua/src/exceptions.dart'; import 'package:sip_ua/src/logger.dart'; +import 'package:sip_ua/src/rtc_session.dart'; import 'package:sip_ua/src/sip_message.dart'; import 'package:sip_ua/src/timers.dart'; import 'package:sip_ua/src/ua.dart'; @@ -54,7 +55,7 @@ class Notifier extends EventManager { // Use contact from extraHeaders or create it. String? c = _headers - .firstWhereOrNull((String header) => header.startsWith('Contact')); + .firstWhereOrNull((dynamic header) => header.startsWith('Contact')); if (c == null) { _contact = 'Contact: ${ua.contact.toString()}'; @@ -72,7 +73,7 @@ class Notifier extends EventManager { _initialSubscribe.to_tag = newTag(); // Create dialog for normal and fetch-subscribe. - Dialog dialog = Dialog(this, _initialSubscribe, 'UAS'); + Dialog dialog = Dialog(RTCSession(ua), _initialSubscribe, 'UAS'); _dialog = dialog; @@ -181,8 +182,8 @@ class Notifier extends EventManager { String subs_state = _stateNumberToString(_state); if (_state != C.STATE_TERMINATED) { - num expires = Math.floor( - (_expires_timestamp!.subtract(DateTime.now())).millisecond / 1000); + num expires = + Math.floor(_expires_timestamp!.difference(DateTime.now()).inSeconds); if (expires < 0) { expires = 0; diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart index 0be7b7cc..8c31653b 100644 --- a/lib/src/subscriber.dart +++ b/lib/src/subscriber.dart @@ -10,6 +10,8 @@ import 'package:sip_ua/src/exceptions.dart'; import 'package:sip_ua/src/grammar.dart'; import 'package:sip_ua/src/logger.dart'; import 'package:sip_ua/src/request_sender.dart'; +import 'package:sip_ua/src/rtc_session.dart'; +import 'package:sip_ua/src/sanity_check.dart'; import 'package:sip_ua/src/sip_message.dart'; import 'package:sip_ua/src/timers.dart'; import 'package:sip_ua/src/ua.dart'; @@ -101,7 +103,7 @@ class Subscriber extends EventManager { 'Accept: $accept' ]); - if (!_headers.any((String element) => element.startsWith('Contact'))) { + if (!_headers.any((dynamic element) => element.startsWith('Contact'))) { String contact = 'Contact: ${_ua.contact.toString()}'; _headers.add(contact); @@ -222,7 +224,7 @@ class Subscriber extends EventManager { // Expiration time is shorter and the difference is not too small. if (_expires_timestamp!.difference(expiresTimestamp) > - maxTimeDeviation) { + Duration(milliseconds: maxTimeDeviation)) { logger.debug('update sending re-SUBSCRIBE time'); _scheduleSubscribe(expires); @@ -296,9 +298,9 @@ class Subscriber extends EventManager { _terminated = true; // Set header Expires: 0. - Iterable headers = _headers.map((String header) { + List headers = _headers.map((dynamic header) { return header.startsWith('Expires') ? 'Expires: 0' : header; - }); + }).toList(); if (_state == C.STATE_INIT) { // fetch-subscribe - initial subscribe with Expires: 0. @@ -318,7 +320,7 @@ class Subscriber extends EventManager { /** * Private API. */ - void _sendInitialSubscribe(String? body, List headers) { + void _sendInitialSubscribe(String? body, List headers) { if (body != null) { if (_contentType == null) { throw TypeError('content_type is undefined'); @@ -336,14 +338,14 @@ class Subscriber extends EventManager { EventManager handler = EventManager(); handler.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { - _receiveSubscribeResponse(response); + _receiveSubscribeResponse(response.response); }); - handler.on(EventOnRequestTimeout(), () { + handler.on(EventOnRequestTimeout(), (EventOnRequestTimeout timeout) { onRequestTimeout(); }); - handler.on(EventOnTransportError(), () { + handler.on(EventOnTransportError(), (EventOnTransportError event) { onTransportError(); }); @@ -352,15 +354,18 @@ class Subscriber extends EventManager { request_sender.send(); } - void _receiveSubscribeResponse(IncomingRequest response) { - if (response.status_code >= 200 && response.status_code < 300) { + void _receiveSubscribeResponse(IncomingResponse? response) { + if (response == null) { + throw ArgumentError('Incoming response was null'); + } + if (response.status_code >= 200 && response.status_code! < 300) { // Create dialog if (_dialog == null) { try { - Dialog dialog = Dialog(this, response, 'UAC'); + Dialog dialog = Dialog(RTCSession(ua), response, 'UAC'); _dialog = dialog; } catch (e) { - logger.warn(e); + logger.warn(e.toString()); _dialogTerminated(C.SUBSCRIBE_BAD_OK_RESPONSE); return; @@ -400,7 +405,7 @@ class Subscriber extends EventManager { } } - void _sendSubsequentSubscribe(Object? body, List headers) { + void _sendSubsequentSubscribe(Object? body, List headers) { if (_state == C.STATE_TERMINATED) { return; } @@ -482,10 +487,11 @@ class Subscriber extends EventManager { expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds. */ - num timeout = expires >= 140 - ? (expires * 1000 / 2) + - Math.floor(((expires / 2) - 70) * 1000 * Math.random()) - : (expires * 1000) - 5000; + int timeout = (expires >= 140 + ? (expires * 1000 / 2) + + Math.floor(((expires / 2) - 70) * 1000 * Math.random()) + : (expires * 1000) - 5000) + .toInt(); _expires_timestamp = DateTime.now().add(Duration(milliseconds: expires * 1000)); From ef5f56599c1a5c078891ad4f2760859fb9cfb075 Mon Sep 17 00:00:00 2001 From: Perondas Date: Mon, 28 Mar 2022 09:11:28 +0200 Subject: [PATCH 03/10] Made events consistant --- example/.gitignore | 2 ++ lib/src/event_manager/notifier_events.dart | 14 +++++++------- lib/src/event_manager/subscriber_events.dart | 12 ++++++------ lib/src/notifier.dart | 9 +++++++-- lib/src/subscriber.dart | 11 +++++++++-- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index 2ddde2a5..3929287a 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -71,3 +71,5 @@ !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +.flutter-plugins-dependencies \ No newline at end of file diff --git a/lib/src/event_manager/notifier_events.dart b/lib/src/event_manager/notifier_events.dart index 8d577359..ef6e3e09 100644 --- a/lib/src/event_manager/notifier_events.dart +++ b/lib/src/event_manager/notifier_events.dart @@ -3,16 +3,16 @@ import 'package:sip_ua/src/sip_message.dart'; import 'events.dart'; class EventTerminated extends EventType { - EventTerminated(this.terminationCode, this.sendFinalNotify); - int terminationCode; - bool sendFinalNotify; + EventTerminated({this.terminationCode, this.sendFinalNotify}); + int? terminationCode; + bool? sendFinalNotify; } class EventSubscribe extends EventType { EventSubscribe( - this.isUnsubscribe, this.request, this.body, this.content_type); - bool isUnsubscribe; - IncomingRequest request; + {this.isUnsubscribe, this.request, this.body, this.content_type}); + bool? isUnsubscribe; + IncomingRequest? request; String? body; - String content_type; + String? content_type; } diff --git a/lib/src/event_manager/subscriber_events.dart b/lib/src/event_manager/subscriber_events.dart index 747e3a66..099e7365 100644 --- a/lib/src/event_manager/subscriber_events.dart +++ b/lib/src/event_manager/subscriber_events.dart @@ -2,8 +2,8 @@ import '../sip_message.dart'; import 'events.dart'; class EventTerminated extends EventType { - EventTerminated(this.TerminationCode, [this.reason, this.retryAfter]); - int TerminationCode; + EventTerminated({this.TerminationCode, this.reason, this.retryAfter}); + int? TerminationCode; String? reason; int? retryAfter; } @@ -13,11 +13,11 @@ class EventPending extends EventType {} class EventActive extends EventType {} class EventNotify extends EventType { - EventNotify(this.isFinal, this.request, this.body, this.contentType); - bool isFinal; - IncomingRequest request; + EventNotify({this.isFinal, this.request, this.body, this.contentType}); + bool? isFinal; + IncomingRequest? request; String? body; - String contentType; + String? contentType; } class EventAccepted extends EventType {} diff --git a/lib/src/notifier.dart b/lib/src/notifier.dart index 229b2859..6418f7fa 100644 --- a/lib/src/notifier.dart +++ b/lib/src/notifier.dart @@ -135,7 +135,11 @@ class Notifier extends EventManager { } logger.debug('emit "subscribe"'); - emit(EventSubscribe(is_unsubscribe, request, body, content_type)); + emit(EventSubscribe( + isUnsubscribe: is_unsubscribe, + request: request, + body: body, + content_type: content_type)); if (is_unsubscribe) { _dialogTerminated(C.RECEIVE_UNSUBSCRIBE); @@ -285,7 +289,8 @@ class Notifier extends EventManager { logger.debug( 'emit "terminated" code=$termination_code, send final notify=$send_final_notify'); - emit(EventTerminated(termination_code, send_final_notify)); + emit(EventTerminated( + terminationCode: termination_code, sendFinalNotify: send_final_notify)); } void _setExpiresTimer() { diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart index 8c31653b..6abc25ce 100644 --- a/lib/src/subscriber.dart +++ b/lib/src/subscriber.dart @@ -250,7 +250,11 @@ class Subscriber extends EventManager { dynamic contentType = request.getHeader('content-type'); logger.debug('emit "notify"'); - emit(EventNotify(isFinal, request, body, contentType)); + emit(EventNotify( + isFinal: isFinal, + request: request, + body: body, + contentType: contentType)); } if (isFinal) { @@ -469,7 +473,10 @@ class Subscriber extends EventManager { } logger.debug('emit "terminated" code=$terminationCode'); - emit(EventTerminated(terminationCode, reason, retryAfter)); + emit(EventTerminated( + TerminationCode: terminationCode, + reason: reason, + retryAfter: retryAfter)); } void _scheduleSubscribe(int expires) { From 47f61cc84cb957f6b42ca3578020cdfedbfb574e Mon Sep 17 00:00:00 2001 From: Perondas <114622@fhwn.ac.at> Date: Thu, 31 Mar 2022 08:57:26 +0200 Subject: [PATCH 04/10] Chore: Began Subscriber rework --- .gitignore | 3 + lib/src/constants.dart | 2 +- lib/src/event_manager/internal_events.dart | 5 + lib/src/sip_ua_helper.dart | 68 ++++++++++++++ lib/src/subscriber.dart | 102 +++++++++++---------- lib/src/ua.dart | 1 + pubspec.yaml | 3 +- 7 files changed, 134 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 353da704..216c1d16 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ pubspec.lock doc/api/ .DS_Store example/lib/generated_plugin_registrant.dart + +.flutter-plugins +.flutter-plugins-dependencies diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 878354ee..cda2f969 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -209,7 +209,7 @@ Map REASON_PHRASE = { }; const String ALLOWED_METHODS = - 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO'; + 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY'; const String ACCEPTED_BODY_TYPES = 'application/sdp, application/dtmf-relay'; const int MAX_FORWARDS = 69; const int SESSION_EXPIRES = 90; diff --git a/lib/src/event_manager/internal_events.dart b/lib/src/event_manager/internal_events.dart index 31f8b4dc..c6bd37c3 100644 --- a/lib/src/event_manager/internal_events.dart +++ b/lib/src/event_manager/internal_events.dart @@ -166,3 +166,8 @@ class EventOnNewSubscribe extends EventType { EventOnNewSubscribe({this.request}); IncomingRequest? request; } + +class EventOnPresence extends EventType { + EventOnPresence({this.request}); + IncomingRequest? request; +} diff --git a/lib/src/sip_ua_helper.dart b/lib/src/sip_ua_helper.dart index 93aef7ca..dd87c38a 100644 --- a/lib/src/sip_ua_helper.dart +++ b/lib/src/sip_ua_helper.dart @@ -1,9 +1,13 @@ import 'dart:async'; +import 'package:xml/xml.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:logger/logger.dart'; +import 'package:sip_ua/src/event_manager/internal_events.dart'; +import 'package:sip_ua/src/event_manager/subscriber_events.dart'; import 'package:sip_ua/src/rtc_session/refer_subscriber.dart'; +import 'package:sip_ua/src/subscriber.dart'; import 'config.dart'; import 'constants.dart' as DartSIP_C; import 'event_manager/event_manager.dart'; @@ -328,6 +332,17 @@ class SIPUAHelper extends EventManager { return _ua!.sendMessage(target, body, options); } + void subscribe(String target) { + var s = _ua!.subscribe( + target, "Presence", "application/sdp, application/dtmf-relay"); + + s.on(EventOnPresence(), (EventOnPresence event) { + _notifyPresenceListeners(event); + }); + + s.subscribe(); + } + void terminateSessions(Map options) { _ua!.terminateSessions(options as Map); } @@ -372,6 +387,18 @@ class SIPUAHelper extends EventManager { listener.onNewMessage(msg); } } + + void _notifyPresenceListeners(EventOnPresence event) { + // TODO: Error handling + XmlDocument body = XmlDocument.parse(event.request!.body!); + XmlElement stateNode = body.firstElementChild!.getElement('note')!; + XmlElement extNode = body.firstElementChild!.getElement('tuple')!; + PresenceStates state = parseState(stateNode.innerText)!; + String ext = extNode.getAttribute('id')!; + for (SipUaHelperListener listener in _sipUaHelperListeners) { + listener.onNewPresence(Presence(ext, state)); + } + } } enum CallStateEnum { @@ -580,12 +607,53 @@ class SIPMessageRequest { Message? message; } +class Notify { + Notify({this.notify}); + EventNotify? notify; +} + abstract class SipUaHelperListener { void transportStateChanged(TransportState state); void registrationStateChanged(RegistrationState state); void callStateChanged(Call call, CallState state); //For SIP messaga coming void onNewMessage(SIPMessageRequest msg); + void onNewPresence(Presence prs); +} + +class Presence { + Presence(this.ext, this.state); + String ext; + PresenceStates state; +} + +enum PresenceStates { + Ready, + OnThePhone, + Ringing, + OnHold, + Unavailable, + NotOnline, + _None +} + +PresenceStates? parseState(String s) { + switch (s) { + case 'Ready': + return PresenceStates.Ready; + case 'OnThePhone': + return PresenceStates.OnThePhone; + case 'Ringing': + return PresenceStates.Ringing; + case 'OnHold': + return PresenceStates.OnHold; + case 'Unavailable': + return PresenceStates.Unavailable; + case 'NotOnline': + return PresenceStates.NotOnline; + default: + return null; + } } class RegisterParams { diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart index 6abc25ce..e74f0998 100644 --- a/lib/src/subscriber.dart +++ b/lib/src/subscriber.dart @@ -55,7 +55,7 @@ class Subscriber extends EventManager { // Used to subscribe with body. _contentType = contentType; - _params = requestParams; + _params = Map.from(requestParams); _params['from_tag'] = newTag(); @@ -63,9 +63,9 @@ class Subscriber extends EventManager { _params['call_id'] = createRandomToken(20); - if (_params['cseq'] == null) { - _params['cseq'] = Math.floor((Math.random() * 10000) + 1); - } + //if (_params['cseq'] == null) { + // _params['cseq'] = Math.floor((Math.random() * 10000) + 1); + //} _state = C.STATE_INIT; @@ -87,7 +87,7 @@ class Subscriber extends EventManager { _event_name = parsed.event; // this._event_id = parsed.params && parsed.params.id; - _event_id = parsed.params && parsed.params.id; + _event_id = parsed.params['id']; String eventValue = _event_name; @@ -97,11 +97,7 @@ class Subscriber extends EventManager { _headers = cloneArray(extraHeaders); - _headers.addAll([ - 'Event: $eventValue', - 'Expires: $_expires', - 'Accept: $accept' - ]); + _headers.addAll(['Event: $eventValue', 'Expires: $_expires']); if (!_headers.any((dynamic element) => element.startsWith('Contact'))) { String contact = 'Contact: ${_ua.contact.toString()}'; @@ -143,7 +139,7 @@ class Subscriber extends EventManager { late String _event_name; - bool? _event_id; + num? _event_id; late List _headers; @@ -156,10 +152,12 @@ class Subscriber extends EventManager { return C(); } + @override void onRequestTimeout() { _dialogTerminated(C.SUBSCRIBE_RESPONSE_TIMEOUT); } + @override void onTransportError() { _dialogTerminated(C.SUBSCRIBE_TRANSPORT_ERROR); } @@ -167,7 +165,7 @@ class Subscriber extends EventManager { /** * Dialog callback. */ - void receiveRequest(IncomingRequest request) { + void receiveNotifyRequest(IncomingRequest request) { if (request.method != SipMethod.NOTIFY) { logger.warn('received non-NOTIFY request'); request.reply(405); @@ -277,7 +275,7 @@ class Subscriber extends EventManager { * Send the initial (non-fetch) and subsequent subscribe. * @param {string} body - subscribe request body. */ - void subscribe([String? body]) { + void subscribe([String? target, String? body]) { logger.debug('subscribe()'); if (_state == C.STATE_INIT) { @@ -292,7 +290,8 @@ class Subscriber extends EventManager { * Send un-subscribe or fetch-subscribe (with Expires: 0). * @param {string} body - un-subscribe request body */ - void terminate([String? body]) { + @override + void terminate([Map? body]) { logger.debug('terminate()'); // Prevent duplication un-subscribe sending. @@ -366,8 +365,7 @@ class Subscriber extends EventManager { // Create dialog if (_dialog == null) { try { - Dialog dialog = Dialog(RTCSession(ua), response, 'UAC'); - _dialog = dialog; + // TODO: Check for ok Response } catch (e) { logger.warn(e.toString()); _dialogTerminated(C.SUBSCRIBE_BAD_OK_RESPONSE); @@ -387,9 +385,11 @@ class Subscriber extends EventManager { } // Check expires value. - dynamic expires_value = response.getHeader('expires'); + String? expires_value = response.getHeader('Expires'); - if (expires_value != 0 && !expires_value) { + if (expires_value != null && + expires_value == '' && + expires_value == '0') { logger.warn('response without Expires header'); // RFC 6665 3.1.1 subscribe OK response must contain Expires header. @@ -397,7 +397,7 @@ class Subscriber extends EventManager { expires_value = '900'; } - int? expires = parseInt(expires_value, 10); + int? expires = parseInt(expires_value!, 10); if (expires! > 0) { _scheduleSubscribe(expires); @@ -409,7 +409,8 @@ class Subscriber extends EventManager { } } - void _sendSubsequentSubscribe(Object? body, List headers) { + void _sendSubsequentSubscribe(String? body, List headers) { + // TODO: Rework this if (_state == C.STATE_TERMINATED) { return; } @@ -431,27 +432,33 @@ class Subscriber extends EventManager { headers.add('Content-Type: $_contentType'); } - _dialog!.sendRequest(SipMethod.SUBSCRIBE, { + var manager = EventManager(); + manager.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { + _receiveSubscribeResponse(response.response); + }); + + manager.on(EventOnRequestTimeout(), (EventOnRequestTimeout timeout) { + onRequestTimeout(); + }); + + manager.on(EventOnTransportError(), (EventOnTransportError event) { + onTransportError(); + }); + + OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, + _ua.normalizeTarget(_target), _ua, _params, headers, body); + + RequestSender request_sender = RequestSender(_ua, request, manager); + + request_sender.send(); + + var s = _dialog!.sendRequest(SipMethod.SUBSCRIBE, { 'body': body, 'extraHeaders': headers, - 'eventHandlers': { - 'onRequestTimeout': () { - onRequestTimeout(); - }, - 'onTransportError': () { - onTransportError(); - }, - 'onSuccessResponse': (IncomingResponse response) { - _receiveSubscribeResponse(response); - }, - 'onErrorResponse': (IncomingResponse response) { - _receiveSubscribeResponse(response); - }, - 'onDialogError': (IncomingResponse response) { - _receiveSubscribeResponse(response); - } - } + 'eventHandlers': manager, }); + + print(s); } void _dialogTerminated(int terminationCode, @@ -494,23 +501,18 @@ class Subscriber extends EventManager { expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds. */ - int timeout = (expires >= 140 - ? (expires * 1000 / 2) + - Math.floor(((expires / 2) - 70) * 1000 * Math.random()) - : (expires * 1000) - 5000) - .toInt(); + num timeout = 10; - _expires_timestamp = - DateTime.now().add(Duration(milliseconds: expires * 1000)); + _expires_timestamp = DateTime.now().add(Duration(seconds: expires)); - logger.debug( - 'next SUBSCRIBE will be sent in ${Math.floor(timeout / 1000)} sec'); + logger.debug('next SUBSCRIBE will be sent in $timeout sec'); clearTimeout(_expires_timer); + _expires_timer = setTimeout(() { _expires_timer = null; _sendSubsequentSubscribe(null, _headers); - }, timeout); + }, timeout.toInt() * 1000); } int _stateStringToNumber(String? strState) { @@ -529,4 +531,8 @@ class Subscriber extends EventManager { throw TypeError('wrong state value'); } } + + void _handlePresence(EventOnPresence event) { + emit(event); + } } diff --git a/lib/src/ua.dart b/lib/src/ua.dart index 487c151e..ea3e7841 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -686,6 +686,7 @@ class UA extends EventManager { if (session != null) { session.receiveRequest(request); } else { + // TODO: Look for subscriver to notify. logger .debug('received NOTIFY request for a non existent subscription'); request.reply(481, 'Subscription does not exist'); diff --git a/pubspec.yaml b/pubspec.yaml index 868e20c0..472210f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.5.1 description: A SIP UA stack for Flutter/Dart, based on flutter-webrtc, support iOS/Android/Destkop/Web. homepage: https://github.com/cloudwebrtc/dart-sip-ua environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=2.16.0 <3.0.0" flutter: ">=2.0.0" dependencies: @@ -17,6 +17,7 @@ dependencies: recase: ^4.0.0 sdp_transform: ^0.3.2 uuid: ^3.0.2 + xml: ^5.3.0 dev_dependencies: From 1a25ca72219394900e0e7cf02538f78b15bbb423 Mon Sep 17 00:00:00 2001 From: Perondas <114622@fhwn.ac.at> Date: Mon, 4 Apr 2022 10:49:31 +0200 Subject: [PATCH 05/10] Feat: Implemented subscribe support --- example/lib/src/callscreen.dart | 5 + example/lib/src/dialpad.dart | 5 + example/lib/src/register.dart | 5 + lib/src/dialog.dart | 13 +- lib/src/dialog/request_sender.dart | 2 +- lib/src/event_manager/internal_events.dart | 5 - lib/src/grammar_parser.dart | 4 +- lib/src/notifier.dart | 324 --------------------- lib/src/rtc_session.dart | 5 +- lib/src/sip_ua_helper.dart | 68 +---- lib/src/subscriber.dart | 306 ++++++++++--------- lib/src/ua.dart | 66 +++-- pubspec.yaml | 1 - 13 files changed, 252 insertions(+), 557 deletions(-) delete mode 100644 lib/src/notifier.dart diff --git a/example/lib/src/callscreen.dart b/example/lib/src/callscreen.dart index e2e9cf1d..5269768c 100644 --- a/example/lib/src/callscreen.dart +++ b/example/lib/src/callscreen.dart @@ -581,4 +581,9 @@ class _MyCallScreenWidget extends State void onNewMessage(SIPMessageRequest msg) { // NO OP } + + @override + void onNewNotify(Notify ntf) { + // TODO: implement onNewNotify + } } diff --git a/example/lib/src/dialpad.dart b/example/lib/src/dialpad.dart index 8b12ce20..b891a1d7 100644 --- a/example/lib/src/dialpad.dart +++ b/example/lib/src/dialpad.dart @@ -311,4 +311,9 @@ class _MyDialPadWidget extends State receivedMsg = msgBody; }); } + + @override + void onNewNotify(Notify ntf) { + // TODO: implement onNewNotify + } } diff --git a/example/lib/src/register.dart b/example/lib/src/register.dart index 0e18339f..f49f73af 100644 --- a/example/lib/src/register.dart +++ b/example/lib/src/register.dart @@ -293,4 +293,9 @@ class _MyRegisterWidget extends State void onNewMessage(SIPMessageRequest msg) { // NO OP } + + @override + void onNewNotify(Notify ntf) { + // TODO: implement onNewNotify + } } diff --git a/lib/src/dialog.dart b/lib/src/dialog.dart index 78b6ec3d..baff98f8 100644 --- a/lib/src/dialog.dart +++ b/lib/src/dialog.dart @@ -36,7 +36,7 @@ class Id { // RFC 3261 12.1. class Dialog { - Dialog(RTCSession owner, dynamic message, String type, [int? state]) { + Dialog(Owner owner, dynamic message, String type, [int? state]) { state = state ?? DialogStatus.STATUS_CONFIRMED; _owner = owner; _ua = owner.ua; @@ -91,7 +91,7 @@ class Dialog { '$type dialog created with status ${_state == DialogStatus.STATUS_EARLY ? 'EARLY' : 'CONFIRMED'}'); } - RTCSession? _owner; + Owner? _owner; UA? _ua; bool uac_pending_reply = false; bool uas_pending_reply = false; @@ -108,7 +108,7 @@ class Dialog { UA? get ua => _ua; Id? get id => _id; - RTCSession? get owner => _owner; + Owner? get owner => _owner; void update(dynamic message, String type) { _state = DialogStatus.STATUS_CONFIRMED; @@ -263,3 +263,10 @@ class Dialog { return true; } } + +abstract class Owner { + Function(IncomingRequest) get receiveRequest; + int? get status; + int get TerminatedCode; + UA? get ua; +} diff --git a/lib/src/dialog/request_sender.dart b/lib/src/dialog/request_sender.dart index f0240684..46fd1e0a 100644 --- a/lib/src/dialog/request_sender.dart +++ b/lib/src/dialog/request_sender.dart @@ -91,7 +91,7 @@ class DialogRequestSender { _request.cseq = _dialog.local_seqnum!.toInt(); _reattemptTimer = setTimeout(() { // TODO(cloudwebrtc): look at dialog state instead. - if (_dialog.owner!.status != RTCSession.C.STATUS_TERMINATED) { + if (_dialog.owner!.status != _dialog.owner!.TerminatedCode) { _reattempt = true; _request_sender.send(); } diff --git a/lib/src/event_manager/internal_events.dart b/lib/src/event_manager/internal_events.dart index c6bd37c3..31f8b4dc 100644 --- a/lib/src/event_manager/internal_events.dart +++ b/lib/src/event_manager/internal_events.dart @@ -166,8 +166,3 @@ class EventOnNewSubscribe extends EventType { EventOnNewSubscribe({this.request}); IncomingRequest? request; } - -class EventOnPresence extends EventType { - EventOnPresence({this.request}); - IncomingRequest? request; -} diff --git a/lib/src/grammar_parser.dart b/lib/src/grammar_parser.dart index 5383dc1c..2bafb875 100644 --- a/lib/src/grammar_parser.dart +++ b/lib/src/grammar_parser.dart @@ -17093,7 +17093,7 @@ class GrammarParser { _failure(_expect85); } if (success) { - final $1 = $$; + final $1 = $$[2]; final $start = startPos1; var pos0 = _startPos; $$ = ((offset, expires) { @@ -22438,7 +22438,7 @@ class GrammarParser { if (!success && _cursor > _testing) { _failure(_expect115); } - return $$; + return data; } dynamic parse_Supported() { diff --git a/lib/src/notifier.dart b/lib/src/notifier.dart deleted file mode 100644 index 6418f7fa..00000000 --- a/lib/src/notifier.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:sip_ua/src/constants.dart'; -import 'package:sip_ua/src/dialog.dart'; -import 'package:sip_ua/src/event_manager/notifier_events.dart'; -import 'package:sip_ua/src/exceptions.dart'; -import 'package:sip_ua/src/logger.dart'; -import 'package:sip_ua/src/rtc_session.dart'; -import 'package:sip_ua/src/sip_message.dart'; -import 'package:sip_ua/src/timers.dart'; -import 'package:sip_ua/src/ua.dart'; -import 'package:sip_ua/src/utils.dart'; - -import 'event_manager/event_manager.dart'; - -/** - * Termination codes. - */ -class C { - // Termination codes. - static const int NOTIFY_RESPONSE_TIMEOUT = 0; - static const int NOTIFY_TRANSPORT_ERROR = 1; - static const int NOTIFY_NON_OK_RESPONSE = 2; - static const int NOTIFY_FAILED_AUTHENTICATION = 3; - static const int SEND_FINAL_NOTIFY = 4; - static const int RECEIVE_UNSUBSCRIBE = 5; - static const int SUBSCRIPTION_EXPIRED = 6; - - // Notifier states - static const int STATE_PENDING = 0; - static const int STATE_ACTIVE = 1; - static const int STATE_TERMINATED = 2; -} - -class Notifier extends EventManager { - Notifier(this.ua, this._initialSubscribe, this._content_type, - [bool pending = false, - List? extraHeaders = null, - String? allowEvents = null]) { - logger.debug('new'); - if (!_initialSubscribe.hasHeader('contact')) { - throw TypeError('subscribe - no contact header'); - } - _state = pending ? C.STATE_PENDING : C.STATE_ACTIVE; - - _terminated_reason = null; - _terminated_retry_after = null; - - String eventName = _initialSubscribe.getHeader('event'); - - _expires = parseInt(_initialSubscribe.getHeader('expires'), 10)!; - _headers = cloneArray(extraHeaders); - _headers.add('Event: $eventName'); - - // Use contact from extraHeaders or create it. - String? c = _headers - .firstWhereOrNull((dynamic header) => header.startsWith('Contact')); - if (c == null) { - _contact = 'Contact: ${ua.contact.toString()}'; - - _headers.add(_contact); - } else { - _contact = c; - } - - if (allowEvents != null) { - _headers.add('Allow-Events: $allowEvents'); - } - - _target = _initialSubscribe.from?.uri?.user; - - _initialSubscribe.to_tag = newTag(); - - // Create dialog for normal and fetch-subscribe. - Dialog dialog = Dialog(RTCSession(ua), _initialSubscribe, 'UAS'); - - _dialog = dialog; - - if (_expires > 0) { - // Set expires timer and time-stamp. - _setExpiresTimer(); - } - } - - late int _state; - String? _terminated_reason; - num? _terminated_retry_after; - late Map data; - late int _expires; - late List _headers; - late String _contact; - String? _target; - - static C getC() { - return C(); - } - - UA ua; - final IncomingRequest _initialSubscribe; - final String _content_type; - DateTime? _expires_timestamp; - Timer? _expires_timer; - late Dialog? _dialog; - - /** - * Dialog callback. - * Called also for initial subscribe. - * Supported RFC 6665 4.4.3: initial fetch subscribe (with expires: 0). - */ - void receiveRequest(IncomingRequest request) { - if (request.method != SipMethod.NOTIFY) { - request.reply(405); - - return; - } - - if (request.hasHeader('expires')) { - _expires = parseInt(request.getHeader('expires'), 10)!; - } else { - // RFC 6665 3.1.1, default expires value. - _expires = 900; - - logger - .debug('missing Expires header field, default value set: $_expires'); - } - request.reply(200, null, ['Expires: $_expires', _contact]); - - String? body = request.body; - String content_type = request.getHeader('content-type'); - bool is_unsubscribe = _expires == 0; - - if (!is_unsubscribe) { - _setExpiresTimer(); - } - - logger.debug('emit "subscribe"'); - emit(EventSubscribe( - isUnsubscribe: is_unsubscribe, - request: request, - body: body, - content_type: content_type)); - - if (is_unsubscribe) { - _dialogTerminated(C.RECEIVE_UNSUBSCRIBE); - } - } - - /** - * User API - */ - /** - * Please call after creating the Notifier instance and setting the event handlers. - */ - void start() { - logger.debug('start()'); - - receiveRequest(_initialSubscribe); - } - - /** - * Switch pending dialog state to active. - */ - void setActiveState() { - logger.debug('setActiveState()'); - - if (_state == C.STATE_PENDING) { - _state = C.STATE_ACTIVE; - } - } - - /** - * Send the initial and subsequent notify request. - * @param {string} body - notify request body. - */ - void notify([String? body = null]) { - logger.debug('notify()'); - - // Prevent send notify after final notify. - if (_dialog == null) { - logger.warn('final notify has sent'); - - return; - } - - String subs_state = _stateNumberToString(_state); - - if (_state != C.STATE_TERMINATED) { - num expires = - Math.floor(_expires_timestamp!.difference(DateTime.now()).inSeconds); - - if (expires < 0) { - expires = 0; - } - - subs_state += ';expires=$expires'; - } else { - if (_terminated_reason != null) { - subs_state += ';reason=$_terminated_reason'; - } - if (_terminated_retry_after != null) { - subs_state += ';retry-after=$_terminated_retry_after'; - } - } - - ListSlice headers = _headers.slice(0); - - headers.add('Subscription-State: $subs_state'); - - if (body != null) { - headers.add('Content-Type: $_content_type'); - } - - _dialog!.sendRequest(SipMethod.NOTIFY, { - 'body': body, - 'extraHeaders': headers, - 'eventHandlers': { - 'onRequestTimeout': () { - _dialogTerminated(C.NOTIFY_RESPONSE_TIMEOUT); - }, - 'onTransportError': () { - _dialogTerminated(C.NOTIFY_TRANSPORT_ERROR); - }, - 'onErrorResponse': (IncomingResponse response) { - if (response.status_code == 401 || response.status_code == 407) { - _dialogTerminated(C.NOTIFY_FAILED_AUTHENTICATION); - } else { - _dialogTerminated(C.NOTIFY_NON_OK_RESPONSE); - } - }, - 'onDialogError': () { - _dialogTerminated(C.NOTIFY_NON_OK_RESPONSE); - } - } - }); - } - - /** - * Terminate. (Send the final NOTIFY request). - * - * @param {string} body - Notify message body. - * @param {string} reason - Set Subscription-State reason parameter. - * @param {number} retryAfter - Set Subscription-State retry-after parameter. - */ - void terminate( - [String? body = null, String? reason = null, num? retryAfter = null]) { - logger.debug('terminate()'); - - _state = C.STATE_TERMINATED; - _terminated_reason = reason; - _terminated_retry_after = retryAfter; - - notify(body); - - _dialogTerminated(C.SEND_FINAL_NOTIFY); - } - - /** - * Get dialog state. - */ - int get state { - return _state; - } - - /** - * Get dialog id. - */ - Id? get id { - return _dialog?.id; - } - - /** - * Private API - */ - void _dialogTerminated(int termination_code) { - if (_dialog == null) { - return; - } - - _state = C.STATE_TERMINATED; - clearTimeout(_expires_timer); - - if (_dialog != null) { - _dialog!.terminate(); - _dialog = null; - } - - bool send_final_notify = termination_code == C.SUBSCRIPTION_EXPIRED; - - logger.debug( - 'emit "terminated" code=$termination_code, send final notify=$send_final_notify'); - emit(EventTerminated( - terminationCode: termination_code, sendFinalNotify: send_final_notify)); - } - - void _setExpiresTimer() { - _expires_timestamp = - DateTime.now().add(Duration(milliseconds: _expires * 1000)); - - clearTimeout(_expires_timer); - _expires_timer = setTimeout(() { - if (_dialog == null) { - return; - } - - _terminated_reason = 'timeout'; - notify(); - _dialogTerminated(C.SUBSCRIPTION_EXPIRED); - }, _expires * 1000); - } - - String _stateNumberToString(int state) { - switch (state) { - case C.STATE_PENDING: - return 'pending'; - case C.STATE_ACTIVE: - return 'active'; - case C.STATE_TERMINATED: - return 'terminated'; - default: - throw TypeError('wrong state value'); - } - } -} diff --git a/lib/src/rtc_session.dart b/lib/src/rtc_session.dart index 085dcb75..b2e40153 100644 --- a/lib/src/rtc_session.dart +++ b/lib/src/rtc_session.dart @@ -64,7 +64,7 @@ class RFC4028Timers { Timer? timer; } -class RTCSession extends EventManager { +class RTCSession extends EventManager implements Owner { RTCSession(UA? ua) { logger.debug('new'); @@ -195,6 +195,9 @@ class RTCSession extends EventManager { RTCPeerConnection? get connection => _connection; + @override + int get TerminatedCode => C.STATUS_TERMINATED; + RTCDTMFSender get dtmfSender => _connection!.createDtmfSender(_localMediaStream!.getAudioTracks()[0]); diff --git a/lib/src/sip_ua_helper.dart b/lib/src/sip_ua_helper.dart index dd87c38a..fa634ea9 100644 --- a/lib/src/sip_ua_helper.dart +++ b/lib/src/sip_ua_helper.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:xml/xml.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:logger/logger.dart'; @@ -7,7 +6,8 @@ import 'package:sip_ua/src/event_manager/internal_events.dart'; import 'package:sip_ua/src/event_manager/subscriber_events.dart'; import 'package:sip_ua/src/rtc_session/refer_subscriber.dart'; -import 'package:sip_ua/src/subscriber.dart'; +import 'package:sip_ua/src/sip_message.dart'; +import 'subscriber.dart'; import 'config.dart'; import 'constants.dart' as DartSIP_C; import 'event_manager/event_manager.dart'; @@ -332,12 +332,11 @@ class SIPUAHelper extends EventManager { return _ua!.sendMessage(target, body, options); } - void subscribe(String target) { - var s = _ua!.subscribe( - target, "Presence", "application/sdp, application/dtmf-relay"); + void subscribe(String target, String event, String contentType) { + Subscriber s = _ua!.subscribe(target, event, contentType); - s.on(EventOnPresence(), (EventOnPresence event) { - _notifyPresenceListeners(event); + s.on(EventNotify(), (EventNotify event) { + _notifyNotifyListeners(event); }); s.subscribe(); @@ -388,15 +387,9 @@ class SIPUAHelper extends EventManager { } } - void _notifyPresenceListeners(EventOnPresence event) { - // TODO: Error handling - XmlDocument body = XmlDocument.parse(event.request!.body!); - XmlElement stateNode = body.firstElementChild!.getElement('note')!; - XmlElement extNode = body.firstElementChild!.getElement('tuple')!; - PresenceStates state = parseState(stateNode.innerText)!; - String ext = extNode.getAttribute('id')!; + void _notifyNotifyListeners(EventNotify event) { for (SipUaHelperListener listener in _sipUaHelperListeners) { - listener.onNewPresence(Presence(ext, state)); + listener.onNewNotify(Notify(request: event.request)); } } } @@ -607,53 +600,18 @@ class SIPMessageRequest { Message? message; } -class Notify { - Notify({this.notify}); - EventNotify? notify; -} - abstract class SipUaHelperListener { void transportStateChanged(TransportState state); void registrationStateChanged(RegistrationState state); void callStateChanged(Call call, CallState state); - //For SIP messaga coming + //For SIP message coming void onNewMessage(SIPMessageRequest msg); - void onNewPresence(Presence prs); -} - -class Presence { - Presence(this.ext, this.state); - String ext; - PresenceStates state; + void onNewNotify(Notify ntf); } -enum PresenceStates { - Ready, - OnThePhone, - Ringing, - OnHold, - Unavailable, - NotOnline, - _None -} - -PresenceStates? parseState(String s) { - switch (s) { - case 'Ready': - return PresenceStates.Ready; - case 'OnThePhone': - return PresenceStates.OnThePhone; - case 'Ringing': - return PresenceStates.Ringing; - case 'OnHold': - return PresenceStates.OnHold; - case 'Unavailable': - return PresenceStates.Unavailable; - case 'NotOnline': - return PresenceStates.NotOnline; - default: - return null; - } +class Notify { + Notify({this.request}); + IncomingRequest? request; } class RegisterParams { diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart index e74f0998..7d7bde60 100644 --- a/lib/src/subscriber.dart +++ b/lib/src/subscriber.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:sip_ua/sip_ua.dart'; @@ -41,8 +42,46 @@ class C { static const int STATE_NOTIFY_WAIT = 4; } -class Subscriber extends EventManager { - Subscriber(this._ua, this._target, String eventName, String accept, +class Subscriber extends EventManager implements Owner { + String? _id; + + final String _target; + + late int _expires; + + String? _contentType; + + late Map _params; + + late int _state; + + late Dialog? _dialog; + + DateTime? _expires_timestamp; + + Timer? _expires_timer; + + late bool _terminated; + + Timer? _unsubscribe_timeout_timer; + + late Map _data; + + late String _event_name; + + num? _event_id; + + late List _headers; + + late List> _queue; + + @override + late Function(IncomingRequest p1) receiveRequest; + + @override + UA ua; + + Subscriber(this.ua, this._target, String eventName, String accept, [int expires = 900, String? contentType, String? allowEvents, @@ -100,7 +139,7 @@ class Subscriber extends EventManager { _headers.addAll(['Event: $eventValue', 'Expires: $_expires']); if (!_headers.any((dynamic element) => element.startsWith('Contact'))) { - String contact = 'Contact: ${_ua.contact.toString()}'; + String contact = 'Contact: ${ua.contact.toString()}'; _headers.add(contact); } @@ -109,54 +148,27 @@ class Subscriber extends EventManager { _headers.add('Allow-Events: $allowEvents'); } + receiveRequest = receiveNotifyRequest; + // To enqueue subscribes created before receive initial subscribe OK. _queue = >[]; } + String? get id => _id; - final UA _ua; - - final String _target; - - late int _expires; - - String? _contentType; - - late Map _params; - - late int _state; - - late Dialog? _dialog; - - DateTime? _expires_timestamp; - - Timer? _expires_timer; - - late bool _terminated; - - Timer? _unsubscribe_timeout_timer; - - late Map _data; - - late String _event_name; - - num? _event_id; - - late List _headers; - - late List> _queue; + int? get status => _state; - /** - * Expose C object. - */ - static C getC() { - return C(); - } + @override + int get TerminatedCode => C.STATE_TERMINATED; @override void onRequestTimeout() { _dialogTerminated(C.SUBSCRIBE_RESPONSE_TIMEOUT); } + /** + * User API + */ + @override void onTransportError() { _dialogTerminated(C.SUBSCRIBE_TRANSPORT_ERROR); @@ -176,7 +188,7 @@ class Subscriber extends EventManager { // RFC 6665 8.2.1. Check if event header matches. dynamic eventHeader = request.parseHeader('Event'); - if (!eventHeader) { + if (eventHeader == null) { logger.warn('missed Event header'); request.reply(400); _dialogTerminated(C.RECEIVE_BAD_NOTIFY); @@ -185,7 +197,7 @@ class Subscriber extends EventManager { } dynamic eventName = eventHeader.event; - bool eventId = eventHeader.params && eventHeader.params.id; + num? eventId = eventHeader.params['id']; if (eventName != _event_name || eventId != _event_id) { logger.warn('Event header does not match SUBSCRIBE'); @@ -198,7 +210,7 @@ class Subscriber extends EventManager { // Process Subscription-State header. dynamic subsState = request.parseHeader('subscription-state'); - if (!subsState) { + if (subsState == null) { logger.warn('missed Subscription-State header'); request.reply(400); _dialogTerminated(C.RECEIVE_BAD_NOTIFY); @@ -267,10 +279,6 @@ class Subscriber extends EventManager { } } - /** - * User API - */ - /** * Send the initial (non-fetch) and subsequent subscribe. * @param {string} body - subscribe request body. @@ -290,8 +298,7 @@ class Subscriber extends EventManager { * Send un-subscribe or fetch-subscribe (with Expires: 0). * @param {string} body - un-subscribe request body */ - @override - void terminate([Map? body]) { + void terminate(String? body) { logger.debug('terminate()'); // Prevent duplication un-subscribe sending. @@ -320,52 +327,47 @@ class Subscriber extends EventManager { }, final_notify_timeout); } - /** - * Private API. - */ - void _sendInitialSubscribe(String? body, List headers) { - if (body != null) { - if (_contentType == null) { - throw TypeError('content_type is undefined'); - } - - headers = headers.slice(0); - headers.add('Content-Type: $_contentType'); + void _dialogTerminated(int terminationCode, + [String? reason, int? retryAfter]) { + // To prevent duplicate emit terminated event. + if (_state == C.STATE_TERMINATED) { + return; } - _state = C.STATE_NOTIFY_WAIT; - - OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, - _ua.normalizeTarget(_target), _ua, _params, headers, body); - - EventManager handler = EventManager(); - - handler.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { - _receiveSubscribeResponse(response.response); - }); + _state = C.STATE_TERMINATED; - handler.on(EventOnRequestTimeout(), (EventOnRequestTimeout timeout) { - onRequestTimeout(); - }); + // Clear timers. + clearTimeout(_expires_timer); + clearTimeout(_unsubscribe_timeout_timer); - handler.on(EventOnTransportError(), (EventOnTransportError event) { - onTransportError(); - }); + if (_dialog != null) { + _dialog!.terminate(); + _dialog = null; + } - RequestSender request_sender = RequestSender(_ua, request, handler); + logger.debug('emit "terminated" code=$terminationCode'); + emit(EventTerminated( + TerminationCode: terminationCode, + reason: reason, + retryAfter: retryAfter)); + } - request_sender.send(); + void _handlePresence(EventNotify event) { + emit(event); } void _receiveSubscribeResponse(IncomingResponse? response) { if (response == null) { throw ArgumentError('Incoming response was null'); } + if (response.status_code >= 200 && response.status_code! < 300) { // Create dialog if (_dialog == null) { + _id = response.call_id! + response.from_tag! + response.to_tag!; try { - // TODO: Check for ok Response + Dialog dialog = Dialog(this, response, 'UAC'); + _dialog = dialog; } catch (e) { logger.warn(e.toString()); _dialogTerminated(C.SUBSCRIBE_BAD_OK_RESPONSE); @@ -382,8 +384,14 @@ class Subscriber extends EventManager { _sendSubsequentSubscribe(sub['body'], sub['headers']); } + } else { + ua.destroySubscriber(this); + _id = response.call_id! + response.from_tag! + response.to_tag!; + ua.newSubscriber(sub: this); } + ua.newSubscriber(sub: this); + // Check expires value. String? expires_value = response.getHeader('Expires'); @@ -409,8 +417,73 @@ class Subscriber extends EventManager { } } + void _scheduleSubscribe(int expires) { + /* + If the expires time is less than 140 seconds we do not support Chrome intensive timer throttling mode. + In this case, the re-subscribe is sent 5 seconds before the subscription expiration. + + When Chrome is in intensive timer throttling mode, in the worst case, + the timer will be 60 seconds late. + We give the server 10 seconds to make sure it will execute the command even if it is heavily loaded. + As a result, we order the time no later than 70 seconds before the subscription expiration. + Resulting time calculated as half time interval + (half interval - 70) * random. + + E.g. expires is 140, re-subscribe will be ordered to send in 70 seconds. + expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds. + */ + + num timeout = expires / 2; + + _expires_timestamp = DateTime.now().add(Duration(seconds: expires)); + + logger.debug('next SUBSCRIBE will be sent in $timeout sec'); + + clearTimeout(_expires_timer); + + _expires_timer = setTimeout(() { + _expires_timer = null; + _sendSubsequentSubscribe(null, _headers); + }, timeout.toInt() * 1000); + } + + /** + * Private API. + */ + void _sendInitialSubscribe(String? body, List headers) { + if (body != null) { + if (_contentType == null) { + throw TypeError('content_type is undefined'); + } + + headers = headers.slice(0); + headers.add('Content-Type: $_contentType'); + } + + _state = C.STATE_NOTIFY_WAIT; + + OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, + ua.normalizeTarget(_target), ua, _params, headers, body); + + EventManager handler = EventManager(); + + handler.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { + _receiveSubscribeResponse(response.response); + }); + + handler.on(EventOnRequestTimeout(), (EventOnRequestTimeout timeout) { + onRequestTimeout(); + }); + + handler.on(EventOnTransportError(), (EventOnTransportError event) { + onTransportError(); + }); + + RequestSender request_sender = RequestSender(ua, request, handler); + + request_sender.send(); + } + void _sendSubsequentSubscribe(String? body, List headers) { - // TODO: Rework this if (_state == C.STATE_TERMINATED) { return; } @@ -446,9 +519,9 @@ class Subscriber extends EventManager { }); OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, - _ua.normalizeTarget(_target), _ua, _params, headers, body); + ua.normalizeTarget(_target), ua, _params, headers, body); - RequestSender request_sender = RequestSender(_ua, request, manager); + RequestSender request_sender = RequestSender(ua, request, manager); request_sender.send(); @@ -457,62 +530,6 @@ class Subscriber extends EventManager { 'extraHeaders': headers, 'eventHandlers': manager, }); - - print(s); - } - - void _dialogTerminated(int terminationCode, - [String? reason, int? retryAfter]) { - // To prevent duplicate emit terminated event. - if (_state == C.STATE_TERMINATED) { - return; - } - - _state = C.STATE_TERMINATED; - - // Clear timers. - clearTimeout(_expires_timer); - clearTimeout(_unsubscribe_timeout_timer); - - if (_dialog != null) { - _dialog!.terminate(); - _dialog = null; - } - - logger.debug('emit "terminated" code=$terminationCode'); - emit(EventTerminated( - TerminationCode: terminationCode, - reason: reason, - retryAfter: retryAfter)); - } - - void _scheduleSubscribe(int expires) { - /* - If the expires time is less than 140 seconds we do not support Chrome intensive timer throttling mode. - In this case, the re-subscribe is sent 5 seconds before the subscription expiration. - - When Chrome is in intensive timer throttling mode, in the worst case, - the timer will be 60 seconds late. - We give the server 10 seconds to make sure it will execute the command even if it is heavily loaded. - As a result, we order the time no later than 70 seconds before the subscription expiration. - Resulting time calculated as half time interval + (half interval - 70) * random. - - E.g. expires is 140, re-subscribe will be ordered to send in 70 seconds. - expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds. - */ - - num timeout = 10; - - _expires_timestamp = DateTime.now().add(Duration(seconds: expires)); - - logger.debug('next SUBSCRIBE will be sent in $timeout sec'); - - clearTimeout(_expires_timer); - - _expires_timer = setTimeout(() { - _expires_timer = null; - _sendSubsequentSubscribe(null, _headers); - }, timeout.toInt() * 1000); } int _stateStringToNumber(String? strState) { @@ -532,7 +549,16 @@ class Subscriber extends EventManager { } } - void _handlePresence(EventOnPresence event) { - emit(event); + /** + * Expose C object. + */ + static C getC() { + return C(); } } + +class SubscriptionId { + String target; + String event; + SubscriptionId(this.target, this.event); +} diff --git a/lib/src/ua.dart b/lib/src/ua.dart index ea3e7841..4379563c 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:sip_ua/src/data.dart'; -import 'package:sip_ua/src/subscriber.dart'; +import 'data.dart'; +import 'subscriber.dart'; import 'config.dart' as config; import 'config.dart'; import 'constants.dart' as DartSIP_C; @@ -12,7 +12,6 @@ import 'event_manager/internal_events.dart'; import 'exceptions.dart' as Exceptions; import 'logger.dart'; import 'message.dart'; -import 'notifier.dart'; import 'parser.dart' as Parser; import 'registrator.dart'; import 'rtc_session.dart'; @@ -124,8 +123,11 @@ class UA extends EventManager { // Initialize registrator. _registrator = Registrator(this); + + _subscribers = {}; } + late Map _subscribers; Map? _cache; Settings? _configuration; DynamicSettings? _dynConfiguration; @@ -226,22 +228,6 @@ class UA extends EventManager { allowEvents, requestParams, extraHeaders); } - /** - * Create notifier instance - */ - Notifier notify( - IncomingRequest subscribe, - String contentType, [ - bool pending = false, - List? extraHeaders = null, - String? allowEvents = null, - ]) { - logger.debug('notify()'); - - return Notifier( - this, subscribe, contentType, pending, extraHeaders, allowEvents); - } - /** * Get the Registrator instance. */ @@ -455,6 +441,20 @@ class UA extends EventManager { emit(EventTransactionDestroyed(transaction: transaction)); } + /** + * Subscriber + */ + void newSubscriber({required Subscriber sub}) { + _subscribers[sub.id] = sub; + } + + /** + * Subscriber destroyed. + */ + void destroySubscriber(Subscriber sub) { + _subscribers.remove(sub.id); + } + /** * Dialog */ @@ -623,7 +623,7 @@ class UA extends EventManager { dialog = _findDialog( replaces.call_id, replaces.from_tag!, replaces.to_tag!); if (dialog != null) { - session = dialog.owner; + session = dialog.owner as RTCSession?; if (!session!.isEnded()) { session.receiveRequest(request); } else { @@ -681,12 +681,11 @@ class UA extends EventManager { if (dialog != null) { dialog.receiveRequest(request); } else if (method == SipMethod.NOTIFY) { - session = - _findSession(request.call_id!, request.from_tag, request.to_tag); - if (session != null) { - session.receiveRequest(request); + Subscriber? sub = _findSubscriber( + request.call_id!, request.from_tag!, request.to_tag!); + if (sub != null) { + sub.receiveRequest(request); } else { - // TODO: Look for subscriver to notify. logger .debug('received NOTIFY request for a non existent subscription'); request.reply(481, 'Subscription does not exist'); @@ -708,6 +707,23 @@ class UA extends EventManager { // Utils. // ============ + Subscriber? _findSubscriber(String call_id, String from_tag, String to_tag) { + String id = call_id + from_tag + to_tag; + Subscriber? sub = _subscribers[id]; + + if (sub != null) { + return sub; + } else { + id = call_id + to_tag + from_tag; + sub = _subscribers[id]; + if (sub != null) { + return sub; + } else { + return null; + } + } + } + /** * Get the session to which the request belongs to, if any. */ diff --git a/pubspec.yaml b/pubspec.yaml index 472210f1..00178691 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,6 @@ dependencies: recase: ^4.0.0 sdp_transform: ^0.3.2 uuid: ^3.0.2 - xml: ^5.3.0 dev_dependencies: From 7d390115ecb71b552e9067bdaa2220912a523d2c Mon Sep 17 00:00:00 2001 From: Perondas Date: Thu, 7 Apr 2022 11:00:02 +0200 Subject: [PATCH 06/10] Changed subscriber id to be the call id --- lib/src/subscriber.dart | 4 ++-- lib/src/ua.dart | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart index 7d7bde60..56fa925e 100644 --- a/lib/src/subscriber.dart +++ b/lib/src/subscriber.dart @@ -364,7 +364,7 @@ class Subscriber extends EventManager implements Owner { if (response.status_code >= 200 && response.status_code! < 300) { // Create dialog if (_dialog == null) { - _id = response.call_id! + response.from_tag! + response.to_tag!; + _id = response.call_id!; try { Dialog dialog = Dialog(this, response, 'UAC'); _dialog = dialog; @@ -386,7 +386,7 @@ class Subscriber extends EventManager implements Owner { } } else { ua.destroySubscriber(this); - _id = response.call_id! + response.from_tag! + response.to_tag!; + _id = response.call_id!; ua.newSubscriber(sub: this); } diff --git a/lib/src/ua.dart b/lib/src/ua.dart index 4379563c..62b88629 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -708,20 +708,10 @@ class UA extends EventManager { // ============ Subscriber? _findSubscriber(String call_id, String from_tag, String to_tag) { - String id = call_id + from_tag + to_tag; + String id = call_id; Subscriber? sub = _subscribers[id]; - if (sub != null) { - return sub; - } else { - id = call_id + to_tag + from_tag; - sub = _subscribers[id]; - if (sub != null) { - return sub; - } else { - return null; - } - } + return sub; } /** From 0af5e7b7d58b8726c9aeec9692ae80479e316e0f Mon Sep 17 00:00:00 2001 From: Perondas Date: Thu, 7 Apr 2022 11:07:32 +0200 Subject: [PATCH 07/10] Added termination to subscribers --- lib/src/ua.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/ua.dart b/lib/src/ua.dart index 62b88629..99c1569e 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -332,6 +332,19 @@ class UA extends EventManager { } }); + // Run _terminate on ever subscription + _subscribers.forEach((String? key, _) { + if (_subscribers.containsKey(key)) { + logger.debug('closing subscription $key'); + try { + Subscriber subscriber = _subscribers[key]!; + subscriber.terminate(null); + } catch (error, s) { + Log.e(error.toString(), null, s); + } + } + }); + // Run _close_ on every applicant. for (Message message in _applicants) { try { From cf0b90e96d045d38a6069acd494c487772906142 Mon Sep 17 00:00:00 2001 From: Perondas Date: Thu, 7 Apr 2022 14:56:35 +0200 Subject: [PATCH 08/10] Moved back sdk version to 2.14.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 00178691..868e20c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.5.1 description: A SIP UA stack for Flutter/Dart, based on flutter-webrtc, support iOS/Android/Destkop/Web. homepage: https://github.com/cloudwebrtc/dart-sip-ua environment: - sdk: ">=2.16.0 <3.0.0" + sdk: ">=2.14.0 <3.0.0" flutter: ">=2.0.0" dependencies: From f1d942372a9138c2ada8c16fe9933f8086f5a38a Mon Sep 17 00:00:00 2001 From: Perondas Date: Fri, 8 Apr 2022 13:44:12 +0200 Subject: [PATCH 09/10] Fixed unrealted bug in hangup --- lib/src/sip_ua_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/sip_ua_helper.dart b/lib/src/sip_ua_helper.dart index fa634ea9..534eace7 100644 --- a/lib/src/sip_ua_helper.dart +++ b/lib/src/sip_ua_helper.dart @@ -442,7 +442,7 @@ class Call { void hangup([Map? options]) { assert(_session != null, 'ERROR(hangup): rtc session is invalid!'); - _session.terminate(options as Map?); + _session.terminate(options); } void hold() { From ad7865f77c78ef1765b8e8a89b0821753cddfe98 Mon Sep 17 00:00:00 2001 From: Perondas Date: Sat, 9 Apr 2022 17:22:37 +0200 Subject: [PATCH 10/10] Made imports relative --- lib/src/sip_ua_helper.dart | 9 ++++----- lib/src/subscriber.dart | 28 ++++++++++++---------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/lib/src/sip_ua_helper.dart b/lib/src/sip_ua_helper.dart index 534eace7..74683e68 100644 --- a/lib/src/sip_ua_helper.dart +++ b/lib/src/sip_ua_helper.dart @@ -2,19 +2,18 @@ import 'dart:async'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:logger/logger.dart'; -import 'package:sip_ua/src/event_manager/internal_events.dart'; -import 'package:sip_ua/src/event_manager/subscriber_events.dart'; -import 'package:sip_ua/src/rtc_session/refer_subscriber.dart'; -import 'package:sip_ua/src/sip_message.dart'; -import 'subscriber.dart'; import 'config.dart'; import 'constants.dart' as DartSIP_C; import 'event_manager/event_manager.dart'; +import 'event_manager/subscriber_events.dart'; import 'logger.dart'; import 'message.dart'; import 'rtc_session.dart'; +import 'rtc_session/refer_subscriber.dart'; +import 'sip_message.dart'; import 'stack_trace_nj.dart'; +import 'subscriber.dart'; import 'transports/websocket_interface.dart'; import 'ua.dart'; diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart index 56fa925e..c48dd8d5 100644 --- a/lib/src/subscriber.dart +++ b/lib/src/subscriber.dart @@ -1,24 +1,20 @@ import 'dart:async'; -import 'dart:math'; import 'package:collection/collection.dart'; -import 'package:sip_ua/sip_ua.dart'; -import 'package:sip_ua/src/constants.dart'; -import 'package:sip_ua/src/dialog.dart'; -import 'package:sip_ua/src/event_manager/internal_events.dart'; -import 'package:sip_ua/src/event_manager/subscriber_events.dart'; -import 'package:sip_ua/src/exceptions.dart'; -import 'package:sip_ua/src/grammar.dart'; -import 'package:sip_ua/src/logger.dart'; -import 'package:sip_ua/src/request_sender.dart'; -import 'package:sip_ua/src/rtc_session.dart'; -import 'package:sip_ua/src/sanity_check.dart'; -import 'package:sip_ua/src/sip_message.dart'; -import 'package:sip_ua/src/timers.dart'; -import 'package:sip_ua/src/ua.dart'; -import 'package:sip_ua/src/utils.dart'; +import 'constants.dart'; +import 'dialog.dart'; import 'event_manager/event_manager.dart'; +import 'event_manager/internal_events.dart'; +import 'event_manager/subscriber_events.dart'; +import 'exceptions.dart'; +import 'grammar.dart'; +import 'logger.dart'; +import 'request_sender.dart'; +import 'sip_message.dart'; +import 'timers.dart'; +import 'ua.dart'; +import 'utils.dart'; /** * Termination codes.