diff --git a/moxxmpp_socket/.flutter-plugins b/moxxmpp_socket/.flutter-plugins new file mode 100644 index 0000000..4822cf8 --- /dev/null +++ b/moxxmpp_socket/.flutter-plugins @@ -0,0 +1,4 @@ +# This is a generated file; do not edit or check into version control. +moxdns=/home/alexander/.pub-cache/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxdns-0.1.4/ +moxdns_android=/home/alexander/.pub-cache/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxdns_android-0.1.4/ +moxdns_linux=/home/alexander/.pub-cache/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxdns_linux-0.1.4/ diff --git a/moxxmpp_socket/.flutter-plugins-dependencies b/moxxmpp_socket/.flutter-plugins-dependencies new file mode 100644 index 0000000..f1159d7 --- /dev/null +++ b/moxxmpp_socket/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[],"android":[{"name":"moxdns_android","path":"/home/alexander/.pub-cache/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxdns_android-0.1.4/","native_build":true,"dependencies":[]}],"macos":[],"linux":[{"name":"moxdns_linux","path":"/home/alexander/.pub-cache/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxdns_linux-0.1.4/","native_build":true,"dependencies":[]}],"windows":[],"web":[]},"dependencyGraph":[{"name":"moxdns","dependencies":["moxdns_android","moxdns_linux"]},{"name":"moxdns_android","dependencies":["moxdns"]},{"name":"moxdns_linux","dependencies":["moxdns"]}],"date_created":"2022-11-05 14:01:31.714716","version":"3.3.3"} \ No newline at end of file diff --git a/moxxmpp_socket/CHANGELOG.md b/moxxmpp_socket/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/moxxmpp_socket/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/moxxmpp_socket/LICENSE b/moxxmpp_socket/LICENSE new file mode 100644 index 0000000..dc6a3cd --- /dev/null +++ b/moxxmpp_socket/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Alexander "PapaTutuWawa" + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/moxxmpp_socket/README.md b/moxxmpp_socket/README.md new file mode 100644 index 0000000..99115ab --- /dev/null +++ b/moxxmpp_socket/README.md @@ -0,0 +1,7 @@ +# moxxmpp_socket + +A socket for moxxmpp that does SRV resolution. + +## License + +See `./LICENSE`. diff --git a/moxxmpp_socket/lib/moxxmpp_socket.dart b/moxxmpp_socket/lib/moxxmpp_socket.dart new file mode 100644 index 0000000..1caf261 --- /dev/null +++ b/moxxmpp_socket/lib/moxxmpp_socket.dart @@ -0,0 +1,3 @@ +library moxxmpp_socket; + +export 'src/socket.dart'; diff --git a/moxxmpp_socket/lib/src/rfc_2782.dart b/moxxmpp_socket/lib/src/rfc_2782.dart new file mode 100644 index 0000000..f04ceeb --- /dev/null +++ b/moxxmpp_socket/lib/src/rfc_2782.dart @@ -0,0 +1,21 @@ +import 'package:moxdns/moxdns.dart'; + +/// Sorts the SRV records according to priority and weight. +int srvRecordSortComparator(SrvRecord a, SrvRecord b) { + if (a.priority < b.priority) { + return -1; + } else { + if (a.priority > b.priority) { + return 1; + } + + // a.priority == b.priority + if (a.weight < b.weight) { + return -1; + } else if (a.weight > b.weight) { + return 1; + } else { + return 0; + } + } +} diff --git a/moxxmpp_socket/lib/src/socket.dart b/moxxmpp_socket/lib/src/socket.dart new file mode 100644 index 0000000..a77c249 --- /dev/null +++ b/moxxmpp_socket/lib/src/socket.dart @@ -0,0 +1,286 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:logging/logging.dart'; +import 'package:moxdns/moxdns.dart'; +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxmpp_socket/src/rfc_2782.dart'; + +/// TCP socket implementation for XmppConnection +class TCPSocketWrapper extends BaseSocketWrapper { + TCPSocketWrapper(this._logData) + : _log = Logger('TCPSocketWrapper'), + _dataStream = StreamController.broadcast(), + _eventStream = StreamController.broadcast(), + _secure = false, + _ignoreSocketClosure = false; + Socket? _socket; + bool _ignoreSocketClosure; + final StreamController _dataStream; + final StreamController _eventStream; + StreamSubscription? _socketSubscription; + + final Logger _log; + final bool _logData; + + bool _secure; + + @override + bool isSecure() => _secure; + + @override + bool whitespacePingAllowed() => true; + + @override + bool managesKeepalives() => false; + + /// Allow the socket to be destroyed by cancelling internal subscriptions. + void destroy() { + _socketSubscription?.cancel(); + } + + bool _onBadCertificate(dynamic certificate, String domain) { + _log.fine('Bad certificate: ${certificate.toString()}'); + //final isExpired = certificate.endValidity.isAfter(DateTime.now()); + // TODO(Unknown): Either validate the certificate ourselves or use a platform native + // hostname verifier (or Dart adds it themselves) + return false; + } + + Future _xep368Connect(String domain) async { + // TODO(Unknown): Maybe do DNSSEC one day + final results = await MoxdnsPlugin.srvQuery('_xmpps-client._tcp.$domain', false); + if (results.isEmpty) { + return false; + } + + results.sort(srvRecordSortComparator); + for (final srv in results) { + try { + _log.finest('Attempting secure connection to ${srv.target}:${srv.port}...'); + _ignoreSocketClosure = true; + _socket = await SecureSocket.connect( + srv.target, + srv.port, + timeout: const Duration(seconds: 5), + supportedProtocols: const [ xmppClientALPNId ], + onBadCertificate: (cert) => _onBadCertificate(cert, domain), + ); + + _ignoreSocketClosure = false; + _secure = true; + _log.finest('Success!'); + return true; + } on SocketException catch(e) { + _log.finest('Failure! $e'); + _ignoreSocketClosure = false; + } + } + + return false; + } + + Future _rfc6120Connect(String domain) async { + // TODO(Unknown): Maybe do DNSSEC one day + final results = await MoxdnsPlugin.srvQuery('_xmpp-client._tcp.$domain', false); + results.sort(srvRecordSortComparator); + + for (final srv in results) { + try { + _log.finest('Attempting connection to ${srv.target}:${srv.port}...'); + _ignoreSocketClosure = true; + _socket = await Socket.connect( + srv.target, + srv.port, + timeout: const Duration(seconds: 5), + ); + + _ignoreSocketClosure = false; + _log.finest('Success!'); + return true; + } on SocketException catch(e) { + _log.finest('Failure! $e'); + _ignoreSocketClosure = false; + continue; + } + } + + return _rfc6120FallbackConnect(domain); + } + + /// Connect to [host] with port [port] and returns true if the connection + /// was successfully established. Does not setup the streams as this has + /// to be done by the caller. + Future _hostPortConnect(String host, int port) async { + try { + _log.finest('Attempting fallback connection to $host:$port...'); + _ignoreSocketClosure = true; + _socket = await Socket.connect( + host, + port, + timeout: const Duration(seconds: 5), + ); + _log.finest('Success!'); + return true; + } on SocketException catch(e) { + _log.finest('Failure! $e'); + _ignoreSocketClosure = false; + return false; + } + } + + /// Connect to [domain] using the default C2S port of XMPP. Returns + /// true if the connection was successful. Does not setup the streams + /// as [_rfc6120FallbackConnect] should only be called from + /// [_rfc6120Connect], which already sets the streams up on a successful + /// connection. + Future _rfc6120FallbackConnect(String domain) async { + return _hostPortConnect(domain, 5222); + } + + @override + Future secure(String domain) async { + if (_secure) { + _log.warning('Connection is already marked as secure. Doing nothing'); + return true; + } + + if (_socket == null) { + _log.severe('Failed to secure socket since _socket is null'); + return false; + } + + _ignoreSocketClosure = true; + + try { + _socket = await SecureSocket.secure( + _socket!, + supportedProtocols: const [ xmppClientALPNId ], + onBadCertificate: (cert) => _onBadCertificate(cert, domain), + ); + + _secure = true; + _ignoreSocketClosure = false; + _setupStreams(); + return true; + } on SocketException { + _ignoreSocketClosure = false; + return false; + } + } + + void _setupStreams() { + if (_socket == null) { + _log.severe('Failed to setup streams as _socket is null'); + return; + } + + _socketSubscription = _socket!.listen( + (List event) { + final data = utf8.decode(event); + if (_logData) { + _log.finest('<== $data'); + } + _dataStream.add(data); + }, + onError: (Object error) { + _log.severe(error.toString()); + _eventStream.add(XmppSocketErrorEvent(error)); + }, + ); + // ignore: implicit_dynamic_parameter + _socket!.done.then((_) { + if (!_ignoreSocketClosure) { + _eventStream.add(XmppSocketClosureEvent()); + } + }); + } + + @override + Future connect(String domain, { String? host, int? port }) async { + _ignoreSocketClosure = false; + _secure = false; + + // Connection order: + // 1. host:port, if given + // 2. XEP-0368 + // 3. RFC 6120 + // 4. RFC 6120 fallback + + if (host != null && port != null) { + _log.finest('Specific host and port given'); + if (await _hostPortConnect(host, port)) { + _setupStreams(); + return true; + } + } + + if (await _xep368Connect(domain)) { + _setupStreams(); + return true; + } + + // NOTE: _rfc6120Connect already attempts the fallback + if (await _rfc6120Connect(domain)) { + _setupStreams(); + return true; + } + + return false; + } + + @override + void close() { + if (_socketSubscription != null) { + _log.finest('Closing socket subscription'); + _socketSubscription!.cancel(); + } + + if (_socket == null) { + _log.warning('Failed to close socket since _socket is null'); + return; + } + + _ignoreSocketClosure = true; + try { + _socket!.close(); + } catch(e) { + _log.warning('Closing socket threw exception: $e'); + } + _ignoreSocketClosure = false; + } + + @override + Stream getDataStream() => _dataStream.stream.asBroadcastStream(); + + @override + Stream getEventStream() => _eventStream.stream.asBroadcastStream(); + + @override + void write(Object? data, { String? redact }) { + if (_socket == null) { + _log.severe('Failed to write to socket as _socket is null'); + return; + } + + if (data != null && data is String && _logData) { + if (redact != null) { + _log.finest('**> $redact'); + } else { + _log.finest('==> $data'); + } + } + + try { + _socket!.write(data); + } on SocketException catch (e) { + _log.severe(e); + _eventStream.add(XmppSocketErrorEvent(e)); + } + } + + @override + void prepareDisconnect() { + _ignoreSocketClosure = true; + } +} diff --git a/moxxmpp_socket/pubspec.yaml b/moxxmpp_socket/pubspec.yaml new file mode 100644 index 0000000..9bb3f25 --- /dev/null +++ b/moxxmpp_socket/pubspec.yaml @@ -0,0 +1,22 @@ +name: moxxmpp_socket +description: A socket for moxxmpp that resolves SRV records +version: 0.1.0 +homepage: https://codeberg.org/moxxy/moxxmpp +publish_to: https://git.polynom.me/api/packages/Moxxy/pub + +environment: + sdk: '>=2.18.0 <3.0.0' + flutter: '>=2.13.0-0.1' + +dependencies: + moxdns: + hosted: https://git.polynom.me/api/packages/Moxxy/pub + version: 0.1.4 + moxxmpp: + hosted: https://git.polynom.me/api/packages/Moxxy/pub + version: 0.1.0 + +dev_dependencies: + lints: ^2.0.0 + test: ^1.16.0 + very_good_analysis: ^3.0.1