Chromium Code Reviews| 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 |