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