diff --git a/lib/src/event_manager/event_manager.dart b/lib/src/event_manager/event_manager.dart index 14c6b76d..1527eeaf 100644 --- a/lib/src/event_manager/event_manager.dart +++ b/lib/src/event_manager/event_manager.dart @@ -4,6 +4,7 @@ import 'events.dart'; export 'call_events.dart'; export 'events.dart'; export 'message_events.dart'; +export 'options_events.dart'; export 'refer_events.dart'; export 'register_events.dart'; export 'transport_events.dart'; diff --git a/lib/src/event_manager/options_events.dart b/lib/src/event_manager/options_events.dart new file mode 100644 index 00000000..668c2b5d --- /dev/null +++ b/lib/src/event_manager/options_events.dart @@ -0,0 +1,9 @@ +import '../options.dart'; +import 'events.dart'; + +class EventNewOptions extends EventType { + EventNewOptions({this.message, this.originator, this.request}); + dynamic request; + String? originator; + Options? message; +} diff --git a/lib/src/message.dart b/lib/src/message.dart index 9822079a..0cf678db 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -11,7 +11,7 @@ import 'ua.dart'; import 'uri.dart'; import 'utils.dart' as Utils; -class Message extends EventManager { +class Message extends EventManager with Applicant { Message(UA ua) { _ua = ua; _request = null; @@ -184,6 +184,7 @@ class Message extends EventManager { 'Transport Error'); } + @override void close() { _closed = true; _ua!.destroyMessage(this); diff --git a/lib/src/options.dart b/lib/src/options.dart new file mode 100644 index 00000000..b1958dd1 --- /dev/null +++ b/lib/src/options.dart @@ -0,0 +1,233 @@ +import 'package:sip_ua/src/name_addr_header.dart'; +import 'constants.dart' as DartSIP_C; +import 'constants.dart'; +import 'event_manager/event_manager.dart'; +import 'event_manager/internal_events.dart'; +import 'exceptions.dart' as Exceptions; +import 'logger.dart'; +import 'request_sender.dart'; +import 'sip_message.dart'; +import 'ua.dart'; +import 'uri.dart'; +import 'utils.dart' as Utils; + +class Options extends EventManager with Applicant { + Options(UA ua) { + _ua = ua; + _request = null; + _closed = false; + + _direction = null; + _local_identity = null; + _remote_identity = null; + + // Whether an incoming Options has been replied. + _is_replied = false; + + // Custom Options empty object for high level use. + _data = {}; + } + + UA? _ua; + dynamic _request; + bool? _closed; + String? _direction; + NameAddrHeader? _local_identity; + NameAddrHeader? _remote_identity; + bool? _is_replied; + Map? _data; + String? get direction => _direction; + + NameAddrHeader? get local_identity => _local_identity; + + NameAddrHeader? get remote_identity => _remote_identity; + + Map? get data => _data; + + void send(String target, String body, [Map? options]) { + String originalTarget = target; + options = options ?? {}; + + if (target == null) { + throw Exceptions.TypeError('A target is required for OPTIONS'); + } + + // Check target validity. + URI? normalized = _ua!.normalizeTarget(target); + if (normalized == null) { + throw Exceptions.TypeError('Invalid target: $originalTarget'); + } + + // Get call options. + List extraHeaders = Utils.cloneArray(options['extraHeaders']); + EventManager eventHandlers = options['eventHandlers'] ?? EventManager(); + String contentType = options['contentType'] ?? 'application/sdp'; + + // Set event handlers. + addAllEventHandlers(eventHandlers); + + extraHeaders.add('Content-Type: $contentType'); + + _request = + OutgoingRequest(SipMethod.OPTIONS, normalized, _ua, null, extraHeaders); + if (body != null) { + _request.body = body; + } + + EventManager handlers = EventManager(); + handlers.on(EventOnRequestTimeout(), (EventOnRequestTimeout value) { + _onRequestTimeout(); + }); + handlers.on(EventOnTransportError(), (EventOnTransportError value) { + _onTransportError(); + }); + handlers.on(EventOnReceiveResponse(), (EventOnReceiveResponse event) { + _receiveResponse(event.response); + }); + + RequestSender request_sender = RequestSender(_ua!, _request, handlers); + + _newOptions('local', _request); + + request_sender.send(); + } + + void init_incoming(IncomingRequest request) { + _request = request; + + _newOptions('remote', request); + + // Reply with a 200 OK if the user didn't reply. + if (_is_replied == null || _is_replied == false) { + _is_replied = true; + request.reply(200); + } + + close(); + } + + /* + * Accept the incoming Options + * Only valid for incoming Options + */ + void accept(Map options) { + List extraHeaders = Utils.cloneArray(options['extraHeaders']); + String? body = options['body']; + + if (_direction != 'incoming') { + throw Exceptions.NotSupportedError( + '"accept" not supported for outgoing Options'); + } + + if (_is_replied != null) { + throw AssertionError('incoming Options already replied'); + } + + _is_replied = true; + _request.reply(200, null, extraHeaders, body); + } + + /** + * Reject the incoming Options + * Only valid for incoming Optionss + */ + void reject(Map options) { + int status_code = options['status_code'] ?? 480; + String? reason_phrase = options['reason_phrase']; + List extraHeaders = Utils.cloneArray(options['extraHeaders']); + String? body = options['body']; + + if (_direction != 'incoming') { + throw Exceptions.NotSupportedError( + '"reject" not supported for outgoing Options'); + } + + if (_is_replied != null) { + throw AssertionError('incoming Options already replied'); + } + + if (status_code < 300 || status_code >= 700) { + throw Exceptions.TypeError('Invalid status_code: $status_code'); + } + + _is_replied = true; + _request.reply(status_code, reason_phrase, extraHeaders, body); + } + + void _receiveResponse(IncomingResponse? response) { + if (_closed != null) { + return; + } + if (RegExp(r'^1[0-9]{2}$').hasMatch(response!.status_code)) { + // Ignore provisional responses. + } else if (RegExp(r'^2[0-9]{2}$').hasMatch(response.status_code)) { + _succeeded('remote', response); + } else { + String cause = Utils.sipErrorCause(response.status_code); + _failed('remote', response.status_code, cause, response.reason_phrase); + } + } + + void _onRequestTimeout() { + if (_closed != null) { + return; + } + _failed( + 'system', 408, DartSIP_C.CausesType.REQUEST_TIMEOUT, 'Request Timeout'); + } + + void _onTransportError() { + if (_closed != null) { + return; + } + _failed('system', 500, DartSIP_C.CausesType.CONNECTION_ERROR, + 'Transport Error'); + } + + @override + void close() { + _closed = true; + _ua!.destroyOptions(this); + } + + /** + * Internal Callbacks + */ + + void _newOptions(String originator, dynamic request) { + if (originator == 'remote') { + _direction = 'incoming'; + _local_identity = request.to; + _remote_identity = request.from; + } else if (originator == 'local') { + _direction = 'outgoing'; + _local_identity = request.from; + _remote_identity = request.to; + } + + _ua!.newOptions(this, originator, request); + } + + void _failed(String originator, int? status_code, String cause, + String? reason_phrase) { + logger.debug('OPTIONS failed'); + close(); + logger.debug('emit "failed"'); + emit(EventCallFailed( + originator: originator, + cause: ErrorCause( + cause: cause, + status_code: status_code, + reason_phrase: reason_phrase))); + } + + void _succeeded(String originator, IncomingResponse? response) { + logger.debug('OPTIONS succeeded'); + + close(); + + logger.debug('emit "succeeded"'); + + emit(EventSucceeded(originator: originator, response: response)); + } +} diff --git a/lib/src/ua.dart b/lib/src/ua.dart index bf338acb..e39e45a8 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -11,6 +11,7 @@ import 'event_manager/internal_events.dart'; import 'exceptions.dart' as Exceptions; import 'logger.dart'; import 'message.dart'; +import 'options.dart'; import 'parser.dart' as Parser; import 'registrator.dart'; import 'rtc_session.dart'; @@ -91,8 +92,8 @@ class UA extends EventManager { _dynConfiguration = DynamicSettings(); _dialogs = {}; - // User actions outside any session/dialog (MESSAGE). - _applicants = {}; + // User actions outside any session/dialog (MESSAGE/OPTIONS). + _applicants = {}; _sessions = {}; _transport = null; @@ -128,7 +129,7 @@ class UA extends EventManager { Settings? _configuration; DynamicSettings? _dynConfiguration; late Map _dialogs; - late Set _applicants; + late Set _applicants; Map _sessions = {}; Transport? _transport; Contact? _contact; @@ -260,6 +261,24 @@ class UA extends EventManager { return message; } + /** + * Send a Options. + * + * -param {String} target + * -param {String} body + * -param {Object} [options] + * + * -throws {TypeError} + * + */ + Options sendOptions( + String target, String body, Map? options) { + logger.debug('sendOptions()'); + Options message = Options(this); + message.send(target, body, options); + return message; + } + /** * Terminate ongoing sessions. */ @@ -310,9 +329,9 @@ class UA extends EventManager { }); // Run _close_ on every applicant. - for (Message message in _applicants) { + for (Applicant applicant in _applicants) { try { - message.close(); + applicant.close(); } catch (error) {} } @@ -441,6 +460,15 @@ class UA extends EventManager { message: message, originator: originator, request: request)); } + /** + * Options + */ + void newOptions(Options message, String originator, dynamic request) { + _applicants.add(message); + emit(EventNewOptions( + message: message, originator: originator, request: request)); + } + /** * Message destroyed. */ @@ -448,6 +476,13 @@ class UA extends EventManager { _applicants.remove(message); } + /** + * Options destroyed. + */ + void destroyOptions(Options message) { + _applicants.remove(message); + } + /** * RTCSession */ @@ -548,7 +583,13 @@ class UA extends EventManager { * They are processed as if they had been received outside the dialog. */ if (method == SipMethod.OPTIONS) { - request.reply(200); + if (!hasListeners(EventNewOptions())) { + request.reply(200); + return; + } + Options message = Options(this); + message.init_incoming(request); + return; } else if (method == SipMethod.MESSAGE) { if (!hasListeners(EventNewMessage())) { request.reply(405); @@ -883,3 +924,7 @@ class UA extends EventManager { } } } + +mixin Applicant { + void close(); +}