Skip to content

Commit

Permalink
feat: implement first version of HTTP(S) binding
Browse files Browse the repository at this point in the history
  • Loading branch information
JKRhb committed Jan 9, 2022
1 parent 200bc62 commit 8d0da46
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 0 deletions.
212 changes: 212 additions & 0 deletions lib/src/binding_http/http_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright 2022 The NAMIB Project Developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<http.Response> _createRequest(
Form form, OperationType operationType, Object? payload) async {
final requestMethod = _getRequestMethod(form, operationType);

final Future<http.Response> 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<String, String> _getHeadersFromForm(Form form) {
final Map<String, String> headers = {};

final dynamic formHeaders = form.additionalFields["htv:headers"];
if (formHeaders is List<Map<String, String>>) {
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<String> _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<Content> 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<Content> readResource(Form form) async {
final response =
await _createRequest(form, OperationType.readproperty, null);
return _contentFromResponse(form, response);
}

@override
bool setSecurity(List<SecurityScheme> metaData, Credentials? credentials) {
// TODO: implement setSecurity
throw UnimplementedError();
}

@override
Future<void> start() async {
// Do nothing
// TODO(JKRhb): Check if this enough.
}

@override
Future<void> stop() async {
// Do nothing
// TODO(JKRhb): Check if this enough.
}

@override
Future<Content> unsubscribeResource(Form form) {
// TODO: implement unsubscribeResource
throw UnimplementedError();
}

@override
Future<void> writeResource(Form form, Content content) async {
final input = await _getInputFromContent(content);
await _createRequest(form, OperationType.writeproperty, input);
}

@override
Future<Subscription> 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);
}
41 changes: 41 additions & 0 deletions lib/src/binding_http/http_client_factory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2022 The NAMIB Project Developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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;
}
}
23 changes: 23 additions & 0 deletions lib/src/binding_http/http_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 The NAMIB Project Developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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});
}
61 changes: 61 additions & 0 deletions lib/src/binding_http/http_server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2022 The NAMIB Project Developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<String, Credentials> _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<void> expose(ExposedThing thing) {
// TODO: implement expose
throw UnimplementedError();
}

@override
int get port => _port;

@override
String get scheme => _scheme;

@override
Future<void> start(Map<String, Credentials> credentials) async {
_credentials = credentials;
// TODO(JKRhb): implement start
}

@override
Future<void> stop() async {
// TODO(JKRhb): implement stop
}
}
26 changes: 26 additions & 0 deletions lib/src/binding_http/https_client_factory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2022 The NAMIB Project Developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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);
}

0 comments on commit 8d0da46

Please sign in to comment.