Skip to content

Commit 58f5c57

Browse files
authored
SIP OPTIONS support (#270)
* Allow sending and responding to SIP OPTIONS messages * Add sendOptions and handle incoming OPTIONS * Check bool value with null check * Changes after running 'flutter format lib/ test/'
1 parent 0e5256a commit 58f5c57

File tree

5 files changed

+296
-7
lines changed

5 files changed

+296
-7
lines changed

lib/src/event_manager/event_manager.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'events.dart';
44
export 'call_events.dart';
55
export 'events.dart';
66
export 'message_events.dart';
7+
export 'options_events.dart';
78
export 'refer_events.dart';
89
export 'register_events.dart';
910
export 'transport_events.dart';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import '../options.dart';
2+
import 'events.dart';
3+
4+
class EventNewOptions extends EventType {
5+
EventNewOptions({this.message, this.originator, this.request});
6+
dynamic request;
7+
String? originator;
8+
Options? message;
9+
}

lib/src/message.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'ua.dart';
1111
import 'uri.dart';
1212
import 'utils.dart' as Utils;
1313

14-
class Message extends EventManager {
14+
class Message extends EventManager with Applicant {
1515
Message(UA ua) {
1616
_ua = ua;
1717
_request = null;
@@ -184,6 +184,7 @@ class Message extends EventManager {
184184
'Transport Error');
185185
}
186186

187+
@override
187188
void close() {
188189
_closed = true;
189190
_ua!.destroyMessage(this);

lib/src/options.dart

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import 'package:sip_ua/src/name_addr_header.dart';
2+
import 'constants.dart' as DartSIP_C;
3+
import 'constants.dart';
4+
import 'event_manager/event_manager.dart';
5+
import 'event_manager/internal_events.dart';
6+
import 'exceptions.dart' as Exceptions;
7+
import 'logger.dart';
8+
import 'request_sender.dart';
9+
import 'sip_message.dart';
10+
import 'ua.dart';
11+
import 'uri.dart';
12+
import 'utils.dart' as Utils;
13+
14+
class Options extends EventManager with Applicant {
15+
Options(UA ua) {
16+
_ua = ua;
17+
_request = null;
18+
_closed = false;
19+
20+
_direction = null;
21+
_local_identity = null;
22+
_remote_identity = null;
23+
24+
// Whether an incoming Options has been replied.
25+
_is_replied = false;
26+
27+
// Custom Options empty object for high level use.
28+
_data = <String, dynamic>{};
29+
}
30+
31+
UA? _ua;
32+
dynamic _request;
33+
bool? _closed;
34+
String? _direction;
35+
NameAddrHeader? _local_identity;
36+
NameAddrHeader? _remote_identity;
37+
bool? _is_replied;
38+
Map<String, dynamic>? _data;
39+
String? get direction => _direction;
40+
41+
NameAddrHeader? get local_identity => _local_identity;
42+
43+
NameAddrHeader? get remote_identity => _remote_identity;
44+
45+
Map<String, dynamic>? get data => _data;
46+
47+
void send(String target, String body, [Map<String, dynamic>? options]) {
48+
String originalTarget = target;
49+
options = options ?? <String, dynamic>{};
50+
51+
if (target == null) {
52+
throw Exceptions.TypeError('A target is required for OPTIONS');
53+
}
54+
55+
// Check target validity.
56+
URI? normalized = _ua!.normalizeTarget(target);
57+
if (normalized == null) {
58+
throw Exceptions.TypeError('Invalid target: $originalTarget');
59+
}
60+
61+
// Get call options.
62+
List<dynamic> extraHeaders = Utils.cloneArray(options['extraHeaders']);
63+
EventManager eventHandlers = options['eventHandlers'] ?? EventManager();
64+
String contentType = options['contentType'] ?? 'application/sdp';
65+
66+
// Set event handlers.
67+
addAllEventHandlers(eventHandlers);
68+
69+
extraHeaders.add('Content-Type: $contentType');
70+
71+
_request =
72+
OutgoingRequest(SipMethod.OPTIONS, normalized, _ua, null, extraHeaders);
73+
if (body != null) {
74+
_request.body = body;
75+
}
76+
77+
EventManager handlers = EventManager();
78+
handlers.on(EventOnRequestTimeout(), (EventOnRequestTimeout value) {
79+
_onRequestTimeout();
80+
});
81+
handlers.on(EventOnTransportError(), (EventOnTransportError value) {
82+
_onTransportError();
83+
});
84+
handlers.on(EventOnReceiveResponse(), (EventOnReceiveResponse event) {
85+
_receiveResponse(event.response);
86+
});
87+
88+
RequestSender request_sender = RequestSender(_ua!, _request, handlers);
89+
90+
_newOptions('local', _request);
91+
92+
request_sender.send();
93+
}
94+
95+
void init_incoming(IncomingRequest request) {
96+
_request = request;
97+
98+
_newOptions('remote', request);
99+
100+
// Reply with a 200 OK if the user didn't reply.
101+
if (_is_replied == null || _is_replied == false) {
102+
_is_replied = true;
103+
request.reply(200);
104+
}
105+
106+
close();
107+
}
108+
109+
/*
110+
* Accept the incoming Options
111+
* Only valid for incoming Options
112+
*/
113+
void accept(Map<String, dynamic> options) {
114+
List<dynamic> extraHeaders = Utils.cloneArray(options['extraHeaders']);
115+
String? body = options['body'];
116+
117+
if (_direction != 'incoming') {
118+
throw Exceptions.NotSupportedError(
119+
'"accept" not supported for outgoing Options');
120+
}
121+
122+
if (_is_replied != null) {
123+
throw AssertionError('incoming Options already replied');
124+
}
125+
126+
_is_replied = true;
127+
_request.reply(200, null, extraHeaders, body);
128+
}
129+
130+
/**
131+
* Reject the incoming Options
132+
* Only valid for incoming Optionss
133+
*/
134+
void reject(Map<String, dynamic> options) {
135+
int status_code = options['status_code'] ?? 480;
136+
String? reason_phrase = options['reason_phrase'];
137+
List<dynamic> extraHeaders = Utils.cloneArray(options['extraHeaders']);
138+
String? body = options['body'];
139+
140+
if (_direction != 'incoming') {
141+
throw Exceptions.NotSupportedError(
142+
'"reject" not supported for outgoing Options');
143+
}
144+
145+
if (_is_replied != null) {
146+
throw AssertionError('incoming Options already replied');
147+
}
148+
149+
if (status_code < 300 || status_code >= 700) {
150+
throw Exceptions.TypeError('Invalid status_code: $status_code');
151+
}
152+
153+
_is_replied = true;
154+
_request.reply(status_code, reason_phrase, extraHeaders, body);
155+
}
156+
157+
void _receiveResponse(IncomingResponse? response) {
158+
if (_closed != null) {
159+
return;
160+
}
161+
if (RegExp(r'^1[0-9]{2}$').hasMatch(response!.status_code)) {
162+
// Ignore provisional responses.
163+
} else if (RegExp(r'^2[0-9]{2}$').hasMatch(response.status_code)) {
164+
_succeeded('remote', response);
165+
} else {
166+
String cause = Utils.sipErrorCause(response.status_code);
167+
_failed('remote', response.status_code, cause, response.reason_phrase);
168+
}
169+
}
170+
171+
void _onRequestTimeout() {
172+
if (_closed != null) {
173+
return;
174+
}
175+
_failed(
176+
'system', 408, DartSIP_C.CausesType.REQUEST_TIMEOUT, 'Request Timeout');
177+
}
178+
179+
void _onTransportError() {
180+
if (_closed != null) {
181+
return;
182+
}
183+
_failed('system', 500, DartSIP_C.CausesType.CONNECTION_ERROR,
184+
'Transport Error');
185+
}
186+
187+
@override
188+
void close() {
189+
_closed = true;
190+
_ua!.destroyOptions(this);
191+
}
192+
193+
/**
194+
* Internal Callbacks
195+
*/
196+
197+
void _newOptions(String originator, dynamic request) {
198+
if (originator == 'remote') {
199+
_direction = 'incoming';
200+
_local_identity = request.to;
201+
_remote_identity = request.from;
202+
} else if (originator == 'local') {
203+
_direction = 'outgoing';
204+
_local_identity = request.from;
205+
_remote_identity = request.to;
206+
}
207+
208+
_ua!.newOptions(this, originator, request);
209+
}
210+
211+
void _failed(String originator, int? status_code, String cause,
212+
String? reason_phrase) {
213+
logger.debug('OPTIONS failed');
214+
close();
215+
logger.debug('emit "failed"');
216+
emit(EventCallFailed(
217+
originator: originator,
218+
cause: ErrorCause(
219+
cause: cause,
220+
status_code: status_code,
221+
reason_phrase: reason_phrase)));
222+
}
223+
224+
void _succeeded(String originator, IncomingResponse? response) {
225+
logger.debug('OPTIONS succeeded');
226+
227+
close();
228+
229+
logger.debug('emit "succeeded"');
230+
231+
emit(EventSucceeded(originator: originator, response: response));
232+
}
233+
}

lib/src/ua.dart

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'event_manager/internal_events.dart';
1212
import 'exceptions.dart' as Exceptions;
1313
import 'logger.dart';
1414
import 'message.dart';
15+
import 'options.dart';
1516
import 'parser.dart' as Parser;
1617
import 'registrator.dart';
1718
import 'rtc_session.dart';
@@ -92,8 +93,8 @@ class UA extends EventManager {
9293
_dynConfiguration = DynamicSettings();
9394
_dialogs = <String, Dialog>{};
9495

95-
// User actions outside any session/dialog (MESSAGE).
96-
_applicants = <Message>{};
96+
// User actions outside any session/dialog (MESSAGE/OPTIONS).
97+
_applicants = <Applicant>{};
9798

9899
_sessions = <String?, RTCSession>{};
99100
_transport = null;
@@ -132,7 +133,7 @@ class UA extends EventManager {
132133
Settings? _configuration;
133134
DynamicSettings? _dynConfiguration;
134135
late Map<String, Dialog> _dialogs;
135-
late Set<Message> _applicants;
136+
late Set<Applicant> _applicants;
136137
Map<String?, RTCSession> _sessions = <String?, RTCSession>{};
137138
Transport? _transport;
138139
Contact? _contact;
@@ -283,6 +284,24 @@ class UA extends EventManager {
283284
return message;
284285
}
285286

287+
/**
288+
* Send a Options.
289+
*
290+
* -param {String} target
291+
* -param {String} body
292+
* -param {Object} [options]
293+
*
294+
* -throws {TypeError}
295+
*
296+
*/
297+
Options sendOptions(
298+
String target, String body, Map<String, dynamic>? options) {
299+
logger.debug('sendOptions()');
300+
Options message = Options(this);
301+
message.send(target, body, options);
302+
return message;
303+
}
304+
286305
/**
287306
* Terminate ongoing sessions.
288307
*/
@@ -346,9 +365,9 @@ class UA extends EventManager {
346365
});
347366

348367
// Run _close_ on every applicant.
349-
for (Message message in _applicants) {
368+
for (Applicant applicant in _applicants) {
350369
try {
351-
message.close();
370+
applicant.close();
352371
} catch (error) {}
353372
}
354373

@@ -491,13 +510,29 @@ class UA extends EventManager {
491510
message: message, originator: originator, request: request));
492511
}
493512

513+
/**
514+
* Options
515+
*/
516+
void newOptions(Options message, String originator, dynamic request) {
517+
_applicants.add(message);
518+
emit(EventNewOptions(
519+
message: message, originator: originator, request: request));
520+
}
521+
494522
/**
495523
* Message destroyed.
496524
*/
497525
void destroyMessage(Message message) {
498526
_applicants.remove(message);
499527
}
500528

529+
/**
530+
* Options destroyed.
531+
*/
532+
void destroyOptions(Options message) {
533+
_applicants.remove(message);
534+
}
535+
501536
/**
502537
* RTCSession
503538
*/
@@ -598,7 +633,13 @@ class UA extends EventManager {
598633
* They are processed as if they had been received outside the dialog.
599634
*/
600635
if (method == SipMethod.OPTIONS) {
601-
request.reply(200);
636+
if (!hasListeners(EventNewOptions())) {
637+
request.reply(200);
638+
return;
639+
}
640+
Options message = Options(this);
641+
message.init_incoming(request);
642+
return;
602643
} else if (method == SipMethod.MESSAGE) {
603644
if (!hasListeners(EventNewMessage())) {
604645
request.reply(405);
@@ -949,3 +990,7 @@ class UA extends EventManager {
949990
}
950991
}
951992
}
993+
994+
mixin Applicant {
995+
void close();
996+
}

0 commit comments

Comments
 (0)