3 let assert = require('assert')
4 let BuildCache = require('build_cache')
5 let commander = require('commander')
7 let http = require('http')
8 let https = require('https')
9 let jstemplate = require('jstemplate')
10 let less = require('less/lib/less-node')
11 let querystring = require('querystring')
12 let util = require('util')
13 let url = require('url')
14 let zetjs = require('zetjs')
16 let readFileAsync = util.promisify(fs.readFile)
17 let statAsync = util.promisify(fs.stat)
19 commander.version('1.0.0').option(
20 '-c, --enable-caching',
23 '-j, --ssl-cert [path]',
24 'Set SSL certificate [ssl/localhost_cert_bundle.pem]',
25 'ssl/localhost_cert_bundle.pem'
27 '-k, --ssl-key [path]',
28 'Set SSL private key [ssl/localhost_key.pem]',
29 'ssl/localhost_key.pem'
31 '-p, --http-port [port]',
32 'Set HTTP listen port, -1 disable [8080]',
35 '-q, --https-port [port]',
36 'Set HTTPS listen port, -1 disable [8443]',
40 let sites = JSON.parse(fs.readFileSync('config/sites.json'))
41 let mime_types = JSON.parse(fs.readFileSync('config/mime_types.json'))
42 let mime_type_default = 'application/octet-stream'
43 let mime_type_css = mime_types['css'] || mime_type_default
44 let mime_type_html = mime_types['html'] || mime_type_default
45 let build_cache_js = new BuildCache()
46 let build_cache_json = new BuildCache()
47 let build_cache_less = new BuildCache()
48 let build_cache_text = new BuildCache()
49 let build_cache_zet = new BuildCache()
51 let serve = (res, status, mime_type, data) => {
52 res.statusCode = status
53 // html files will be direct recipient of links/bookmarks so can't have
54 // a long lifetime, other files like css or images are often large files
55 // and won't change frequently (but we'll need cache busting eventually)
56 if (commander.enableCaching && mime_type !== mime_type_html)
57 res.setHeader('Cache-Control', 'max-age=3600')
58 res.setHeader('Content-Type', mime_type)
59 res.setHeader('Content-Length', data.length)
64 let body = '<html><body>Page not found</body></html>'
65 serve(res, 404, 'text/html; charset=utf-8', new Buffer(body, 'utf8'))
68 let redirect = (res, location) => {
70 res.setHeader('Location', location)
71 res.end('Redirecting to ' + location)
74 let app = async (req, res, protocol) => {
75 let site = req.headers.host || 'localhost'
76 let temp = site.indexOf(':')
77 let port_suffix = temp === -1 ? '' : site.substring(temp)
78 site = site.substring(0, site.length - port_suffix.length)
79 if (!sites.hasOwnProperty(site)) {
80 console.log('nonexistent site', site)
86 if (temp.type === 'redirect') {
87 let site_domain = temp.domain
88 console.log('redirecting', site, 'to', site_domain)
89 redirect(res, protocol + '://' + site_domain + port_suffix + req.url)
92 else if (temp.type === 'site')
97 // parse the pathname portion of url
98 // this is actually cheating since it's not a complete url
99 let parsed_url = url.parse(req.url, true)
100 let path = parsed_url.pathname.split('/')
102 // path must begin with /
103 if (path.length === 0 || path[0].length)
106 // path elements must be findable in the file system (thus can't be empty)
108 let dir_name_is_pub = false
109 for (var i = 1; i < path.length - 1; ++i) {
110 dir_name += '/' + path[i]
111 if (path[i].length === 0 || path[i].charAt(0) === '.') {
112 console.log(site, 'bad path component', dir_name)
117 stats = await statAsync(site_root + dir_name)
120 if (err.code !== 'ENOENT')
122 if (!dir_name_is_pub) {
123 temp = dir_name + '.pub'
125 stats = await statAsync(site_root + temp)
127 dir_name_is_pub = true
130 if (err2.code !== 'ENOENT')
132 console.log(site, 'directory not found', dir_name)
136 if (!stats.isDirectory()) {
137 console.log(site, 'not directory', dir_name)
143 file_name = path[path.length - 1]
144 if (file_name === '') {
145 path[path.length - 1] = 'index.html'
146 path = path.join('/')
147 console.log(site, 'redirecting', parsed_url.pathname, 'to', path)
148 redirect(res, path + (parsed_url.search || ''))
152 temp = file_name.lastIndexOf('.')
153 let file_type = temp === -1 ? '' : file_name.substring(temp + 1)
154 let mime_type = mime_types[file_type] || mime_type_default
156 let page = dir_name + '/' + file_name, data
157 if (dir_name_is_pub) {
159 data = await readFileAsync(site_root + page)
168 serve(res, 200, mime_type, data)
172 if (err.code !== 'ENOENT')
179 data = await readFileAsync(site_root + temp)
188 serve(res, 200, mime_type, data)
192 if (err.code !== 'ENOENT')
204 query: parsed_url.query,
208 let out = str => {buffers.push(Buffer.from(str))}
209 let req = async (str, type) => {
211 str.length > 0 && str.charAt(0) === '/' ?
213 site_root + dir_name + '/'
218 let render_func = await build_cache_js.get(key)
219 if (render_func === undefined) {
220 console.log(site, 'compiling', key)
221 render_func = await jstemplate(key)
222 build_cache_js.set(key, render_func)
224 result = await render_func(env, out, req)
227 result = await build_cache_json.get(key)
228 if (result === undefined) {
229 console.log(site, 'parsing', key)
230 result = JSON.parse(await readFileAsync(key))
231 build_cache_json.set(key, result)
235 result = await build_cache_text.get(key)
236 if (result === undefined) {
237 console.log(site, 'reading', key)
238 result = await readFileAsync(key, {encoding: 'utf-8'})
239 build_cache_text.set(key, result)
243 result = await build_cache_zet.get(key)
244 if (result === undefined) {
245 console.log(site, 'opening', key)
246 result = new zetjs.Index(key)
265 data = Buffer.concat(buffers)
274 serve(res, 200, mime_type, data)
278 if (err.code !== 'ENOENT') // note: err.code might be undefined
284 temp = page + '.less'
286 let key = site_root + temp
287 let data = await build_cache_less.get(key)
288 if (data === undefined) {
289 console.log(site, 'compiling', key)
290 let result = await less.render(
291 await readFileAsync(site_root + temp, {encoding: 'utf-8'}),
300 //javascriptEnabled: false,
304 paths: [site_root + dir_name],
306 //reUsePluginManager: true,
307 //rewriteUrls: false,
308 rootpath: site_root//,
309 //strictImports: false,
310 //strictUnits: false,
314 data = new Buffer(result.css, 'utf-8')
315 build_cache_less.set(key, data, result.imports)
325 serve(res, 200, mime_type, data)
329 if (err.code !== 'ENOENT') // note: err.code might be undefined
336 console.log(site, 'file not found', page)
340 let tryApp = (req, res, protocol) => {
341 app(req, res, protocol).catch(
343 console.log(err.stack || err.message)
345 '<html><body><pre>' +
346 (err.stack || err.message) +
347 '</pre></body></html>'
348 serve(res, 500, 'text/html; charset=utf-8', new Buffer(body, 'utf8'))
351 // note: the promise is forgotten about here, so each incoming request
352 // proceeds in an unsupervised fashion to eventual completion or error
355 if (commander.httpPort !== -1) {
357 (req, res) => tryApp(req, res, 'http')
358 ).listen(commander.httpPort)
359 console.log('HTTP server listening on port', commander.httpPort)
361 if (commander.httpsPort !== -1) {
364 'cert': fs.readFileSync(commander.sslCert),
365 'key': fs.readFileSync(commander.sslKey)
367 (req, res) => tryApp(req, res, 'https')
368 ).listen(commander.httpsPort)
369 console.log('HTTPS server listening on port', commander.httpsPort)