7750dc2b76ef1ff05bfcecae24137a3d3d081de8
[jst_server.git] / Server.js
1 let BuildCache = require('BuildCache')
2 let JSONCache = require('JSONCache')
3 let Site = require('./Site')
4 let assert = require('assert')
5 let fs = require('fs')
6 let js_template = require('js_template')
7 let url = require('url')
8 let util = require('util')
9
10 let fs_readFile = util.promisify(fs.readFile)
11
12 let Server = function(socket_io, caching) {
13   if (!this instanceof Server)
14     throw Error('Server is a constructor')
15
16   this.socket_io = socket_io
17   this.caching = caching || false
18   this.site_cache = {}
19
20   this.build_cache_email = new BuildCache()
21   this.build_cache_json = new BuildCache()
22   this.build_cache_less = new BuildCache()
23   this.build_cache_text = new BuildCache()
24   this.build_cache_zet = new BuildCache()
25   this.build_cache_zip = new BuildCache()
26   this.json_cache = new JSONCache(true)
27
28   this.sites = undefined
29   this.mime_types = undefined
30   this.mime_type_html = undefined
31   this.mime_type_default = 'application/octet-stream'
32 }
33
34 Server.prototype.attach = function(server, protocol) {
35   server.on(
36     'request',
37     (request, response) =>
38       this.respond(request, response, protocol) // ignore returned promise
39   )
40   if (this.socket_io !== undefined)
41     this.socket_io.attach(server)
42 }
43
44 Server.prototype.refresh_config = async function() {
45   this.sites = await this.build_cache_json.get(
46     'config/sites.json',
47     async result => {
48       result.value = JSON.parse(await fs_readFile('config/sites.json'))
49     }
50   )
51   this.mime_types = await this.build_cache_json.get(
52     'config/mime_types.json',
53     async result => {
54       result.value = JSON.parse(await fs_readFile('config/mime_types.json'))
55     }
56   )
57   this.mime_type_html =
58     Object.prototype.hasOwnProperty.call(this.mime_types, '.html') ?
59     this.mime_types['.html'] :
60     this.mime_type_default
61 }
62
63 Server.prototype.serve = function(response, status, mime_type, data) {
64   response.statusCode = status
65   // html files will be direct recipient of links/bookmarks so can't have
66   // a long lifetime, other files like css or images are often large files
67   // and won't change frequently (but we'll need cache busting eventually)
68   if (this.caching && mime_type !== this.mime_types_html)
69     response.setHeader('Cache-Control', 'max-age=3600')
70   response.setHeader('Content-Type', mime_type)
71   response.setHeader('Content-Length', data.length)
72   response.end(data)
73 }
74
75 Server.prototype.die = function(response) {
76   let body = '<html><body>Page not found</body></html>'
77   this.serve(response, 404, this.mime_type_html, Buffer.from(body))
78 }
79
80 Server.prototype.redirect = function(response, location) {
81   response.statusCode = 301
82   response.setHeader('Location', location)
83   response.end('Redirecting to ' + location)
84 }
85
86 let site_factory_default = async root => new Site(root)
87
88 Server.prototype.respond = async function(request, response, protocol) {
89   try {
90     await this.refresh_config()
91     let parsed_url = url.parse(
92       protocol + '//' + (request.headers.host || 'localhost') + request.url,
93       true
94     )
95     //console.log('parsed_url', parsed_url)
96
97     if (!Object.prototype.hasOwnProperty.call(this.sites, parsed_url.hostname)) {
98       console.log('nonexistent site', parsed_url.hostname)
99       this.die(response)
100       return
101     }
102     let temp = this.sites[parsed_url.hostname]
103     switch (temp.type) {
104     case 'redirect':
105       let hostname = temp.domain
106       if (parsed_url.port !== undefined)
107         hostname += ':' + parsed_url.port
108       console.log('redirecting', parsed_url.host, 'to', hostname)
109       this.redirect(response, parsed_url.protocol + '//' + hostname + request.url)
110       break
111     case 'site':
112       let site_factory
113       try {
114         site_factory = await js_template(
115           temp.root,
116           temp.root,
117           'site_factory.jst'
118         )
119       }
120       catch (err) {
121         if (err.code !== 'ENOENT') // note: err.code might be undefined
122           throw err
123         site_factory = site_factory_default
124       }
125       let site = undefined
126       if (
127         !Object.prototype.hasOwnProperty.call(this.site_cache, temp.root) ||
128         (site = this.site_cache[temp.root]).factory !== site_factory
129       ) {
130         if (site !== undefined)
131           for (let i of site.object.socket_io_connect_listeners) {
132             assert(this.socket_io !== undefined)
133             this.socket_io.removeListener('connect', i)
134           }
135         site = {
136           factory: site_factory,
137           object: await site_factory(this, temp.root)
138         }
139         for (let i of site.object.socket_io_connect_listeners) {
140           assert(this.socket_io !== undefined)
141           this.socket_io.on('connect', i)
142         }
143         this.site_cache[temp.root] = site
144       }
145       await site.object.respond(
146         {
147           parsed_url: parsed_url,
148           pathname: parsed_url.pathname,
149           pathname_pos: 0,
150           response: response,
151           request: request,
152           server: this,
153           site: site.object
154         }
155       )
156       break
157     default:
158       assert(false)
159     }
160   }
161   catch (err) {
162     let message = (err.stack || err.message).toString()
163     console.error(message)
164     let body = '<html><body><pre>' + message + '</pre></body></html>'
165     this.serve(response, 500, this.mime_type_html, Buffer.from(body, 'utf8'))
166   }
167 }
168
169 module.exports = Server