OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library shelf_web_socket.web_socket_handler; | |
6 | |
7 import 'dart:convert'; | |
8 | |
9 import 'package:crypto/crypto.dart'; | |
kevmoo
2014/05/20 21:54:49
not used?
nweiz
2014/05/20 22:17:42
Done.
| |
10 import 'package:http_parser/http_parser.dart'; | |
11 import 'package:shelf/shelf.dart'; | |
12 | |
13 /// A class that exposes a handler for upgrading WebSocket requests. | |
14 class WebSocketHandler { | |
15 /// The function to call when a request is upgraded. | |
16 final Function _onConnection; | |
17 | |
18 /// The set of protocols the user supports, or `null`. | |
19 final Set<String> _protocols; | |
20 | |
21 /// The set of allowed browser origin connections, or `null`.. | |
22 final Set<String> _allowedOrigins; | |
23 | |
24 /// Returns a 404 Not Found response. | |
25 Response get _notFound => _htmlResponse(404, "404 Not Found", | |
kevmoo
2014/05/20 21:54:49
Make this a (static? top-level?) method
Definitel
nweiz
2014/05/20 22:17:42
Done.
| |
26 "Only WebSocket connections are supported."); | |
27 | |
28 WebSocketHandler(this._onConnection, this._protocols, this._allowedOrigins); | |
29 | |
30 /// The [Handler]. | |
31 Response handle(Request request) { | |
32 if (request.method != 'GET') return _notFound; | |
33 | |
34 var connection = request.headers['Connection']; | |
35 if (connection == null) return _notFound; | |
36 if (connection.toLowerCase() != 'upgrade') return _notFound; | |
37 | |
38 var upgrade = request.headers['Upgrade']; | |
39 if (upgrade == null) return _notFound; | |
40 if (upgrade.toLowerCase() != 'websocket') return _notFound; | |
41 | |
42 var version = request.headers['Sec-WebSocket-Version']; | |
43 if (version == null) { | |
44 return _badRequest('missing Sec-WebSocket-Version header.'); | |
45 } else if (version != '13') { | |
46 return _notFound; | |
47 } | |
48 | |
49 if (request.protocolVersion != '1.1') { | |
50 return _badRequest('unexpected HTTP version ' | |
51 '"${request.protocolVersion}".'); | |
52 } | |
53 | |
54 var key = request.headers['Sec-WebSocket-Key']; | |
55 if (key == null) return _badRequest('missing Sec-WebSocket-Key header.'); | |
56 | |
57 if (!request.canHijack) { | |
58 throw new ArgumentError("webSocketHandler may only be used with a server " | |
59 "that supports request hijacking."); | |
60 } | |
61 | |
62 // The Origin header is always set by browser connections. By filtering out | |
63 // unexpected origins, we ensure that malicious JavaScript is unable to fake | |
64 // a WebSocket handshake. | |
65 var origin = request.headers['Origin']; | |
66 if (origin != null && _allowedOrigins != null && | |
67 !_allowedOrigins.contains(origin.toLowerCase())) { | |
68 return _forbidden('invalid origin "$origin".'); | |
69 } | |
70 | |
71 var protocol = _chooseProtocol(request); | |
72 request.hijack((stream, byteSink) { | |
73 var sink = UTF8.encoder.startChunkedConversion(byteSink); | |
74 sink.add( | |
75 "HTTP/1.1 101 Switching Protocols\r\n" | |
76 "Upgrade: websocket\r\n" | |
77 "Connection: Upgrade\r\n" | |
78 "Sec-WebSocket-Accept: ${CompatibleWebSocket.signKey(key)}\r\n"); | |
79 if (protocol != null) sink.add("Sec-WebSocket-Protocol: $protocol\r\n"); | |
80 sink.add("\r\n"); | |
81 | |
82 _onConnection(new CompatibleWebSocket(stream, sink: byteSink), protocol); | |
83 }); | |
84 | |
85 assert(false); | |
kevmoo
2014/05/20 21:54:49
Explain the flow here a bit in a comment. This is
nweiz
2014/05/20 22:17:42
Done.
| |
86 return null; | |
87 } | |
88 | |
89 /// Selects a subprotocol to use for the given connection. | |
90 /// | |
91 /// If no matching protocol can be found, returns `null`. | |
92 String _chooseProtocol(Request request) { | |
kevmoo
2014/05/20 21:54:49
Style nit: if methods don't access instance state,
nweiz
2014/05/20 22:17:42
I disagree with this principle. Private methods sh
| |
93 var protocols = request.headers['Sec-WebSocket-Protocol']; | |
94 if (protocols == null) return null; | |
95 for (var protocol in protocols.split(',')) { | |
96 protocol = protocol.trim(); | |
97 if (_protocols.contains(protocol)) return protocol; | |
98 } | |
99 return null; | |
100 } | |
101 | |
102 /// Returns a 400 Bad Request response. | |
103 /// | |
104 /// [message] will be HTML-escaped before being included in the response body. | |
105 Response _badRequest(String message) => _htmlResponse(400, "400 Bad Request", | |
106 "Invalid WebSocket upgrade request: $message"); | |
107 | |
108 /// Returns a 403 Forbidden response. | |
109 /// | |
110 /// [message] will be HTML-escaped before being included in the response body. | |
111 Response _forbidden(String message) => _htmlResponse(403, "403 Forbidden", | |
112 "WebSocket upgrade refused: $message"); | |
113 | |
114 /// Creates an HTTP response with the given [statusCode] and an HTML body with | |
115 /// [title] and [message]. | |
116 Response _htmlResponse(int statusCode, String title, String message) { | |
kevmoo
2014/05/20 21:54:49
clarify that this method expects unescaped params
nweiz
2014/05/20 22:17:42
Done.
| |
117 title = HTML_ESCAPE.convert(title); | |
118 message = HTML_ESCAPE.convert(message); | |
119 return new Response(statusCode, body: """ | |
120 <!doctype html> | |
121 <html> | |
122 <head><title>$title</title></head> | |
123 <body> | |
124 <h1>$title</h1> | |
125 <p>$message</p> | |
126 </body> | |
127 </html> | |
128 """, headers: {'content-type': 'text/html'}); | |
129 } | |
130 } | |
OLD | NEW |