bug fixes
[henge/kiak.git] / strappServer.js
1 /**
2 * @file Node entry and main driver
3 * @author Jordan Lavatai, Ken Grimes
4 * @version 0.0.3
5 * @license AGPL-3.0
6 * @copyright Strapp.io 2017
7 * @summary HTTP(S) Router that uses the first directory in the requested URL
8 * as the route name
9 */
10 'use strict'
11 const opts = require('./opts.js')
12
13 const dlog = (msg) => console.log(msg)
14
15 class Server {
16 constructor(...props) {
17 Object.assign(this, new.target.defaults, ...props)
18 this.pendingResponses = new Array(this.responseCacheSize)
19 this.init()
20 return this
21 }
22 init() {
23 if (this.cacheDir)
24 this.loadCache(this.cacheDir, '')
25 require('fs').readFile('./www/strapp.min.js', (err, data) => {
26 if (err)
27 throw new Error(err)
28 this.cache['strapp.min.js'] = ['application/javascript',data]
29 })
30 this.startHttpd()
31 }
32 loadCache(dirPath, prefix) {
33 require('fs').readdir(this.cacheDir, (err, files) => {
34 if (err)
35 console.log(err)
36 else
37 files.forEach((entName) => {
38 let filePath = `${dirPath}${require('path').sep}${entName}`
39 require('fs').stat(filePath, (err, stat) => {
40 if (err)
41 console.log(err)
42 else if (stat && stat.isDirectory())
43 this.loadCache(filePath, `${entName}/`)
44 else
45 require('fs').readFile(filePath, (err, data) => {
46 if (err)
47 console.log(err)
48 else
49 this.cache[`${prefix}${entName}`] =
50 [require('mime').lookup(filePath), data]
51 })
52 })
53 })
54 })
55 }
56 startHttpd() {
57 if (this.tls)
58 this.httpd = require('https')
59 .createServer(this.tls, (rq, rs) => this.httpRequest(rq, rs))
60 else
61 this.httpd = require('http')
62 .createServer((rq, rs) => this.httpRequest(rq, rs))
63 this.httpd.listen(this, () => this.port = this.httpd.address().port)
64 }
65 httpRequest(request, response) {
66 const htArgv = request.url.slice(1).split('?')
67 dlog(`request for ${request.url} received`)
68 if (request.method === 'GET' && htArgv[0] in this.cache) {
69 dlog(`found ${htArgv[0]} in cache`)
70 response.writeHead(200, { 'Content-Type': this.cache[htArgv[0]][0] })
71 response.write(this.cache[htArgv[0]][1])
72 response.end()
73 return
74 }
75 let pubKey = ''
76 const authHeader = request.headers['Authorization']
77 if (authHeader) {
78 const authInfo = authHeader.split(' ')
79 dlog(`authInfo: ${authInfo}`)
80 if (authInfo[0] === 'STRAPP') {
81 let answer = ''
82 switch (authInfo.length) {
83 case 3: answer = authInfo[2]
84 case 2: pubKey = authInfo[1]
85 break
86 default:
87 response.writeHead(400)
88 response.end()
89 return
90 }
91 this.authenticate(pubKey, answer).then((question, authenticated) => {
92 response.setHeader('WWW-Authenticate',
93 `STRAPP ${this.question(pubKey)} ${this.tls.key}`)
94 if (authenticated) {
95 this.processRequestData(request, response, htArgv, pubKey)
96 }
97 else {
98 response.writeHead(401)
99 response.end()
100 }
101 }).catch((err) => console.log(err))
102 }
103 }
104 else
105 this.processRequestData(request, response, htArgv, pubKey)
106 }
107 processRequestData(request, response, args, pubKey) {
108 if (request.method === 'PUT' || request.method === 'POST') {
109 let data = ''
110 request.on('data', (chunk) => {
111 data += chunk
112 if (data.length > 5e5)
113 request.connection.destroy()
114 })
115 request.on('end', this.sendData(request, response, args, pubKey, data))
116 }
117 else
118 this.sendData(request, response, args, pubKey)
119 }
120 authenticate(pubKey, answer) {
121 return new Promise((resolve, reject) => {
122 require('crypto').randomBytes(256, (err, buf) => {
123 if (err)
124 reject(err)
125 else {
126 let authenticated = false
127 if (pubKey in this.answers) {
128 const answerBuf = Buffer.from(answer)
129 require('crypto').privateDecrypt({ key: this.tls.cert }, answerBuf)
130 authenticated = this.answers[pubKey].equals(answerBuf)
131 }
132 this.answers[pubKey] = buf
133 let question = Buffer.from(buf)
134 require('crypto').publicEncrypt({ key: pubKey }, question)
135 resolve(question.toString(), authenticated)
136 }
137 })
138 })
139 }
140 sendData(request, response, args, pubKey, data) {
141 if (this.app) {
142 let msgID = this.msgID++
143 if (this.msgID >= this.pendingResponses.length)
144 this.msgID = 0
145 this.pendingResponses[msgID] = response
146 if (!data)
147 data = ''
148 this.app.send(`${args[0]} ${request.method} ${pubKey} ${msgID} ${data}`)
149 }
150 else {
151 require('fs').readFile('./bootstrapp.html', (err, data) => {
152 if (err) {
153 response.writeHead(500)
154 response.end()
155 }
156 else {
157 response.writeHead(200, { 'Content-Type': 'text/html' })
158 response.write(data)
159 response.end()
160 }
161 })
162 }
163 }
164 }
165 Server.defaults = {
166 port: 0,
167 tls: { key: '', cert: '' },
168 host: undefined,
169 cache: {},
170 cacheDir: './www',
171 responseCacheSize: 40,
172 msgID: 0,
173 answers: {},
174 remoteAdminKey: undefined,
175 controller: undefined,
176 app: undefined,
177 httpd: undefined,
178 wsd: undefined
179 }
180
181 //TODO: module.exports = Server
182
183 const server = new Server({
184 port: 2443,
185 tls: { key: require('fs').readFileSync('../certs/key.pem'),
186 cert: require('fs').readFileSync('../certs/cert.pem')
187 }
188 })
189
190
191 // //TTL implementation
192 // setInterval(30000, () => {
193 // applications.forEach((app) => {
194 // app.sockets.forEach((socket, idx) => {
195 // if (!socket.connected) {
196 // console.log(`Socket ${socket} timed out`)
197 // socket.terminate()
198 // app.sockets.splice(idx, 1)
199 // }
200 // else {
201 // socket.connected = false
202 // socket.ping('', false, true)
203 // }
204 // })
205 // })
206 // })
207
208 // const startApplication = (conf) => {
209 // let httpd
210 // if (conf.tls)
211 // httpd = require('https')
212 // .createServer(conf.tls, listen)
213 // .listen(conf, conf.callback)
214 // else
215 // httpd = require('http')
216 // .createServer(listen)
217 // .listen(conf, conf.callback)
218
219 // let app = {
220 // httpd: httpd,
221 // sockets: [],
222 // openSocket: function(pubKey) {
223 // const wsd = require('ws').Server()({
224 // noServer: true,
225 // verifyClient: (info) => pubKey === parseAuth(info.req).pubKey
226 // && authenticate(info.req)
227 // })
228 // let timeout = setTimeout(30000, wsd.close())
229 // wsd.on('connection', (socket) => {
230 // clearTimeout(timeout)
231 // socket.on('pong', socket.connected = true)
232 // socket.emit('pong')
233 // this.sockets.push(socket)
234 // })
235 // }
236 // }
237 // return app
238 // }
239
240 // startApplication(opts.port, opts.tls)
241
242 // if (conf.tls)
243 // httpd = require('https')
244 // .createServer(conf.tls)
245 // .listen(conf, resolve)
246 // else
247 // httpd = require('http')
248 // .createServer()
249 // .listen(resolve)
250
251
252 // const listener = function(port, conf) Promise((resolve, reject) => {
253 // resolve(Object.create(null, {
254 // httpd: { value: null },
255 // port: { value: 0 },
256 // transceiver: { value: null, writable: true, configurable: true }
257 // }))
258 // })
259
260 // const a = {
261 // httpd: null,
262 // port: 0,
263 // transceiver: null
264 // }
265
266
267 // /** @func
268 // * @summary Authenticate a request
269 // * @arg {http.ClientRequest} request
270 // * @return {bool} true if the request is authentic
271 // */
272 // const authenticate = (request) => {
273 // return true
274 // }
275
276 // const startPortForward = function(port, httpOpts, transceiver) {
277 // const httpd, port, channel
278 // }
279
280 // /** @func
281 // * Assumes Authorization header of form STRAPP {pubKey} {answer}
282 // * @summary parse the components of a STRAPP authorization header
283 // * @arg {http.ClientRequest} request
284 // * @return {string} the empty string '' on error
285 // */
286 // const parseAuth = (request) => {
287 // if (request.headers['Authorization']) {
288 // request.auth = request.headers['Authorization'].split(' ')
289 // if (request.auth[0] === 'STRAPP')
290 // switch(request.auth.length) {
291 // case 3: request.answer = request.auth[2]
292 // case 2: request.pubKey = request.auth[1]
293 // default:
294 // }
295 // }
296 // return request
297 // }
298
299 // /** @func
300 // * @summary Creates a promise that resolves a websocket to an application host
301 // * @arg {string} pubKey - the public key authorized to connect.
302 // * @arg {Object} [conf] - configuration for the websocket.
303 // * @arg {Object} [conf.tls] - TLS configuration for HTTPS layer security
304 // * @arg {string} [conf.tls.cert] - domain certificate for HTTPS
305 // * @arg {string} [conf.tls.key] - domain public key for HTTPS
306 // * @arg {string} [conf.host] - host to attach the port listener to (all IPs)
307 // * @arg {number} [conf.backlog] - how many connections to buffer (511)
308 // * @arg {boolean} [conf.exclusive] - don't share the port (false)
309 // */
310 // const pWebSocket = function(pubKey, conf) Promise((resolve, reject) => {
311 // let timeout
312 // conf = conf || {}
313 // if (!pubKey)
314 // reject(new Error('no pubKey'))
315 // require('get-port')().then((port) => {
316 // conf.port = port
317 // new Promise((resolve, reject) => {
318 // if (conf.tls)
319 // httpd = require('https')
320 // .createServer(conf.tls)
321 // .listen(conf, resolve)
322 // else
323 // httpd = require('http')
324 // .createServer()
325 // .listen(resolve)
326 // }).then(() => {
327 // const wsd = new Promise((resolve, reject) => {
328 // (require('ws').Server())({
329 // server: httpd,
330 // verifyClient: (info) => pubKey === parseAuth(info.req).pubKey
331 // && authenticate(info.req)
332 // }).on('connection', resolve)
333 // timeout = setTimeout(30000, reject('timeout'))
334 // }).then((socket) => {
335 // clearTimeout(timeout)
336 // const webSocket = Object.create(null, {
337 // socket: { value: socket },
338 // httpd: { value: httpd },
339 // port: { value: port },
340 // transmit: { value: socket.send },
341 // receive: { value: this.receive }
342 // })
343 // socket.on('message', webSocket.receive)
344 // resolve(webSocket)
345 // }).catch(reject)
346 // }).catch(reject)
347 // }).catch(reject)
348 // })
349
350 // Object.keys(opts['bindings']).forEach((key) => {
351 // router.createBind(key, opts['bindings'][key])
352 // })
353
354 // router.startHttpServer({
355 // port: opts['port'],
356 // skelFile: './skel.html',
357 // clientJS: opts['client-js'],
358 // hostJS: opts['host-js'],
359 // httpdRoot: opts['file-dir'],
360 // tls: {
361 // certFile: opts['ca-cert'],
362 // keyFile: opts['ca-key']
363 // }
364 // })