Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor network code: TCP support, error handling #113

Merged
merged 2 commits into from
Aug 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 64 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,71 @@
[![Build Status](https://github.com/shamblett/coap/actions/workflows/ci.yml/badge.svg)](https://github.com/shamblett/coap/actions/workflows/ci.yml)
# coap
A CoAP client library for Dart developers.
The Constrained Application Protocol ([CoAP](https://datatracker.ietf.org/doc/html/rfc7252))
is a RESTful web transfer protocol for resource-constrained networks and nodes.
COAP is an implementation in Dart providing CoAP-based services to Dart applications.
The code is a port from the C# .NET project [CoAP.NET](https://github.com/smeshlink/CoAP.NET). The dart implementation is that
of a CoAP client only, not a server although the CoAP.NET project does supply a server.
The COAP client provides many high level functions to control the request/response nature of the CoAP protocol,
fine grained control however can be obtained by users directly constructing their own request messages.
Configuration is achieved by editing a yaml based config file containing many of CoAP protocol configurations.
This is a full implementation of the CoAP protocol including block wise transfer, deduplication, transmission retries using
request/response token/id matching, piggy-backed and separate response handling is also supported. Proxying options can be set in request messages however full proxying support is
not guaranteed. All CoAP options(if-match, if-none match, uri path/query, location path/query, content format, max age,
etags et al.) are supported

Observation of resources is supported with the client 'listening' for observed resource updates
when configured for this. The client supports both IPV4 and IPV6 communications and multicast operation.

Experimental CoAPS support (secure CoAP over DTLS) is provided with via an OpenSSL binding
using the [dtls](https://pub.dev/packages/dtls) library for certificate support and an
[Eclipse tinydtls](https://github.com/eclipse/tinydtls/tree/develop) binding for Pre-Shared Keys and
Raw Public Keys.
In order to use CoAPS, OpenSSL and/or tinydtls binaries need to be available on your/the target system, and you
need to enable at least one of the two backends in the config you are using.

For Flutter apps, you can provide tinydtls binaries for Linux, Android, and Windows via the Plugin
[dart_tinydtls_libs](https://pub.dev/packages/dart_tinydtls_libs). Alternatively, you can also download
the binaries directly from the Plugin's [GitHub Repository](https://github.com/namib-project/dart_tinydtls_libs).

Many examples of usage are provided in the examples directory using the [coap.me](https://coap.me/) and [californium](https://www.eclipse.org/californium/) test servers.
The Constrained Application Protocol is a RESTful web transfer protocol for resource-constrained networks and nodes.
The CoAP library is an implementation in Dart providing a CoAP client, the code was initially a port from the C# .NET project [CoAP.NET](https://github.com/smeshlink/CoAP.NET).

## Features
* CoAP over UDP [RFC 7252](https://tools.ietf.org/html/rfc7252)
* Observe resources [RFC 7641](https://tools.ietf.org/html/rfc7641)
* Block-wise transfers [RFC 7959](https://tools.ietf.org/html/rfc7959)
* Multicast over UDP (not DTLS)
* **Experimental**: CoAP over DTLS (using FFI)
* [dtls](https://pub.dev/packages/dtls) for OpenSSL
* [tinydtls](https://github.com/eclipse/tinydtls) for Pre-Shared Keys and Raw Public Keys
* **Experimental**: Request proxying

### Roadmap
* CoAP over TCP/TLS [RFC 8323](https://tools.ietf.org/html/rfc8323)

## Example

```dart
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);

try {
final response =
await client.get('multi-format', accept: CoapMediaType.textPlain);
print('/multi-format response payload: ${response.payloadString}');
} on Exception catch (e) {
print('CoAP encountered an exception: $e');
}

client.close();
}
```

For more detailed examples, see [examples](./example/).

## Setup
* Add this as dependency in your `pubspec.yaml`:
* Add the dependencies in your `pubspec.yaml`:
```yaml
dependencies:
coap:
coap: ^4.2.1

devDependencies:
build_runner: ^2.1.11
```
* Create a `.yaml` file containing your CoAP's configurations.
* The file name must be separated by `_`. Example: `coap_config`
* The file name must start with `coap_config`
* Example: `coap_config_all`. This will generate a file called `CoapConfigAll` that you will use in your code.
* Example: `coap_config_debug`. This will generate a file called `CoapConfigDebug` that you will use in your code.
* This file must contains at least the protocol version. See the example bellow.
This is a valid configuration file with all possible properties: [example/config/coap_config.yaml](./example/config/coap_config.yaml).
* Run the command that will generate the configuration class.
* Run `pub run build_runner build` in a Dart project;
* Run `flutter pub run build_runner build` in a Flutter project;
After running the command above the configuration class will be generated next to the `.yaml` configuration file.

See the [examples](./example/) for example usage.
* Create a `.yaml` file containing your CoAP's configurations:
* The file name must be separated by `_` and must start with `coap_config`
* Example: `coap_config_all`. This will generate a file called `CoapConfigAll` that you will use in your code.
* Example: `coap_config_debug`. This will generate a file called `CoapConfigDebug` that you will use in your code.
* This file must contain at least the protocol version.
This is a valid configuration file with all possible properties: [example/config/coap_config.yaml](./example/config/coap_config.yaml).
* Run the command that will generate the configuration class:
* Run `dart pub run build_runner build` in your Dart project
* Run `flutter pub run build_runner build` in your Flutter project
After running the command above the configuration class will be generated next to the `.yaml` configuration file.

## Considerations

### Binaries for DTLS

You can provide `tinydtls` binaries via the plugin [dart_tinydtls_libs](https://pub.dev/packages/dart_tinydtls_libs). Alternatively, you can also download the binaries directly from the Plugin's [GitHub Repository](https://github.com/namib-project/dart_tinydtls_libs).

Likewise, if you are planning to use DTLS with OpenSSL, note that not all platforms support OpenSSL natively (iOS and Windows for example), in which case you need to ship the required binaries with your app.

### Connectivity

If connectivity is lost, the CoAP client will continuously try to re-initalize the socket. The library relies heavily on futures however, which might not survive in Flutter when the app runs in the background or when the display of the device is turned off. In this case, you might need to extend the `WidgetsBindingObserver` class and re-initialize the `CoapClient` in `didChangeAppLifecycleState` on `AppLifecycleState.resumed`.
2 changes: 1 addition & 1 deletion example/cancel_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/delete_resource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(
scheme: 'coap',
Expand Down
2 changes: 1 addition & 1 deletion example/discover_resources.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/get_blockwise.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/get_resource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/get_resource_secure.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DtlsConfig extends DefaultCoapConfig {
final dtlsBackend = DtlsBackend.TinyDtls;
}

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = DtlsConfig();
final uri = Uri(
scheme: 'coaps',
Expand Down
2 changes: 1 addition & 1 deletion example/log_events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:coap/coap.dart';
import 'config/coap_config.dart';
import 'utils.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/multi_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf1 = CoapConfig();
final uri1 = Uri(scheme: 'coap', host: 'coap.me', port: conf1.defaultPort);
final client1 = CoapClient(uri1, conf1);
Expand Down
2 changes: 1 addition & 1 deletion example/ping.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(
scheme: 'coap',
Expand Down
2 changes: 1 addition & 1 deletion example/post_blockwise.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:coap/coap.dart';
import 'config/coap_config.dart';
import 'utils.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/post_resource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
2 changes: 1 addition & 1 deletion example/put_resource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'dart:async';
import 'package:coap/coap.dart';
import 'config/coap_config.dart';

FutureOr<void> main(final List<String> args) async {
FutureOr<void> main() async {
final conf = CoapConfig();
final uri = Uri(scheme: 'coap', host: 'coap.me', port: conf.defaultPort);
final client = CoapClient(uri, conf);
Expand Down
24 changes: 9 additions & 15 deletions lib/src/coap_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import 'event/coap_event_bus.dart';
import 'exceptions/coap_request_exception.dart';
import 'net/coap_endpoint.dart';
import 'net/coap_iendpoint.dart';
import 'net/coap_internet_address.dart';
import 'network/coap_inetwork.dart';
import 'network/credentials/ecdsa_keys.dart';
import 'network/credentials/psk_credentials.dart';
Expand Down Expand Up @@ -81,7 +80,7 @@ class CoapClient {
CoapClient(
this.uri,
this._config, {
this.addressType = InternetAddressType.IPv4,
this.addressType = InternetAddressType.any,
this.bindAddress,
final EcdsaKeys? ecdsaKeys,
final PskCredentialsCallback? pskCredentialsCallback,
Expand Down Expand Up @@ -292,13 +291,13 @@ class CoapClient {
final relation = CoapObserveClientRelation(request);
unawaited(
() async {
_endpoint!.sendEpRequest(request);
final resp = await _waitForResponse(request);
if (!resp.hasOption(OptionType.observe)) {
relation.isCancelled = true;
}
}(),
);
_endpoint!.sendEpRequest(request);
return relation;
}

Expand Down Expand Up @@ -424,43 +423,38 @@ class CoapClient {
await _lock.synchronized(() async {
// Set endpoint if missing
if (_endpoint == null) {
final destination =
await _lookupHost(uri.host, addressType, bindAddress);
final destination = await _lookupHost(uri.host, addressType);
final socket = CoapINetwork.fromUri(
uri,
address: destination,
bindAddress: bindAddress,
config: _config,
namespace: _eventBus.namespace,
pskCredentialsCallback: _pskCredentialsCallback,
ecdsaKeys: _ecdsaKeys,
);
await socket.bind();
await socket.init();
_endpoint =
CoapEndPoint(socket, _config, namespace: _eventBus.namespace);
await _endpoint!.start();
_endpoint!.start();
}
});

request.endpoint = _endpoint;
}

Future<CoapInternetAddress> _lookupHost(
Future<InternetAddress> _lookupHost(
final String host,
final InternetAddressType addressType,
final InternetAddress? bindAddress,
) async {
final parsedAddress = InternetAddress.tryParse(host);
if (parsedAddress != null) {
return CoapInternetAddress(
parsedAddress.type,
parsedAddress,
bindAddress,
);
return parsedAddress;
}

final addresses = await InternetAddress.lookup(host, type: addressType);
if (addresses.isNotEmpty) {
return CoapInternetAddress(addressType, addresses[0], bindAddress);
return addresses[0];
}

throw SocketException("Failed host lookup: '$host'");
Expand Down
7 changes: 3 additions & 4 deletions lib/src/coap_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import 'coap_message_type.dart';
import 'coap_option.dart';
import 'coap_option_type.dart';
import 'event/coap_event_bus.dart';
import 'net/coap_internet_address.dart';
import 'util/coap_byte_array_util.dart';

typedef HookFunction = void Function();
Expand Down Expand Up @@ -182,11 +181,11 @@ class CoapMessage {

/// The destination endpoint.
@internal
CoapInternetAddress? destination;
InternetAddress? destination;

/// The source endpoint.
@internal
CoapInternetAddress? source;
InternetAddress? source;

/// Acknowledged hook for attaching a callback if needed
HookFunction? acknowledgedHook;
Expand Down Expand Up @@ -302,7 +301,7 @@ class CoapMessage {
try {
final ret = _utfDecoder.convert(payload);
return ret;
} on FormatException {
} on FormatException catch (_) {
// The payload may be incomplete, if so and the conversion
// fails indicate this.
return '<<<< Payload incomplete >>>>>';
Expand Down
2 changes: 1 addition & 1 deletion lib/src/coap_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class CoapRequest extends CoapMessage {
}

/// Indicates whether this request is a multicast request or not.
bool get isMulticast => destination?.address.isMulticast ?? false;
bool get isMulticast => destination?.isMulticast ?? false;

Uri? _uri;

Expand Down
4 changes: 2 additions & 2 deletions lib/src/codec/decoders/coap_message_decoder_rfc7252.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ class CoapMessageDecoder18 extends CoapMessageDecoder {
try {
final optionType = OptionType.fromTypeNumber(currentOption);
opt = CoapOption.create(optionType);
} on UnknownElectiveOptionException {
} on UnknownElectiveOptionException catch (_) {
// Unknown elective options must be silently ignored
continue;
} on UnknownCriticalOptionException {
} on UnknownCriticalOptionException catch (_) {
// Messages with unknown critical options must be rejected
message.hasUnknownCriticalOption = true;
return;
Expand Down
Loading