UNPKG

4.55 kBJavaScriptView Raw
1const toPull = require('stream-to-pull-stream')
2const scopes = require('multiserver-scopes')
3const debug = require('debug')('multiserver:net')
4let net
5try {
6 net = require('net')
7} catch (_) {
8 // This only throws in browsers because they don't have access to the Node net
9 // library, which is safe to ignore because they shouldn't be running any
10 // methods that require the net library. Maybe we should be setting a flag
11 // somewhere rather than checking whether `net == null`?
12}
13
14function toAddress(host, port) {
15 return ['net', host, port].join(':')
16}
17
18function toDuplex(str) {
19 const stream = toPull.duplex(str)
20 stream.address = toAddress(str.remoteAddress, str.remotePort)
21 return stream
22}
23
24// Choose a dynamic port between 49152 and 65535
25// https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Dynamic,_private_or_ephemeral_ports
26function getRandomPort() {
27 return Math.floor(49152 + (65535 - 49152 + 1) * Math.random())
28}
29
30module.exports = function Net({
31 scope = 'device',
32 host,
33 port,
34 external,
35 allowHalfOpen,
36 pauseOnConnect,
37}) {
38 // Arguments are `scope` and `external` plus selected options for
39 // `net.createServer()` and `server.listen()`.
40 host = host || (typeof scope === 'string' && scopes.host(scope))
41 port = port || getRandomPort()
42
43 function isAllowedScope(s) {
44 return s === scope || (Array.isArray(scope) && scope.includes(s))
45 }
46
47 return {
48 name: 'net',
49 scope: () => scope,
50 server(onConnection, startedCB) {
51 debug('Listening on %s:%d', host, port)
52 let tempStartedCB = startedCB
53
54 // TODO: We convert `allowHalfOpen` to boolean for legacy reasons, this
55 // might not be getting used anywhere but I'm too scared to change it.
56 // This should probably be removed when we do a major version bump.
57 const serverOpts = {
58 allowHalfOpen: Boolean(allowHalfOpen),
59 pauseOnConnect,
60 }
61
62 const server = net.createServer(
63 serverOpts,
64 function connectionListener(stream) {
65 onConnection(toDuplex(stream))
66 }
67 )
68
69 server.addListener('error', function onError(err) {
70 if (tempStartedCB) {
71 tempStartedCB(err)
72 tempStartedCB = null
73 } else {
74 console.error(err)
75 }
76 })
77
78 server.listen(port, host, function onListening() {
79 if (tempStartedCB) {
80 tempStartedCB()
81 tempStartedCB = null
82 }
83 })
84
85 return function closeNetServer(cb) {
86 debug('Closing server on %s:%d', host, port)
87 server.close(function onNetServerClosing(err) {
88 if (err) console.error(err)
89 else debug('No longer listening on %s:%d', host, port)
90 if (cb) cb(err)
91 })
92 }
93 },
94
95 client(opts, cb) {
96 let started = false
97 const stream = net
98 .connect(opts)
99 .on('connect', function onConnect() {
100 if (started) return
101 started = true
102 cb(null, toDuplex(stream))
103 })
104 .on('error', function onError(err) {
105 if (started) return
106 started = true
107 cb(err)
108 })
109
110 return function closeNetClient() {
111 started = true
112 stream.destroy()
113 cb(new Error('multiserver.net: aborted'))
114 }
115 },
116
117 // MUST be net:<host>:<port>
118 parse(s) {
119 if (net == null) return null
120 const ary = s.split(':')
121 if (ary.length < 3) return null
122 if ('net' !== ary.shift()) return null
123 const port = Number(ary.pop())
124 if (isNaN(port)) return null
125 return {
126 name: 'net',
127 host: ary.join(':') || 'localhost',
128 port: port,
129 }
130 },
131
132 stringify(targetScope = 'device') {
133 if (isAllowedScope(targetScope) === false) {
134 return null
135 }
136
137 // We want to avoid using `host` if the target scope is public and some
138 // external host (like example.com) is defined.
139 const externalHost = targetScope === 'public' && external
140 let resultHost = externalHost || host || scopes.host(targetScope)
141
142 if (resultHost == null) {
143 // The device has no network interface for a given `targetScope`.
144 return null
145 }
146
147 // convert to an array for easier formatting
148 if (typeof resultHost === 'string') {
149 resultHost = [resultHost]
150 }
151
152 return resultHost
153 .map((h) => {
154 // Remove IPv6 scopeid suffix, if any, e.g. `%wlan0`
155 return toAddress(h.replace(/(\%\w+)$/, ''), port)
156 })
157 .join(';')
158 },
159 }
160}