forked from logux/server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbase-server.js
More file actions
319 lines (286 loc) · 9.93 KB
/
base-server.js
File metadata and controls
319 lines (286 loc) · 9.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
var ServerConnection = require('logux-sync').ServerConnection
var createTimer = require('logux-core').createTimer
var MemoryStore = require('logux-core').MemoryStore
var WebSocket = require('ws')
var shortid = require('shortid')
var https = require('https')
var http = require('http')
var path = require('path')
var Log = require('logux-core').Log
var fs = require('fs')
var remoteAddress = require('./remote-address')
var promisify = require('./promisify')
var Client = require('./client')
var PEM_PREAMBLE = '-----BEGIN'
function isPem (content) {
if (typeof content === 'object' && content.pem) {
return true
} else {
return content.toString().trim().indexOf(PEM_PREAMBLE) === 0
}
}
function readFile (root, file) {
file = file.toString()
if (!path.isAbsolute(file)) {
file = path.join(root, file)
}
return promisify(function (done) {
fs.readFile(file, done)
})
}
/**
* Basic Logux Server API without good UI. Use it only if you need
* to create some special hacks on top of Logux Server.
*
* In most use cases you should use {@link Server}.
*
* @param {object} options Server options.
* @param {string} options.subprotocol Server current application
* subprotocol version in SemVer format.
* @param {string} options.supports npm’s version requirements for client
* subprotocol version.
* @param {string|number} [options.nodeId] Unique server ID. Be default,
* `server:` with compacted UUID.
* @param {string} [options.root=process.cwd()] Application root to load files
* and show errors.
* @param {number} [options.timeout=20000] Timeout in milliseconds
* to disconnect connection.
* @param {number} [options.ping=10000] Milliseconds since last message to test
* connection by sending ping.
* @param {function} [options.timer] Timer to use in log. Will be default
* timer with server `nodeId`, by default.
* @param {Store} [options.store] Store to save log. Will be `MemoryStore`,
* by default.
* @param {"production"|"development"} [options.env] Development or production
* server mode. By default,
* it will be taken from
* `NODE_ENV` environment
* variable. On empty
* `NODE_ENV` it will
* be `"development"`.
* @param {number} [options.pid] Process ID, to display in reporter.
* @param {function} [reporter] Function to show current server status.
*
* @example
* import { BaseServer } from 'logux-server'
* class MyLoguxHack extends BaseServer {
* …
* }
*
* @class
*/
function BaseServer (options, reporter) {
/**
* Server options.
* @type {object}
*
* @example
* console.log(app.options.nodeId + ' was started')
*/
this.options = options || { }
this.reporter = reporter || function () { }
if (typeof this.options.subprotocol === 'undefined') {
throw new Error('Missed subprotocol version')
}
if (typeof this.options.supports === 'undefined') {
throw new Error('Missed supported subprotocol major versions')
}
if (typeof this.options.nodeId === 'undefined') {
this.options.nodeId = 'server:' + shortid.generate()
}
this.options.root = this.options.root || process.cwd()
var timer = this.options.timer || createTimer(this.options.nodeId)
var store = this.options.store || new MemoryStore()
/**
* Server events log.
* @type {Log}
*
* @example
* app.log.keep(customKeeper)
*/
this.log = new Log({ store: store, timer: timer })
/**
* Production or development mode.
* @type {"production"|"development"}
*
* @example
* if (app.env === 'development') {
* logDebugData()
* }
*/
this.env = this.options.env || process.env.NODE_ENV || 'development'
this.unbind = []
/**
* Connected clients.
* @type {Client[]}
*
* @example
* for (let nodeId in app.clients) {
* console.log(app.clients[nodeId].remoteAddress)
* }
*/
this.clients = { }
this.lastClient = 0
var app = this
this.unbind.push(function () {
for (var i in app.clients) {
app.clients[i].destroy()
}
})
}
BaseServer.prototype = {
/**
* Set authenticate function. It will receive client credentials
* and node ID. It should return a Promise with `false`
* on bad authentication or with {@link User} on correct credentials.
*
* @param {authenticator} authenticator The authentication callback.
*
* @return {undefined}
*
* @example
* app.auth(token => {
* return findUserByToken(token).then(user => {
* return user.blocked ? false : user
* })
* })
*/
auth: function auth (authenticator) {
this.authenticator = authenticator
},
/**
* Start WebSocket server and listen for clients.
*
* @param {object} options Connection options.
* @param {http.Server} [options.server] HTTP server to connect WebSocket
* server to it.
* Same as in ws.WebSocketServer.
* @param {number} [option.port=1337] Port to bind server. It will create
* HTTP server manually to connect
* WebSocket server to it.
* @param {string} [option.host="127.0.0.1"] IP-address to bind server.
* @param {string} [option.key] SSL key or path to it. Path could be relative
* from server root. It is required in production
* mode, because WSS is highly recommended.
* @param {string} [option.cert] SSL certificate or path to it. Path could
* be relative from server root. It is required
* in production mode, because WSS
* is highly recommended.
*
* @return {Promise} When the server has been bound.
*
* @example
* app.listen({ cert: 'cert.pem', key: 'key.pem' })
*/
listen: function listen (options) {
/**
* Options used to start server.
* @type {object}
*/
this.listenOptions = options || { }
if (this.listenOptions.key && !this.listenOptions.cert) {
throw new Error('You must set cert option too if you use key option')
}
if (!this.listenOptions.key && this.listenOptions.cert) {
throw new Error('You must set key option too if you use cert option')
}
if (this.env === 'production') {
if (!this.listenOptions.server && !this.listenOptions.key) {
throw new Error('SSL is required in production mode. ' +
'Set key and cert options or use server option.')
}
}
if (!this.authenticator) {
throw new Error('You must set authentication callback by app.auth()')
}
if (!this.listenOptions.server) {
if (!this.listenOptions.port) this.listenOptions.port = 1337
if (!this.listenOptions.host) this.listenOptions.host = '127.0.0.1'
}
var app = this
var promise = Promise.resolve()
if (this.listenOptions.server) {
this.ws = new WebSocket.Server({ server: this.listenOptions.server })
} else {
var before = []
if (this.listenOptions.key && !isPem(this.listenOptions.key)) {
before.push(readFile(this.options.root, this.listenOptions.key))
} else {
before.push(Promise.resolve(this.listenOptions.key))
}
if (this.listenOptions.cert && !isPem(this.listenOptions.cert)) {
before.push(readFile(this.options.root, this.listenOptions.cert))
} else {
before.push(Promise.resolve(this.listenOptions.cert))
}
promise = promise.then(function () {
return Promise.all(before)
}).then(function (keys) {
if (keys[0]) {
app.http = https.createServer({ key: keys[0], cert: keys[1] })
} else {
app.http = http.createServer()
}
app.ws = new WebSocket.Server({ server: app.http })
return promisify(function (done) {
app.http.listen(app.listenOptions.port, app.listenOptions.host, done)
})
})
}
app.unbind.push(function () {
return promisify(function (done) {
promise.then(function () {
app.ws.close(function () {
if (app.http) {
app.http.close(done)
} else {
done()
}
})
})
})
})
return promise.then(function () {
app.ws.on('connection', function (ws) {
app.reporter('connect', app, remoteAddress(ws))
app.lastClient += 1
var client = new Client(app, new ServerConnection(ws), app.lastClient)
app.clients[app.lastClient] = client
})
}).then(function () {
app.reporter('listen', app)
})
},
/**
* Stop server and unbind all listeners.
*
* @return {Promise} Promise when all listeners will be removed.
*
* @example
* afterEach(() => {
* testApp.destroy()
* })
*/
destroy: function destroy () {
this.reporter('destroy', this)
return Promise.all(this.unbind.map(function (unbind) {
return unbind()
}))
}
}
module.exports = BaseServer
/**
* @callback authenticator
* @param {any} credentials The client credentials.
* @param {string|number} nodeId Unique client node name.
* @param {Client} client Client object.
* @return {Promise} Promise with `false` or {@link User} data.
*/
/**
* Developer defined user data. It is open structure. But you should define
* at least `id` property to show it in logs.
*
* @typedef {object} User
*
* @property {string|number} id Any user ID to display in server logs.
*/