| 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:http_parser/http_parser.dart'; | |
| 10 import 'package:shelf/shelf.dart'; | |
| 11 | |
| 12 /// A class that exposes a handler for upgrading WebSocket requests. | |
| 13 class WebSocketHandler { | |
| 14 /// The function to call when a request is upgraded. | |
| 15 final Function _onConnection; | |
| 16 | |
| 17 /// The set of protocols the user supports, or `null`. | |
| 18 final Set<String> _protocols; | |
| 19 | |
| 20 /// The set of allowed browser origin connections, or `null`.. | |
| 21 final Set<String> _allowedOrigins; | |
| 22 | |
| 23 WebSocketHandler(this._onConnection, this._protocols, this._allowedOrigins); | |
| 24 | |
| 25 /// The [Handler]. | |
| 26 Response handle(Request request) { | |
| 27 if (request.method != 'GET') return _notFound(); | |
| 28 | |
| 29 var connection = request.headers['Connection']; | |
| 30 if (connection == null) return _notFound(); | |
| 31 var tokens = connection.toLowerCase().split(',') | |
| 32 .map((token) => token.trim()); | |
| 33 if (!tokens.contains('upgrade')) return _notFound(); | |
| 34 | |
| 35 var upgrade = request.headers['Upgrade']; | |
| 36 if (upgrade == null) return _notFound(); | |
| 37 if (upgrade.toLowerCase() != 'websocket') return _notFound(); | |
| 38 | |
| 39 var version = request.headers['Sec-WebSocket-Version']; | |
| 40 if (version == null) { | |
| 41 return _badRequest('missing Sec-WebSocket-Version header.'); | |
| 42 } else if (version != '13') { | |
| 43 return _notFound(); | |
| 44 } | |
| 45 | |
| 46 if (request.protocolVersion != '1.1') { | |
| 47 return _badRequest('unexpected HTTP version ' | |
| 48 '"${request.protocolVersion}".'); | |
| 49 } | |
| 50 | |
| 51 var key = request.headers['Sec-WebSocket-Key']; | |
| 52 if (key == null) return _badRequest('missing Sec-WebSocket-Key header.'); | |
| 53 | |
| 54 if (!request.canHijack) { | |
| 55 throw new ArgumentError("webSocketHandler may only be used with a server " | |
| 56 "that supports request hijacking."); | |
| 57 } | |
| 58 | |
| 59 // The Origin header is always set by browser connections. By filtering out | |
| 60 // unexpected origins, we ensure that malicious JavaScript is unable to fake | |
| 61 // a WebSocket handshake. | |
| 62 var origin = request.headers['Origin']; | |
| 63 if (origin != null && _allowedOrigins != null && | |
| 64 !_allowedOrigins.contains(origin.toLowerCase())) { | |
| 65 return _forbidden('invalid origin "$origin".'); | |
| 66 } | |
| 67 | |
| 68 var protocol = _chooseProtocol(request); | |
| 69 request.hijack((stream, byteSink) { | |
| 70 var sink = UTF8.encoder.startChunkedConversion(byteSink); | |
| 71 sink.add( | |
| 72 "HTTP/1.1 101 Switching Protocols\r\n" | |
| 73 "Upgrade: websocket\r\n" | |
| 74 "Connection: Upgrade\r\n" | |
| 75 "Sec-WebSocket-Accept: ${CompatibleWebSocket.signKey(key)}\r\n"); | |
| 76 if (protocol != null) sink.add("Sec-WebSocket-Protocol: $protocol\r\n"); | |
| 77 sink.add("\r\n"); | |
| 78 | |
| 79 _onConnection(new CompatibleWebSocket(stream, sink: byteSink), protocol); | |
| 80 }); | |
| 81 | |
| 82 // [request.hijack] is guaranteed to throw a [HijackException], so we'll | |
| 83 // never get here. | |
| 84 assert(false); | |
| 85 return null; | |
| 86 } | |
| 87 | |
| 88 /// Selects a subprotocol to use for the given connection. | |
| 89 /// | |
| 90 /// If no matching protocol can be found, returns `null`. | |
| 91 String _chooseProtocol(Request request) { | |
| 92 var protocols = request.headers['Sec-WebSocket-Protocol']; | |
| 93 if (protocols == null) return null; | |
| 94 for (var protocol in protocols.split(',')) { | |
| 95 protocol = protocol.trim(); | |
| 96 if (_protocols.contains(protocol)) return protocol; | |
| 97 } | |
| 98 return null; | |
| 99 } | |
| 100 | |
| 101 /// Returns a 404 Not Found response. | |
| 102 Response _notFound() => _htmlResponse(404, "404 Not Found", | |
| 103 "Only WebSocket connections are supported."); | |
| 104 | |
| 105 /// Returns a 400 Bad Request response. | |
| 106 /// | |
| 107 /// [message] will be HTML-escaped before being included in the response body. | |
| 108 Response _badRequest(String message) => _htmlResponse(400, "400 Bad Request", | |
| 109 "Invalid WebSocket upgrade request: $message"); | |
| 110 | |
| 111 /// Returns a 403 Forbidden response. | |
| 112 /// | |
| 113 /// [message] will be HTML-escaped before being included in the response body. | |
| 114 Response _forbidden(String message) => _htmlResponse(403, "403 Forbidden", | |
| 115 "WebSocket upgrade refused: $message"); | |
| 116 | |
| 117 /// Creates an HTTP response with the given [statusCode] and an HTML body with | |
| 118 /// [title] and [message]. | |
| 119 /// | |
| 120 /// [title] and [message] will be automatically HTML-escaped. | |
| 121 Response _htmlResponse(int statusCode, String title, String message) { | |
| 122 title = HTML_ESCAPE.convert(title); | |
| 123 message = HTML_ESCAPE.convert(message); | |
| 124 return new Response(statusCode, body: """ | |
| 125 <!doctype html> | |
| 126 <html> | |
| 127 <head><title>$title</title></head> | |
| 128 <body> | |
| 129 <h1>$title</h1> | |
| 130 <p>$message</p> | |
| 131 </body> | |
| 132 </html> | |
| 133 """, headers: {'content-type': 'text/html'}); | |
| 134 } | |
| 135 } | |
| OLD | NEW |