huskee-install
Version:
Huskee server installer
231 lines (197 loc) • 7.11 kB
JavaScript
// I use Google Closure compiler to optimize this file.
// Command: npx google-closure-compiler --js=routing.js --js_output_file=routing.compiled.js --module_resolution=NODE
const url = require('url')
const path = require('path')
// const Cache = require('./cache')
const fs = require('fs')
const zlib = require('zlib')
const mimeModule = require('./mime')
const punycode = require('punycode')
const forbidden = path.join(__dirname, '../www')
// let cache = new Cache()
let mtimeCache = new Map()
const net = require('net')
const config = require('../conf/main')
const hostsConfig = require('../conf/hosts')
const { proxyport } = config
let winStatic
let sendfile
const os = require('os')
if(os.platform() === 'win32') {
winStatic = require('./winstatic')
}
else {
sendfile = require('../build/Release/addon')
}
const fsStatAsync = path => new Promise(resolve => {
fs.stat(path, (err, stats) => err ? resolve(0) : resolve(stats))
})
class Connection {
constructor(req, res, data) {
this.req = req
this.res = res
this.rejected = false
this.urlParsed = url.parse(req.url, true)
if(data) {
try {
this.data = JSON.parse(data)
}
catch(e) {
console.log(e);
this.data = {}
}
}
else this.data = this.urlParsed.query
this.hostname = punycode.toUnicode(req.headers.host || '').replace(':', '_port_')
this.requestPath = this.urlParsed.pathname
this.extname = path.extname(this.requestPath).substring(1)
this.fullPath = path.join(__dirname, '../www', String(this.hostname), String(this.requestPath))
this.protocol = req.connection.encrypted ? 'https' : 'http'
// console.log(req.client._handle.fd, res.socket._handle.fd, req.client.server._handle.fd)
this.fd = req.client._handle.fd
this.decline = (error, errorname) => {
this._endConnection(200, { error, errorname })
this.rejected = true
}
this.declineWithError = (error, errorname) => {
this._endConnection(error, { error, errorname })
this.rejected = true
}
this.parseCookies = () => {
const { req } = this
return req.headers.cookie && req.headers.cookie.split(';').reduce ?
req.headers.cookie.split(';').reduce((obj, cookie) => {
const values = cookie.trim().split('=')
obj[values[0]] = values[1]
return obj
}, {}) : false
}
this._connect()
}
async _connect() {
const { fullPath, extname, res, hostname, requestPath } = this
this.fullPath = path.join(this.fullPath, 'index.html')
this.extname = 'html'
if(!fullPath.startsWith(forbidden)) return res.end('403')
const { loader } = ((hostsConfig || []).find(h => h.host === hostname) || {})
if(loader) var loaderResult = await loader({ connection: this, path: requestPath })
if(loaderResult) return
const api = await this._getAPI()
if(api === 'done') return
const page = await this._getPage()
if(page === 'unchanged') return this._endConnection(304)
else if(page === 'done') return
else if(page) return this._endConnectionWithPage(page)
this.fullPath = fullPath
this.extname = extname
const file = await this._getStatic()
if(file === 'unchanged') {
return this._endConnection(304)
}
else if(file === 'done') return
else if(file) return this._endConnection(200, file)
this._endWith404()
}
async renderStatic(receivedPath) {
this.fullPath = path.join(__dirname, '../www', String(this.hostname), String(receivedPath))
this.extname = receivedPath
await this._getStatic()
}
async _getStatic() {
if(winStatic) return await winStatic.bind(this)()
const { req, res, fullPath, extname } = this
const { headers } = req
const mime = mimeModule.see(extname)
const stats = await fsStatAsync(fullPath)
if(!stats) return
const mtime = stats.mtime.toUTCString()
const { size } = stats
// const fdout = this.fd
const gzipped = mime.includes('text/') || mime.includes('application/')
const unchanged = (
headers['cache-control'] !== 'no-cache' &&
headers['if-modified-since'] &&
headers['if-modified-since'] == mtime
)
res.writeHead(unchanged ? 304 : 200, {
'Content-Encoding': gzipped ? 'gzip' : 'plain',
'Content-Type': `${mime}`,
'Cache-Control': 'no-cache',
'Last-Modified': mtime,
})
if(unchanged) {
res.end()
return 'done'
}
await new Promise(resolve => fs.open(fullPath, 'r', async (error, fd) => {
if(error) return console.error(error)
const socket = new net.Socket()
const newfd = await new Promise(resolve => { socket.connect(proxyport, () => resolve(socket._handle.fd)) })
await new Promise(resolve => {
if(gzipped) socket.pipe(zlib.createGzip()).pipe(res)
else socket.pipe(res)
// socket.on('end', () => {
// res.end()
// resolve()
// })
sendfile.send(fd, newfd, size)
socket.end()
})
resolve()
}))
return 'done'
}
async _getAPI() {
const { hostname, requestPath } = this
const apiPath = path.join(__dirname, '../dynamic', hostname, requestPath, 'index.js')
const stats = await fsStatAsync(apiPath)
if(!stats) return ''
const mtime = stats.mtime.toUTCString()
if(mtimeCache.get(apiPath) !== mtime) delete require.cache[require.resolve(apiPath)]
try {
const module = require(apiPath)
mtimeCache.set(apiPath, mtime)
const data = await module.connect(this)
if(!data && this.rejected) return 'done'
const { mime, text, code, isHTML } = data
this._endConnection(code, text, mime, isHTML)
return 'done'
}
catch(e) {
//TODO: make error handling
this.declineWithError(500, 'Internal Server Error')
console.log(e)
return ''
}
}
async _getPage() {
const pageGiven = await this._getStatic()
if(!pageGiven) return ''
else return pageGiven
}
_endConnectionWithPage(data) {
this._endConnection(200, data, 'text/html; charset=utf-8')
}
renderFile({ file = '', mime = 'text/plain', status = 200 }) {
this._endConnection(status, file, mime, true)
}
_endConnection(status = 200, text, mime, isHTML, isFile) {
const { rejected, res } = this
if(rejected) return
mime = mime || (isHTML ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8')
try{
res.statusCode = status
res.setHeader('Content-Type', mime)
res.writeHead(status)
}
catch(e) {
console.log(e)
//TODO: Error checking
}
res.end((isHTML || isFile) ? text : JSON.stringify(text))
}
_endWith404() {
this._endConnection(404, 'There is no such file on the disk', 'text/html')
}
}
exports.Connection = Connection