3 var assert = require('assert')
4 var BuildCache = require('build_cache')
5 var commander = require('commander')
7 var http = require('http')
8 var https = require('https')
9 var jstemplate = require('jstemplate')
10 var less = require('less/lib/less-node')
11 var querystring = require('querystring')
12 var util = require('util')
13 var url = require('url')
15 var readFileAsync = util.promisify(fs.readFile)
16 var statAsync = util.promisify(fs.stat)
18 commander.version('1.0.0').option(
19 '-c, --enable-caching',
22 '-j, --ssl-cert [path]',
23 'Set SSL certificate [ssl/localhost_cert_bundle.pem]',
24 'ssl/localhost_cert_bundle.pem'
26 '-k, --ssl-key [path]',
27 'Set SSL private key [ssl/localhost_key.pem]',
28 'ssl/localhost_key.pem'
30 '-p, --http-port [port]',
31 'Set HTTP listen port, -1 disable [8080]',
34 '-q, --https-port [port]',
35 'Set HTTPS listen port, -1 disable [8443]',
39 var sites = JSON.parse(fs.readFileSync('config/sites.json'))
40 var mime_types = JSON.parse(fs.readFileSync('config/mime_types.json'))
41 var mime_type_default = 'application/octet-stream'
42 var mime_type_css = mime_types['css'] || mime_type_default
43 var mime_type_html = mime_types['html'] || mime_type_default
44 var build_cache_js = new BuildCache()
45 var build_cache_json = new BuildCache()
46 var build_cache_less = new BuildCache()
47 var build_cache_text = new BuildCache()
49 var serve = function(res, status, mime_type, data) {
50 res.statusCode = status
51 // html files will be direct recipient of links/bookmarks so can't have
52 // a long lifetime, other files like css or images are often large files
53 // and won't change frequently (but we'll need cache busting eventually)
54 if (commander.enableCaching && mime_type !== mime_type_html)
55 res.setHeader('Cache-Control', 'max-age=3600')
56 res.setHeader('Content-Type', mime_type)
57 res.setHeader('Content-Length', data.length)
61 var die = function(res) {
62 var body = '<html><body>Page not found</body></html>'
63 serve(res, 404, 'text/html; charset=utf-8', new Buffer(body, 'utf8'))
66 var redirect = function(res, location) {
68 res.setHeader('Location', location)
69 res.end('Redirecting to ' + location)
72 var app = async function(req, res, protocol) {
73 var site = req.headers.host || 'localhost'
74 var temp = site.indexOf(':')
75 var port_suffix = temp === -1 ? '' : site.substring(temp)
76 site = site.substring(0, site.length - port_suffix.length)
77 if (!sites.hasOwnProperty(site)) {
78 console.log('nonexistent site', site)
83 if (temp.type === 'redirect') {
84 var site_domain = temp.domain
85 console.log('redirecting', site, 'to', site_domain)
86 redirect(res, protocol + '://' + site_domain + port_suffix + req.url)
89 else if (temp.type === 'site')
90 var site_root = temp.root
94 // parse the pathname portion of url
95 // this is actually cheating since it's not a complete url
96 var parsed_url = url.parse(req.url, true)
97 var path = parsed_url.pathname.split('/')
99 // path must begin with /
100 if (path.length === 0 || path[0].length)
103 // path elements must be findable in the file system (thus can't be empty)
105 var dir_name_is_pub = false
106 for (var i = 1; i < path.length - 1; ++i) {
107 dir_name += '/' + path[i]
108 if (path[i].length === 0 || path[i].charAt(0) === '.') {
111 'bad path component',
117 var 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')
134 'directory not found',
140 if (!stats.isDirectory()) {
151 file_name = path[path.length - 1]
152 if (file_name === '') {
153 path[path.length - 1] = 'index.html'
154 path = path.join('/')
155 console.log(site, 'redirecting', parsed_url.pathname, 'to', path)
156 redirect(res, path + (parsed_url.search || ''))
160 temp = file_name.lastIndexOf('.')
161 var file_type = temp === -1 ? '' : file_name.substring(temp + 1)
162 var mime_type = mime_types[file_type] || mime_type_default
164 var site_path = dir_name + '/' + file_name, data
165 if (dir_name_is_pub) {
167 data = await readFileAsync(site_root + site_path)
176 serve(res, 200, mime_type, data)
180 if (err.code !== 'ENOENT')
185 temp = site_path + '.pub'
187 data = await readFileAsync(site_root + temp)
196 serve(res, 200, mime_type, data)
200 if (err.code !== 'ENOENT')
206 temp = site_path + '.js'
214 var out = str => {buffers.push(Buffer.from(str))}
215 var req = async (str, type) => {
217 str.length > 0 && str.charAt(0) === '/' ?
219 site_root + dir_name + '/'
224 var render_func = await build_cache_js.get(key)
225 if (render_func === undefined) {
226 console.log(site, 'compiling', key)
227 render_func = await jstemplate(key)
228 build_cache_js.set(key, render_func)
230 result = await render_func(env, out, req)
233 result = await build_cache_json.get(key)
234 if (result === undefined) {
235 console.log(site, 'parsing', key)
236 result = JSON.parse(await readFileAsync(key))
237 build_cache_json.set(key, result)
241 result = await build_cache_text.get(key)
242 if (result === undefined) {
243 console.log(site, 'reading', key)
244 result = await readFileAsync(key, {encoding: 'utf-8'})
245 build_cache_text.set(key, result)
254 data = Buffer.concat(buffers)
263 serve(res, 200, mime_type, data)
267 if (err.code !== 'ENOENT') // note: err.code might be undefined
273 temp = site_path + '.less'
275 var key = site_root + temp
276 var data = await build_cache_less.get(key)
277 if (data === undefined) {
278 console.log(site, 'compiling', key)
279 var result = await less.render(
280 await readFileAsync(site_root + temp, {encoding: 'utf-8'}),
289 //javascriptEnabled: false,
293 paths: [site_root + dir_name],
295 //reUsePluginManager: true,
296 //rewriteUrls: false,
297 rootpath: site_root//,
298 //strictImports: false,
299 //strictUnits: false,
303 data = new Buffer(result.css, 'utf-8')
304 build_cache_less.set(key, data, result.imports)
314 serve(res, 200, mime_type, data)
318 if (err.code !== 'ENOENT') // note: err.code might be undefined
332 // enable this when we want to serve particular files programmatically:
334 //case '/search.html':
335 // var first = parseInt(parsed_url.query.first || '0')
336 // var temp = sites[site].zet_index.search(
337 // parsed_url.query.query,
344 // parsed_url.query.query,
346 // 'results', temp.results.length,
348 // temp.total_results
350 // console.log('sites[site]', sites[site])
351 // console.log('sites[site].globals', sites[site].globals)
352 // //console.log('globals.menu', sites[site].globals.menu, 'globals.blog', sites[site].globals.blog)
353 // var body = sites[site].render_search(
355 // 'globals': sites[site].globals,
356 // 'modules': sites[site].modules,
359 // 'query': parsed_url.query.query,
361 // 'results': temp.results,
362 // 'total_results': temp.total_results
366 // serve(res, 200, 'text/html; charset=utf-8', new Buffer(body, 'utf8'))
371 var tryApp = function(req, res, protocol) {
372 app(req, res, protocol).catch(
374 console.log(err.stack || err.message)
376 '<html><body><pre>' +
377 (err.stack || err.message) +
378 '</pre></body></html>'
379 serve(res, 500, 'text/html; charset=utf-8', new Buffer(body, 'utf8'))
382 // note: the promise is forgotten about here, so each incoming request
383 // proceeds in an unsupervised fashion to eventual completion or error
386 if (commander.httpPort !== -1)
389 return tryApp(req, res, 'http')
391 ).listen(commander.httpPort)
392 console.log('HTTP server listening on port', commander.httpPort)
393 if (commander.httpsPort !== -1)
396 'cert': fs.readFileSync(commander.sslCert),
397 'key': fs.readFileSync(commander.sslKey)
400 return tryApp(req, res, 'https')
402 ).listen(commander.httpsPort)
403 console.log('HTTPS server listening on port', commander.httpsPort)