From 8d0da464199b23da1bf3fe71a76013bd2d525374 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sun, 9 Jan 2022 01:52:13 +0100 Subject: [PATCH] feat: implement first version of HTTP(S) binding --- lib/src/binding_http/http_client.dart | 212 ++++++++++++++++++ lib/src/binding_http/http_client_factory.dart | 41 ++++ lib/src/binding_http/http_config.dart | 23 ++ lib/src/binding_http/http_server.dart | 61 +++++ .../binding_http/https_client_factory.dart | 26 +++ 5 files changed, 363 insertions(+) create mode 100644 lib/src/binding_http/http_client.dart create mode 100644 lib/src/binding_http/http_client_factory.dart create mode 100644 lib/src/binding_http/http_config.dart create mode 100644 lib/src/binding_http/http_server.dart create mode 100644 lib/src/binding_http/https_client_factory.dart diff --git a/lib/src/binding_http/http_client.dart b/lib/src/binding_http/http_client.dart new file mode 100644 index 00000000..9b1584a0 --- /dev/null +++ b/lib/src/binding_http/http_client.dart @@ -0,0 +1,212 @@ +// Copyright 2022 The NAMIB Project Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../core/content.dart'; +import '../core/credentials.dart'; +import '../core/operation_type.dart'; +import '../core/protocol_interfaces/protocol_client.dart'; +import '../core/subscription.dart'; +import '../definitions/form.dart'; +import '../definitions/security_scheme.dart'; +import 'http_config.dart'; + +/// Defines the available HTTP request methods. +enum HttpRequestMethod { + /// Corresponds with the GET request method. + get, + + /// Corresponds with the PUT request method. + put, + + /// Corresponds with the POST request method. + post, + + /// Corresponds with the DELETE request method. + delete, + + /// Corresponds with the PATCH request method. + patch, +} + +/// A [ProtocolClient] for the Hypertext Transfer Protocol (HTTP). +class HttpClient extends ProtocolClient { + /// An (optional) custom [HttpConfig] which overrides the default values. + final HttpConfig? _httpConfig; + + /// Creates a new [HttpClient] based on an optional [HttpConfig]. + HttpClient([this._httpConfig]); + + Future _createRequest( + Form form, OperationType operationType, Object? payload) async { + final requestMethod = _getRequestMethod(form, operationType); + + final Future response; + final headers = _getHeadersFromForm(form); + switch (requestMethod) { + case HttpRequestMethod.get: + response = http.get(Uri.parse(form.href), headers: headers); + break; + case HttpRequestMethod.post: + response = + http.post(Uri.parse(form.href), headers: headers, body: payload); + break; + case HttpRequestMethod.delete: + response = + http.delete(Uri.parse(form.href), headers: headers, body: payload); + break; + case HttpRequestMethod.put: + response = + http.put(Uri.parse(form.href), headers: headers, body: payload); + break; + case HttpRequestMethod.patch: + response = + http.patch(Uri.parse(form.href), headers: headers, body: payload); + break; + } + return response; + } + + static Map _getHeadersFromForm(Form form) { + final Map headers = {}; + + final dynamic formHeaders = form.additionalFields["htv:headers"]; + if (formHeaders is List>) { + for (final formHeader in formHeaders) { + final key = formHeader["htv:fieldName"]; + final value = formHeader["htv:fieldValue"]; + + if (key != null && value != null) { + headers[key] = value; + } + } + } + + final contentType = form.contentType; + if (contentType != null) { + headers["Content-Type"] = contentType; + } + + return headers; + } + + Future _getInputFromContent(Content content) async { + final inputBuffer = await content.byteBuffer; + return utf8.decoder + .convert(inputBuffer.asUint8List().toList(growable: false)); + } + + static Content _contentFromResponse(Form form, http.Response response) { + final type = response.headers["Content-Type"] ?? + form.contentType ?? + "application/octet-stream"; + final body = Stream.value(response.bodyBytes); + return Content(type, body); + } + + @override + Future invokeResource(Form form, Content content) async { + final input = await _getInputFromContent(content); + final response = + await _createRequest(form, OperationType.invokeaction, input); + return _contentFromResponse(form, response); + } + + @override + Future readResource(Form form) async { + final response = + await _createRequest(form, OperationType.readproperty, null); + return _contentFromResponse(form, response); + } + + @override + bool setSecurity(List metaData, Credentials? credentials) { + // TODO: implement setSecurity + throw UnimplementedError(); + } + + @override + Future start() async { + // Do nothing + // TODO(JKRhb): Check if this enough. + } + + @override + Future stop() async { + // Do nothing + // TODO(JKRhb): Check if this enough. + } + + @override + Future unsubscribeResource(Form form) { + // TODO: implement unsubscribeResource + throw UnimplementedError(); + } + + @override + Future writeResource(Form form, Content content) async { + final input = await _getInputFromContent(content); + await _createRequest(form, OperationType.writeproperty, input); + } + + @override + Future subscribeResource( + Form form, + void Function(Content content) next, + void Function(Exception error)? error, + void Function()? complete) async { + // TODO: implement subscribeResource + return Subscription(); + } +} + +HttpRequestMethod _requestMethodFromOperationType(OperationType operationType) { + // TODO(JKRhb): Handle observe/subscribe case + switch (operationType) { + case OperationType.readproperty: + return HttpRequestMethod.get; + case OperationType.writeproperty: + return HttpRequestMethod.put; + case OperationType.invokeaction: + return HttpRequestMethod.post; + } +} + +HttpRequestMethod? _requestMethodFromString(String formDefinition) { + switch (formDefinition) { + case "POST": + return HttpRequestMethod.post; + case "PUT": + return HttpRequestMethod.put; + case "DELETE": + return HttpRequestMethod.delete; + case "GET": + return HttpRequestMethod.get; + case "PATCH": + return HttpRequestMethod.patch; + default: + return null; + } +} + +HttpRequestMethod _getRequestMethod(Form form, OperationType operationType) { + final dynamic formDefinition = form.additionalFields["htv:methodName"]; + if (formDefinition is String) { + final requestMethod = _requestMethodFromString(formDefinition); + if (requestMethod != null) { + return requestMethod; + } + } + + return _requestMethodFromOperationType(operationType); +} diff --git a/lib/src/binding_http/http_client_factory.dart b/lib/src/binding_http/http_client_factory.dart new file mode 100644 index 00000000..533563d8 --- /dev/null +++ b/lib/src/binding_http/http_client_factory.dart @@ -0,0 +1,41 @@ +// Copyright 2022 The NAMIB Project Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import '../core/protocol_interfaces/protocol_client.dart'; +import '../core/protocol_interfaces/protocol_client_factory.dart'; +import 'http_client.dart'; +import 'http_config.dart'; + +/// A [ProtocolClientFactory] that produces HTTP clients. +class HttpClientFactory extends ProtocolClientFactory { + @override + String get scheme => "http"; + + /// The [HttpConfig] used to configure new clients. + final HttpConfig? httpConfig; + + /// Creates a new [HttpClientFactory] based on an optional [HttpConfig]. + HttpClientFactory([this.httpConfig]); + + @override + bool destroy() { + // TODO(JKRhb): Check if there is anything to destroy. + return true; + } + + @override + ProtocolClient createClient() => HttpClient(httpConfig); + + @override + bool init() { + // TODO(JKRhb): Check if there is anything to init. + return true; + } +} diff --git a/lib/src/binding_http/http_config.dart b/lib/src/binding_http/http_config.dart new file mode 100644 index 00000000..bfd9c82f --- /dev/null +++ b/lib/src/binding_http/http_config.dart @@ -0,0 +1,23 @@ +// Copyright 2022 The NAMIB Project Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/// Allows for configuring the behavior of HTTP clients and servers. +class HttpConfig { + /// Custom port number that should be used by a server. + /// + /// Defaults to 80 for HTTP and 443 for HTTPS. + int? port; + + /// Indicates if the client or server should use HTTPS. + bool? secure; + + /// Creates a new [HttpConfig] object. + HttpConfig({this.port, this.secure}); +} diff --git a/lib/src/binding_http/http_server.dart b/lib/src/binding_http/http_server.dart new file mode 100644 index 00000000..a0641ca9 --- /dev/null +++ b/lib/src/binding_http/http_server.dart @@ -0,0 +1,61 @@ +// Copyright 2022 The NAMIB Project Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import 'package:dart_wot/src/binding_http/http_config.dart'; + +import '../core/credentials.dart'; +import '../core/protocol_interfaces/protocol_server.dart'; +import '../scripting_api/exposed_thing.dart'; + +/// A [ProtocolServer] for the Hypertext Transfer Protocol (HTTP). +class HttpServer extends ProtocolServer { + final String _scheme; + + final int _port; + + final HttpConfig? _httpConfig; + + Map _credentials = {}; + + /// Create a new [HttpServer] from an optional [HttpConfig]. + HttpServer(this._httpConfig) + // TODO(JKRhb): Check if the scheme should be determined differently. + : _scheme = _httpConfig?.secure ?? false ? "https" : "http", + _port = _portFromConfig(_httpConfig); + + static int _portFromConfig(HttpConfig? httpConfig) { + final secure = httpConfig?.secure ?? false; + + return httpConfig?.port ?? (secure ? 443 : 80); + } + + @override + Future expose(ExposedThing thing) { + // TODO: implement expose + throw UnimplementedError(); + } + + @override + int get port => _port; + + @override + String get scheme => _scheme; + + @override + Future start(Map credentials) async { + _credentials = credentials; + // TODO(JKRhb): implement start + } + + @override + Future stop() async { + // TODO(JKRhb): implement stop + } +} diff --git a/lib/src/binding_http/https_client_factory.dart b/lib/src/binding_http/https_client_factory.dart new file mode 100644 index 00000000..b5103a7b --- /dev/null +++ b/lib/src/binding_http/https_client_factory.dart @@ -0,0 +1,26 @@ +// Copyright 2022 The NAMIB Project Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import '../core/protocol_interfaces/protocol_client_factory.dart'; +import 'http_client_factory.dart'; +import 'http_config.dart'; + +/// A [ProtocolClientFactory] that produces HTTPS clients. +// TODO(JKRhb): Not sure if two Factory classes make that much sense. Maybe it +// would be better to have one Factory that has a List of supported +// schemes (i. e. both http and https in this case). +// At the moment, this is the approach taken from node-wot, though. +class HttpsClientFactory extends HttpClientFactory { + @override + String get scheme => "https"; + + /// Creates a new [HttpClientFactory] based on an optional [HttpConfig]. + HttpsClientFactory([HttpConfig? _httpConfig]) : super(_httpConfig); +}