UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

305 lines (267 loc) 7.8 kB
import * as Primus from "primus"; import * as fs from "fs"; import * as path from "path"; import * as util from "util"; import * as uuid from "uuid"; import { api, config, utils, log, Server, Connection } from "../index"; export class WebSocketServer extends Server { server: Primus; constructor() { super(); this.type = "websocket"; this.attributes = { canChat: true, logConnections: true, logExits: true, sendWelcomeMessage: true, verbs: [ "quit", "exit", "documentation", "roomAdd", "roomLeave", "roomView", "detailsView", "say", ], }; } async initialize() { // we rely on the web server :D } async start() { const webserver = api.servers.servers.web; if (!webserver) { throw new Error(`websocket server requires web server to be enabled`); } this.server = new Primus(webserver.server, this.config.server); this.writeClientJS(); this.server.on("connection", (rawConnection) => { this.handleConnection(rawConnection); }); this.server.on("disconnection", (rawConnection) => { this.handleDisconnection(rawConnection); }); this.log( `webSockets bound to ${webserver.config.bindIP}:${webserver.config.port}`, "debug", ); this.on("connection", (connection: Connection) => { connection.rawConnection.on("data", (data: Record<string, any>) => { this.handleData(connection, data); }); }); this.on("actionComplete", (data) => { if (data.toRender !== false) { data.connection.response.messageId = data.messageId; this.sendMessage(data.connection, data.response, data.messageId); } }); } async stop() { if (!this.server) return; if (this.config.destroyClientsOnShutdown === true) { this.connections().forEach((connection: Connection) => { connection.destroy(); }); } //@ts-ignore this.server.destroy(); } async sendMessage( connection: Connection, message: Record<string, any>, messageId: string, ) { if (message.error) { message.error = config.errors.serializers.servers.websocket( message.error, ); } if (!message.context) { message.context = "response"; } if (!messageId) { messageId = connection.messageId; } if (message.context === "response" && !message.messageId) { message.messageId = messageId; } connection.rawConnection.write(message); } async sendFile( connection: Connection, error: NodeJS.ErrnoException, fileStream: any, mime: string, length: number, lastModified: Date, ) { const messageId = connection.messageId; let content = ""; const response = { error: error, content: null as string, mime: mime, length: length, lastModified: lastModified, }; try { if (!error) { fileStream.on("data", (d: string) => { content += d; }); fileStream.on("end", () => { response.content = content; this.sendMessage(connection, response, messageId); }); } else { this.sendMessage(connection, response, messageId); } } catch (e) { this.log(e, "warning"); this.sendMessage(connection, response, messageId); } } //@ts-ignore goodbye(connection: Connection) { connection.rawConnection.end(); } compileActionheroWebsocketClientJS() { let ahClientSource = fs .readFileSync( path.join(__dirname, "/../../client/ActionheroWebsocketClient.js"), ) .toString(); const url = this.config.clientUrl; ahClientSource = ahClientSource.replace(/%%URL%%/g, url); const defaults: { [key: string]: any; } = {}; for (const i in this.config.client) { defaults[i] = this.config.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; } renderClientJS() { const libSource = api.servers.servers.websocket.server.library(); let ahClientSource = this.compileActionheroWebsocketClientJS(); ahClientSource = ";;;\r\n" + "(function(exports){ \r\n" + ahClientSource + "\r\n" + "exports.ActionheroWebsocketClient = ActionheroWebsocketClient; \r\n" + "exports.ActionheroWebsocketClient = ActionheroWebsocketClient; \r\n" + "})(typeof exports === 'undefined' ? window : exports);"; return libSource + "\r\n\r\n\r\n" + ahClientSource; } writeClientJS() { if ( !config.general.paths.public || config.general.paths.public.length === 0 ) { return; } if (this.config.clientJsPath && this.config.clientJsName) { const clientJSPath = path.normalize( config.general.paths.public[0] + path.sep + this.config.clientJsPath + path.sep, ); const clientJSName = this.config.clientJsName; const clientJSFullPath = clientJSPath + clientJSName; try { if (!fs.existsSync(clientJSPath)) { fs.mkdirSync(clientJSPath, { recursive: true }); } fs.writeFileSync(clientJSFullPath + ".js", this.renderClientJS()); log(`wrote ${clientJSFullPath}.js`, "debug"); } catch (e) { log("Cannot write client-side JS for websocket server:", "alert", e); throw e; } } } handleConnection(rawConnection: Primus.Spark) { const fingerprint = rawConnection.query[config.web.fingerprintOptions.cookieKey]; const { ip, port } = utils.parseHeadersForClientAddress( rawConnection.headers, ); this.buildConnection({ rawConnection: rawConnection, remoteAddress: ip || rawConnection.address.ip, remotePort: port || rawConnection.address.port, fingerprint: fingerprint, }); } handleDisconnection(rawConnection: Primus.Spark) { const connections = this.connections(); for (const i in connections) { if ( connections[i] && rawConnection.id === connections[i].rawConnection.id ) { connections[i].destroy(); break; } } } async handleData(connection: Connection, data: Record<string, any>) { const verb = data.event; delete data.event; connection.messageId = data.messageId || uuid.v4(); delete data.messageId; connection.params = {}; if (verb === "action") { for (const v in data.params) { connection.params[v] = data.params[v]; } connection.error = null; connection.response = {}; return this.processAction(connection); } if (verb === "file") { connection.params = { file: data.file, }; return this.processFile(connection); } const words = []; let message; if (data.room) { words.push(data.room); delete data.room; } for (const i in data) { words.push(data[i]); } const messageId = connection.messageId; try { const data = await connection.verbs(verb, words); message = { status: "OK", context: "response", data: data }; return this.sendMessage(connection, message, messageId); } catch (error) { const formattedError = error.toString(); message = { status: formattedError, error: formattedError, context: "response", data: data, }; return this.sendMessage(connection, message, messageId); } } }