1 | const pullWS = require('pull-websocket')
|
2 | const URL = require('url')
|
3 | const pull = require('pull-stream/pull')
|
4 | const Map = require('pull-stream/throughs/map')
|
5 | const scopes = require('multiserver-scopes')
|
6 | const http = require('http')
|
7 | const https = require('https')
|
8 | const fs = require('fs')
|
9 | const debug = require('debug')('multiserver:ws')
|
10 |
|
11 | function safeOrigin(origin, address, port) {
|
12 |
|
13 |
|
14 |
|
15 | if (!(address === '::1' || address === '127.0.0.1') || origin == undefined)
|
16 | return 'ws:' + address + (port ? ':' + port : '')
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | if (origin === 'null') return 'ws:null'
|
24 |
|
25 |
|
26 |
|
27 | return origin.replace(/^http/, 'ws')
|
28 | }
|
29 |
|
30 |
|
31 |
|
32 | function getRandomPort() {
|
33 | return Math.floor(49152 + (65535 - 49152 + 1) * Math.random())
|
34 | }
|
35 |
|
36 | module.exports = function WS(opts = {}) {
|
37 |
|
38 |
|
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 |
|
56 |
|
57 |
|
58 |
|
59 |
|
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 |
|
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 |
|
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 | }
|