+let analytics = require('./analytics')
+let assert = require('assert')
+let cookie = require('cookie')
+let crypto = require('crypto')
+let fs = require('fs')
+let config = require('./config')
+let resources = require('./resources')
+let util = require('util')
+let url = require('url')
+let XDate = require('xdate')
+
+let fs_readFile = util.promisify(fs.readFile)
+let fs_stat = util.promisify(fs.stat)
+
+let serve = (res, status, mime_type, data) => {
+ res.statusCode = status
+ // html files will be direct recipient of links/bookmarks so can't have
+ // a long lifetime, other files like css or images are often large files
+ // and won't change frequently (but we'll need cache busting eventually)
+ if (
+ false && //commander.enableCaching &&
+ mime_type !== config.mime_types_html
+ )
+ res.setHeader('Cache-Control', 'max-age=3600')
+ res.setHeader('Content-Type', mime_type)
+ res.setHeader('Content-Length', data.length)
+ res.end(data)
+}
+
+let die = res => {
+ let body = '<html><body>Page not found</body></html>'
+ serve(res, 404, config.mime_type_html, new Buffer(body, 'utf8'))
+}
+
+let redirect = (res, location) => {
+ res.statusCode = 301
+ res.setHeader('Location', location)
+ res.end('Redirecting to ' + location)
+}
+
+let app = async (req, res, protocol) => {
+ let site = req.headers.host || 'localhost'
+ let temp = site.indexOf(':')
+ let port_suffix = temp === -1 ? '' : site.substring(temp)
+ site = site.substring(0, site.length - port_suffix.length)
+ if (!config.sites.hasOwnProperty(site)) {
+ console.log('nonexistent site', site)
+ die(res)
+ return
+ }
+ temp = config.sites[site]
+ let site_root
+ if (temp.type === 'redirect') {
+ let site_domain = temp.domain
+ console.log('redirecting', site, 'to', site_domain)
+ redirect(res, protocol + '://' + site_domain + port_suffix + req.url)
+ return
+ }
+ else if (temp.type === 'site')
+ site_root = temp.root
+ else
+ assert(false)
+
+ // parse the pathname portion of url
+ // this is actually cheating since it's not a complete url
+ let parsed_url = url.parse(req.url, true)
+ let path = parsed_url.pathname.split('/')
+
+ // path must begin with /
+ if (path.length === 0 || path[0].length)
+ return die(res)
+
+ // path elements must be findable in the file system (thus can't be empty)
+ let dir_name = ''
+ let dir_name_is_pub = false
+ for (let i = 1; i < path.length - 1; ++i) {
+ dir_name += '/' + path[i]
+ if (path[i].length === 0 || path[i].charAt(0) === '.') {
+ console.log(site, 'bad path component', dir_name)
+ return die(res)
+ }
+ let stats
+ try {
+ stats = await fs_stat(site_root + dir_name)
+ }
+ catch (err) {
+ if (err.code !== 'ENOENT')
+ throw err
+ if (!dir_name_is_pub) {
+ temp = dir_name + '.pub'
+ try {
+ stats = await fs_stat(site_root + temp)
+ dir_name = temp
+ dir_name_is_pub = true
+ }
+ catch (err2) {
+ if (err2.code !== 'ENOENT')
+ throw err2
+ console.log(site, 'directory not found', dir_name)
+ return die(res)
+ }
+ }
+ if (!stats.isDirectory()) {
+ console.log(site, 'not directory', dir_name)
+ return die(res)
+ }
+ }
+ }
+
+ file_name = path[path.length - 1]
+ if (file_name === '') {
+ path[path.length - 1] = 'index.html'
+ path = path.join('/')
+ console.log(site, 'redirecting', parsed_url.pathname, 'to', path)
+ redirect(res, path + (parsed_url.search || ''))
+ return
+ }
+ let page = path.slice(1).join('/')
+
+ temp = file_name.lastIndexOf('.')
+ let file_type = temp === -1 ? '' : file_name.substring(temp + 1)
+ let mime_type =
+ config.mime_types.hasOwnProperty(file_type) ?
+ config.mime_types[file_type] :
+ config.mime_type_default
+
+ if (file_type == 'html') {
+ if (!analytics.sessions.hasOwnProperty(site))
+ analytics.sessions[site] = {}
+ let site_sessions = analytics.sessions[site]
+ let cookies = cookie.parse(req.headers.cookie || ''), session_key
+ if (
+ !cookies.hasOwnProperty('session_key') ||
+ !site_sessions.hasOwnProperty(session_key = cookies.session_key)
+ ) {
+ session_key = crypto.randomBytes(16).toString('hex')
+ site_sessions[session_key] = {}
+ }
+ let session = site_sessions[session_key]
+
+ let expires = new XDate()
+ expires.addMonths(1)
+ expires = expires.toUTCString()
+ res.setHeader(
+ 'Set-Cookie',
+ 'session_key=' + session_key + '; expires=' + expires + '; path=/;'
+ )
+ session.expires = expires
+
+ if (!analytics.pageviews.hasOwnProperty(site))
+ analytics.pageviews[site] = {}
+ let site_pageviews = analytics.pageviews[site]
+ if (!site_pageviews.hasOwnProperty(page))
+ site_pageviews[page] = {visits: 0, unique_visits: 0}
+ let pageview = site_pageviews[page]
+ ++pageview.visits;
+
+
+ if (!session.hasOwnProperty('analytics.pageviews'))
+ session.pageviews = {}
+ let session_pageviews = session.pageviews
+ if (!session_pageviews.hasOwnProperty(page)) {
+ session_pageviews[page] = 0
+ ++pageview.unique_visits
+ }
+ ++session_pageviews[page]
+
+ analytics.sessions_dirty()
+ analytics.pageviews_dirty()
+ }
+
+ /*let*/ page = dir_name + '/' + file_name; let data
+ if (dir_name_is_pub) {
+ try {
+ let data = await fs_readFile(site_root + page)
+ console.log(
+ site,
+ 'serving',
+ page,
+ 'length',
+ data.length,
+ 'from pub'
+ )
+ serve(res, 200, mime_type, data)
+ return
+ }
+ catch (err) {
+ if (err.code !== 'ENOENT')
+ throw err
+ }
+ }
+ else {
+ temp = page + '.pub'
+ try {
+ let data = await fs_readFile(site_root + temp)
+ console.log(
+ site,
+ 'serving',
+ temp,
+ 'length',
+ data.length,
+ 'from pub'
+ )
+ serve(res, 200, mime_type, data)
+ return
+ }
+ catch (err) {
+ if (err.code !== 'ENOENT')
+ throw err
+ }
+
+ switch (file_type) {
+ case 'html':
+ temp = page + '.js'
+ try {
+ let buffers = []
+ let env = {
+ lang: 'en',
+ page: page,
+ query: parsed_url.query,
+ site: site,
+ site_root: site_root
+ }
+ let out = str => {buffers.push(Buffer.from(str))}
+ let req = async (str, type) => {
+ let path = (
+ str.length > 0 && str.charAt(0) === '/' ?
+ site_root :
+ site_root + dir_name + '/'
+ ) + str, result
+ switch (type) {
+ case undefined:
+ result = await (await resources.req_js(path))(env, out, req)
+ break
+ case 'js':
+ result = await resources.req_js(path)
+ break
+ case 'json':
+ result = await resources.req_json(path)
+ break
+ case 'text':
+ result = await resources.req_text(path)
+ break
+ case 'zet':
+ result = await resources.req_zet(path)
+ break
+ default:
+ assert(false)
+ }
+ return result
+ }
+ await req(temp)
+ let data = Buffer.concat(buffers)
+ console.log(
+ site,
+ 'serving',
+ temp,
+ 'length',
+ data.length,
+ 'from js'
+ )
+ serve(res, 200, mime_type, data)
+ return
+ }
+ catch (err) {
+ if (err.code !== 'ENOENT') // should check error type
+ throw err
+ }
+ break
+
+ case 'css':
+ temp = page + '.less'
+ try {
+ let data = await resources.req_less(site_root + temp, site_root, dir_name)
+ console.log(
+ site,
+ 'serving',
+ temp,
+ 'length',
+ data.length,
+ 'from less'
+ )
+ serve(res, 200, mime_type, data)
+ return
+ }
+ catch (err) {
+ if (err.code !== 'ENOENT') // note: err.code might be undefined
+ throw err
+ }
+ break
+ }
+ }
+
+ let favicons = await resources.req_zip(site_root + '/favicons.zip')
+ temp = page.substring(1) // fix this to avoid leading / on all absolute paths
+ if (favicons.hasOwnProperty(temp)) {
+ let data = favicons[temp]
+ console.log(
+ site,
+ 'serving',
+ page,
+ 'length',
+ data.length,
+ 'from favicons'
+ )
+ serve(res, 200, mime_type, data)
+ return
+ }
+
+ console.log(site, 'file not found', page)
+ return die(res)
+}
+
+let tryApp = async (req, res, protocol) => {
+ await config.refresh()
+ try {
+ await app(req, res, protocol)
+ }
+ catch (err) {
+ let message = (err.stack || err.message).toString()
+ console.error(message)
+ let body = '<html><body><pre>' + message + '</pre></body></html>'
+ serve(res, 500, config.mime_type_html, new Buffer(body, 'utf8'))
+ }
+}
+
+exports.serve = serve
+exports.die = die
+exports.redirect = redirect
+exports.app = app
+exports.tryApp = tryApp