actionhero
Version:
actionhero.js is a multi-transport API Server with integrated cluster capabilities and delayed tasks
263 lines (231 loc) • 7.87 kB
JavaScript
const Primus = require('primus')
const UglifyJS = require('uglify-js')
const fs = require('fs')
const path = require('path')
const util = require('util')
const browserFingerprint = require('browser_fingerprint')
const initialize = function (api, options, next) {
// ////////
// INIT //
// ////////
const type = 'websocket'
const attributes = {
canChat: true,
logConnections: true,
logExits: true,
sendWelcomeMessage: true,
verbs: [
'quit',
'exit',
'documentation',
'roomAdd',
'roomLeave',
'roomView',
'detailsView',
'say'
]
}
const server = new api.GenericServer(type, options, attributes)
// ////////////////////
// REQUIRED METHODS //
// ////////////////////
server.start = function (next) {
const webserver = api.servers.servers.web
server.server = new Primus(webserver.server, api.config.servers.websocket.server)
server.server.on('connection', (rawConnection) => {
handleConnection(rawConnection)
})
server.server.on('disconnection', (rawConnection) => {
handleDisconnection(rawConnection)
})
api.log(`webSockets bound to ${webserver.options.bindIP}: ${webserver.options.port}`, 'debug')
server.active = true
server.writeClientJS()
next()
}
server.stop = function (next) {
server.active = false
if (api.config.servers.websocket.destroyClientsOnShutdown === true) {
server.connections().forEach((connection) => {
connection.destroy()
})
}
process.nextTick(next)
}
server.sendMessage = function (connection, message, messageCount) {
if (message.error) {
message.error = api.config.errors.serializers.servers.websocket(message.error)
}
if (!message.context) { message.context = 'response' }
if (!messageCount) { messageCount = connection.messageCount }
if (message.context === 'response' && !message.messageCount) { message.messageCount = messageCount }
connection.rawConnection.write(message)
}
server.sendFile = function (connection, error, fileStream, mime, length, lastModified) {
let content = ''
let response = {
error: error,
content: null,
mime: mime,
length: length,
lastModified: lastModified
}
try {
if (!error) {
fileStream.on('data', function (d) { content += d })
fileStream.on('end', () => {
response.content = content
server.sendMessage(connection, response, connection.messageCount)
})
} else {
server.sendMessage(connection, response, connection.messageCount)
}
} catch (e) {
api.log(e, 'warning')
server.sendMessage(connection, response, connection.messageCount)
}
}
server.goodbye = function (connection) {
connection.rawConnection.end()
}
// //////////
// EVENTS //
// //////////
server.on('connection', function (connection) {
connection.rawConnection.on('data', (data) => {
handleData(connection, data)
})
})
server.on('actionComplete', function (data) {
if (data.toRender !== false) {
data.connection.response.messageCount = data.messageCount
server.sendMessage(data.connection, data.response, data.messageCount)
}
})
// //////////
// CLIENT //
// //////////
server.compileActionheroClientJS = function () {
let ahClientSource = fs.readFileSync(path.join(__dirname, '/../client/actionheroClient.js')).toString()
let url = api.config.servers.websocket.clientUrl
ahClientSource = ahClientSource.replace(/%%URL%%/g, url)
let defaults = {}
for (let i in api.config.servers.websocket.client) {
defaults[i] = api.config.servers.websocket.client[i]
}
defaults.url = url
let defaultsString = util.inspect(defaults)
defaultsString = defaultsString.replace('\'window.location.origin\'', 'window.location.origin')
ahClientSource = ahClientSource.replace('%%DEFAULTS%%', 'return ' + defaultsString)
return ahClientSource
}
server.renderClientJS = function (minimize) {
if (!minimize) { minimize = false }
let libSource = api.servers.servers.websocket.server.library()
let ahClientSource = server.compileActionheroClientJS()
ahClientSource =
';;;\r\n' +
'(function(exports){ \r\n' +
ahClientSource +
'\r\n' +
'exports.ActionheroClient = ActionheroClient; \r\n' +
'exports.actionheroClient = actionheroClient; \r\n' +
'})(typeof exports === \'undefined\' ? window : exports);'
if (minimize) {
return UglifyJS.minify(libSource + '\r\n\r\n\r\n' + ahClientSource).code
} else {
return (libSource + '\r\n\r\n\r\n' + ahClientSource)
}
}
server.writeClientJS = function () {
if (!api.config.general.paths['public'] || api.config.general.paths['public'].length === 0) {
return
}
if (api.config.servers.websocket.clientJsPath && api.config.servers.websocket.clientJsName) {
let clientJSPath = path.normalize(
api.config.general.paths['public'][0] +
path.sep +
api.config.servers.websocket.clientJsPath +
path.sep
)
let clientJSName = api.config.servers.websocket.clientJsName
let clientJSFullPath = clientJSPath + clientJSName
try {
if (!fs.existsSync(clientJSPath)) {
fs.mkdirSync(clientJSPath)
}
fs.writeFileSync(clientJSFullPath + '.js', server.renderClientJS(false))
api.log(`wrote ${clientJSFullPath}.js`, 'debug')
fs.writeFileSync(clientJSFullPath + '.min.js', server.renderClientJS(true))
api.log(`wrote ${clientJSFullPath}.min.js`, 'debug')
} catch (e) {
api.log('Cannot write client-side JS for websocket server:', 'warning')
api.log(e, 'warning')
throw e
}
}
}
// ///////////
// HELPERS //
// ///////////
const handleConnection = function (rawConnection) {
const parsedCookies = browserFingerprint.parseCookies(rawConnection)
const fingerprint = parsedCookies[api.config.servers.web.fingerprintOptions.cookieKey]
server.buildConnection({
rawConnection: rawConnection,
remoteAddress: rawConnection.address.ip,
remotePort: rawConnection.address.port,
fingerprint: fingerprint
})
}
const handleDisconnection = function (rawConnection) {
for (let i in server.connections()) {
if (server.connections()[i] && rawConnection.id === server.connections()[i].rawConnection.id) {
server.connections()[i].destroy()
break
}
}
}
const handleData = function (connection, data) {
const verb = data.event
delete data.event
connection.messageCount++
const messageCount = connection.messageCount
connection.params = {}
if (verb === 'action') {
for (let v in data.params) {
connection.params[v] = data.params[v]
}
connection.error = null
connection.response = {}
server.processAction(connection)
} else if (verb === 'file') {
connection.params = {
file: data.file
}
server.processFile(connection)
} else {
let words = []
let message
if (data.room) {
words.push(data.room)
delete data.room
}
for (let i in data) { words.push(data[i]) }
connection.verbs(verb, words, (error, data) => {
if (!error) {
message = {status: 'OK', context: 'response', data: data}
server.sendMessage(connection, message, messageCount)
} else {
message = {status: error, context: 'response', data: data}
server.sendMessage(connection, message, messageCount)
}
})
}
}
next(server)
}
// ///////////////////////////////////////////////////////////////////
// exports
exports.initialize = initialize