UNPKG

7.36 kBJavaScriptView Raw
1/*
2 * OS.js - JavaScript Cloud/Web Desktop Platform
3 *
4 * Copyright (c) 2011-2020, Anders Evenrud <andersevenrud@gmail.com>
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright notice, this
11 * list of conditions and the following disclaimer
12 * 2. Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 *
27 * @author Anders Evenrud <andersevenrud@gmail.com>
28 * @licence Simplified BSD License
29 */
30
31const fs = require('fs-extra');
32const http = require('http');
33const https = require('https');
34const path = require('path');
35const morgan = require('morgan');
36const express = require('express');
37const minimist = require('minimist');
38const deepmerge = require('deepmerge');
39const consola = require('consola');
40const {CoreBase} = require('@osjs/common');
41const {argvToConfig, createSession, createWebsocket, parseJson} = require('./utils/core.js');
42const {defaultConfiguration} = require('./config.js');
43const logger = consola.withTag('Core');
44
45let _instance;
46
47/**
48 * OS.js Server Core
49 */
50class Core extends CoreBase {
51
52 /**
53 * Creates a new instance
54 * @param {Object} cfg Configuration tree
55 * @param {Object} [options] Options
56 */
57 constructor(cfg, options = {}) {
58 options = {
59 argv: process.argv.splice(2),
60 root: process.cwd(),
61 ...options
62 };
63
64 const argv = minimist(options.argv);
65 const val = k => argvToConfig[k](parseJson(argv[k]));
66 const keys = Object.keys(argvToConfig).filter(k => Object.prototype.hasOwnProperty.call(argv, k));
67 const argvConfig = keys.reduce((o, k) => {
68 logger.info(`CLI argument '--${k}' overrides`, val(k));
69 return {...o, ...deepmerge(o, val(k))};
70 }, {});
71
72 super(defaultConfiguration, deepmerge(cfg, argvConfig), options);
73
74 this.logger = consola.withTag('Internal');
75 this.app = express();
76
77 if (!this.configuration.public) {
78 throw new Error('The public option is required');
79 }
80
81 this.httpServer = this.config('https.enabled')
82 ? https.createServer(this.config('https.options'), this.app)
83 : http.createServer(this.app);
84
85 this.session = createSession(this.app, this.configuration);
86 this.ws = createWebsocket(this.app, this.configuration, this.session, this.httpServer);
87 this.wss = this.ws.getWss();
88
89 _instance = this;
90 }
91
92 /**
93 * Destroys the instance
94 */
95 async destroy(done = () => {}) {
96 if (this.destroyed) {
97 return;
98 }
99
100 this.emit('osjs/core:destroy');
101
102 logger.info('Shutting down...');
103
104 if (this.wss) {
105 this.wss.close();
106 }
107
108 const finish = (error) => {
109 if (error) {
110 logger.error(error);
111 }
112
113 if (this.httpServer) {
114 this.httpServer.close(done);
115 } else {
116 done();
117 }
118 };
119
120 try {
121 await super.destroy();
122 finish();
123 } catch (e) {
124 finish(e);
125 }
126 }
127
128 /**
129 * Starts the server
130 * @return {Promise<boolean>}
131 */
132 async start() {
133 if (!this.started) {
134 logger.info('Starting services...');
135
136 await super.start();
137
138 logger.success('Initialized!');
139
140 this.listen();
141 }
142
143 return true;
144 }
145
146 /**
147 * Initializes the server
148 * @return {Promise<boolean>}
149 */
150 async boot() {
151 if (this.booted) {
152 return true;
153 }
154
155 this.emit('osjs/core:start');
156
157 if (this.configuration.logging) {
158 this.wss.on('connection', (c) => {
159 logger.log('WebSocket connection opened');
160 c.on('close', () => logger.log('WebSocket connection closed'));
161 });
162
163 if (this.configuration.morgan) {
164 this.app.use(morgan(this.configuration.morgan));
165 }
166 }
167
168
169 logger.info('Initializing services...');
170
171 await super.boot();
172 this.emit('init');
173 await this.start();
174 this.emit('osjs/core:started');
175
176 return true;
177 }
178
179 /**
180 * Opens HTTP server
181 */
182 listen() {
183 const httpPort = this.config('port');
184 const wsPort = this.config('ws.port') || httpPort;
185 const pub = this.config('public');
186 const session = path.basename(path.dirname(this.config('session.store.module')));
187 const dist = pub.replace(process.cwd(), '');
188 const secure = this.config('https.enabled', false);
189 const proto = prefix => `${prefix}${secure ? 's' : ''}://`;
190 const host = port => `${this.config('hostname')}:${port}`;
191
192 logger.info('Opening server connection');
193
194 const checkFile = path.join(pub, this.configuration.index);
195 if (!fs.existsSync(checkFile)) {
196 logger.warn('Missing files in "dist/" directory. Did you forget to run "npm run build" ?');
197 }
198
199 this.httpServer.listen(httpPort, () => {
200 logger.success(`Using '${session}' sessions`);
201 logger.success(`Serving '${dist}'`);
202 logger.success(`WebSocket listening on ${proto('ws')}${host(wsPort)}`);
203 logger.success(`Server listening on ${proto('http')}${host(httpPort)}`);
204 });
205 }
206
207 /**
208 * Broadcast given event to client
209 * @param {string} name Event name
210 * @param {Array} params A list of parameters to send to client
211 * @param {Function} [filter] A function to filter clients
212 */
213 broadcast(name, params, filter) {
214 filter = filter || (() => true);
215
216 if (this.ws) {
217 this.wss.clients // This is a Set
218 .forEach(client => {
219 if (!client._osjs_client) {
220 return;
221 }
222
223 if (filter(client)) {
224 client.send(JSON.stringify({
225 params,
226 name
227 }));
228 }
229 });
230 }
231 }
232
233 /**
234 * Broadcast given event to all clients
235 * @param {string} name Event name
236 * @param {Array} ...params A list of parameters to send to client
237 */
238 broadcastAll(name, ...params) {
239 return this.broadcast(name, params);
240 }
241
242 /**
243 * Broadcast given event to client filtered by username
244 * @param {String} username Username to send to
245 * @param {string} name Event name
246 * @param {Array} ...params A list of parameters to send to client
247 */
248 broadcastUser(username, name, ...params) {
249 return this.broadcast(name, params, client => {
250 return client._osjs_client.username === username;
251 });
252 }
253
254 /**
255 * Gets the server instance
256 * @return {Core}
257 */
258 static getInstance() {
259 return _instance;
260 }
261}
262
263module.exports = Core;