UNPKG

5.41 kBJavaScriptView Raw
1const pullWS = require('pull-websocket')
2const URL = require('url')
3const pull = require('pull-stream/pull')
4const Map = require('pull-stream/throughs/map')
5const scopes = require('multiserver-scopes')
6const http = require('http')
7const https = require('https')
8const fs = require('fs')
9const debug = require('debug')('multiserver:ws')
10
11function safeOrigin(origin, address, port) {
12 // If the connection is not localhost, we shouldn't trust the origin header.
13 // So, use address instead of origin if origin not set, then it's definitely
14 // not a browser.
15 if (!(address === '::1' || address === '127.0.0.1') || origin == undefined)
16 return 'ws:' + address + (port ? ':' + port : '')
17
18 // Note: origin "null" (as string) can happen a bunch of ways:
19 // * it can be a html opened as a file
20 // * or certain types of CORS
21 // * https://www.w3.org/TR/cors/#resource-sharing-check-0
22 // * and webworkers if loaded from data-url?
23 if (origin === 'null') return 'ws:null'
24
25 // A connection from the browser on localhost, we choose to trust this came
26 // from a browser.
27 return origin.replace(/^http/, 'ws')
28}
29
30// Choose a dynamic port between 49152 and 65535
31// https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Dynamic,_private_or_ephemeral_ports
32function getRandomPort() {
33 return Math.floor(49152 + (65535 - 49152 + 1) * Math.random())
34}
35
36module.exports = function WS(opts = {}) {
37 // This takes options for `WebSocket.Server()`:
38 // https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback
39
40 opts.binaryType = opts.binaryType || 'arraybuffer'
41 const scope = opts.scope || 'device'
42
43 function isAllowedScope(s) {
44 return s === scope || (Array.isArray(scope) && ~scope.indexOf(s))
45 }
46
47 const secure =
48 (opts.server && !!opts.server.key) || (!!opts.key && !!opts.cert)
49 return {
50 name: 'ws',
51 scope: () => scope,
52 server(onConnect, startedCb) {
53 if (pullWS.createServer == null) return null
54
55 // Maybe weird: this sets a random port each time that `server()` is run
56 // whereas the net plugin sets the port when the outer function is run.
57 //
58 // This server has a random port generated at runtime rather than when
59 // the interface is instantiated. Is that the way it should work?
60 opts.port = opts.port || getRandomPort()
61
62 if (typeof opts.key === 'string') opts.key = fs.readFileSync(opts.key)
63 if (typeof opts.cert === 'string') opts.cert = fs.readFileSync(opts.cert)
64
65 const server =
66 opts.server ||
67 (opts.key && opts.cert
68 ? https.createServer({ key: opts.key, cert: opts.cert }, opts.handler)
69 : http.createServer(opts.handler))
70
71 const serverOpts = Object.assign({}, opts, { server: server })
72 const wsServer = pullWS.createServer(
73 serverOpts,
74 function connectionListener(stream) {
75 stream.address = safeOrigin(
76 stream.headers.origin,
77 stream.remoteAddress,
78 stream.remotePort
79 )
80 onConnect(stream)
81 }
82 )
83
84 if (!opts.server) {
85 debug('Listening on %s:%d', opts.host, opts.port)
86 server.listen(opts.port, opts.host, function onListening() {
87 startedCb && startedCb(null, true)
88 })
89 } else startedCb && startedCb(null, true)
90
91 return function closeWsServer(cb) {
92 debug('Closing server on %s:%d', opts.host, opts.port)
93 wsServer.close((err) => {
94 debug('after WS close', err)
95 if (err) console.error(err)
96 else debug('No longer listening on %s:%d', opts.host, opts.port)
97 if (cb) cb(err)
98 })
99 }
100 },
101
102 client(addr, cb) {
103 if (!addr.host) {
104 addr.hostname = addr.hostname || opts.host || 'localhost'
105 addr.slashes = true
106 addr = URL.format(addr)
107 }
108 if (typeof addr !== 'string') addr = URL.format(addr)
109
110 const stream = pullWS.connect(addr, {
111 binaryType: opts.binaryType,
112 onConnect: function connectionListener(err) {
113 // Ensure stream is a stream of node buffers
114 stream.source = pull(stream.source, Map(Buffer.from.bind(Buffer)))
115 cb(err, stream)
116 },
117 })
118 stream.address = addr
119
120 return function closeWsClient() {
121 stream.close()
122 }
123 },
124
125 stringify(targetScope = 'device') {
126 if (pullWS.createServer == null) {
127 return null
128 }
129 if (isAllowedScope(targetScope) === false) {
130 return null
131 }
132
133 const port = opts.server ? opts.server.address().port : opts.port
134 const externalHost = targetScope === 'public' && opts.external
135 let resultHost = externalHost || opts.host || scopes.host(targetScope)
136
137 if (resultHost == null) {
138 // The device has no network interface for a given `targetScope`.
139 return null
140 }
141
142 if (typeof resultHost === 'string') {
143 resultHost = [resultHost]
144 }
145
146 return resultHost
147 .map((h) =>
148 URL.format({
149 protocol: secure ? 'wss' : 'ws',
150 slashes: true,
151 hostname: h,
152 port: (secure ? port === 443 : port === 80) ? undefined : port,
153 })
154 )
155 .join(';')
156 },
157
158 parse(str) {
159 const addr = URL.parse(str)
160 if (!/^wss?\:$/.test(addr.protocol)) return null
161 return addr
162 },
163 }
164}