├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── README.md ├── lib ├── socket_io.dart └── src │ ├── adapter │ └── adapter.dart │ ├── client.dart │ ├── engine │ ├── connect.dart │ ├── engine.dart │ ├── server.dart │ ├── socket.dart │ └── transport │ │ ├── jsonp_transport.dart │ │ ├── polling_transport.dart │ │ ├── transports.dart │ │ ├── websocket_transport.dart │ │ └── xhr_transport.dart │ ├── namespace.dart │ ├── server.dart │ ├── socket.dart │ └── util │ └── event_emitter.dart ├── pubspec.yaml └── test └── socket.test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool 3 | .packages 4 | .pub/ 5 | build/ 6 | packages 7 | # Remove the following pattern if you wish to check in your lock file 8 | pubspec.lock 9 | 10 | # Files created by dart2js 11 | *.dart.js 12 | *.part.js 13 | *.js.deps 14 | *.js.map 15 | *.info.json 16 | 17 | # Directory created by dartdoc 18 | doc/api/ 19 | 20 | # JetBrains IDEs 21 | .idea/ 22 | *.iml 23 | *.ipr 24 | *.iws 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 2 | 3 | **Bug Fix:** 4 | 5 | * [#45](https://github.com/rikulo/socket.io-dart/issues/45) [BUG] Calling close on Server raises ConcurrentModificationError 6 | 7 | **New Feature:** 8 | 9 | * [#47](https://github.com/rikulo/socket.io-dart/pull/47) Added Future to start and stop server 10 | 11 | ## 1.0.0 12 | 13 | **New Feature:** 14 | 15 | * [#33](https://github.com/rikulo/socket.io-dart/issues/33) Null safety 16 | 17 | 18 | ## 0.9.4 19 | 20 | **Bug Fix:** 21 | 22 | * [#23](https://github.com/rikulo/socket.io-dart/issues/23) [BUG] Calling close on Server raises ConcurrentModificationError 23 | 24 | ## 0.9.3 25 | 26 | **Bug Fix:** 27 | 28 | * [#18](https://github.com/rikulo/socket.io-dart/pull/18) make sure it is accessing the rooms map 29 | 30 | 31 | ## 0.9.2 32 | 33 | **Bug Fix:** 34 | 35 | * [#17](https://github.com/rikulo/socket.io-dart/pull/17) inference error 36 | 37 | 38 | ## 0.9.1+1 39 | 40 | **New Feature:** 41 | 42 | * [#16](https://github.com/rikulo/socket.io-dart/pull/16) Apply Pedantic recommendations 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2018 Potix corporation 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socket.io-dart 2 | 3 | Port of awesome JavaScript Node.js library - [Socket.io v2.0.1](https://github.com/socketio/socket.io) - in Dart 4 | 5 | ## Usage 6 | 7 | ```dart 8 | import 'package:socket_io/socket_io.dart'; 9 | 10 | main() { 11 | var io = new Server(); 12 | var nsp = io.of('/some'); 13 | nsp.on('connection', (client) { 14 | print('connection /some'); 15 | client.on('msg', (data) { 16 | print('data from /some => $data'); 17 | client.emit('fromServer', "ok 2"); 18 | }); 19 | }); 20 | io.on('connection', (client) { 21 | print('connection default namespace'); 22 | client.on('msg', (data) { 23 | print('data from default => $data'); 24 | client.emit('fromServer', "ok"); 25 | }); 26 | }); 27 | io.listen(3000); 28 | } 29 | ``` 30 | 31 | ```js 32 | // JS client 33 | var socket = io('http://localhost:3000'); 34 | socket.on('connect', function(){console.log('connect')}); 35 | socket.on('event', function(data){console.log(data)}); 36 | socket.on('disconnect', function(){console.log('disconnect')}); 37 | socket.on('fromServer', function(e){console.log(e)}); 38 | ``` 39 | 40 | ```dart 41 | // Dart client 42 | import 'package:socket_io_client/socket_io_client.dart' as IO; 43 | 44 | IO.Socket socket = IO.io('http://localhost:3000'); 45 | socket.on('connect', (_) { 46 | print('connect'); 47 | socket.emit('msg', 'test'); 48 | }); 49 | socket.on('event', (data) => print(data)); 50 | socket.on('disconnect', (_) => print('disconnect')); 51 | socket.on('fromServer', (_) => print(_)); 52 | ``` 53 | 54 | ## Multiplexing support 55 | 56 | Same as Socket.IO, this project allows you to create several Namespaces, which will act as separate communication channels but will share the same underlying connection. 57 | 58 | ## Room support 59 | 60 | Within each Namespace, you can define arbitrary channels, called Rooms, that sockets can join and leave. You can then broadcast to any given room, reaching every socket that has joined it. 61 | 62 | ## Transports support 63 | Refers to [engine.io](https://github.com/socketio/engine.io) 64 | 65 | - `polling`: XHR / JSONP polling transport. 66 | - `websocket`: WebSocket transport. 67 | 68 | ## Adapters support 69 | 70 | * Default socket.io in-memory adapter class. Refers to [socket.io-adapter](https://github.com/socketio/socket.io-adapter) 71 | 72 | ## Notes to Contributors 73 | 74 | ### Fork socket.io-dart 75 | 76 | If you'd like to contribute back to the core, you can [fork this repository](https://help.github.com/articles/fork-a-repo) and send us a pull request, when it is ready. 77 | 78 | If you are new to Git or GitHub, please read [this guide](https://help.github.com/) first. 79 | 80 | ## Who Uses 81 | 82 | * [Quire](https://quire.io) - a simple, collaborative, multi-level task management tool. 83 | * [KEIKAI](https://keikai.io/) - a web spreadsheet for Big Data. 84 | 85 | ## Socket.io Dart Client 86 | 87 | * [socket.io-client-dart](https://github.com/rikulo/socket.io-client-dart) 88 | 89 | ## Contributors 90 | * Thanks [@felangel](https://github.com/felangel) for https://github.com/rikulo/socket.io-dart/issues/7 91 | * Thanks [@ThinkDigitalSoftware](https://github.com/ThinkDigitalSoftware) for https://github.com/rikulo/socket.io-dart/pull/15 92 | * Thanks [@guilhermecaldas](https://github.com/guilhermecaldas) for https://github.com/rikulo/socket.io-dart/pull/16 93 | * Thanks [@jodinathan](https://github.com/jodinathan) for https://github.com/rikulo/socket.io-dart/pull/17 94 | * Thanks [@jodinathan](https://github.com/jodinathan) for https://github.com/rikulo/socket.io-dart/pull/18 95 | * Thanks [@nicobritos](https://github.com/nicobritos) for https://github.com/rikulo/socket.io-dart/pull/46 96 | * Thanks [@nicobritos](https://github.com/nicobritos) for https://github.com/rikulo/socket.io-dart/pull/47 -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file allows you to configure the Dart analyzer. 2 | # 3 | # The commented part below is just for inspiration. Read the guide here: 4 | # https://www.dartlang.org/guides/language/analysis-options 5 | include: package:pedantic/analysis_options.yaml 6 | analyzer: 7 | # exclude: 8 | # - path/to/excluded/files/** 9 | # linter: 10 | # rules: 11 | # # see catalog here: http://dart-lang.github.io/linter/lints/ 12 | # - hash_and_equals 13 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # socket.io-dart 2 | 3 | Port of awesome JavaScript Node.js library - [Socket.io v2.0.1](https://github.com/socketio/socket.io) - in Dart 4 | 5 | ## Usage 6 | 7 | ```dart 8 | import 'package:socket_io/socket_io.dart'; 9 | 10 | main() { 11 | var io = new Server(); 12 | var nsp = io.of('/some'); 13 | nsp.on('connection', (client) { 14 | print('connection /some'); 15 | client.on('msg', (data) { 16 | print('data from /some => $data'); 17 | client.emit('fromServer', "ok 2"); 18 | }); 19 | }); 20 | io.on('connection', (client) { 21 | print('connection default namespace'); 22 | client.on('msg', (data) { 23 | print('data from default => $data'); 24 | client.emit('fromServer', "ok"); 25 | }); 26 | }); 27 | io.listen(3000); 28 | } 29 | ``` 30 | 31 | ```js 32 | // JS client 33 | var socket = io('http://localhost:3000'); 34 | socket.on('connect', function(){console.log('connect')}); 35 | socket.on('event', function(data){console.log(data)}); 36 | socket.on('disconnect', function(){console.log('disconnect')}); 37 | socket.on('fromServer', function(e){console.log(e)}); 38 | ``` 39 | 40 | ```dart 41 | // Dart client 42 | import 'package:socket_io_client/socket_io_client.dart' as IO; 43 | 44 | IO.Socket socket = IO.io('http://localhost:3000'); 45 | socket.on('connect', (_) { 46 | print('connect'); 47 | socket.emit('msg', 'test'); 48 | }); 49 | socket.on('event', (data) => print(data)); 50 | socket.on('disconnect', (_) => print('disconnect')); 51 | socket.on('fromServer', (_) => print(_)); 52 | ``` 53 | 54 | ## Multiplexing support 55 | 56 | Same as Socket.IO, this project allows you to create several Namespaces, which will act as separate communication channels but will share the same underlying connection. 57 | 58 | ## Room support 59 | 60 | Within each Namespace, you can define arbitrary channels, called Rooms, that sockets can join and leave. You can then broadcast to any given room, reaching every socket that has joined it. 61 | 62 | ## Transports support 63 | Refers to [engine.io](https://github.com/socketio/engine.io) 64 | 65 | - `polling`: XHR / JSONP polling transport. 66 | - `websocket`: WebSocket transport. 67 | 68 | ## Adapters support 69 | 70 | * Default socket.io in-memory adapter class. Refers to [socket.io-adapter](https://github.com/socketio/socket.io-adapter) 71 | -------------------------------------------------------------------------------- /lib/socket_io.dart: -------------------------------------------------------------------------------- 1 | library socket_io; 2 | 3 | export 'src/server.dart'; 4 | export 'src/socket.dart'; 5 | export 'src/engine/transport/transports.dart' show Transport, MessageHandler; 6 | export 'src/engine/transport/jsonp_transport.dart' show JSONPTransport; 7 | export 'src/engine/transport/polling_transport.dart' show PollingTransport; 8 | export 'src/engine/transport/websocket_transport.dart' show WebSocketTransport; 9 | 10 | export 'package:socket_io_common/src/engine/parser/parser.dart' 11 | show PacketParser; 12 | -------------------------------------------------------------------------------- /lib/src/adapter/adapter.dart: -------------------------------------------------------------------------------- 1 | /// adapter.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 16/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:async'; 12 | import 'package:socket_io/src/namespace.dart'; 13 | import 'package:socket_io_common/src/parser/parser.dart'; 14 | import 'package:socket_io/src/util/event_emitter.dart'; 15 | 16 | abstract class Adapter { 17 | Map nsps = {}; 18 | Map rooms = {}; 19 | Map sids = {}; 20 | 21 | void add(String id, String room, [dynamic Function([dynamic]) fn]); 22 | void del(String id, String room, [dynamic Function([dynamic]) fn]); 23 | void delAll(String id, [dynamic Function([dynamic]) fn]); 24 | void broadcast(Map packet, [Map opts]); 25 | void clients([List rooms, dynamic Function([dynamic]) fn]); 26 | void clientRooms(String id, [dynamic Function(dynamic, [dynamic]) fn]); 27 | 28 | static Adapter newInstance(String key, Namespace nsp) { 29 | if ('default' == key) { 30 | return _MemoryStoreAdapter(nsp); 31 | } 32 | throw UnimplementedError('not supported other adapter yet.'); 33 | } 34 | } 35 | 36 | class _MemoryStoreAdapter extends EventEmitter implements Adapter { 37 | @override 38 | Map nsps = {}; 39 | @override 40 | Map rooms = {}; 41 | 42 | @override 43 | Map sids = {}; 44 | late Encoder encoder; 45 | late Namespace nsp; 46 | 47 | _MemoryStoreAdapter(Namespace nsp) { 48 | this.nsp = nsp; 49 | encoder = nsp.server.encoder; 50 | } 51 | 52 | /// Adds a socket to a room. 53 | /// 54 | /// @param {String} socket id 55 | /// @param {String} room name 56 | /// @param {Function} callback 57 | /// @api public 58 | 59 | @override 60 | void add(String id, String room, [dynamic Function([dynamic])? fn]) { 61 | sids[id] = sids[id] ?? {}; 62 | sids[id]![room] = true; 63 | rooms[room] = rooms[room] ?? _Room(); 64 | rooms[room]!.add(id); 65 | if (fn != null) scheduleMicrotask(() => fn(null)); 66 | } 67 | 68 | /// Removes a socket from a room. 69 | /// 70 | /// @param {String} socket id 71 | /// @param {String} room name 72 | /// @param {Function} callback 73 | /// @api public 74 | @override 75 | void del(String id, String room, [dynamic Function([dynamic])? fn]) { 76 | sids[id] = sids[id] ?? {}; 77 | sids[id]!.remove(room); 78 | if (rooms.containsKey(room)) { 79 | rooms[room]!.del(id); 80 | if (rooms[room]!.length == 0) rooms.remove(room); 81 | } 82 | 83 | if (fn != null) scheduleMicrotask(() => fn(null)); 84 | } 85 | 86 | /// Removes a socket from all rooms it's joined. 87 | /// 88 | /// @param {String} socket id 89 | /// @param {Function} callback 90 | /// @api public 91 | @override 92 | void delAll(String id, [dynamic Function([dynamic])? fn]) { 93 | var rooms = sids[id]; 94 | if (rooms != null) { 95 | for (var room in rooms.keys) { 96 | if (this.rooms.containsKey(room)) { 97 | this.rooms[room]!.del(id); 98 | if (this.rooms[room]!.length == 0) this.rooms.remove(room); 99 | } 100 | } 101 | } 102 | sids.remove(id); 103 | 104 | if (fn != null) scheduleMicrotask(() => fn(null)); 105 | } 106 | 107 | /// Broadcasts a packet. 108 | /// 109 | /// Options: 110 | /// - `flags` {Object} flags for this packet 111 | /// - `except` {Array} sids that should be excluded 112 | /// - `rooms` {Array} list of rooms to broadcast to 113 | /// 114 | /// @param {Object} packet object 115 | /// @api public 116 | @override 117 | void broadcast(Map packet, [Map? opts]) { 118 | opts = opts ?? {}; 119 | List rooms = opts['rooms'] ?? []; 120 | List except = opts['except'] ?? []; 121 | Map flags = opts['flags'] ?? {}; 122 | var packetOpts = { 123 | 'preEncoded': true, 124 | 'volatile': flags['volatile'], 125 | 'compress': flags['compress'] 126 | }; 127 | var ids = {}; 128 | var socket; 129 | 130 | packet['nsp'] = nsp.name; 131 | encoder.encode(packet, (encodedPackets) { 132 | if (rooms.isNotEmpty) { 133 | for (var i = 0; i < rooms.length; i++) { 134 | var room = this.rooms[rooms[i]]; 135 | if (room == null) continue; 136 | var sockets = room.sockets; 137 | for (var id in sockets.keys) { 138 | if (sockets.containsKey(id)) { 139 | if (ids[id] != null || except.contains(id)) continue; 140 | socket = nsp.connected[id]; 141 | if (socket != null) { 142 | socket.packet(encodedPackets, packetOpts); 143 | ids[id] = true; 144 | } 145 | } 146 | } 147 | } 148 | } else { 149 | for (var id in sids.keys) { 150 | if (except.contains(id)) continue; 151 | socket = nsp.connected[id]; 152 | if (socket != null) socket.packet(encodedPackets, packetOpts); 153 | } 154 | } 155 | }); 156 | } 157 | 158 | /// Gets a list of clients by sid. 159 | /// 160 | /// @param {Array} explicit set of rooms to check. 161 | /// @param {Function} callback 162 | /// @api public 163 | @override 164 | void clients( 165 | [List rooms = const [], dynamic Function([dynamic])? fn]) { 166 | var ids = {}; 167 | var sids = []; 168 | var socket; 169 | 170 | if (rooms.isNotEmpty) { 171 | for (var i = 0; i < rooms.length; i++) { 172 | var room = this.rooms[rooms[i]]; 173 | if (room == null) continue; 174 | var sockets = room.sockets; 175 | for (var id in sockets.keys) { 176 | if (sockets.containsKey(id)) { 177 | if (ids[id] != null) continue; 178 | socket = nsp.connected[id]; 179 | if (socket != null) { 180 | sids.add(id); 181 | ids[id] = true; 182 | } 183 | } 184 | } 185 | } 186 | } else { 187 | for (var id in this.sids.keys) { 188 | socket = nsp.connected[id]; 189 | if (socket != null) sids.add(id); 190 | } 191 | } 192 | 193 | if (fn != null) scheduleMicrotask(() => fn(sids)); 194 | } 195 | 196 | /// Gets the list of rooms a given client has joined. 197 | /// 198 | /// @param {String} socket id 199 | /// @param {Function} callback 200 | /// @api public 201 | @override 202 | void clientRooms(String id, [dynamic Function(dynamic, [dynamic])? fn]) { 203 | var rooms = sids[id]; 204 | if (fn != null) scheduleMicrotask(() => fn(null, rooms?.keys)); 205 | } 206 | } 207 | 208 | /// Room constructor. 209 | /// 210 | /// @api private 211 | class _Room { 212 | Map sockets = {}; 213 | int length = 0; 214 | 215 | /// Adds a socket to a room. 216 | /// 217 | /// @param {String} socket id 218 | /// @api private 219 | void add(String id) { 220 | if (!sockets.containsKey(id)) { 221 | sockets[id] = true; 222 | length++; 223 | } 224 | } 225 | 226 | /// Removes a socket from a room. 227 | /// 228 | /// @param {String} socket id 229 | /// @api private 230 | void del(String id) { 231 | if (sockets.containsKey(id)) { 232 | sockets.remove(id); 233 | length--; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /lib/src/client.dart: -------------------------------------------------------------------------------- 1 | /// client.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 22/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'package:logging/logging.dart'; 12 | 13 | import 'package:socket_io/src/engine/socket.dart'; 14 | import 'package:socket_io_common/src/parser/parser.dart'; 15 | import 'package:socket_io/src/server.dart'; 16 | 17 | class Client { 18 | Server server; 19 | Socket conn; 20 | dynamic id; 21 | dynamic request; 22 | Encoder encoder = Encoder(); 23 | Decoder decoder = Decoder(); 24 | List sockets = []; 25 | Map nsps = {}; 26 | List connectBuffer = []; 27 | final Logger _logger = Logger('socket_io:Client'); 28 | 29 | /// Client constructor. 30 | /// 31 | /// @param {Server} server instance 32 | /// @param {Socket} connection 33 | /// @api private 34 | Client(this.server, this.conn) 35 | : id = conn.id, 36 | request = conn.connect.request { 37 | setup(); 38 | } 39 | 40 | /// Sets up event listeners. 41 | /// 42 | /// @api private 43 | void setup() { 44 | decoder.on('decoded', ondecoded); 45 | conn.on('data', ondata); 46 | conn.on('error', onerror); 47 | conn.on('close', onclose); 48 | } 49 | 50 | /// Connects a client to a namespace. 51 | /// 52 | /// @param {String} namespace name 53 | /// @api private 54 | void connect(String name, [query]) { 55 | _logger.fine('connecting to namespace $name'); 56 | if (!server.nsps.containsKey(name)) { 57 | packet({ 58 | 'type': ERROR, 59 | 'nsp': name, 60 | 'data': 'Invalid namespace' 61 | }); 62 | return; 63 | } 64 | var nsp = server.of(name); 65 | if ('/' != name && !nsps.containsKey('/')) { 66 | connectBuffer.add(name); 67 | return; 68 | } 69 | 70 | var self = this; 71 | nsp.add(this, query, (socket) { 72 | self.sockets.add(socket); 73 | self.nsps[nsp.name] = socket; 74 | 75 | if ('/' == nsp.name && self.connectBuffer.isNotEmpty) { 76 | self.connectBuffer.forEach(self.connect); 77 | self.connectBuffer = []; 78 | } 79 | }); 80 | } 81 | 82 | /// Disconnects from all namespaces and closes transport. 83 | /// 84 | /// @api private 85 | void disconnect() { 86 | // we don't use a for loop because the length of 87 | // `sockets` changes upon each iteration 88 | sockets.toList().forEach((socket) { 89 | socket.disconnect(); 90 | }); 91 | sockets.clear(); 92 | 93 | close(); 94 | } 95 | 96 | /// Removes a socket. Called by each `Socket`. 97 | /// 98 | /// @api private 99 | void remove(socket) { 100 | var i = sockets.indexOf(socket); 101 | if (i >= 0) { 102 | var nsp = sockets[i].nsp.name; 103 | sockets.removeAt(i); 104 | nsps.remove(nsp); 105 | } else { 106 | _logger.fine('ignoring remove for ${socket.id}'); 107 | } 108 | } 109 | 110 | /// Closes the underlying connection. 111 | /// 112 | /// @api private 113 | void close() { 114 | if ('open' == conn.readyState) { 115 | _logger.fine('forcing transport close'); 116 | conn.close(); 117 | onclose('forced server close'); 118 | } 119 | } 120 | 121 | /// Writes a packet to the transport. 122 | /// 123 | /// @param {Object} packet object 124 | /// @param {Object} options 125 | /// @api private 126 | void packet(packet, [Map? opts]) { 127 | var self = this; 128 | opts ??= {}; 129 | // this writes to the actual connection 130 | void writeToEngine(encodedPackets) { 131 | if (opts!['volatile'] != null && self.conn.transport.writable != true) { 132 | return; 133 | } 134 | for (var i = 0; i < encodedPackets.length; i++) { 135 | self.conn.write(encodedPackets[i], {'compress': opts['compress']}); 136 | } 137 | } 138 | 139 | if ('open' == conn.readyState) { 140 | _logger.fine('writing packet $packet'); 141 | if (opts['preEncoded'] != true) { 142 | // not broadcasting, need to encode 143 | encoder.encode(packet, (encodedPackets) { 144 | // encode, then write results to engine 145 | writeToEngine(encodedPackets); 146 | }); 147 | } else { 148 | // a broadcast pre-encodes a packet 149 | writeToEngine(packet); 150 | } 151 | } else { 152 | _logger.fine('ignoring packet write $packet'); 153 | } 154 | } 155 | 156 | /// Called with incoming transport data. 157 | /// 158 | /// @api private 159 | void ondata(data) { 160 | // try/catch is needed for protocol violations (GH-1880) 161 | try { 162 | decoder.add(data); 163 | } catch (e, st) { 164 | _logger.severe(e, st); 165 | onerror(e); 166 | } 167 | } 168 | 169 | /// Called when parser fully decodes a packet. 170 | /// 171 | /// @api private 172 | void ondecoded(packet) { 173 | if (CONNECT == packet['type']) { 174 | final nsp = packet['nsp']; 175 | final uri = Uri.parse(nsp); 176 | connect(uri.path, uri.queryParameters); 177 | } else { 178 | var socket = nsps[packet['nsp']]; 179 | if (socket != null) { 180 | socket.onpacket(packet); 181 | } else { 182 | _logger.fine('no socket for namespace packet.nsp'); 183 | } 184 | } 185 | } 186 | 187 | /// Handles an error. 188 | /// 189 | /// @param {Objcet} error object 190 | /// @api private 191 | void onerror(err) { 192 | sockets.forEach((socket) { 193 | socket.onerror(err); 194 | }); 195 | onclose('client error'); 196 | } 197 | 198 | /// Called upon transport close. 199 | /// 200 | /// @param {String} reason 201 | /// @api private 202 | void onclose(reason) { 203 | _logger.fine('client close with reason $reason'); 204 | 205 | // ignore a potential subsequent `close` event 206 | destroy(); 207 | 208 | // `nsps` and `sockets` are cleaned up seamlessly 209 | if (sockets.isNotEmpty) { 210 | List.from(sockets).forEach((socket) { 211 | socket.onclose(reason); 212 | }); 213 | sockets.clear(); 214 | } 215 | decoder.destroy(); // clean up decoder 216 | } 217 | 218 | /// Cleans up event listeners. 219 | /// 220 | /// @api private 221 | void destroy() { 222 | conn.off('data', ondata); 223 | conn.off('error', onerror); 224 | conn.off('close', onclose); 225 | decoder.off('decoded', ondecoded); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /lib/src/engine/connect.dart: -------------------------------------------------------------------------------- 1 | /// connect.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 06/03/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'package:stream/stream.dart'; 12 | import 'dart:async'; 13 | import 'dart:io'; 14 | 15 | class SocketConnect extends HttpConnectWrapper { 16 | WebSocket? _socket; 17 | Completer? _done; 18 | bool? _completed; 19 | SocketConnect(HttpConnect origin) : super(origin); 20 | 21 | SocketConnect.fromWebSocket(HttpConnect origin, WebSocket socket) 22 | : super(origin) { 23 | _socket = socket; 24 | } 25 | 26 | bool isUpgradeRequest() => _socket != null; 27 | 28 | WebSocket? get websocket => _socket; 29 | 30 | Future get done { 31 | if (_completed == true) { 32 | return Future.value('done'); 33 | } 34 | if (_socket != null) { 35 | return _socket!.done; 36 | } else { 37 | _done = Completer(); 38 | return _done!.future; 39 | } 40 | } 41 | 42 | /// Closes the current connection. 43 | void close() { 44 | if (_done != null) { 45 | _done!.complete('done'); 46 | } else if (_socket != null) { 47 | _socket!.close(); 48 | } else { 49 | _completed = true; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/engine/engine.dart: -------------------------------------------------------------------------------- 1 | /// engine.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 16/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'package:socket_io/src/engine/server.dart'; 12 | import 'package:socket_io/src/util/event_emitter.dart'; 13 | 14 | class Engine extends EventEmitter { 15 | static Engine attach(server, [Map? options]) { 16 | var engine = Server(options); 17 | engine.attachTo(server, options); 18 | return engine; 19 | } 20 | 21 | dynamic operator [](Object key) {} 22 | 23 | /// Associates the [key] with the given [value]. 24 | /// 25 | /// If the key was already in the map, its associated value is changed. 26 | /// Otherwise the key-value pair is added to the map. 27 | void operator []=(String key, dynamic value) {} 28 | // init() {} 29 | // upgrades() {} 30 | // verify() {} 31 | // prepare() {} 32 | void close() {} 33 | // handleRequest() {} 34 | // handshake() {} 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/engine/server.dart: -------------------------------------------------------------------------------- 1 | /// server 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 17/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:convert'; 12 | import 'dart:io' hide Socket; 13 | import 'package:logging/logging.dart'; 14 | import 'package:socket_io/src/engine/connect.dart'; 15 | import 'package:socket_io/src/engine/engine.dart'; 16 | import 'package:socket_io/src/engine/socket.dart'; 17 | import 'package:socket_io/src/engine/transport/transports.dart'; 18 | import 'package:stream/stream.dart'; 19 | import 'package:uuid/uuid.dart'; 20 | 21 | /// Server constructor. 22 | /// 23 | /// @param {Object} options 24 | /// @api public 25 | class ServerErrors { 26 | static const int UNKNOWN_TRANSPORT = 0; 27 | static const int UNKNOWN_SID = 1; 28 | static const int BAD_HANDSHAKE_METHOD = 2; 29 | static const int BAD_REQUEST = 3; 30 | static const int FORBIDDEN = 4; 31 | } 32 | 33 | const Map ServerErrorMessages = { 34 | 0: 'Transport unknown', 35 | 1: 'Session ID unknown', 36 | 2: 'Bad handshake method', 37 | 3: 'Bad request', 38 | 4: 'Forbidden' 39 | }; 40 | 41 | class Server extends Engine { 42 | static final Logger _logger = Logger('socket_io:engine.Server'); 43 | Map clients = {}; 44 | int clientsCount = 0; 45 | late int pingTimeout; 46 | late int pingInterval; 47 | late int upgradeTimeout; 48 | late double maxHttpBufferSize; 49 | List transports = ['polling', 'websocket']; 50 | late bool allowUpgrades; 51 | Function? allowRequest; 52 | late dynamic cookie; 53 | late dynamic cookiePath; 54 | late bool cookieHttpOnly; 55 | late Map perMessageDeflate; 56 | late Map httpCompression; 57 | dynamic initialPacket; 58 | final Uuid _uuid = Uuid(); 59 | 60 | Server([Map? opts]) { 61 | opts = opts ?? {}; 62 | 63 | pingTimeout = opts['pingTimeout'] ?? 60000; 64 | pingInterval = opts['pingInterval'] ?? 25000; 65 | upgradeTimeout = opts['upgradeTimeout'] ?? 10000; 66 | maxHttpBufferSize = opts['maxHttpBufferSize'] ?? 10E7; 67 | allowUpgrades = false != opts['allowUpgrades']; 68 | allowRequest = opts['allowRequest']; 69 | cookie = opts['cookie'] == false 70 | ? false 71 | : opts['cookie'] ?? 72 | 'io'; //false != opts.cookie ? (opts.cookie || 'io') : false; 73 | cookiePath = opts['cookiePath'] == false 74 | ? false 75 | : opts['cookiePath'] ?? 76 | '/'; //false != opts.cookiePath ? (opts.cookiePath || '/') : false; 77 | cookieHttpOnly = opts['cookieHttpOnly'] != false; 78 | 79 | if (!opts.containsKey('perMessageDeflate') || 80 | opts['perMessageDeflate'] == true) { 81 | perMessageDeflate = 82 | opts['perMessageDeflate'] is Map ? opts['perMessageDeflate'] : {}; 83 | if (!perMessageDeflate.containsKey('threshold')) { 84 | perMessageDeflate['threshold'] = 1024; 85 | } 86 | } 87 | httpCompression = opts['httpCompression'] ?? {}; 88 | if (!httpCompression.containsKey('threshold')) { 89 | httpCompression['threshold'] = 1024; 90 | } 91 | 92 | initialPacket = opts['initialPacket']; 93 | _init(); 94 | } 95 | 96 | /// Initialize websocket server 97 | /// 98 | /// @api private 99 | 100 | void _init() { 101 | // if (this.transports.indexOf('websocket') == -1) return; 102 | 103 | // if (this.ws) this.ws.close(); 104 | // 105 | // var wsModule; 106 | // try { 107 | // wsModule = require(this.wsEngine); 108 | // } catch (ex) { 109 | // this.wsEngine = 'ws'; 110 | // // keep require('ws') as separate expression for packers (browserify, etc) 111 | // wsModule = require('ws'); 112 | // } 113 | // this.ws = new wsModule.Server({ 114 | // noServer: true, 115 | // clientTracking: false, 116 | // perMessageDeflate: this.perMessageDeflate, 117 | // maxPayload: this.maxHttpBufferSize 118 | // }); 119 | } 120 | 121 | /// Returns a list of available transports for upgrade given a certain transport. 122 | /// 123 | /// @return {Array} 124 | /// @api public 125 | 126 | List upgrades(String transport) { 127 | if (!allowUpgrades) return List.empty(); 128 | return Transports.upgradesTo(transport); 129 | } 130 | 131 | /// Verifies a request. 132 | /// 133 | /// @param {http.IncomingMessage} 134 | /// @return {Boolean} whether the request is valid 135 | /// @api private 136 | 137 | void verify(SocketConnect connect, bool upgrade, fn) { 138 | // transport check 139 | var req = connect.request; 140 | var transport = req.uri.queryParameters['transport']; 141 | if (!transports.contains(transport)) { 142 | _logger.fine('unknown transport "$transport"'); 143 | return fn(ServerErrors.UNKNOWN_TRANSPORT, false); 144 | } 145 | 146 | // sid check 147 | var sid = req.uri.queryParameters['sid']; 148 | if (sid != null) { 149 | if (!clients.containsKey(sid)) { 150 | return fn(ServerErrors.UNKNOWN_SID, false); 151 | } 152 | if (!upgrade && clients[sid].transport.name != transport) { 153 | _logger.fine('bad request: unexpected transport without upgrade'); 154 | return fn(ServerErrors.BAD_REQUEST, false); 155 | } 156 | } else { 157 | // handshake is GET only 158 | if ('GET' != req.method) { 159 | return fn(ServerErrors.BAD_HANDSHAKE_METHOD, false); 160 | } 161 | if (allowRequest == null) return fn(null, true); 162 | return allowRequest!(req, fn); 163 | } 164 | 165 | fn(null, true); 166 | } 167 | 168 | /// Closes all clients. 169 | /// 170 | /// @api public 171 | 172 | @override 173 | void close() { 174 | _logger.fine('closing all open clients'); 175 | for (var key in clients.keys.toList(growable: false)) { 176 | if (clients[key] != null) { 177 | clients[key].close(true); 178 | } 179 | } 180 | // if (this.ws) { 181 | // _logger.fine('closing webSocketServer'); 182 | // this.ws.close(); 183 | // // don't delete this.ws because it can be used again if the http server starts listening again 184 | // } 185 | } 186 | 187 | /// Handles an Engine.IO HTTP request. 188 | /// 189 | /// @param {http.IncomingMessage} request 190 | /// @param {http.ServerResponse|http.OutgoingMessage} response 191 | /// @api public 192 | 193 | void handleRequest(SocketConnect connect) { 194 | var req = connect.request; 195 | _logger.fine('handling ${req.method} http request ${req.uri.path}'); 196 | // this.prepare(req); 197 | // req.res = res; 198 | 199 | var self = this; 200 | verify(connect, false, (err, success) { 201 | if (!success) { 202 | sendErrorMessage(req, err); 203 | return; 204 | } 205 | //print('sid ${req.uri.queryParameters['sid']}'); 206 | if (req.uri.queryParameters['sid'] != null) { 207 | _logger.fine('setting new request for existing client'); 208 | self.clients[req.uri.queryParameters['sid']].transport 209 | .onRequest(connect); 210 | } else { 211 | self.handshake(req.uri.queryParameters['transport'] as String, connect); 212 | } 213 | }); 214 | } 215 | 216 | /// Sends an Engine.IO Error Message 217 | /// 218 | /// @param {http.ServerResponse} response 219 | /// @param {code} error code 220 | /// @api private 221 | 222 | static void sendErrorMessage(HttpRequest req, code) { 223 | var res = req.response; 224 | var isForbidden = !ServerErrorMessages.containsKey(code); 225 | if (isForbidden) { 226 | res.statusCode = HttpStatus.forbidden; 227 | res.headers.contentType = ContentType.json; 228 | res.write(json.encode({ 229 | 'code': ServerErrors.FORBIDDEN, 230 | 'message': code ?? ServerErrorMessages[ServerErrors.FORBIDDEN] 231 | })); 232 | return; 233 | } 234 | if (req.headers.value('origin') != null) { 235 | res.headers.add('Access-Control-Allow-Credentials', 'true'); 236 | res.headers 237 | .add('Access-Control-Allow-Origin', req.headers.value('origin')!); 238 | } else { 239 | res.headers.add('Access-Control-Allow-Origin', '*'); 240 | } 241 | res.statusCode = HttpStatus.badRequest; 242 | res.write( 243 | json.encode({'code': code, 'message': ServerErrorMessages[code]})); 244 | } 245 | 246 | /// generate a socket id. 247 | /// Overwrite this method to generate your custom socket id 248 | /// 249 | /// @param {Object} request object 250 | /// @api public 251 | String generateId(SocketConnect connect) { 252 | return _uuid.v1().replaceAll('-', ''); 253 | } 254 | 255 | /// Handshakes a new client. 256 | /// 257 | /// @param {String} transport name 258 | /// @param {Object} request object 259 | /// @api private 260 | void handshake(String transportName, SocketConnect connect) { 261 | var id = generateId(connect); 262 | 263 | _logger.fine('handshaking client $id'); 264 | var transport; 265 | var req = connect.request; 266 | try { 267 | transport = Transports.newInstance(transportName, connect); 268 | if ('polling' == transportName) { 269 | transport.maxHttpBufferSize = maxHttpBufferSize; 270 | transport.httpCompression = httpCompression; 271 | } else if ('websocket' == transportName) { 272 | transport.perMessageDeflate = perMessageDeflate; 273 | } 274 | 275 | if (req.uri.hasQuery && req.uri.queryParameters.containsKey('b64')) { 276 | transport.supportsBinary = false; 277 | } else { 278 | transport.supportsBinary = true; 279 | } 280 | } catch (e) { 281 | sendErrorMessage(req, ServerErrors.BAD_REQUEST); 282 | return; 283 | } 284 | var socket = Socket(id, this, transport, connect); 285 | 286 | if (cookie?.isNotEmpty == true) { 287 | transport.on('headers', (headers) { 288 | headers['Set-Cookie'] = '$cookie=${Uri.encodeComponent(id)}' + 289 | (cookiePath?.isNotEmpty == true ? '; Path=$cookiePath' : '') + 290 | (cookiePath?.isNotEmpty == true && cookieHttpOnly == true 291 | ? '; HttpOnly' 292 | : ''); 293 | }); 294 | } 295 | 296 | transport.onRequest(connect); 297 | 298 | clients[id] = socket; 299 | clientsCount++; 300 | 301 | socket.once('close', (_) { 302 | clients.remove(id); 303 | clientsCount--; 304 | }); 305 | 306 | emit('connection', socket); 307 | } 308 | 309 | /// Handles an Engine.IO HTTP Upgrade. 310 | /// 311 | /// @api public 312 | void handleUpgrade(SocketConnect connect) { 313 | // this.prepare(req); 314 | 315 | verify(connect, true, (err, success) { 316 | if (!success) { 317 | abortConnection(connect, err); 318 | return; 319 | } 320 | 321 | // var head = new Buffer(upgradeHead.length); 322 | // upgradeHead.copy(head); 323 | // upgradeHead = null; 324 | 325 | // delegate to ws 326 | // self.ws.handleUpgrade(req, socket, head, function (conn) { 327 | onWebSocket(connect); 328 | // }); 329 | }); 330 | } 331 | 332 | /// Called upon a ws.io connection. 333 | /// 334 | /// @param {ws.Socket} websocket 335 | /// @api private 336 | 337 | void onWebSocket(SocketConnect connect) { 338 | // socket.listen((_) {}, 339 | // onError: () => _logger.fine('websocket error before upgrade')); 340 | 341 | // if (!transports[req._query.transport].handlesUpgrades) { 342 | // _logger.fine('transport doesnt handle upgraded requests'); 343 | // socket.close(); 344 | // return; 345 | // } 346 | if (connect.request.connectionInfo == null) { 347 | _logger.fine('WebSocket connection closed: ${connect.request.uri.path}'); 348 | return; 349 | } 350 | // get client id 351 | var id = connect.request.uri.queryParameters['sid']; 352 | 353 | // keep a reference to the ws.Socket 354 | // req.websocket = socket; 355 | 356 | if (id != null) { 357 | var client = clients[id]; 358 | if (client == null) { 359 | _logger.fine('upgrade attempt for closed client'); 360 | connect.websocket?.close(); 361 | } else if (client.upgrading == true) { 362 | _logger.fine('transport has already been trying to upgrade'); 363 | connect.websocket?.close(); 364 | } else if (client.upgraded == true) { 365 | _logger.fine('transport had already been upgraded'); 366 | connect.websocket?.close(); 367 | } else { 368 | _logger.fine('upgrading existing transport'); 369 | var req = connect.request; 370 | var transport = Transports.newInstance( 371 | req.uri.queryParameters['transport'] as String, connect); 372 | // ignore: unrelated_type_equality_checks 373 | if (req.uri.queryParameters['b64'] == true) { 374 | transport.supportsBinary = false; 375 | } else { 376 | transport.supportsBinary = true; 377 | } 378 | transport.perMessageDeflate = perMessageDeflate; 379 | client.maybeUpgrade(transport); 380 | } 381 | } else { 382 | handshake( 383 | connect.request.uri.queryParameters['transport'] as String, connect); 384 | } 385 | } 386 | 387 | /// Captures upgrade requests for a http.Server. 388 | /// 389 | /// @param {http.Server} server 390 | /// @param {Object} options 391 | /// @api public 392 | void attachTo(StreamServer server, Map? options) { 393 | options = options ?? {}; 394 | var path = 395 | (options['path'] ?? '/engine.io').replaceFirst(RegExp(r'\/$'), ''); 396 | 397 | // normalize path 398 | path += '/'; 399 | 400 | // cache and clean up listeners 401 | server.map('$path.*', (HttpConnect connect) async { 402 | var req = connect.request; 403 | 404 | _logger.fine('intercepting request for path "$path"'); 405 | if (WebSocketTransformer.isUpgradeRequest(req) && 406 | transports.contains('websocket')) { 407 | // print('init websocket... ${req.uri}'); 408 | var socket = await WebSocketTransformer.upgrade(req); 409 | var socketConnect = SocketConnect.fromWebSocket(connect, socket); 410 | socketConnect.dataset['options'] = options; 411 | handleUpgrade(socketConnect); 412 | return socketConnect.done; 413 | } else { 414 | var socketConnect = SocketConnect(connect); 415 | socketConnect.dataset['options'] = options; 416 | handleRequest(socketConnect); 417 | return socketConnect.done; 418 | } 419 | }, preceding: true); 420 | } 421 | 422 | /// Closes the connection 423 | /// 424 | /// @param {net.Socket} socket 425 | /// @param {code} error code 426 | /// @api private 427 | 428 | static void abortConnection(SocketConnect connect, code) { 429 | var socket = connect.websocket; 430 | if (socket?.readyState == HttpStatus.ok) { 431 | var message = ServerErrorMessages.containsKey(code) 432 | ? ServerErrorMessages[code] 433 | : code; 434 | var length = utf8.encode(message).length; 435 | socket!.add('HTTP/1.1 400 Bad Request\r\n' 436 | 'Connection: close\r\n' 437 | 'Content-type: text/html\r\n' 438 | 'Content-Length: $length\r\n' 439 | '\r\n' + 440 | message); 441 | } 442 | socket?.close(); 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /lib/src/engine/socket.dart: -------------------------------------------------------------------------------- 1 | /// socket.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 17/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:async'; 12 | import 'dart:convert'; 13 | import 'dart:io'; 14 | import 'package:logging/logging.dart'; 15 | import 'package:socket_io/src/engine/connect.dart'; 16 | import 'package:socket_io/src/engine/server.dart'; 17 | import 'package:socket_io/src/engine/transport/transports.dart'; 18 | import 'package:socket_io/src/util/event_emitter.dart'; 19 | 20 | /// Client class (abstract). 21 | /// 22 | /// @api private 23 | class Socket extends EventEmitter { 24 | static final Logger _logger = Logger('socket_io:engine.Socket'); 25 | String id; 26 | Server server; 27 | Transport transport; 28 | bool upgrading = false; 29 | bool upgraded = false; 30 | String readyState = 'opening'; 31 | List writeBuffer = []; 32 | List packetsFn = []; 33 | List sentCallbackFn = []; 34 | List cleanupFn = []; 35 | SocketConnect connect; 36 | late InternetAddress remoteAddress; 37 | Timer? checkIntervalTimer; 38 | Timer? upgradeTimeoutTimer; 39 | Timer? pingTimeoutTimer; 40 | 41 | Socket(this.id, this.server, this.transport, this.connect) { 42 | // Cache IP since it might not be in the req later 43 | remoteAddress = connect.request.connectionInfo!.remoteAddress; 44 | 45 | checkIntervalTimer = null; 46 | upgradeTimeoutTimer = null; 47 | pingTimeoutTimer = null; 48 | 49 | setTransport(transport); 50 | onOpen(); 51 | } 52 | 53 | /// Called upon transport considered open. 54 | /// 55 | /// @api private 56 | 57 | void onOpen() { 58 | readyState = 'open'; 59 | 60 | // sends an `open` packet 61 | transport.sid = id; 62 | sendPacket('open', 63 | data: json.encode({ 64 | 'sid': id, 65 | 'upgrades': getAvailableUpgrades(), 66 | 'pingInterval': server.pingInterval, 67 | 'pingTimeout': server.pingTimeout 68 | })); 69 | 70 | // if (this.server.initialPacket != null) { 71 | // this.sendPacket('message', data: this.server.initialPacket); 72 | // } 73 | 74 | emit('open'); 75 | setPingTimeout(); 76 | } 77 | 78 | /// Called upon transport packet. 79 | /// 80 | /// @param {Object} packet 81 | /// @api private 82 | 83 | void onPacket(packet) { 84 | if ('open' == readyState) { 85 | // export packet event 86 | _logger.fine('packet'); 87 | emit('packet', packet); 88 | 89 | // Reset ping timeout on any packet, incoming data is a good sign of 90 | // other side's liveness 91 | setPingTimeout(); 92 | switch (packet['type']) { 93 | case 'ping': 94 | _logger.fine('got ping'); 95 | sendPacket('pong'); 96 | emit('heartbeat'); 97 | break; 98 | 99 | case 'error': 100 | onClose('parse error'); 101 | break; 102 | 103 | case 'message': 104 | var data = packet['data']; 105 | emit('data', data); 106 | emit('message', data); 107 | break; 108 | } 109 | } else { 110 | _logger.fine('packet received with closed socket'); 111 | } 112 | } 113 | 114 | /// Called upon transport error. 115 | /// 116 | /// @param {Error} error object 117 | /// @api private 118 | void onError(err) { 119 | _logger.fine('transport error'); 120 | onClose('transport error', err); 121 | } 122 | 123 | /// Sets and resets ping timeout timer based on client pings. 124 | /// 125 | /// @api private 126 | void setPingTimeout() { 127 | if (pingTimeoutTimer != null) { 128 | pingTimeoutTimer!.cancel(); 129 | } 130 | pingTimeoutTimer = Timer( 131 | Duration(milliseconds: server.pingInterval + server.pingTimeout), () { 132 | onClose('ping timeout'); 133 | }); 134 | } 135 | 136 | /// Attaches handlers for the given transport. 137 | /// 138 | /// @param {Transport} transport 139 | /// @api private 140 | void setTransport(Transport transport) { 141 | var onError = this.onError; 142 | var onPacket = this.onPacket; 143 | var flush = (_) => this.flush(); 144 | var onClose = (_) { 145 | this.onClose('transport close'); 146 | }; 147 | 148 | this.transport = transport; 149 | this.transport.once('error', onError); 150 | this.transport.on('packet', onPacket); 151 | this.transport.on('drain', flush); 152 | this.transport.once('close', onClose); 153 | // this function will manage packet events (also message callbacks) 154 | setupSendCallback(); 155 | 156 | cleanupFn.add(() { 157 | transport.off('error', onError); 158 | transport.off('packet', onPacket); 159 | transport.off('drain', flush); 160 | transport.off('close', onClose); 161 | }); 162 | } 163 | 164 | /// Upgrades socket to the given transport 165 | /// 166 | /// @param {Transport} transport 167 | /// @api private 168 | void maybeUpgrade(transport) { 169 | _logger.fine( 170 | 'might upgrade socket transport from ${this.transport.name} to ${transport.name}'); 171 | 172 | upgrading = true; 173 | var cleanupFn = {}; 174 | // set transport upgrade timer 175 | upgradeTimeoutTimer = 176 | Timer(Duration(milliseconds: server.upgradeTimeout), () { 177 | _logger.fine('client did not complete upgrade - closing transport'); 178 | cleanupFn['cleanup'](); 179 | if ('open' == transport.readyState) { 180 | transport.close(); 181 | } 182 | }); 183 | 184 | // we force a polling cycle to ensure a fast upgrade 185 | var check = () { 186 | if ('polling' == this.transport.name && this.transport.writable == true) { 187 | _logger.fine('writing a noop packet to polling for fast upgrade'); 188 | this.transport.send([ 189 | {'type': 'noop'} 190 | ]); 191 | } 192 | }; 193 | 194 | var onPacket = (packet) { 195 | if ('ping' == packet['type'] && 'probe' == packet['data']) { 196 | transport.send([ 197 | {'type': 'pong', 'data': 'probe'} 198 | ]); 199 | emit('upgrading', transport); 200 | if (checkIntervalTimer != null) { 201 | checkIntervalTimer!.cancel(); 202 | } 203 | checkIntervalTimer = 204 | Timer.periodic(Duration(milliseconds: 100), (_) => check()); 205 | } else if ('upgrade' == packet['type'] && readyState != 'closed') { 206 | _logger.fine('got upgrade packet - upgrading'); 207 | cleanupFn['cleanup'](); 208 | this.transport.discard(); 209 | upgraded = true; 210 | clearTransport(); 211 | setTransport(transport); 212 | emit('upgrade', transport); 213 | setPingTimeout(); 214 | flush(); 215 | if (readyState == 'closing') { 216 | transport.close(() { 217 | this.onClose('forced close'); 218 | }); 219 | } 220 | } else { 221 | cleanupFn['cleanup'](); 222 | transport.close(); 223 | } 224 | }; 225 | 226 | var onError = (err) { 227 | _logger.fine('client did not complete upgrade - $err'); 228 | cleanupFn['cleanup'](); 229 | transport.close(); 230 | transport = null; 231 | }; 232 | 233 | var onTransportClose = (_) { 234 | onError('transport closed'); 235 | }; 236 | 237 | var onClose = (_) { 238 | onError('socket closed'); 239 | }; 240 | 241 | var cleanup = () { 242 | upgrading = false; 243 | checkIntervalTimer?.cancel(); 244 | checkIntervalTimer = null; 245 | 246 | upgradeTimeoutTimer?.cancel(); 247 | upgradeTimeoutTimer = null; 248 | 249 | transport.off('packet', onPacket); 250 | transport.off('close', onTransportClose); 251 | transport.off('error', onError); 252 | off('close', onClose); 253 | }; 254 | cleanupFn['cleanup'] = cleanup; // define it later 255 | transport.on('packet', onPacket); 256 | transport.once('close', onTransportClose); 257 | transport.once('error', onError); 258 | 259 | once('close', onClose); 260 | } 261 | 262 | /// Clears listeners and timers associated with current transport. 263 | /// 264 | /// @api private 265 | void clearTransport() { 266 | var cleanup; 267 | 268 | var toCleanUp = cleanupFn.length; 269 | 270 | for (var i = 0; i < toCleanUp; i++) { 271 | cleanup = cleanupFn.removeAt(0); 272 | cleanup(); 273 | } 274 | 275 | // silence further transport errors and prevent uncaught exceptions 276 | transport.on('error', (_) { 277 | _logger.fine('error triggered by discarded transport'); 278 | }); 279 | 280 | // ensure transport won't stay open 281 | transport.close(); 282 | 283 | pingTimeoutTimer?.cancel(); 284 | } 285 | 286 | /// Called upon transport considered closed. 287 | /// Possible reasons: `ping timeout`, `client error`, `parse error`, 288 | /// `transport error`, `server close`, `transport close` 289 | void onClose(reason, [description]) { 290 | if ('closed' != readyState) { 291 | readyState = 'closed'; 292 | pingTimeoutTimer?.cancel(); 293 | checkIntervalTimer?.cancel(); 294 | checkIntervalTimer = null; 295 | upgradeTimeoutTimer?.cancel(); 296 | 297 | // clean writeBuffer in next tick, so developers can still 298 | // grab the writeBuffer on 'close' event 299 | scheduleMicrotask(() { 300 | writeBuffer = []; 301 | }); 302 | packetsFn = []; 303 | sentCallbackFn = []; 304 | clearTransport(); 305 | emit('close', [reason, description]); 306 | } 307 | } 308 | 309 | /// Setup and manage send callback 310 | /// 311 | /// @api private 312 | void setupSendCallback() { 313 | // the message was sent successfully, execute the callback 314 | var onDrain = (_) { 315 | if (sentCallbackFn.isNotEmpty) { 316 | var seqFn = sentCallbackFn[0]; 317 | if (seqFn is Function) { 318 | _logger.fine('executing send callback'); 319 | seqFn(transport); 320 | } 321 | 322 | /// else if (Array.isArray(seqFn)) { 323 | /// _logger.fine('executing batch send callback'); 324 | /// for (var l = seqFn.length, i = 0; i < l; i++) { 325 | /// if ('function' === typeof seqFn[i]) { 326 | /// seqFn[i](self.transport); 327 | /// } 328 | /// } 329 | /// } 330 | } 331 | }; 332 | 333 | transport.on('drain', onDrain); 334 | 335 | cleanupFn.add(() { 336 | transport.off('drain', onDrain); 337 | }); 338 | } 339 | 340 | /// Sends a message packet. 341 | /// 342 | /// @param {String} message 343 | /// @param {Object} options 344 | /// @param {Function} callback 345 | /// @return {Socket} for chaining 346 | /// @api public 347 | void send(data, options, [callback]) => write(data, options, callback); 348 | Socket write(data, options, [callback]) { 349 | sendPacket('message', data: data, options: options, callback: callback); 350 | return this; 351 | } 352 | 353 | /// Sends a packet. 354 | /// 355 | /// @param {String} packet type 356 | /// @param {String} optional, data 357 | /// @param {Object} options 358 | /// @api private 359 | void sendPacket(type, {data, options, callback}) { 360 | options = options ?? {}; 361 | options['compress'] = false != options['compress']; 362 | 363 | if ('closing' != readyState && 'closed' != readyState) { 364 | // _logger.fine('sending packet "%s" (%s)', type, data); 365 | 366 | var packet = {'type': type, 'options': options}; 367 | if (data != null) packet['data'] = data; 368 | 369 | // exports packetCreate event 370 | emit('packetCreate', packet); 371 | 372 | writeBuffer.add(packet); 373 | 374 | // add send callback to object, if defined 375 | if (callback != null) packetsFn.add(callback); 376 | 377 | flush(); 378 | } 379 | } 380 | 381 | /// Attempts to flush the packets buffer. 382 | /// 383 | /// @api private 384 | void flush() { 385 | if ('closed' != readyState && 386 | transport.writable == true && 387 | writeBuffer.isNotEmpty) { 388 | _logger.fine('flushing buffer to transport'); 389 | emit('flush', writeBuffer); 390 | server.emit('flush', [this, writeBuffer]); 391 | var wbuf = writeBuffer; 392 | writeBuffer = []; 393 | if (transport.supportsFraming == false) { 394 | sentCallbackFn.add((_) => packetsFn.forEach((f) => f(_))); 395 | } else { 396 | sentCallbackFn.addAll(packetsFn); 397 | } 398 | packetsFn = []; 399 | transport.send(wbuf); 400 | emit('drain'); 401 | server.emit('drain', this); 402 | } 403 | } 404 | 405 | /// Get available upgrades for this socket. 406 | /// 407 | /// @api private 408 | List getAvailableUpgrades() { 409 | var availableUpgrades = []; 410 | var allUpgrades = server.upgrades(transport.name!); 411 | for (var i = 0, l = allUpgrades.length; i < l; ++i) { 412 | var upg = allUpgrades[i]; 413 | if (server.transports.contains(upg)) { 414 | availableUpgrades.add(upg); 415 | } 416 | } 417 | return availableUpgrades; 418 | } 419 | 420 | /// Closes the socket and underlying transport. 421 | /// 422 | /// @param {Boolean} optional, discard 423 | /// @return {Socket} for chaining 424 | /// @api public 425 | 426 | void close([discard = false]) { 427 | if ('open' != readyState) return; 428 | readyState = 'closing'; 429 | 430 | if (writeBuffer.isNotEmpty) { 431 | once('drain', (_) => closeTransport(discard)); 432 | return; 433 | } 434 | 435 | closeTransport(discard); 436 | } 437 | 438 | /// Closes the underlying transport. 439 | /// 440 | /// @param {Boolean} discard 441 | /// @api private 442 | void closeTransport(discard) { 443 | if (discard == true) transport.discard(); 444 | transport.close(() => onClose('forced close')); 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /lib/src/engine/transport/jsonp_transport.dart: -------------------------------------------------------------------------------- 1 | /// jsonp_transport.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 22/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:convert'; 12 | import 'package:socket_io/src/engine/connect.dart'; 13 | import 'package:socket_io/src/engine/transport/polling_transport.dart'; 14 | 15 | class JSONPTransport extends PollingTransport { 16 | late String head; 17 | late String foot; 18 | JSONPTransport(SocketConnect connect) : super(connect) { 19 | head = '___eio[' + 20 | (connect.request.uri.queryParameters['j'] ?? '') 21 | .replaceAll(RegExp('[^0-9]'), '') + 22 | ']('; 23 | foot = ');'; 24 | } 25 | 26 | /// Handles incoming data. 27 | /// Due to a bug in \n handling by browsers, we expect a escaped string. 28 | /// 29 | /// @api private 30 | @override 31 | void onData(data) { 32 | // we leverage the qs module so that we get built-in DoS protection 33 | // and the fast alternative to decodeURIComponent 34 | data = parse(data)['d']; 35 | if (data is String) { 36 | // client will send already escaped newlines as \\\\n and newlines as \\n 37 | // \\n must be replaced with \n and \\\\n with \\n 38 | data = data.replaceAllMapped(RegExp(r'(\\)?\\n'), (match) { 39 | throw UnimplementedError('Not implemented yet'); 40 | // print(match); 41 | // match 42 | // return slashes ? match : '\n'; 43 | }); 44 | super.onData(data.replaceAll(RegExp(r'\\\\n'), '\\n')); 45 | } 46 | } 47 | 48 | /// Performs the write. 49 | /// 50 | /// @api private 51 | @override 52 | void doWrite(data, options, [callback]) { 53 | // we must output valid javascript, not valid json 54 | // see: http://timelessrepo.com/json-isnt-a-javascript-subset 55 | var js = json 56 | .encode(data) 57 | .replaceAll(RegExp(r'\u2028'), '\\u2028') 58 | .replaceAll(RegExp(r'\u2029'), '\\u2029'); 59 | 60 | // prepare response 61 | data = head + js + foot; 62 | 63 | super.doWrite(data, options, callback); 64 | } 65 | 66 | static Map parse(String query) { 67 | var search = RegExp('([^&=]+)=?([^&]*)'); 68 | var result = {}; 69 | 70 | // Get rid off the beginning ? in query strings. 71 | if (query.startsWith('?')) query = query.substring(1); 72 | 73 | // A custom decoder. 74 | String decode(String s) => Uri.decodeComponent(s.replaceAll('+', ' ')); 75 | 76 | // Go through all the matches and build the result map. 77 | for (Match match in search.allMatches(query)) { 78 | result[decode(match.group(1)!)] = decode(match.group(2)!); 79 | } 80 | 81 | return result; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/engine/transport/polling_transport.dart: -------------------------------------------------------------------------------- 1 | /// polling_transport.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 22/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:async'; 12 | import 'dart:convert'; 13 | import 'dart:io'; 14 | import 'package:logging/logging.dart'; 15 | import 'package:socket_io/src/engine/connect.dart'; 16 | import 'package:socket_io_common/src/engine/parser/parser.dart'; 17 | import 'package:socket_io/src/engine/transport/transports.dart'; 18 | 19 | class PollingTransport extends Transport { 20 | @override 21 | bool get handlesUpgrades => false; 22 | 23 | @override 24 | bool get supportsFraming => false; 25 | 26 | static final Logger _logger = Logger('socket_io:transport.PollingTransport'); 27 | int closeTimeout = 30 * 1000; 28 | Function? shouldClose; 29 | SocketConnect? dataReq; 30 | PollingTransport(connect) : super(connect) { 31 | maxHttpBufferSize = null; 32 | httpCompression = null; 33 | name = 'polling'; 34 | } 35 | 36 | @override 37 | void onRequest(SocketConnect connect) { 38 | var res = connect.response; 39 | 40 | if ('GET' == connect.request.method) { 41 | onPollRequest(connect); 42 | } else if ('POST' == connect.request.method) { 43 | onDataRequest(connect); 44 | } else { 45 | res.statusCode = 500; 46 | res.close(); 47 | } 48 | } 49 | 50 | final Map _reqCleanups = {}; 51 | final Map _reqCloses = {}; 52 | 53 | /// The client sends a request awaiting for us to send data. 54 | /// 55 | /// @api private 56 | void onPollRequest(SocketConnect connect) { 57 | if (this.connect != null) { 58 | _logger.fine('request overlap'); 59 | // assert: this.res, '.req and .res should be (un)set together' 60 | onError('overlap from client'); 61 | this.connect!.response.statusCode = 500; 62 | this.connect!.close(); 63 | return; 64 | } 65 | 66 | _logger.fine('setting request'); 67 | 68 | this.connect = connect; 69 | 70 | var onClose = () { 71 | onError('poll connection closed prematurely'); 72 | }; 73 | 74 | var cleanup = () { 75 | _reqCloses.remove(connect); 76 | this.connect = null; 77 | }; 78 | 79 | _reqCleanups[connect] = cleanup; 80 | _reqCloses[connect] = onClose; 81 | 82 | writable = true; 83 | emit('drain'); 84 | 85 | // if we're still writable but had a pending close, trigger an empty send 86 | if (writable == true && shouldClose != null) { 87 | _logger.fine('triggering empty send to append close packet'); 88 | send([ 89 | {'type': 'noop'} 90 | ]); 91 | } 92 | } 93 | 94 | /// The client sends a request with data. 95 | /// 96 | /// @api private 97 | void onDataRequest(SocketConnect connect) { 98 | if (dataReq != null) { 99 | // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together' 100 | onError('data request overlap from client'); 101 | connect.response.statusCode = 500; 102 | connect.close(); 103 | return; 104 | } 105 | 106 | var isBinary = 'application/octet-stream' == 107 | connect.request.headers.value('content-type'); 108 | 109 | dataReq = connect; 110 | 111 | dynamic chunks = isBinary ? [0] : ''; 112 | var self = this; 113 | StreamSubscription? subscription; 114 | var cleanup = () { 115 | chunks = isBinary ? [0] : ''; 116 | if (subscription != null) { 117 | subscription.cancel(); 118 | } 119 | self.dataReq = null; 120 | }; 121 | 122 | var onData = (List data) { 123 | var contentLength; 124 | if (data is String) { 125 | chunks += data; 126 | contentLength = utf8.encode(chunks).length; 127 | } else { 128 | if (chunks is String) { 129 | chunks += String.fromCharCodes(data); 130 | } else { 131 | chunks.addAll(String.fromCharCodes(data) 132 | .split(',') 133 | .map((s) => int.parse(s)) 134 | .toList()); 135 | } 136 | contentLength = chunks.length; 137 | } 138 | 139 | if (contentLength > self.maxHttpBufferSize) { 140 | chunks = ''; 141 | connect.close(); 142 | } 143 | }; 144 | 145 | var onEnd = () { 146 | self.onData(chunks); 147 | 148 | var headers = {'Content-Type': 'text/html', 'Content-Length': 2}; 149 | 150 | var res = connect.response; 151 | 152 | res.statusCode = 200; 153 | 154 | res.headers.clear(); 155 | // text/html is required instead of text/plain to avoid an 156 | // unwanted download dialog on certain user-agents (GH-43) 157 | self.headers(connect, headers).forEach((key, value) { 158 | res.headers.set(key, value); 159 | }); 160 | res.write('ok'); 161 | connect.close(); 162 | cleanup(); 163 | }; 164 | 165 | subscription = connect.request.listen(onData, onDone: onEnd); 166 | if (!isBinary) { 167 | connect.response.headers.contentType = 168 | ContentType.text; // for encoding utf-8 169 | } 170 | } 171 | 172 | /// Processes the incoming data payload. 173 | /// 174 | /// @param {String} encoded payload 175 | /// @api private 176 | @override 177 | void onData(data) { 178 | _logger.fine('received "$data"'); 179 | if (messageHandler != null) { 180 | messageHandler!.handle(this, data); 181 | } else { 182 | var self = this; 183 | var callback = (packet, [foo, bar]) { 184 | if ('close' == packet['type']) { 185 | _logger.fine('got xhr close packet'); 186 | self.onClose(); 187 | return false; 188 | } 189 | 190 | self.onPacket(packet); 191 | return true; 192 | }; 193 | 194 | PacketParser.decodePayload(data, callback: callback); 195 | } 196 | } 197 | 198 | /// Overrides onClose. 199 | /// 200 | /// @api private 201 | @override 202 | void onClose() { 203 | if (writable == true) { 204 | // close pending poll request 205 | send([ 206 | {'type': 'noop'} 207 | ]); 208 | } 209 | super.onClose(); 210 | } 211 | 212 | /// Writes a packet payload. 213 | /// 214 | /// @param {Object} packet 215 | /// @api private 216 | @override 217 | void send(List packets) { 218 | writable = false; 219 | 220 | if (shouldClose != null) { 221 | _logger.fine('appending close packet to payload'); 222 | packets.add({'type': 'close'}); 223 | shouldClose!(); 224 | shouldClose = null; 225 | } 226 | 227 | var self = this; 228 | PacketParser.encodePayload(packets, supportsBinary: supportsBinary == true, 229 | callback: (data) { 230 | var compress = packets.any((packet) { 231 | var opt = packet['options']; 232 | return opt != null && opt['compress'] == true; 233 | }); 234 | self.write(data, {'compress': compress}); 235 | }); 236 | } 237 | 238 | /// Writes data as response to poll request. 239 | /// 240 | /// @param {String} data 241 | /// @param {Object} options 242 | /// @api private 243 | void write(data, [options]) { 244 | _logger.fine('writing "$data"'); 245 | doWrite(data, options, () { 246 | var fn = _reqCleanups.remove(connect); 247 | if (fn != null) fn(); 248 | }); 249 | } 250 | 251 | /// Performs the write. 252 | /// 253 | /// @api private 254 | void doWrite(data, options, [callback]) { 255 | var self = this; 256 | 257 | // explicit UTF-8 is required for pages not served under utf 258 | var isString = data is String; 259 | var contentType = 260 | isString ? 'text/plain; charset=UTF-8' : 'application/octet-stream'; 261 | 262 | final headers = {'Content-Type': contentType}; 263 | 264 | var respond = (data) { 265 | headers[HttpHeaders.contentLengthHeader] = 266 | data is String ? utf8.encode(data).length : data.length; 267 | var res = self.connect!.response; 268 | 269 | // If the status code is 101 (aka upgrade), then 270 | // we assume the WebSocket transport has already 271 | // sent the response and closed the socket 272 | if (res.statusCode != 101) { 273 | res.statusCode = 200; 274 | 275 | res.headers.clear(); // remove all default headers. 276 | this.headers(connect!, headers).forEach((k, v) { 277 | res.headers.set(k, v); 278 | }); 279 | try { 280 | if (data is String) { 281 | res.write(data); 282 | connect!.close(); 283 | } else { 284 | if (headers.containsKey(HttpHeaders.contentEncodingHeader)) { 285 | res.add(data); 286 | } else { 287 | res.write(String.fromCharCodes(data)); 288 | } 289 | connect!.close(); 290 | } 291 | } catch (e) { 292 | var fn = _reqCloses.remove(connect); 293 | if (fn != null) fn(); 294 | rethrow; 295 | } 296 | } 297 | callback(); 298 | }; 299 | 300 | if (httpCompression == null || options['compress'] != true) { 301 | respond(data); 302 | return; 303 | } 304 | 305 | var len = isString ? utf8.encode(data).length : data.length; 306 | if (len < httpCompression?['threshold']) { 307 | respond(data); 308 | return; 309 | } 310 | 311 | var encodings = 312 | connect!.request.headers.value(HttpHeaders.acceptEncodingHeader); 313 | var hasGzip = encodings!.contains('gzip'); 314 | if (!hasGzip && !encodings.contains('deflate')) { 315 | respond(data); 316 | return; 317 | } 318 | var encoding = hasGzip ? 'gzip' : 'deflate'; 319 | // this.compress(data, encoding, (err, data) { 320 | // if (err != null) { 321 | // self.req.response..statusCode = 500..close(); 322 | // callback(err); 323 | // return; 324 | // } 325 | 326 | headers[HttpHeaders.contentEncodingHeader] = encoding; 327 | respond(hasGzip 328 | ? gzip.encode(utf8.encode( 329 | data is List ? String.fromCharCodes(data as List) : data)) 330 | : data); 331 | // }); 332 | } 333 | 334 | /// Closes the transport. 335 | /// 336 | /// @api private 337 | @override 338 | void doClose([dynamic Function()? fn]) { 339 | _logger.fine('closing'); 340 | 341 | var self = this; 342 | Timer? closeTimeoutTimer; 343 | 344 | if (dataReq != null) { 345 | _logger.fine('aborting ongoing data request'); 346 | dataReq = null; 347 | } 348 | 349 | var onClose = () { 350 | if (closeTimeoutTimer != null) closeTimeoutTimer.cancel(); 351 | if (fn != null) fn(); 352 | self.onClose(); 353 | }; 354 | if (writable == true) { 355 | _logger.fine('transport writable - closing right away'); 356 | send([ 357 | {'type': 'close'} 358 | ]); 359 | onClose(); 360 | } else if (discarded) { 361 | _logger.fine('transport discarded - closing right away'); 362 | onClose(); 363 | } else { 364 | _logger.fine('transport not writable - buffering orderly close'); 365 | shouldClose = onClose; 366 | closeTimeoutTimer = Timer(Duration(milliseconds: closeTimeout), onClose); 367 | } 368 | } 369 | 370 | /// Returns headers for a response. 371 | /// 372 | /// @param {http.IncomingMessage} request 373 | /// @param {Object} extra headers 374 | /// @api private 375 | Map headers(SocketConnect connect, [Map? headers]) { 376 | headers = headers ?? {}; 377 | 378 | // prevent XSS warnings on IE 379 | // https://github.com/LearnBoost/socket.io/pull/1333 380 | var ua = connect.request.headers.value('user-agent'); 381 | if (ua != null && (ua.contains(';MSIE') || ua.contains('Trident/'))) { 382 | headers['X-XSS-Protection'] = '0'; 383 | } 384 | 385 | emit('headers', headers); 386 | return headers; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /lib/src/engine/transport/transports.dart: -------------------------------------------------------------------------------- 1 | /// transports.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 17/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'package:logging/logging.dart'; 12 | import 'package:socket_io/src/engine/connect.dart'; 13 | import 'package:socket_io_common/src/engine/parser/parser.dart'; 14 | import 'package:socket_io/src/engine/transport/jsonp_transport.dart'; 15 | import 'package:socket_io/src/engine/transport/websocket_transport.dart'; 16 | import 'package:socket_io/src/engine/transport/xhr_transport.dart'; 17 | import 'package:socket_io/src/util/event_emitter.dart'; 18 | 19 | class Transports { 20 | static List upgradesTo(String from) { 21 | if ('polling' == from) { 22 | return ['websocket']; 23 | } 24 | return []; 25 | } 26 | 27 | static Transport newInstance(String name, SocketConnect connect) { 28 | if ('websocket' == name) { 29 | return WebSocketTransport(connect); 30 | } else if ('polling' == name) { 31 | if (connect.request.uri.queryParameters.containsKey('j')) { 32 | return JSONPTransport(connect); 33 | } else { 34 | return XHRTransport(connect); 35 | } 36 | } else { 37 | throw UnsupportedError('Unknown transport $name'); 38 | } 39 | } 40 | } 41 | 42 | abstract class Transport extends EventEmitter { 43 | static final Logger _logger = Logger('socket_io:transport.Transport'); 44 | double? maxHttpBufferSize; 45 | Map? httpCompression; 46 | Map? perMessageDeflate; 47 | bool? supportsBinary; 48 | String? sid; 49 | String? name; 50 | bool? writable; 51 | String readyState = 'open'; 52 | bool discarded = false; 53 | SocketConnect? connect; 54 | MessageHandler? messageHandler; 55 | 56 | Transport(connect) { 57 | var options = connect.dataset['options']; 58 | if (options != null) { 59 | messageHandler = options.containsKey('messageHandlerFactory') 60 | ? options['messageHandlerFactory'](this, connect) 61 | : null; 62 | } 63 | } 64 | 65 | void discard() { 66 | discarded = true; 67 | } 68 | 69 | void onRequest(SocketConnect connect) { 70 | this.connect = connect; 71 | } 72 | 73 | void close([dynamic Function()? closeFn]) { 74 | if ('closed' == readyState || 'closing' == readyState) return; 75 | readyState = 'closing'; 76 | doClose(closeFn); 77 | } 78 | 79 | void doClose([dynamic Function()? callback]); 80 | 81 | void onError(msg, [desc]) { 82 | writable = false; 83 | if (hasListeners('error')) { 84 | emit('error', {'msg': msg, 'desc': desc, 'type': 'TransportError'}); 85 | } else { 86 | _logger.fine('ignored transport error $msg ($desc)'); 87 | } 88 | } 89 | 90 | void onPacket(Map packet) { 91 | emit('packet', packet); 92 | } 93 | 94 | void onData(data) { 95 | if (messageHandler != null) { 96 | messageHandler!.handle(this, data); 97 | } else { 98 | onPacket(PacketParser.decodePacket(data, utf8decode: true)); 99 | } 100 | } 101 | 102 | void onClose() { 103 | readyState = 'closed'; 104 | emit('close'); 105 | } 106 | 107 | void send(List data); 108 | 109 | bool get supportsFraming; 110 | bool get handlesUpgrades; 111 | } 112 | 113 | abstract class MessageHandler { 114 | void handle(Transport transport, /*String|List*/ message); 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/engine/transport/websocket_transport.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | /// websocket_transport.dart 4 | /// 5 | /// Purpose: 6 | /// 7 | /// Description: 8 | /// 9 | /// History: 10 | /// 22/02/2017, Created by jumperchen 11 | /// 12 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 13 | import 'package:logging/logging.dart'; 14 | import 'package:socket_io_common/src/engine/parser/parser.dart'; 15 | import 'package:socket_io/src/engine/transport/transports.dart'; 16 | 17 | class WebSocketTransport extends Transport { 18 | static final Logger _logger = 19 | Logger('socket_io:transport.WebSocketTransport'); 20 | @override 21 | bool get handlesUpgrades => true; 22 | @override 23 | bool get supportsFraming => true; 24 | StreamSubscription? subscription; 25 | WebSocketTransport(connect) : super(connect) { 26 | name = 'websocket'; 27 | this.connect = connect; 28 | subscription = 29 | connect.websocket.listen(onData, onError: onError, onDone: onClose); 30 | writable = true; 31 | } 32 | 33 | @override 34 | void send(List packets) { 35 | var send = (data, Map packet) { 36 | _logger.fine('writing "$data"'); 37 | 38 | // always creates a new object since ws modifies it 39 | // var opts = {}; 40 | // if (packet.options != null) { 41 | // opts['compress'] = packet.options['compress']; 42 | // } 43 | // 44 | // if (this.perMessageDeflate != null) { 45 | // var len = data is String ? UTF8.encode(data).length : data.length; 46 | // if (len < this.perMessageDeflate['threshold']) { 47 | // opts['compress'] = false; 48 | // } 49 | // } 50 | 51 | // this.writable = false; 52 | connect!.websocket?.add(data); 53 | }; 54 | 55 | // function onEnd (err) { 56 | // if (err) return self.onError('write error', err.stack); 57 | // self.writable = true; 58 | // self.emit('drain'); 59 | // } 60 | for (var i = 0; i < packets.length; i++) { 61 | var packet = packets[i]; 62 | PacketParser.encodePacket(packet, 63 | supportsBinary: supportsBinary, callback: (_) => send(_, packet)); 64 | } 65 | } 66 | 67 | @override 68 | void onClose() { 69 | super.onClose(); 70 | 71 | // workaround for https://github.com/dart-lang/sdk/issues/27414 72 | if (subscription != null) { 73 | subscription!.cancel(); 74 | subscription = null; 75 | } 76 | } 77 | 78 | @override 79 | void doClose([fn]) { 80 | connect!.websocket?.close(); 81 | if (fn != null) fn(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/engine/transport/xhr_transport.dart: -------------------------------------------------------------------------------- 1 | /// xhr_transport.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 22/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'package:socket_io/src/engine/connect.dart'; 12 | import 'package:socket_io/src/engine/transport/polling_transport.dart'; 13 | 14 | class XHRTransport extends PollingTransport { 15 | XHRTransport(SocketConnect connect) : super(connect); 16 | 17 | /// Overrides `onRequest` to handle `OPTIONS`.. 18 | /// 19 | /// @param {http.IncomingMessage} 20 | /// @api private 21 | @override 22 | void onRequest(SocketConnect connect) { 23 | var req = connect.request; 24 | if ('OPTIONS' == req.method) { 25 | var res = req.response; 26 | var headers = this.headers(connect); 27 | headers['Access-Control-Allow-Headers'] = 'Content-Type'; 28 | headers.forEach((key, value) { 29 | res.headers.set(key, value); 30 | }); 31 | res.statusCode = 200; 32 | 33 | connect.close(); 34 | } else { 35 | super.onRequest(connect); 36 | } 37 | } 38 | 39 | /// Returns headers for a response. 40 | /// 41 | /// @param {http.IncomingMessage} request 42 | /// @param {Object} extra headers 43 | /// @api private 44 | @override 45 | Map headers(SocketConnect connect, [Map? headers]) { 46 | headers = headers ?? {}; 47 | var req = connect.request; 48 | if (req.headers.value('origin') != null) { 49 | headers['Access-Control-Allow-Credentials'] = 'true'; 50 | headers['Access-Control-Allow-Origin'] = req.headers.value('origin'); 51 | } else { 52 | headers['Access-Control-Allow-Origin'] = '*'; 53 | } 54 | return super.headers(connect, headers); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/namespace.dart: -------------------------------------------------------------------------------- 1 | /// namespace.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 17/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:async'; 12 | import 'package:logging/logging.dart'; 13 | import 'package:socket_io/src/adapter/adapter.dart'; 14 | import 'package:socket_io/src/client.dart'; 15 | import 'package:socket_io_common/src/parser/parser.dart'; 16 | import 'package:socket_io/src/server.dart'; 17 | import 'package:socket_io/src/socket.dart'; 18 | import 'package:socket_io/src/util/event_emitter.dart'; 19 | 20 | /// Blacklisted events. 21 | 22 | List events = [ 23 | 'connect', // for symmetry with client 24 | 'connection', 'newListener' 25 | ]; 26 | 27 | /// Flags. 28 | List flags = ['json', 'volatile']; 29 | 30 | class Namespace extends EventEmitter { 31 | String name; 32 | Server server; 33 | List sockets = []; 34 | Map connected = {}; 35 | List fns = []; 36 | int ids = 0; 37 | List rooms = []; 38 | Map flags = {}; 39 | late Adapter adapter; 40 | final Logger _logger = Logger('socket_io:Namespace'); 41 | 42 | /// Namespace constructor. 43 | /// 44 | /// @param {Server} server instance 45 | /// @param {Socket} name 46 | /// @api private 47 | Namespace(this.server, this.name) { 48 | initAdapter(); 49 | } 50 | 51 | /// Initializes the `Adapter` for this nsp. 52 | /// Run upon changing adapter by `Server#adapter` 53 | /// in addition to the constructor. 54 | /// 55 | /// @api private 56 | void initAdapter() { 57 | adapter = Adapter.newInstance(server.adapter, this); 58 | } 59 | 60 | /// Sets up namespace middleware. 61 | /// 62 | /// @return {Namespace} self 63 | /// @api public 64 | Namespace use(fn) { 65 | fns.add(fn); 66 | return this; 67 | } 68 | 69 | /// Executes the middleware for an incoming client. 70 | /// 71 | /// @param {Socket} socket that will get added 72 | /// @param {Function} last fn call in the middleware 73 | /// @api private 74 | void run(socket, Function fn) { 75 | var fns = this.fns.sublist(0); 76 | if (fns.isEmpty) return fn(null); 77 | 78 | run0(0, fns, socket, fn); 79 | } 80 | 81 | //TODO: Figure out return type for this method 82 | static Object run0( 83 | int index, List fns, Socket socket, Function fn) { 84 | return fns[index](socket, (err) { 85 | // upon error, short-circuit 86 | if (err) return fn(err); 87 | 88 | // if no middleware left, summon callback 89 | if (fns.length <= index + 1) return fn(null); 90 | 91 | // go on to next 92 | return run0(index + 1, fns, socket, fn); 93 | }); 94 | } 95 | 96 | /// Targets a room when emitting. 97 | /// 98 | /// @param {String} name 99 | /// @return {Namespace} self 100 | /// @api public 101 | // in(String name) { 102 | // to(name); 103 | // } 104 | 105 | /// Targets a room when emitting. 106 | /// 107 | /// @param {String} name 108 | /// @return {Namespace} self 109 | /// @api public 110 | Namespace to(String name) { 111 | rooms = rooms.isNotEmpty == true ? rooms : []; 112 | if (!rooms.contains(name)) rooms.add(name); 113 | return this; 114 | } 115 | 116 | /// Adds a new client. 117 | /// 118 | /// @return {Socket} 119 | /// @api private 120 | Socket add(Client client, query, Function? fn) { 121 | _logger.fine('adding socket to nsp $name'); 122 | var socket = Socket(this, client, query); 123 | var self = this; 124 | run(socket, (err) { 125 | // don't use Timer.run() here 126 | scheduleMicrotask(() { 127 | if ('open' == client.conn.readyState) { 128 | if (err != null) return socket.error(err.data || err.message); 129 | 130 | // track socket 131 | self.sockets.add(socket); 132 | 133 | // it's paramount that the internal `onconnect` logic 134 | // fires before user-set events to prevent state order 135 | // violations (such as a disconnection before the connection 136 | // logic is complete) 137 | socket.onconnect(); 138 | if (fn != null) fn(socket); 139 | 140 | // fire user-set events 141 | self.emit('connect', socket); 142 | self.emit('connection', socket); 143 | } else { 144 | _logger.fine('next called after client was closed - ignoring socket'); 145 | } 146 | }); 147 | }); 148 | return socket; 149 | } 150 | 151 | /// Removes a client. Called by each `Socket`. 152 | /// 153 | /// @api private 154 | void remove(socket) { 155 | if (sockets.contains(socket)) { 156 | sockets.remove(socket); 157 | } else { 158 | _logger.fine('ignoring remove for ${socket.id}'); 159 | } 160 | } 161 | 162 | /// Emits to all clients. 163 | /// 164 | /// @api public 165 | @override 166 | void emit(String event, [dynamic argument]) { 167 | if (events.contains(event)) { 168 | super.emit(event, argument); 169 | } else { 170 | // @todo check how to handle it with Dart 171 | // if (hasBin(args)) { parserType = ParserType.binaryEvent; } // binary 172 | 173 | // ignore: omit_local_variable_types 174 | List data = argument == null ? [event] : [event, argument]; 175 | 176 | final packet = {'type': EVENT, 'data': data}; 177 | 178 | adapter.broadcast(packet, {'rooms': rooms, 'flags': flags}); 179 | 180 | rooms = []; 181 | flags = {}; 182 | } 183 | } 184 | 185 | /// Sends a `message` event to all clients. 186 | /// 187 | /// @return {Namespace} self 188 | /// @api public 189 | Namespace send([args]) { 190 | return write(args); 191 | } 192 | 193 | Namespace write([args]) { 194 | emit('message', args); 195 | return this; 196 | } 197 | 198 | /// Gets a list of clients. 199 | /// 200 | /// @return {Namespace} self 201 | /// @api public 202 | /// 203 | /// TODO: Fix this description or code. Add type parameters to [fn([_])] 204 | /// 205 | // ignore: use_function_type_syntax_for_parameters 206 | Namespace clients(fn([_])) { 207 | adapter.clients(rooms, fn); 208 | rooms = []; 209 | return this; 210 | } 211 | 212 | /// Sets the compress flag. 213 | /// 214 | /// @param {Boolean} if `true`, compresses the sending data 215 | /// @return {Namespace} self 216 | /// @api public 217 | Namespace compress(compress) { 218 | flags = flags.isEmpty ? flags : {}; 219 | flags['compress'] = compress; 220 | return this; 221 | } 222 | } 223 | 224 | /// Apply flags from `Socket`. 225 | // @todo 226 | //exports.flags.forEach(function(flag){ 227 | // Namespace.prototype.__defineGetter__(flag, function(){ 228 | // this.flags = this.flags || {}; 229 | // this.flags[flag] = true; 230 | // return this; 231 | // }); 232 | //}); 233 | -------------------------------------------------------------------------------- /lib/src/server.dart: -------------------------------------------------------------------------------- 1 | /// server.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 22/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:async'; 12 | import 'dart:io'; 13 | import 'package:logging/logging.dart'; 14 | import 'package:socket_io/src/client.dart'; 15 | import 'package:socket_io/src/engine/engine.dart'; 16 | import 'package:socket_io/src/namespace.dart'; 17 | import 'package:socket_io_common/src/parser/parser.dart'; 18 | import 'package:stream/stream.dart'; 19 | 20 | import 'namespace.dart'; 21 | 22 | /// Socket.IO client source. 23 | /// Old settings for backwards compatibility 24 | Map oldSettings = { 25 | 'transports': 'transports', 26 | 'heartbeat timeout': 'pingTimeout', 27 | 'heartbeat interval': 'pingInterval', 28 | 'destroy buffer size': 'maxHttpBufferSize' 29 | }; 30 | 31 | final Logger _logger = Logger('socket_io:Server'); 32 | 33 | class Server { 34 | // Namespaces 35 | Map nsps = {}; 36 | late Namespace sockets; 37 | dynamic _origins; 38 | bool? _serveClient; 39 | String? _path; 40 | String _adapter = 'default'; 41 | StreamServer? httpServer; 42 | Engine? engine; 43 | Encoder encoder = Encoder(); 44 | Future? _ready; 45 | 46 | /// Server is ready 47 | /// 48 | /// @return a Future that resolves to true whenever the server is ready 49 | /// @api public 50 | Future get ready => _ready ?? Future.value(false); 51 | 52 | /// Server's port 53 | /// 54 | /// @return the port number where the server is listening 55 | /// @api public 56 | int? get port { 57 | if (httpServer == null || httpServer!.channels.isEmpty) { 58 | return null; 59 | } 60 | return httpServer!.channels[0].port; 61 | } 62 | 63 | /// Server constructor. 64 | /// 65 | /// @param {http.Server|Number|Object} http server, port or options 66 | /// @param {Object} options 67 | /// @api public 68 | Server({server, Map? options}) { 69 | options ??= {}; 70 | path(options.containsKey('path') ? options['path'] : '/socket.io'); 71 | serveClient(false != options['serveClient']); 72 | adapter = options.containsKey('adapter') ? options['adapter'] : 'default'; 73 | origins(options.containsKey('origins') ? options['origins'] : '*:*'); 74 | sockets = of('/'); 75 | if (server != null) { 76 | _ready = Future(() async { 77 | await attach(server, options); 78 | return true; 79 | }); 80 | } else { 81 | _ready = Future.value(true); 82 | } 83 | } 84 | 85 | /// Server request verification function, that checks for allowed origins 86 | /// 87 | /// @param {http.IncomingMessage} request 88 | /// @param {Function} callback to be called with the result: `fn(err, success)` 89 | void checkRequest(HttpRequest req, [Function? fn]) { 90 | var origin = req.headers.value('origin') ?? req.headers.value('referer'); 91 | 92 | // file:// URLs produce a null Origin which can't be authorized via echo-back 93 | if (origin == null || origin.isEmpty) { 94 | origin = '*'; 95 | } 96 | 97 | if (origin.isNotEmpty && _origins is Function) { 98 | return _origins(origin, fn); 99 | } 100 | 101 | if (_origins.contains('*:*')) { 102 | return fn!(null, true); 103 | } 104 | 105 | if (origin.isNotEmpty) { 106 | try { 107 | var parts = Uri.parse(origin); 108 | var port = parts.port; 109 | var ok = _origins.indexOf(parts.host + ':' + port.toString()) >= 0 || 110 | _origins.indexOf(parts.host + ':*') >= 0 || 111 | _origins.indexOf('*:' + port.toString()) >= 0; 112 | 113 | return fn!(null, ok); 114 | } catch (ex) { 115 | print(ex); 116 | } 117 | } 118 | 119 | fn!(null, false); 120 | } 121 | 122 | /// Sets/gets whether client code is being served. 123 | /// 124 | /// @param {Boolean} whether to serve client code 125 | /// @return {Server|Boolean} self when setting or value when getting 126 | /// @api public 127 | dynamic serveClient([bool? v]) { 128 | if (v == null) { 129 | return _serveClient; 130 | } 131 | 132 | _serveClient = v; 133 | return this; 134 | } 135 | 136 | /// Backwards compatiblity. 137 | /// 138 | /// @api public 139 | Server set(String key, [val]) { 140 | if ('authorization' == key && val != null) { 141 | use((socket, next) { 142 | val(socket.request, (err, authorized) { 143 | if (err) { 144 | return next(Exception(err)); 145 | } 146 | ; 147 | if (!authorized) { 148 | return next(Exception('Not authorized')); 149 | } 150 | 151 | next(); 152 | }); 153 | }); 154 | } else if ('origins' == key && val != null) { 155 | origins(val); 156 | } else if ('resource' == key) { 157 | path(val); 158 | } else if (oldSettings[key] && engine![oldSettings[key]]) { 159 | engine![oldSettings[key]] = val; 160 | } else { 161 | _logger.severe('Option $key is not valid. Please refer to the README.'); 162 | } 163 | 164 | return this; 165 | } 166 | 167 | /// Sets the client serving path. 168 | /// 169 | /// @param {String} pathname 170 | /// @return {Server|String} self when setting or value when getting 171 | /// @api public 172 | dynamic path([String? v]) { 173 | if (v == null || v.isEmpty) return _path; 174 | _path = v.replaceFirst(RegExp(r'/\/$/'), ''); 175 | return this; 176 | } 177 | 178 | /// Sets the adapter for rooms. 179 | /// 180 | /// @param {Adapter} pathname 181 | /// @return {Server|Adapter} self when setting or value when getting 182 | /// @api public 183 | String get adapter => _adapter; 184 | 185 | set adapter(String v) { 186 | _adapter = v; 187 | if (nsps.isNotEmpty) { 188 | nsps.forEach((String i, Namespace nsp) { 189 | nsp.initAdapter(); 190 | }); 191 | } 192 | } 193 | 194 | /// Sets the allowed origins for requests. 195 | /// 196 | /// @param {String} origins 197 | /// @return {Server|Adapter} self when setting or value when getting 198 | /// @api public 199 | dynamic origins([String? v]) { 200 | if (v == null || v.isEmpty) return _origins; 201 | 202 | _origins = v; 203 | return this; 204 | } 205 | 206 | /// Attaches socket.io to a server or port. 207 | /// 208 | /// @param {http.Server|Number} server or port 209 | /// @param {Object} options passed to engine.io 210 | /// @return {Server} self 211 | /// @api public 212 | Future listen(srv, [Map? opts]) async { 213 | await attach(srv, opts); 214 | } 215 | 216 | /// Attaches socket.io to a server or port. 217 | /// 218 | /// @param {http.Server|Number} server or port 219 | /// @param {Object} options passed to engine.io 220 | /// @return {Server} self 221 | /// @api public 222 | Future attach(dynamic srv, [Map? opts]) async { 223 | if (srv is Function) { 224 | var msg = 'You are trying to attach socket.io to an express ' 225 | 'request handler function. Please pass a http.Server instance.'; 226 | throw Exception(msg); 227 | } 228 | 229 | // handle a port as a string 230 | if (srv is String && int.parse(srv.toString()).toString() == srv) { 231 | srv = int.parse(srv.toString()); 232 | } 233 | 234 | opts ??= {}; 235 | 236 | // set engine.io path to `/socket.io` 237 | if (!opts.containsKey('path')) { 238 | opts['path'] = path(); 239 | } 240 | // set origins verification 241 | opts['allowRequest'] = checkRequest; 242 | 243 | if (srv is num) { 244 | _logger.fine('creating http server and binding to $srv'); 245 | var port = srv.toInt(); 246 | var server = StreamServer(); 247 | await server.start(port: port); 248 | // HttpServer.bind(InternetAddress.ANY_IP_V4, port).then(( 249 | // HttpServer server) { 250 | // this.httpServer = server; 251 | //// server.listen((HttpRequest request) { 252 | //// HttpResponse response = request.response; 253 | //// response.statusCode = HttpStatus.NOT_FOUND; 254 | //// response.close(); 255 | //// }); 256 | 257 | var completer = Completer(); 258 | var connectPacket = {'type': CONNECT, 'nsp': '/'}; 259 | encoder.encode(connectPacket, (encodedPacket) { 260 | // the CONNECT packet will be merged with Engine.IO handshake, 261 | // to reduce the number of round trips 262 | opts!['initialPacket'] = encodedPacket; 263 | 264 | _logger.fine('creating engine.io instance with opts $opts'); 265 | // initialize engine 266 | engine = Engine.attach(server, opts); 267 | 268 | // attach static file serving 269 | // if (self._serveClient) self.attachServe(srv); 270 | 271 | // Export http server 272 | httpServer = server; 273 | 274 | // bind to engine events 275 | bind(engine!); 276 | 277 | completer.complete(); 278 | }); 279 | await completer.future; 280 | // }); 281 | } else { 282 | var connectPacket = {'type': CONNECT, 'nsp': '/'}; 283 | encoder.encode(connectPacket, (encodedPacket) { 284 | // the CONNECT packet will be merged with Engine.IO handshake, 285 | // to reduce the number of round trips 286 | opts!['initialPacket'] = encodedPacket; 287 | 288 | _logger.fine('creating engine.io instance with opts $opts'); 289 | // initialize engine 290 | engine = Engine.attach(srv, opts); 291 | 292 | // attach static file serving 293 | // if (self._serveClient) self.attachServe(srv); 294 | 295 | // Export http server 296 | httpServer = srv; 297 | 298 | // bind to engine events 299 | bind(engine!); 300 | }); 301 | } 302 | 303 | return this; 304 | } 305 | 306 | /// Attaches the static file serving. 307 | /// 308 | /// @param {Function|http.Server} http server 309 | /// @api private 310 | /// @todo Include better way to serve files 311 | // attachServe(srv){ 312 | // _logger.fine()('attaching client serving req handler'); 313 | // var url = this._path + '/socket.io.js'; 314 | // var evs = srv.listeners('request').slice(0); 315 | // var self = this; 316 | // srv.removeAllListeners('request'); 317 | // srv.on('request', function(req, res) { 318 | // if (0 === req.url.indexOf(url)) { 319 | // self.serve(req, res); 320 | // } else { 321 | // for (var i = 0; i < evs.length; i++) { 322 | // evs[i].call(srv, req, res); 323 | // } 324 | // } 325 | // }) 326 | // } 327 | 328 | /// Handles a request serving `/socket.io.js` 329 | /// 330 | /// @param {http.Request} req 331 | /// @param {http.Response} res 332 | /// @api private 333 | /// @todo Include better way to serve files 334 | 335 | // serve(req, res){ 336 | // var etag = req.headers['if-none-match']; 337 | // if (etag) { 338 | // if (clientVersion == etag) { 339 | // debug('serve client 304'); 340 | // res.writeHead(304); 341 | // res.end(); 342 | // return; 343 | // } 344 | // } 345 | // 346 | // debug('serve client source'); 347 | // res.setHeader('Content-Type', 'application/javascript'); 348 | // res.setHeader('ETag', clientVersion); 349 | // res.writeHead(200); 350 | // res.end(clientSource); 351 | // } 352 | 353 | /// Binds socket.io to an engine.io instance. 354 | /// 355 | /// @param {engine.Server} engine.io (or compatible) server 356 | /// @return {Server} self 357 | /// @api public 358 | Server bind(Engine engine) { 359 | this.engine = engine; 360 | this.engine!.on('connection', onconnection); 361 | return this; 362 | } 363 | 364 | /// Called with each incoming transport connection. 365 | /// 366 | /// @param {engine.Socket} socket 367 | /// @return {Server} self 368 | /// @api public 369 | Server onconnection(conn) { 370 | _logger.fine('incoming connection with id ${conn.id}'); 371 | var client = Client(this, conn); 372 | client.connect('/'); 373 | return this; 374 | } 375 | 376 | /// Looks up a namespace. 377 | /// 378 | /// @param {String} nsp name 379 | /// @param {Function} optional, nsp `connection` ev handler 380 | /// @api public 381 | Namespace of(name, [fn]) { 382 | if (name.toString()[0] != '/') { 383 | name = '/' + name; 384 | } 385 | 386 | if (!nsps.containsKey(name)) { 387 | _logger.fine('initializing namespace $name'); 388 | var nsp = Namespace(this, name); 389 | nsps[name] = nsp; 390 | } 391 | if (fn != null) nsps[name]!.on('connect', fn); 392 | return nsps[name]!; 393 | } 394 | 395 | /// Closes server connection 396 | /// 397 | /// @return a Future that resolves when the httpServer is closed 398 | /// @api public 399 | Future close() async { 400 | nsps['/']!.sockets.toList(growable: false).forEach((socket) { 401 | socket.onclose(); 402 | }); 403 | 404 | engine?.close(); 405 | 406 | if (httpServer != null) { 407 | await httpServer!.stop(); 408 | } 409 | 410 | _ready = null; 411 | } 412 | 413 | // redirect to sockets method 414 | Namespace to(_) => sockets.to(_); 415 | Namespace use(_) => sockets.use(_); 416 | void send(_) => sockets.send(_); 417 | Namespace write(_) => sockets.write(_); 418 | Namespace clients(_) => sockets.clients(_); 419 | Namespace compress(_) => sockets.compress(_); 420 | 421 | // emitter 422 | void emit(event, data) => sockets.emit(event, data); 423 | void on(event, handler) => sockets.on(event, handler); 424 | void once(event, handler) => sockets.once(event, handler); 425 | void off(event, handler) => sockets.off(event, handler); 426 | } 427 | -------------------------------------------------------------------------------- /lib/src/socket.dart: -------------------------------------------------------------------------------- 1 | /// socket.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 22/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'dart:io'; 12 | import 'package:socket_io/src/adapter/adapter.dart'; 13 | import 'package:socket_io/src/client.dart'; 14 | import 'package:socket_io_common/src/parser/parser.dart'; 15 | import 'package:socket_io/src/namespace.dart'; 16 | import 'package:socket_io/src/server.dart'; 17 | import 'package:socket_io/src/util/event_emitter.dart'; 18 | 19 | /// Module exports. 20 | // 21 | //module.exports = exports = Socket; 22 | 23 | /// Blacklisted events. 24 | /// 25 | /// @api public 26 | 27 | List events = [ 28 | 'error', 29 | 'connect', 30 | 'disconnect', 31 | 'newListener', 32 | 'removeListener' 33 | ]; 34 | 35 | /// Flags. 36 | /// 37 | /// @api private 38 | List flags = ['json', 'volatile', 'broadcast']; 39 | 40 | const List EVENTS = [ 41 | 'error', 42 | 'connect', 43 | 'disconnect', 44 | 'disconnecting', 45 | 'newListener', 46 | 'removeListener' 47 | ]; 48 | 49 | class Socket extends EventEmitter { 50 | // ignore: undefined_class 51 | Namespace nsp; 52 | Client client; 53 | late Server server; 54 | late Adapter adapter; 55 | late String id; 56 | late HttpRequest request; 57 | var conn; 58 | Map roomMap = {}; 59 | List roomList = []; 60 | Map acks = {}; 61 | bool connected = true; 62 | bool disconnected = false; 63 | Map? handshake; 64 | Map? flags; 65 | 66 | // a data store for each socket. 67 | Map data = {}; 68 | 69 | Socket(this.nsp, this.client, query) { 70 | server = nsp.server; 71 | adapter = nsp.adapter; 72 | id = client.id; 73 | request = client.request; 74 | conn = client.conn; 75 | handshake = buildHandshake(query); 76 | } 77 | 78 | /// Builds the `handshake` BC object 79 | /// 80 | /// @api private 81 | Map buildHandshake(query) { 82 | final buildQuery = () { 83 | var requestQuery = request.uri.queryParameters; 84 | //if socket-specific query exist, replace query strings in requestQuery 85 | return query != null 86 | ? (Map.from(query)..addAll(requestQuery)) 87 | : requestQuery; 88 | }; 89 | return { 90 | 'headers': request.headers, 91 | 'time': DateTime.now().toString(), 92 | 'address': conn.remoteAddress, 93 | 'xdomain': request.headers.value('origin') != null, 94 | // TODO 'secure': ! !this.request.connectionInfo.encrypted, 95 | 'issued': DateTime.now().millisecondsSinceEpoch, 96 | 'url': request.uri.path, 97 | 'query': buildQuery() 98 | }; 99 | } 100 | 101 | Socket get json { 102 | flags = flags ?? {}; 103 | flags!['json'] = true; 104 | return this; 105 | } 106 | 107 | Socket get volatile { 108 | flags = flags ?? {}; 109 | flags!['volatile'] = true; 110 | return this; 111 | } 112 | 113 | Socket get broadcast { 114 | flags = flags ?? {}; 115 | flags!['broadcast'] = true; 116 | return this; 117 | } 118 | 119 | @override 120 | void emit(String event, [data]) { 121 | emitWithAck(event, data); 122 | } 123 | 124 | void emitWithBinary(String event, [data]) { 125 | emitWithAck(event, data, binary: true); 126 | } 127 | 128 | /// Emits to this client. 129 | /// 130 | /// @return {Socket} self 131 | /// @api public 132 | void emitWithAck(String event, dynamic data, 133 | {Function? ack, bool binary = false}) { 134 | if (EVENTS.contains(event)) { 135 | super.emit(event, data); 136 | } else { 137 | var packet = {}; 138 | var sendData = data == null ? [event] : [event, data]; 139 | 140 | var flags = this.flags ?? {}; 141 | 142 | if (ack != null) { 143 | if (roomList.isNotEmpty || flags['broadcast'] == true) { 144 | throw UnsupportedError( 145 | 'Callbacks are not supported when broadcasting'); 146 | } 147 | 148 | acks['${nsp.ids}'] = ack; 149 | packet['id'] = '${nsp.ids++}'; 150 | } 151 | 152 | packet['type'] = binary ? BINARY_EVENT : EVENT; 153 | packet['data'] = sendData; 154 | 155 | if (roomList.isNotEmpty || flags['broadcast'] == true) { 156 | adapter.broadcast(packet, { 157 | 'except': [id], 158 | 'rooms': roomList, 159 | 'flags': flags 160 | }); 161 | } else { 162 | // dispatch packet 163 | this.packet(packet, 164 | {'volatile': flags['volatile'], compress: flags['compress']}); 165 | } 166 | 167 | // // reset flags 168 | roomList = []; 169 | this.flags = null; 170 | // } 171 | // return this; 172 | } 173 | } 174 | 175 | /// Targets a room when broadcasting. 176 | /// 177 | /// @param {String} name 178 | /// @return {Socket} self 179 | /// @api public 180 | Socket to(String name) { 181 | if (!roomList.contains(name)) roomList.add(name); 182 | return this; 183 | } 184 | 185 | /// Sends a `message` event. 186 | /// 187 | /// @return {Socket} self 188 | /// @api public 189 | void send(_) { 190 | write(_); 191 | } 192 | 193 | Socket write(List data) { 194 | emit('message', data); 195 | return this; 196 | } 197 | 198 | /// Writes a packet. 199 | /// 200 | /// @param {Object} packet object 201 | /// @param {Object} options 202 | /// @api private 203 | void packet(packet, [opts]) { 204 | // ignore preEncoded = true. 205 | if (packet is Map) { 206 | packet['nsp'] = nsp.name; 207 | } 208 | opts = opts ?? {}; 209 | opts['compress'] = false != opts['compress']; 210 | client.packet(packet, opts); 211 | } 212 | 213 | /// Joins a room. 214 | /// 215 | /// @param {String} room 216 | /// @param {Function} optional, callback 217 | /// @return {Socket} self 218 | /// @api private 219 | Socket join(room, [fn]) { 220 | // debug('joining room %s', room); 221 | if (roomMap.containsKey(room)) { 222 | if (fn != null) fn(null); 223 | return this; 224 | } 225 | adapter.add(id, room, ([err]) { 226 | if (err != null) return fn?.call(err); 227 | // _logger.info('joined room %s', room); 228 | roomMap[room] = room; 229 | if (fn != null) fn(null); 230 | }); 231 | return this; 232 | } 233 | 234 | /// Leaves a room. 235 | /// 236 | /// @param {String} room 237 | /// @param {Function} optional, callback 238 | /// @return {Socket} self 239 | /// @api private 240 | Socket leave(room, fn) { 241 | // debug('leave room %s', room); 242 | adapter.del(id, room, ([err]) { 243 | if (err != null) return fn?.call(err); 244 | // _logger.info('left room %s', room); 245 | roomMap.remove(room); 246 | fn?.call(null); 247 | }); 248 | return this; 249 | } 250 | 251 | /// Leave all rooms. 252 | /// 253 | /// @api private 254 | 255 | void leaveAll() { 256 | adapter.delAll(id); 257 | roomMap = {}; 258 | } 259 | 260 | /// Called by `Namespace` upon succesful 261 | /// middleware execution (ie: authorization). 262 | /// 263 | /// @api private 264 | 265 | void onconnect() { 266 | // debug('socket connected - writing packet'); 267 | nsp.connected[id] = this; 268 | join(id); 269 | packet({'type': CONNECT}); 270 | } 271 | 272 | /// Called with each packet. Called by `Client`. 273 | /// 274 | /// @param {Object} packet 275 | /// @api private 276 | 277 | void onpacket(packet) { 278 | // debug('got packet %j', packet); 279 | switch (packet['type']) { 280 | case EVENT: 281 | onevent(packet); 282 | break; 283 | 284 | case BINARY_EVENT: 285 | onevent(packet); 286 | break; 287 | 288 | case ACK: 289 | onack(packet); 290 | break; 291 | 292 | case BINARY_ACK: 293 | onack(packet); 294 | break; 295 | 296 | case DISCONNECT: 297 | ondisconnect(); 298 | break; 299 | 300 | case ERROR: 301 | emit('error', packet['data']); 302 | } 303 | } 304 | 305 | /// Called upon event packet. 306 | /// 307 | /// @param {Object} packet object 308 | /// @api private 309 | void onevent(packet) { 310 | List args = packet['data'] ?? []; 311 | // debug('emitting event %j', args); 312 | 313 | if (null != packet['id']) { 314 | // debug('attaching ack callback to event'); 315 | args.add(ack(packet['id'])); 316 | } 317 | 318 | // dart doesn't support "String... rest" syntax. 319 | if (args.length > 2) { 320 | Function.apply(super.emit, [args.first, args.sublist(1)]); 321 | } else { 322 | Function.apply(super.emit, args); 323 | } 324 | } 325 | 326 | /// Produces an ack callback to emit with an event. 327 | /// 328 | /// @param {Number} packet id 329 | /// @api private 330 | Function ack(id) { 331 | var sent = false; 332 | return (_) { 333 | // prevent double callbacks 334 | if (sent) return; 335 | // var args = Array.prototype.slice.call(arguments); 336 | // debug('sending ack %j', args); 337 | 338 | var type = /*hasBin(args) ? parser.BINARY_ACK : parser.*/ ACK; 339 | packet({ 340 | 'id': id, 341 | 'type': type, 342 | 'data': [_] 343 | }); 344 | sent = true; 345 | }; 346 | } 347 | 348 | /// Called upon ack packet. 349 | /// 350 | /// @api private 351 | void onack(packet) { 352 | Function ack = acks.remove(packet['id']); 353 | if (ack is Function) { 354 | // debug('calling ack %s with %j', packet.id, packet.data); 355 | Function.apply(ack, packet['data']); 356 | } else { 357 | // debug('bad ack %s', packet.id); 358 | } 359 | } 360 | 361 | /// Called upon client disconnect packet. 362 | /// 363 | /// @api private 364 | void ondisconnect() { 365 | // debug('got disconnect packet'); 366 | onclose('client namespace disconnect'); 367 | } 368 | 369 | /// Handles a client error. 370 | /// 371 | /// @api private 372 | void onerror(err) { 373 | if (hasListeners('error')) { 374 | emit('error', err); 375 | } else { 376 | // console.error('Missing error handler on `socket`.'); 377 | // console.error(err.stack); 378 | } 379 | } 380 | 381 | /// Called upon closing. Called by `Client`. 382 | /// 383 | /// @param {String} reason 384 | /// @param {Error} optional error object 385 | /// @api private 386 | dynamic onclose([reason]) { 387 | if (!connected) return this; 388 | // debug('closing socket - reason %s', reason); 389 | emit('disconnecting', reason); 390 | leaveAll(); 391 | nsp.remove(this); 392 | client.remove(this); 393 | connected = false; 394 | disconnected = true; 395 | nsp.connected.remove(id); 396 | emit('disconnect', reason); 397 | } 398 | 399 | /// Produces an `error` packet. 400 | /// 401 | /// @param {Object} error object 402 | /// @api private 403 | void error(err) { 404 | packet({'type': ERROR, 'data': err}); 405 | } 406 | 407 | /// Disconnects this client. 408 | /// 409 | /// @param {Boolean} if `true`, closes the underlying connection 410 | /// @return {Socket} self 411 | /// @api public 412 | 413 | Socket disconnect([close]) { 414 | if (!connected) return this; 415 | if (close == true) { 416 | client.disconnect(); 417 | } else { 418 | packet({'type': DISCONNECT}); 419 | onclose('server namespace disconnect'); 420 | } 421 | return this; 422 | } 423 | 424 | /// Sets the compress flag. 425 | /// 426 | /// @param {Boolean} if `true`, compresses the sending data 427 | /// @return {Socket} self 428 | /// @api public 429 | Socket compress(compress) { 430 | flags = flags ?? {}; 431 | flags!['compress'] = compress; 432 | return this; 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /lib/src/util/event_emitter.dart: -------------------------------------------------------------------------------- 1 | /// event_emitter.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 11/23/2016, Created by Henri Chen 9 | /// 10 | /// Copyright (C) 2016 Potix Corporation. All Rights Reserved. 11 | import 'dart:collection' show HashMap; 12 | 13 | /// Handler type for handling the event emitted by an [EventEmitter]. 14 | typedef EventHandler = dynamic Function(T data); 15 | 16 | /// Generic event emitting and handling. 17 | class EventEmitter { 18 | /// Mapping of events to a list of event handlers 19 | Map> _events = 20 | HashMap>(); 21 | 22 | /// Mapping of events to a list of one-time event handlers 23 | Map> _eventsOnce = 24 | HashMap>(); 25 | 26 | /// This function triggers all the handlers currently listening 27 | /// to [event] and passes them [data]. 28 | void emit(String event, [dynamic data]) { 29 | final list0 = _events[event]; 30 | // todo: try to optimize this. Maybe remember the off() handlers and remove later? 31 | // handler might be off() inside handler; make a copy first 32 | final list = list0 != null ? List.from(list0) : null; 33 | list?.forEach((handler) { 34 | handler(data); 35 | }); 36 | 37 | _eventsOnce.remove(event)?.forEach((EventHandler handler) { 38 | handler(data); 39 | }); 40 | } 41 | 42 | /// This function binds the [handler] as a listener to the [event] 43 | void on(String event, EventHandler handler) { 44 | _events.putIfAbsent(event, () => []); 45 | _events[event]!.add(handler); 46 | } 47 | 48 | /// This function binds the [handler] as a listener to the first 49 | /// occurrence of the [event]. When [handler] is called once, 50 | /// it is removed. 51 | void once(String event, EventHandler handler) { 52 | _eventsOnce.putIfAbsent(event, () => []); 53 | _eventsOnce[event]!.add(handler); 54 | } 55 | 56 | /// This function attempts to unbind the [handler] from the [event] 57 | void off(String event, [EventHandler? handler]) { 58 | if (handler != null) { 59 | _events[event]?.remove(handler); 60 | _eventsOnce[event]?.remove(handler); 61 | if (_events[event]?.isEmpty == true) { 62 | _events.remove(event); 63 | } 64 | if (_eventsOnce[event]?.isEmpty == true) { 65 | _eventsOnce.remove(event); 66 | } 67 | } else { 68 | _events.remove(event); 69 | _eventsOnce.remove(event); 70 | } 71 | } 72 | 73 | /// This function unbinds all the handlers for all the events. 74 | void clearListeners() { 75 | _events = HashMap>(); 76 | _eventsOnce = HashMap>(); 77 | } 78 | 79 | /// Returns whether the event has registered. 80 | bool hasListeners(String event) { 81 | return _events[event]?.isNotEmpty == true || 82 | _eventsOnce[event]?.isNotEmpty == true; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: socket_io 2 | description: > 3 | Port of JS/Node library Socket.io. It enables real-time, bidirectional and 4 | event-based communication cross-platform. 5 | version: 1.0.1 6 | homepage: https://www.zkoss.org 7 | repository: https://github.com/rikulo/socket.io-dart 8 | issue_tracker: https://github.com/rikulo/socket.io-dart/issues 9 | 10 | environment: 11 | sdk: '>=2.12.0 <3.0.0' 12 | 13 | dependencies: 14 | stream: ^3.0.0 15 | socket_io_common: ^1.0.1 16 | uuid: ^3.0.4 17 | logging: ^1.0.0 18 | dev_dependencies: 19 | test: ^1.16.8 20 | pedantic: ^1.11.0 21 | -------------------------------------------------------------------------------- /test/socket.test.dart: -------------------------------------------------------------------------------- 1 | /// socket.test.dart 2 | /// 3 | /// Purpose: 4 | /// 5 | /// Description: 6 | /// 7 | /// History: 8 | /// 16/02/2017, Created by jumperchen 9 | /// 10 | /// Copyright (C) 2017 Potix Corporation. All Rights Reserved. 11 | import 'package:test/test.dart'; 12 | 13 | import 'package:socket_io/socket_io.dart'; 14 | 15 | void main() { 16 | group('Socket IO', () { 17 | test('Start standalone server', () async { 18 | var io = Server(); 19 | var nsp = io.of('/some'); 20 | nsp.on('connection', (client) { 21 | print('connection /some'); 22 | client.on('msg', (data) { 23 | print('data from /some => $data'); 24 | client.emit('fromServer', 'ok 2'); 25 | }); 26 | }); 27 | io.on('connection', (client) { 28 | print('connection default namespace'); 29 | client.on('msg', (data) { 30 | print('data from default => $data'); 31 | client.emit('fromServer', 'ok'); 32 | }); 33 | }); 34 | await io.listen(3000); 35 | }); 36 | }); 37 | } 38 | --------------------------------------------------------------------------------