Line data Source code
1 : import 'dart:async';
2 : import 'dart:collection';
3 : import 'dart:convert';
4 : import 'dart:io';
5 : import 'dart:typed_data';
6 :
7 : import 'package:at_client/at_client.dart';
8 : import 'package:at_client/src/preference/monitor_preference.dart';
9 : import 'package:at_client/src/response/default_response_parser.dart';
10 : import 'package:at_client/src/util/network_util.dart';
11 : import 'package:at_commons/at_builders.dart';
12 : import 'package:at_commons/at_commons.dart';
13 : import 'package:at_lookup/at_lookup.dart';
14 : import 'package:at_utils/at_logger.dart';
15 : import 'package:crypton/crypton.dart';
16 :
17 : ///
18 : /// A [Monitor] object is used to receive notifications from the secondary server.
19 : ///
20 : class Monitor {
21 : // Regex on with what the monitor is started
22 : String? _regex;
23 :
24 : /// Capacity is represented in bytes.
25 : /// Throws [BufferOverFlowException] if data size exceeds 10MB.
26 : final _buffer = ByteBuffer(capacity: 10240000);
27 :
28 : // Time epoch milliseconds of the last notification received on this monitor
29 : int? _lastNotificationTime;
30 :
31 : final _monitorVerbResponseQueue = Queue();
32 :
33 : // Status on the monitor
34 : MonitorStatus status = MonitorStatus.notStarted;
35 :
36 : final _logger = AtSignLogger('Monitor');
37 :
38 : bool _keepAlive = false;
39 :
40 : late String _atSign;
41 :
42 : late Function _onError;
43 :
44 : late Function _onResponse;
45 :
46 : late Function _retryCallBack;
47 :
48 : late AtClientPreference _preference;
49 :
50 : OutboundConnection? _monitorConnection;
51 :
52 : RemoteSecondary? _remoteSecondary;
53 :
54 : final DefaultResponseParser _defaultResponseParser = DefaultResponseParser();
55 :
56 : ///
57 : /// Creates a [Monitor] object.
58 : ///
59 : /// [onResponse] function is called when a new batch of notifications are received from the server.
60 : /// This cannot be null.
61 : /// Example [onResponse] callback
62 : /// ```
63 : /// void onResponse(String notificationResponse) {
64 : /// // add your notification processing logic
65 : ///}
66 : ///```
67 : /// [onError] function is called when is some thing goes wrong with the processing.
68 : /// For example this could be:
69 : /// - Unavailability of the network
70 : /// - Exception while running the code
71 : /// This cannot be null.
72 : /// Example [onError] callback
73 : /// ```
74 : /// void onError(Monitor monitor, Exception e) {
75 : /// // add your error handling logic
76 : /// }
77 : /// ```
78 : /// After calling [onError] monitor would stop sending any more notifications. If the error is recoverable
79 : /// and if [retry] is true the [Monitor] would continue and waits to recover from the error condition and not call [onError].
80 : ///
81 : /// For example if the app loses internet connection then [Monitor] would wait till the internet comes back and not call
82 : /// [onError]
83 : ///
84 : /// When the [regex] is passed only those notifications matching the [regex] will be notified
85 : /// When the [lastNotificationTime] is passed only those notifications AFTER the time value are notified.
86 : /// This is expressed as EPOCH time milliseconds.
87 : /// When [retry] is true
88 : ////
89 0 : Monitor(
90 : Function onResponse,
91 : Function onError,
92 : String atSign,
93 : AtClientPreference preference,
94 : MonitorPreference monitorPreference,
95 : Function retryCallBack) {
96 0 : _onResponse = onResponse;
97 0 : _onError = onError;
98 0 : _preference = preference;
99 0 : _atSign = atSign;
100 0 : _regex = monitorPreference.regex;
101 0 : _keepAlive = monitorPreference.keepAlive;
102 0 : _lastNotificationTime = monitorPreference.lastNotificationTime;
103 0 : _remoteSecondary ??= RemoteSecondary(atSign, preference);
104 0 : _retryCallBack = retryCallBack;
105 : }
106 :
107 : /// Starts the monitor by establishing a new TCP/IP connection with the secondary server
108 : /// If [lastNotificationTime] expressed as EPOCH milliseconds is passed, only those notifications occurred after
109 : /// that time are notified.
110 : /// Calling start on already started monitor would not cause any exceptions and it will have no side affects.
111 : /// Calling start on monitor that is not started or erred will be started again.
112 : /// Calling [Monitor#getStatus] would return the status of the [Monitor]
113 0 : Future<void> start({int? lastNotificationTime}) async {
114 0 : if (status == MonitorStatus.started) {
115 : // Monitor already started
116 0 : _logger.finer('Monitor is already running');
117 : return;
118 : }
119 : // This enables start method to be called with lastNotificationTime on the same instance of Monitor
120 : if (lastNotificationTime != null) {
121 0 : _logger.finer(
122 0 : 'starting monitor for $_atSign with lastnotificationTime: $lastNotificationTime');
123 0 : _lastNotificationTime = lastNotificationTime;
124 : }
125 : try {
126 0 : await _checkConnectivity();
127 : //1. Get a new outbound connection dedicated to monitor verb.
128 0 : _monitorConnection = await _createNewConnection(
129 0 : _atSign, _preference.rootDomain, _preference.rootPort);
130 0 : _monitorConnection!.getSocket().listen(_messageHandler, onDone: () {
131 0 : _logger.finer('monitor done');
132 0 : _monitorConnection!.getSocket().destroy();
133 0 : status = MonitorStatus.stopped;
134 0 : _retryCallBack();
135 0 : }, onError: (error) {
136 0 : _logger.severe('error in monitor $error');
137 0 : _handleError(error);
138 : });
139 0 : await _authenticateConnection();
140 0 : await _monitorConnection!.write(_buildMonitorCommand());
141 0 : status = MonitorStatus.started;
142 0 : _logger.finer(
143 0 : 'monitor started for $_atSign with last notification time: $_lastNotificationTime');
144 :
145 : return;
146 0 : } on Exception catch (e) {
147 0 : _handleError(e);
148 : }
149 : }
150 :
151 0 : Future<void> _authenticateConnection() async {
152 0 : await _monitorConnection!.write('from:$_atSign\n');
153 0 : var fromResponse = await _getQueueResponse();
154 0 : if (fromResponse.isEmpty) {
155 0 : throw UnAuthenticatedException('From response is empty');
156 : }
157 0 : _logger.finer(
158 0 : 'Authenticating the monitor connection: from result:$fromResponse');
159 0 : var key = RSAPrivateKey.fromString(_preference.privateKey!);
160 : var sha256signature =
161 0 : key.createSHA256Signature(utf8.encode(fromResponse) as Uint8List);
162 0 : var signature = base64Encode(sha256signature);
163 0 : _logger.finer('Authenticating the monitor connection: pkam:$signature');
164 0 : await _monitorConnection!.write('pkam:$signature\n');
165 0 : var pkamResponse = await _getQueueResponse();
166 0 : if (!pkamResponse.contains('success')) {
167 0 : throw UnAuthenticatedException(
168 : 'Monitor connection authentication failed');
169 : }
170 0 : _logger.finer('Monitor connection authentication successful');
171 : }
172 :
173 0 : Future<OutboundConnection> _createNewConnection(
174 : String toAtSign, String rootDomain, int rootPort) async {
175 : //1. find secondary url for atsign from lookup library
176 : var secondaryUrl =
177 0 : await AtLookupImpl.findSecondary(toAtSign, rootDomain, rootPort);
178 : if (secondaryUrl == null) {
179 0 : throw Exception('Secondary url not found');
180 : }
181 0 : var secondaryInfo = _getSecondaryInfo(secondaryUrl);
182 0 : var host = secondaryInfo[0];
183 0 : var port = secondaryInfo[1];
184 :
185 : //2. create a connection to secondary server
186 0 : var secureSocket = await SecureSocket.connect(host, int.parse(port));
187 : OutboundConnection _monitorConnection =
188 0 : OutboundConnectionImpl(secureSocket);
189 : return _monitorConnection;
190 : }
191 :
192 0 : List<String> _getSecondaryInfo(String url) {
193 0 : var result = <String>[];
194 0 : if (url.contains(':')) {
195 0 : var arr = url.split(':');
196 0 : result.add(arr[0]);
197 0 : result.add(arr[1]);
198 : }
199 : return result;
200 : }
201 :
202 : ///Returns the response of the monitor verb queue.
203 0 : Future<String> _getQueueResponse() async {
204 : dynamic monitorResponse;
205 : //waits for 30 seconds
206 0 : for (var i = 0; i < 6000; i++) {
207 0 : if (_monitorVerbResponseQueue.isNotEmpty) {
208 : // result from another secondary is either data or a @<atSign>@ denoting complete
209 : // of the handshake
210 0 : monitorResponse = _defaultResponseParser
211 0 : .parse(_monitorVerbResponseQueue.removeFirst());
212 : break;
213 : }
214 0 : await Future.delayed(Duration(milliseconds: 5));
215 : }
216 : // If monitor response contains error, return error
217 0 : if (monitorResponse.isError) {
218 0 : return '${monitorResponse.errorCode}: ${monitorResponse.errorDescription}';
219 : }
220 0 : return monitorResponse.response;
221 : }
222 :
223 0 : String _buildMonitorCommand() {
224 0 : var monitorVerbBuilder = MonitorVerbBuilder();
225 0 : if (_regex != null && _regex!.isNotEmpty) {
226 0 : monitorVerbBuilder.regex = _regex;
227 : }
228 0 : if (_lastNotificationTime != null) {
229 0 : monitorVerbBuilder.lastNotificationTime = _lastNotificationTime;
230 : }
231 0 : return monitorVerbBuilder.buildCommand();
232 : }
233 :
234 : /// Stops the monitor. Call [Monitor#start] to start it again.
235 0 : void stop() {
236 0 : status = MonitorStatus.stopped;
237 0 : if (_monitorConnection != null) {
238 0 : _monitorConnection!.close();
239 : }
240 : }
241 :
242 : // Stops the monitor from receiving notification
243 0 : MonitorStatus getStatus() {
244 0 : return status;
245 : }
246 :
247 0 : void _handleResponse(String response, Function callback) {
248 0 : if (response.toString().startsWith('notification')) {
249 0 : callback(response);
250 : } else {
251 0 : _monitorVerbResponseQueue.add(response);
252 : }
253 : }
254 :
255 0 : void _handleError(e) {
256 0 : _monitorConnection?.close();
257 0 : status = MonitorStatus.errored;
258 : // Pass monitor and error
259 : // TBD : If retry = true should the onError needs to be called?
260 0 : if (_keepAlive) {
261 : // We will use a strategy here
262 0 : _logger.finer('Retrying start monitor due to error');
263 0 : _retryCallBack();
264 : } else {
265 0 : _onError(e);
266 : }
267 : }
268 :
269 0 : Future<void> _checkConnectivity() async {
270 0 : if (!(await NetworkUtil.isNetworkAvailable())) {
271 0 : throw AtConnectException('Internet connection unavailable to sync');
272 : }
273 0 : if (!(await _remoteSecondary!.isAvailable())) {
274 0 : throw AtConnectException('Secondary server is unavailable');
275 : }
276 : return;
277 : }
278 :
279 : /// Handles messages on the inbound client's connection and calls the verb executor
280 : /// Closes the inbound connection in case of any error.
281 : /// Throw a [BufferOverFlowException] if buffer is unable to hold incoming data
282 0 : Future<void> _messageHandler(data) async {
283 : String result;
284 0 : if (!_buffer.isOverFlow(data)) {
285 : // skip @ prompt. byte code for @ is 64
286 0 : if (data.length == 1 && data.first == 64) {
287 : return;
288 : }
289 : //ignore prompt(@ or @<atSign>@) after '\n'. byte code for \n is 10
290 0 : if (data.last == 64 && data.contains(10)) {
291 0 : data = data.sublist(0, data.lastIndexOf(10) + 1);
292 0 : _buffer.append(data);
293 0 : } else if (data.length > 1 && data.first == 64 && data.last == 64) {
294 : // pol responses do not end with '\n'. Add \n for buffer completion
295 0 : _buffer.append(data);
296 0 : _buffer.addByte(10);
297 : } else {
298 0 : _buffer.append(data);
299 : }
300 : } else {
301 0 : _buffer.clear();
302 0 : throw BufferOverFlowException('Buffer overflow on outbound connection');
303 : }
304 0 : if (_buffer.isEnd()) {
305 0 : result = utf8.decode(_buffer.getData());
306 0 : result = result.trim();
307 0 : _buffer.clear();
308 0 : _handleResponse(result, _onResponse);
309 : }
310 : }
311 : }
312 :
313 7 : enum MonitorStatus { notStarted, started, stopped, errored }
|