2016-12-23 20:57:46 +00:00
|
|
|
/// Server-side support for WebSockets.
|
2016-04-29 01:19:09 +00:00
|
|
|
library angel_websocket.server;
|
|
|
|
|
2016-06-27 00:42:21 +00:00
|
|
|
import 'dart:async';
|
2018-10-02 15:32:06 +00:00
|
|
|
import 'dart:convert';
|
2016-04-29 01:19:09 +00:00
|
|
|
import 'dart:io';
|
2016-09-18 02:53:58 +00:00
|
|
|
import 'dart:mirrors';
|
2017-02-28 14:15:34 +00:00
|
|
|
import 'package:angel_auth/angel_auth.dart';
|
2016-04-29 01:19:09 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2018-12-09 16:40:09 +00:00
|
|
|
import 'package:angel_framework/http.dart';
|
|
|
|
import 'package:angel_framework/http2.dart';
|
2016-07-06 01:28:00 +00:00
|
|
|
import 'package:merge_map/merge_map.dart';
|
2018-11-04 02:11:52 +00:00
|
|
|
import 'package:stream_channel/stream_channel.dart';
|
2017-09-24 16:19:16 +00:00
|
|
|
import 'package:web_socket_channel/io.dart';
|
2018-12-09 16:40:09 +00:00
|
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
2016-06-27 00:42:21 +00:00
|
|
|
import 'angel_websocket.dart';
|
2016-09-03 12:34:01 +00:00
|
|
|
export 'angel_websocket.dart';
|
2016-04-29 01:19:09 +00:00
|
|
|
|
2016-07-06 01:28:00 +00:00
|
|
|
part 'websocket_context.dart';
|
2017-06-03 18:13:38 +00:00
|
|
|
|
2016-09-18 02:53:58 +00:00
|
|
|
part 'websocket_controller.dart';
|
2016-04-29 01:19:09 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
typedef String WebSocketResponseSerializer(data);
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s.
|
2017-09-24 04:37:58 +00:00
|
|
|
class AngelWebSocket {
|
2018-07-10 16:54:55 +00:00
|
|
|
List<WebSocketContext> _clients = <WebSocketContext>[];
|
2016-12-23 10:47:21 +00:00
|
|
|
final List<String> _servicesAlreadyWired = [];
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
final StreamController<WebSocketAction> _onAction =
|
2017-09-24 04:37:58 +00:00
|
|
|
new StreamController<WebSocketAction>();
|
2016-12-23 20:57:46 +00:00
|
|
|
final StreamController _onData = new StreamController();
|
|
|
|
final StreamController<WebSocketContext> _onConnection =
|
2017-09-24 04:37:58 +00:00
|
|
|
new StreamController<WebSocketContext>.broadcast();
|
2016-12-23 20:57:46 +00:00
|
|
|
final StreamController<WebSocketContext> _onDisconnect =
|
2017-09-24 04:37:58 +00:00
|
|
|
new StreamController<WebSocketContext>.broadcast();
|
|
|
|
|
|
|
|
final Angel app;
|
2016-12-23 20:57:46 +00:00
|
|
|
|
2017-02-22 22:34:35 +00:00
|
|
|
/// If this is not `true`, then all client-side service parameters will be
|
|
|
|
/// discarded, other than `params['query']`.
|
|
|
|
final bool allowClientParams;
|
|
|
|
|
2018-12-09 16:40:09 +00:00
|
|
|
/// An optional whitelist of allowed client origins, or [:null:].
|
|
|
|
final List<String> allowedOrigins;
|
|
|
|
|
|
|
|
/// An optional whitelist of allowed client protocols, or [:null:].
|
|
|
|
final List<String> allowedProtocols;
|
|
|
|
|
2017-02-28 14:15:34 +00:00
|
|
|
/// If `true`, then clients can authenticate their WebSockets by sending a valid JWT.
|
|
|
|
final bool allowAuth;
|
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
/// Send error information across WebSockets, without including debug information..
|
2017-09-24 04:37:58 +00:00
|
|
|
final bool sendErrors;
|
2017-01-28 21:38:26 +00:00
|
|
|
|
2016-12-23 10:47:21 +00:00
|
|
|
/// A list of clients currently connected to this server via WebSockets.
|
2017-01-29 20:02:19 +00:00
|
|
|
List<WebSocketContext> get clients => new List.unmodifiable(_clients);
|
2016-12-23 10:47:21 +00:00
|
|
|
|
|
|
|
/// Services that have already been hooked to fire socket events.
|
2016-12-23 20:57:46 +00:00
|
|
|
List<String> get servicesAlreadyWired =>
|
|
|
|
new List.unmodifiable(_servicesAlreadyWired);
|
2016-12-23 10:47:21 +00:00
|
|
|
|
2017-02-28 14:15:34 +00:00
|
|
|
/// Used to notify other nodes of an event's firing. Good for scaled applications.
|
2018-11-15 16:43:51 +00:00
|
|
|
final StreamChannel<WebSocketEvent> synchronizationChannel;
|
2017-02-28 14:15:34 +00:00
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Fired on any [WebSocketAction].
|
|
|
|
Stream<WebSocketAction> get onAction => _onAction.stream;
|
|
|
|
|
|
|
|
/// Fired whenever a WebSocket sends data.
|
|
|
|
Stream get onData => _onData.stream;
|
|
|
|
|
2016-12-23 10:47:21 +00:00
|
|
|
/// Fired on incoming connections.
|
2016-09-18 01:35:16 +00:00
|
|
|
Stream<WebSocketContext> get onConnection => _onConnection.stream;
|
2016-12-23 10:47:21 +00:00
|
|
|
|
|
|
|
/// Fired when a user disconnects.
|
2016-09-18 03:16:51 +00:00
|
|
|
Stream<WebSocketContext> get onDisconnection => _onDisconnect.stream;
|
2016-04-29 01:19:09 +00:00
|
|
|
|
2017-04-23 18:40:30 +00:00
|
|
|
/// Serializes data to WebSockets.
|
2018-07-10 16:54:55 +00:00
|
|
|
WebSocketResponseSerializer serializer;
|
2017-04-23 18:40:30 +00:00
|
|
|
|
|
|
|
/// Deserializes data from WebSockets.
|
|
|
|
Function deserializer;
|
|
|
|
|
2017-09-24 04:37:58 +00:00
|
|
|
AngelWebSocket(this.app,
|
|
|
|
{this.sendErrors: false,
|
|
|
|
this.allowClientParams: false,
|
|
|
|
this.allowAuth: true,
|
2018-11-15 16:43:51 +00:00
|
|
|
this.synchronizationChannel,
|
2017-09-24 04:37:58 +00:00
|
|
|
this.serializer,
|
2018-12-09 16:40:09 +00:00
|
|
|
this.deserializer,
|
|
|
|
this.allowedOrigins,
|
|
|
|
this.allowedProtocols}) {
|
2018-11-04 02:11:52 +00:00
|
|
|
if (serializer == null) serializer = json.encode;
|
2017-04-23 18:40:30 +00:00
|
|
|
if (deserializer == null) deserializer = (params) => params;
|
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
HookedServiceEventListener serviceHook(String path) {
|
2016-07-06 01:28:00 +00:00
|
|
|
return (HookedServiceEvent e) async {
|
2017-02-22 22:34:35 +00:00
|
|
|
if (e.params != null && e.params['broadcast'] == false) return;
|
|
|
|
|
2016-07-06 01:28:00 +00:00
|
|
|
var event = await transformEvent(e);
|
|
|
|
event.eventName = "$path::${event.eventName}";
|
2017-01-29 20:02:19 +00:00
|
|
|
|
|
|
|
_filter(WebSocketContext socket) {
|
2017-10-19 22:26:59 +00:00
|
|
|
if (e.service.configuration.containsKey('ws:filter'))
|
|
|
|
return e.service.configuration['ws:filter'](e, socket);
|
2017-06-30 23:09:03 +00:00
|
|
|
else if (e.params != null && e.params.containsKey('ws:filter'))
|
|
|
|
return e.params['ws:filter'](e, socket);
|
2017-01-29 20:02:19 +00:00
|
|
|
else
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
await batchEvent(event, filter: _filter);
|
2016-07-06 01:28:00 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Slates an event to be dispatched.
|
2017-01-29 20:02:19 +00:00
|
|
|
Future batchEvent(WebSocketEvent event,
|
2017-02-28 14:15:34 +00:00
|
|
|
{filter(WebSocketContext socket), bool notify: true}) async {
|
2016-07-06 01:28:00 +00:00
|
|
|
// Default implementation will just immediately fire events
|
2017-01-29 20:02:19 +00:00
|
|
|
_clients.forEach((client) async {
|
2018-07-10 16:54:55 +00:00
|
|
|
dynamic result = true;
|
2017-01-29 20:02:19 +00:00
|
|
|
if (filter != null) result = await filter(client);
|
2017-02-12 05:05:57 +00:00
|
|
|
if (result == true) {
|
2018-11-04 02:11:52 +00:00
|
|
|
client.channel.sink.add((serializer ?? json.encode)(event.toJson()));
|
2017-02-12 05:05:57 +00:00
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
});
|
2017-02-28 14:15:34 +00:00
|
|
|
|
2018-11-15 16:43:51 +00:00
|
|
|
if (synchronizationChannel != null && notify != false)
|
|
|
|
synchronizationChannel.sink.add(event);
|
2016-07-06 01:28:00 +00:00
|
|
|
}
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Returns a list of events yet to be sent.
|
2016-07-06 01:28:00 +00:00
|
|
|
Future<List<WebSocketEvent>> getBatchedEvents() async => [];
|
2016-06-27 00:42:21 +00:00
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Responds to an incoming action on a WebSocket.
|
2016-07-06 01:28:00 +00:00
|
|
|
Future handleAction(WebSocketAction action, WebSocketContext socket) async {
|
|
|
|
var split = action.eventName.split("::");
|
2016-06-27 00:42:21 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
if (split.length < 2) {
|
|
|
|
socket.sendError(new AngelHttpException.badRequest());
|
|
|
|
return null;
|
|
|
|
}
|
2016-06-27 00:42:21 +00:00
|
|
|
|
2018-10-02 15:32:06 +00:00
|
|
|
var service = app.findService(split[0]);
|
2016-04-29 01:19:09 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
if (service == null) {
|
|
|
|
socket.sendError(new AngelHttpException.notFound(
|
2016-07-06 01:28:00 +00:00
|
|
|
message: "No service \"${split[0]}\" exists."));
|
2018-07-10 16:54:55 +00:00
|
|
|
return null;
|
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
var actionName = split[1];
|
2016-07-06 01:28:00 +00:00
|
|
|
|
2018-10-02 15:32:06 +00:00
|
|
|
if (action.params is! Map) action.params = <String, dynamic>{};
|
2017-02-22 22:34:35 +00:00
|
|
|
|
|
|
|
if (allowClientParams != true) {
|
|
|
|
if (action.params['query'] is Map)
|
|
|
|
action.params = {'query': action.params['query']};
|
|
|
|
else
|
|
|
|
action.params = {};
|
|
|
|
}
|
|
|
|
|
2018-10-02 15:32:06 +00:00
|
|
|
var params = mergeMap<String, dynamic>([
|
|
|
|
((deserializer ?? (params) => params)(action.params))
|
|
|
|
as Map<String, dynamic>,
|
2017-02-12 20:06:54 +00:00
|
|
|
{
|
2017-09-24 04:37:58 +00:00
|
|
|
"provider": Providers.websocket,
|
2017-02-12 20:06:54 +00:00
|
|
|
'__requestctx': socket.request,
|
|
|
|
'__responsectx': socket.response
|
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
]);
|
2016-07-06 13:33:40 +00:00
|
|
|
|
2016-07-06 01:28:00 +00:00
|
|
|
try {
|
2016-12-23 20:57:46 +00:00
|
|
|
if (actionName == ACTION_INDEX) {
|
2018-07-10 16:54:55 +00:00
|
|
|
socket.send(
|
2016-12-23 20:57:46 +00:00
|
|
|
"${split[0]}::" + EVENT_INDEXED, await service.index(params));
|
2018-07-10 16:54:55 +00:00
|
|
|
return null;
|
2016-12-23 20:57:46 +00:00
|
|
|
} else if (actionName == ACTION_READ) {
|
2018-07-10 16:54:55 +00:00
|
|
|
socket.send("${split[0]}::" + EVENT_READ,
|
2016-07-06 01:28:00 +00:00
|
|
|
await service.read(action.id, params));
|
2018-07-10 16:54:55 +00:00
|
|
|
return null;
|
2016-12-23 20:57:46 +00:00
|
|
|
} else if (actionName == ACTION_CREATE) {
|
2016-07-06 01:28:00 +00:00
|
|
|
return new WebSocketEvent(
|
2016-12-23 20:57:46 +00:00
|
|
|
eventName: "${split[0]}::" + EVENT_CREATED,
|
2016-07-06 01:28:00 +00:00
|
|
|
data: await service.create(action.data, params));
|
2016-12-23 20:57:46 +00:00
|
|
|
} else if (actionName == ACTION_MODIFY) {
|
2016-07-06 01:28:00 +00:00
|
|
|
return new WebSocketEvent(
|
2016-12-23 20:57:46 +00:00
|
|
|
eventName: "${split[0]}::" + EVENT_MODIFIED,
|
2016-07-06 01:28:00 +00:00
|
|
|
data: await service.modify(action.id, action.data, params));
|
2016-12-23 20:57:46 +00:00
|
|
|
} else if (actionName == ACTION_UPDATE) {
|
2016-07-06 01:28:00 +00:00
|
|
|
return new WebSocketEvent(
|
2016-12-23 20:57:46 +00:00
|
|
|
eventName: "${split[0]}::" + EVENT_UPDATED,
|
2016-07-06 01:28:00 +00:00
|
|
|
data: await service.update(action.id, action.data, params));
|
2016-12-23 20:57:46 +00:00
|
|
|
} else if (actionName == ACTION_REMOVE) {
|
2016-07-06 01:28:00 +00:00
|
|
|
return new WebSocketEvent(
|
2016-12-23 20:57:46 +00:00
|
|
|
eventName: "${split[0]}::" + EVENT_REMOVED,
|
2016-07-06 01:28:00 +00:00
|
|
|
data: await service.remove(action.id, params));
|
|
|
|
} else {
|
2018-07-10 16:54:55 +00:00
|
|
|
socket.sendError(new AngelHttpException.methodNotAllowed(
|
2016-12-23 20:57:46 +00:00
|
|
|
message: "Method Not Allowed: \"$actionName\""));
|
2018-07-10 16:54:55 +00:00
|
|
|
return null;
|
2016-04-29 01:19:09 +00:00
|
|
|
}
|
2016-12-23 20:57:46 +00:00
|
|
|
} catch (e, st) {
|
2017-09-24 04:37:58 +00:00
|
|
|
catchError(e, st, socket);
|
2016-04-29 01:19:09 +00:00
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
}
|
2016-04-29 01:19:09 +00:00
|
|
|
|
2017-02-28 14:15:34 +00:00
|
|
|
/// Authenticates a [WebSocketContext].
|
|
|
|
Future handleAuth(WebSocketAction action, WebSocketContext socket) async {
|
|
|
|
if (allowAuth != false &&
|
|
|
|
action.eventName == ACTION_AUTHENTICATE &&
|
2017-04-23 18:53:12 +00:00
|
|
|
action.params['query'] is Map &&
|
|
|
|
action.params['query']['jwt'] is String) {
|
2017-02-28 14:15:34 +00:00
|
|
|
try {
|
2018-08-28 14:17:14 +00:00
|
|
|
var auth = socket.request.container.make<AngelAuth>();
|
2017-04-23 18:53:12 +00:00
|
|
|
var jwt = action.params['query']['jwt'] as String;
|
2017-02-28 14:15:34 +00:00
|
|
|
AuthToken token;
|
|
|
|
|
|
|
|
token = new AuthToken.validate(jwt, auth.hmac);
|
|
|
|
var user = await auth.deserializer(token.userId);
|
2018-08-28 14:17:14 +00:00
|
|
|
socket.request
|
|
|
|
..container.registerSingleton<AuthToken>(token)
|
|
|
|
..container.registerSingleton(user, as: user.runtimeType as Type);
|
2017-02-28 14:15:34 +00:00
|
|
|
socket.send(EVENT_AUTHENTICATED,
|
|
|
|
{'token': token.serialize(auth.hmac), 'data': user});
|
|
|
|
} catch (e, st) {
|
2017-09-24 04:37:58 +00:00
|
|
|
catchError(e, st, socket);
|
2017-02-28 14:15:34 +00:00
|
|
|
}
|
2017-04-23 18:53:12 +00:00
|
|
|
} else {
|
|
|
|
socket.sendError(new AngelHttpException.badRequest(
|
|
|
|
message: 'No JWT provided for authentication.'));
|
2017-02-28 14:15:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Hooks a service up to have its events broadcasted.
|
2016-07-06 01:28:00 +00:00
|
|
|
hookupService(Pattern _path, HookedService service) {
|
|
|
|
String path = _path.toString();
|
2017-09-24 04:37:58 +00:00
|
|
|
service.after(
|
|
|
|
[
|
|
|
|
HookedServiceEvent.created,
|
|
|
|
HookedServiceEvent.modified,
|
|
|
|
HookedServiceEvent.updated,
|
|
|
|
HookedServiceEvent.removed
|
|
|
|
],
|
|
|
|
serviceHook(path),
|
|
|
|
);
|
2016-12-23 10:47:21 +00:00
|
|
|
_servicesAlreadyWired.add(path);
|
2016-07-06 01:28:00 +00:00
|
|
|
}
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Runs before firing [onConnection].
|
|
|
|
Future handleConnect(WebSocketContext socket) async {}
|
2016-07-06 13:33:40 +00:00
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Handles incoming data from a WebSocket.
|
|
|
|
handleData(WebSocketContext socket, data) async {
|
2016-07-06 01:28:00 +00:00
|
|
|
try {
|
2016-09-18 01:35:16 +00:00
|
|
|
socket._onData.add(data);
|
2018-07-10 16:54:55 +00:00
|
|
|
var fromJson = json.decode(data.toString());
|
|
|
|
var action = new WebSocketAction.fromJson(fromJson as Map);
|
2016-12-23 20:57:46 +00:00
|
|
|
_onAction.add(action);
|
2016-07-06 01:28:00 +00:00
|
|
|
|
|
|
|
if (action.eventName == null ||
|
|
|
|
action.eventName is! String ||
|
2016-07-06 13:33:40 +00:00
|
|
|
action.eventName.isEmpty) {
|
2017-01-28 21:38:26 +00:00
|
|
|
throw new AngelHttpException.badRequest();
|
2016-07-06 13:33:40 +00:00
|
|
|
}
|
2016-05-03 23:42:06 +00:00
|
|
|
|
2016-09-18 01:35:16 +00:00
|
|
|
if (fromJson is Map && fromJson.containsKey("eventName")) {
|
2016-12-23 20:57:46 +00:00
|
|
|
socket._onAction.add(new WebSocketAction.fromJson(fromJson));
|
|
|
|
socket.on
|
|
|
|
._getStreamForEvent(fromJson["eventName"].toString())
|
2018-07-10 16:54:55 +00:00
|
|
|
.add(fromJson["data"] as Map);
|
2016-09-18 01:35:16 +00:00
|
|
|
}
|
2016-07-06 13:33:40 +00:00
|
|
|
|
2017-06-03 18:13:38 +00:00
|
|
|
if (action.eventName == ACTION_AUTHENTICATE)
|
|
|
|
await handleAuth(action, socket);
|
|
|
|
|
2016-09-18 01:35:16 +00:00
|
|
|
if (action.eventName.contains("::")) {
|
|
|
|
var split = action.eventName.split("::");
|
|
|
|
|
|
|
|
if (split.length >= 2) {
|
2016-12-23 20:57:46 +00:00
|
|
|
if (ACTIONS.contains(split[1])) {
|
2018-07-10 16:54:55 +00:00
|
|
|
var event = await handleAction(action, socket);
|
2016-09-18 01:35:16 +00:00
|
|
|
if (event is Future) event = await event;
|
|
|
|
}
|
|
|
|
}
|
2016-04-29 01:19:09 +00:00
|
|
|
}
|
2016-12-23 20:57:46 +00:00
|
|
|
} catch (e, st) {
|
2017-09-24 04:37:58 +00:00
|
|
|
catchError(e, st, socket);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void catchError(e, StackTrace st, WebSocketContext socket) {
|
|
|
|
// Send an error
|
|
|
|
if (e is AngelHttpException) {
|
|
|
|
socket.sendError(e);
|
|
|
|
app.logger?.severe(e.message, e.error ?? e, e.stackTrace);
|
|
|
|
} else if (sendErrors) {
|
|
|
|
var err = new AngelHttpException(e,
|
|
|
|
message: e.toString(), stackTrace: st, errors: [st.toString()]);
|
|
|
|
socket.sendError(err);
|
|
|
|
app.logger?.severe(err.message, e, st);
|
|
|
|
} else {
|
|
|
|
var err = new AngelHttpException(e);
|
|
|
|
socket.sendError(err);
|
|
|
|
app.logger?.severe(e.toString(), e, st);
|
2016-07-06 01:28:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Transforms a [HookedServiceEvent], so that it can be broadcasted.
|
2016-07-06 01:28:00 +00:00
|
|
|
Future<WebSocketEvent> transformEvent(HookedServiceEvent event) async {
|
|
|
|
return new WebSocketEvent(eventName: event.eventName, data: event.result);
|
|
|
|
}
|
|
|
|
|
2016-12-23 20:57:46 +00:00
|
|
|
/// Hooks any [HookedService]s that are not being broadcasted yet.
|
2016-07-06 01:28:00 +00:00
|
|
|
wireAllServices(Angel app) {
|
|
|
|
for (Pattern key in app.services.keys.where((x) {
|
2016-12-23 10:47:21 +00:00
|
|
|
return !_servicesAlreadyWired.contains(x) &&
|
2016-07-06 01:28:00 +00:00
|
|
|
app.services[x] is HookedService;
|
|
|
|
})) {
|
2018-07-10 16:54:55 +00:00
|
|
|
hookupService(key, app.services[key] as HookedService);
|
2016-07-06 01:28:00 +00:00
|
|
|
}
|
2016-06-27 00:42:21 +00:00
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
/// Configures an [Angel] instance to listen for WebSocket connections.
|
2017-09-24 04:37:58 +00:00
|
|
|
Future configureServer(Angel app) async {
|
2018-08-28 14:17:14 +00:00
|
|
|
app..container.registerSingleton(this);
|
2016-09-17 20:00:17 +00:00
|
|
|
|
|
|
|
if (runtimeType != AngelWebSocket)
|
2018-08-28 14:17:14 +00:00
|
|
|
app..container.registerSingleton<AngelWebSocket>(this);
|
2016-07-06 01:28:00 +00:00
|
|
|
|
|
|
|
// Set up services
|
|
|
|
wireAllServices(app);
|
|
|
|
|
|
|
|
app.onService.listen((_) {
|
|
|
|
wireAllServices(app);
|
|
|
|
});
|
|
|
|
|
2018-11-15 16:43:51 +00:00
|
|
|
if (synchronizationChannel != null) {
|
|
|
|
synchronizationChannel.stream.listen((e) => batchEvent(e, notify: false));
|
2017-09-24 04:37:58 +00:00
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
|
2018-11-15 16:43:51 +00:00
|
|
|
app.shutdownHooks.add((_) => synchronizationChannel?.sink?.close());
|
2018-07-10 16:54:55 +00:00
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
/// Handles an incoming [WebSocketContext].
|
2018-12-09 16:40:09 +00:00
|
|
|
Future<void> handleClient(WebSocketContext socket) async {
|
|
|
|
var origin = socket.request.headers.value('origin');
|
|
|
|
if (allowedOrigins != null && !allowedOrigins.contains(origin)) {
|
|
|
|
throw new AngelHttpException.forbidden(
|
|
|
|
message:
|
|
|
|
'WebSocket connections are not allowed from the origin "$origin".');
|
|
|
|
}
|
|
|
|
|
2017-09-24 04:37:58 +00:00
|
|
|
_clients.add(socket);
|
|
|
|
await handleConnect(socket);
|
2017-01-29 20:02:19 +00:00
|
|
|
|
2017-09-24 04:37:58 +00:00
|
|
|
_onConnection.add(socket);
|
2016-09-17 20:00:17 +00:00
|
|
|
|
2018-08-28 14:17:14 +00:00
|
|
|
socket.request.container.registerSingleton<WebSocketContext>(socket);
|
2017-09-24 04:37:58 +00:00
|
|
|
|
2018-07-10 16:54:55 +00:00
|
|
|
socket.channel.stream.listen(
|
2018-08-28 14:17:14 +00:00
|
|
|
(data) {
|
2016-12-23 20:57:46 +00:00
|
|
|
_onData.add(data);
|
|
|
|
handleData(socket, data);
|
2017-09-24 04:37:58 +00:00
|
|
|
},
|
|
|
|
onDone: () {
|
2016-09-18 03:16:51 +00:00
|
|
|
_onDisconnect.add(socket);
|
2018-07-10 16:54:55 +00:00
|
|
|
_clients.remove(socket);
|
2017-09-24 04:37:58 +00:00
|
|
|
},
|
|
|
|
onError: (e) {
|
2016-09-18 03:16:51 +00:00
|
|
|
_onDisconnect.add(socket);
|
2018-07-10 16:54:55 +00:00
|
|
|
_clients.remove(socket);
|
2017-09-24 04:37:58 +00:00
|
|
|
},
|
|
|
|
cancelOnError: true,
|
|
|
|
);
|
2018-07-10 16:54:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Handles an incoming HTTP request.
|
|
|
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
2018-08-28 14:17:14 +00:00
|
|
|
if (req is HttpRequestContext && res is HttpResponseContext) {
|
|
|
|
if (!WebSocketTransformer.isUpgradeRequest(req.rawRequest))
|
2018-07-10 16:54:55 +00:00
|
|
|
throw new AngelHttpException.badRequest();
|
2018-08-28 14:17:14 +00:00
|
|
|
await res.detach();
|
|
|
|
var ws = await WebSocketTransformer.upgrade(req.rawRequest);
|
2018-07-10 16:54:55 +00:00
|
|
|
var channel = new IOWebSocketChannel(ws);
|
|
|
|
var socket = new WebSocketContext(channel, req, res);
|
|
|
|
handleClient(socket);
|
|
|
|
return false;
|
2018-12-09 16:40:09 +00:00
|
|
|
} else if (req is Http2RequestContext && res is Http2ResponseContext) {
|
|
|
|
var connection =
|
|
|
|
req.headers['connection']?.map((s) => s.toLowerCase().trim());
|
|
|
|
var upgrade = req.headers.value('upgrade')?.toLowerCase();
|
|
|
|
var version = req.headers.value('sec-websocket-version');
|
|
|
|
var key = req.headers.value('sec-websocket-key');
|
|
|
|
var protocol = req.headers.value('sec-websocket-protocol');
|
|
|
|
|
|
|
|
if (connection == null) {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'Missing `connection` header.');
|
|
|
|
} else if (!connection.contains('upgrade')) {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'Missing "upgrade" in `connection` header.');
|
|
|
|
} else if (upgrade != 'websocket') {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'The `upgrade` header must equal "websocket".');
|
|
|
|
} else if (version != '13') {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'The `sec-websocket-version` header must equal "13".');
|
|
|
|
} else if (key == null) {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'Missing `sec-websocket-key` header.');
|
|
|
|
} else if (protocol != null &&
|
|
|
|
allowedProtocols != null &&
|
|
|
|
!allowedProtocols.contains(protocol)) {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'Disallowed `sec-websocket-protocol` header "$protocol".');
|
|
|
|
} else {
|
|
|
|
var stream = res.detach();
|
|
|
|
var ctrl = new StreamChannelController<List<int>>();
|
|
|
|
|
|
|
|
ctrl.local.stream.listen((buf) {
|
|
|
|
stream.sendData(buf);
|
|
|
|
}, onDone: () {
|
|
|
|
stream.outgoingMessages.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
if (req.hasParsedBody) {
|
|
|
|
ctrl.local.sink.close();
|
|
|
|
} else {
|
|
|
|
req.body.pipe(ctrl.local.sink);
|
|
|
|
}
|
|
|
|
|
|
|
|
var sink = utf8.encoder.startChunkedConversion(ctrl.foreign.sink);
|
|
|
|
sink.add("HTTP/1.1 101 Switching Protocols\r\n"
|
|
|
|
"Upgrade: websocket\r\n"
|
|
|
|
"Connection: Upgrade\r\n"
|
|
|
|
"Sec-WebSocket-Accept: ${WebSocketChannel.signKey(key)}\r\n");
|
|
|
|
if (protocol != null) sink.add("Sec-WebSocket-Protocol: $protocol\r\n");
|
|
|
|
sink.add("\r\n");
|
|
|
|
|
|
|
|
var ws = new WebSocketChannel(ctrl.foreign);
|
|
|
|
var socket = new WebSocketContext(ws, req, res);
|
|
|
|
handleClient(socket);
|
|
|
|
return false;
|
|
|
|
}
|
2018-07-10 16:54:55 +00:00
|
|
|
} else {
|
2018-12-09 16:40:09 +00:00
|
|
|
throw new ArgumentError(
|
|
|
|
'Not an HTTP/1.1 or HTTP/2 RequestContext+ResponseContext pair: $req, $res');
|
2018-07-10 16:54:55 +00:00
|
|
|
}
|
2016-07-06 01:28:00 +00:00
|
|
|
}
|
|
|
|
}
|