-
Notifications
You must be signed in to change notification settings - Fork 128
/
Copy pathweb_socket_handler.dart
144 lines (120 loc) · 5.14 KB
/
web_socket_handler.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// A class that exposes a handler for upgrading WebSocket requests.
class WebSocketHandler {
/// The function to call when a request is upgraded.
final Function _onConnection;
/// The set of protocols the user supports, or `null`.
final Set<String>? _protocols;
/// The set of allowed browser origin connections, or `null`..
final Set<String>? _allowedOrigins;
/// The ping interval used for verifying connection, or `null`.
final Duration? _pingInterval;
WebSocketHandler(this._onConnection, this._protocols, this._allowedOrigins,
this._pingInterval);
/// The [Handler].
Response handle(Request request) {
if (request.method != 'GET') return _notFound();
final connection = request.headers['Connection'];
if (connection == null) return _notFound();
final tokens =
connection.toLowerCase().split(',').map((token) => token.trim());
if (!tokens.contains('upgrade')) return _notFound();
final upgrade = request.headers['Upgrade'];
if (upgrade == null) return _notFound();
if (upgrade.toLowerCase() != 'websocket') return _notFound();
final version = request.headers['Sec-WebSocket-Version'];
if (version == null) {
return _badRequest('missing Sec-WebSocket-Version header.');
} else if (version != '13') {
return _notFound();
}
if (request.protocolVersion != '1.1') {
return _badRequest('unexpected HTTP version '
'"${request.protocolVersion}".');
}
final key = request.headers['Sec-WebSocket-Key'];
if (key == null) return _badRequest('missing Sec-WebSocket-Key header.');
if (!request.canHijack) {
throw ArgumentError('webSocketHandler may only be used with a server '
'that supports request hijacking.');
}
// The Origin header is always set by browser connections. By filtering out
// unexpected origins, we ensure that malicious JavaScript is unable to fake
// a WebSocket handshake.
final origin = request.headers['Origin'];
if (origin != null &&
_allowedOrigins != null &&
!_allowedOrigins.contains(origin.toLowerCase())) {
return _forbidden('invalid origin "$origin".');
}
final protocol = _chooseProtocol(request);
request.hijack((channel) {
final sink = utf8.encoder.startChunkedConversion(channel.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');
if (channel.sink is! Socket) {
throw ArgumentError('channel.sink must be a dart:io `Socket`.');
}
final webSocket = WebSocket.fromUpgradedSocket(channel.sink as Socket,
protocol: protocol, serverSide: true)
..pingInterval = _pingInterval;
// ignore: avoid_dynamic_calls
_onConnection(IOWebSocketChannel(webSocket), protocol);
});
}
/// Selects a subprotocol to use for the given connection.
///
/// If no matching protocol can be found, returns `null`.
String? _chooseProtocol(Request request) {
final requestProtocols = request.headers['Sec-WebSocket-Protocol'];
if (requestProtocols == null) return null;
if (_protocols == null) return null;
for (var requestProtocol in requestProtocols.split(',')) {
requestProtocol = requestProtocol.trim();
if (_protocols.contains(requestProtocol)) return requestProtocol;
}
return null;
}
/// Returns a 404 Not Found response.
Response _notFound() => _htmlResponse(
404, '404 Not Found', 'Only WebSocket connections are supported.');
/// Returns a 400 Bad Request response.
///
/// [message] will be HTML-escaped before being included in the response body.
Response _badRequest(String message) => _htmlResponse(
400, '400 Bad Request', 'Invalid WebSocket upgrade request: $message');
/// Returns a 403 Forbidden response.
///
/// [message] will be HTML-escaped before being included in the response body.
Response _forbidden(String message) => _htmlResponse(
403, '403 Forbidden', 'WebSocket upgrade refused: $message');
/// Creates an HTTP response with the given [statusCode] and an HTML body with
/// [title] and [message].
///
/// [title] and [message] will be automatically HTML-escaped.
Response _htmlResponse(int statusCode, String title, String message) {
title = htmlEscape.convert(title);
message = htmlEscape.convert(message);
return Response(statusCode, body: '''
<!doctype html>
<html>
<head><title>$title</title></head>
<body>
<h1>$title</h1>
<p>$message</p>
</body>
</html>
''', headers: {'content-type': 'text/html'});
}
}