WebServerService.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. /*
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. const express = require('express');
  20. const eggspress = require("../api/eggspress");
  21. const { Context, ContextExpressMiddleware } = require("../util/context");
  22. const BaseService = require("./BaseService");
  23. const config = require('../config');
  24. const https = require('https')
  25. var http = require('http');
  26. const fs = require('fs');
  27. const auth = require('../middleware/auth');
  28. const { osclink } = require('../util/strutil');
  29. const { surrounding_box, es_import_promise } = require('../fun/dev-console-ui-utils');
  30. class WebServerService extends BaseService {
  31. static MODULES = {
  32. https: require('https'),
  33. http: require('http'),
  34. fs: require('fs'),
  35. express: require('express'),
  36. helmet: require('helmet'),
  37. cookieParser: require('cookie-parser'),
  38. compression: require('compression'),
  39. ['on-finished']: require('on-finished'),
  40. morgan: require('morgan'),
  41. };
  42. async ['__on_boot.consolidation'] () {
  43. const app = this.app;
  44. const services = this.services;
  45. await services.emit('install.middlewares.context-aware', { app });
  46. await services.emit('install.routes', { app });
  47. await services.emit('install.routes-gui', { app });
  48. }
  49. async ['__on_boot.activation'] () {
  50. const services = this.services;
  51. await services.emit('start.webserver');
  52. await services.emit('ready.webserver');
  53. }
  54. async ['__on_start.webserver'] () {
  55. await es_import_promise;
  56. // error handling middleware goes last, as per the
  57. // expressjs documentation:
  58. // https://expressjs.com/en/guide/error-handling.html
  59. this.app.use(require('../api/api_error_handler'));
  60. const path = require('path')
  61. const { jwt_auth } = require('../helpers');
  62. config.http_port = process.env.PORT ?? config.http_port;
  63. globalThis.deployment_type =
  64. config.http_port === 5101 ? 'green' :
  65. config.http_port === 5102 ? 'blue' :
  66. 'not production';
  67. let server;
  68. const auto_port = config.http_port === 'auto';
  69. let ports_to_try = auto_port ? (() => {
  70. const ports = [];
  71. for ( let i = 0 ; i < 20 ; i++ ) {
  72. ports.push(4100 + i);
  73. }
  74. return ports;
  75. })() : [Number.parseInt(config.http_port)];
  76. for ( let i = 0 ; i < ports_to_try.length ; i++ ) {
  77. const port = ports_to_try[i];
  78. const is_last_port = i === ports_to_try.length - 1;
  79. if ( auto_port ) this.log.info('trying port: ' + port);
  80. try {
  81. server = http.createServer(this.app).listen(port);
  82. server.timeout = 1000 * 60 * 60 * 2; // 2 hours
  83. let should_continue = false;
  84. await new Promise((rslv, rjct) => {
  85. server.on('error', e => {
  86. if ( e.code === 'EADDRINUSE' ) {
  87. if ( ! is_last_port && e.code === 'EADDRINUSE' ) {
  88. this.log.info('port in use: ' + port);
  89. should_continue = true;
  90. }
  91. rslv();
  92. } else {
  93. rjct(e);
  94. }
  95. });
  96. server.on('listening', () => {
  97. rslv();
  98. })
  99. })
  100. if ( should_continue ) continue;
  101. } catch (e) {
  102. if ( ! is_last_port && e.code === 'EADDRINUSE' ) {
  103. this.log.info('port in use:' + port);
  104. continue;
  105. }
  106. throw e;
  107. }
  108. config.http_port = port;
  109. break;
  110. }
  111. ports_to_try = null; // GC
  112. const url = config.origin;
  113. this.startup_widget = () => {
  114. const link = `\x1B[34;1m${osclink(url)}\x1B[0m`;
  115. const lines = [
  116. "",
  117. `Puter is now live at: ${link}`,
  118. `Type web:dismiss to dismiss this message`,
  119. "",
  120. ];
  121. const lengths = [
  122. 0,
  123. (`Puter is now live at: `).length + url.length,
  124. lines[2].length,
  125. 0,
  126. ];
  127. surrounding_box('34;1', lines, lengths);
  128. return lines;
  129. };
  130. {
  131. const svc_devConsole = this.services.get('dev-console', { optional: true });
  132. if ( svc_devConsole ) svc_devConsole.add_widget(this.startup_widget);
  133. }
  134. this.print_puter_logo_();
  135. server.timeout = 1000 * 60 * 60 * 2; // 2 hours
  136. server.requestTimeout = 1000 * 60 * 60 * 2; // 2 hours
  137. server.headersTimeout = 1000 * 60 * 60 * 2; // 2 hours
  138. // server.keepAliveTimeout = 1000 * 60 * 60 * 2; // 2 hours
  139. // Socket.io server instance
  140. const socketio = require('../socketio.js').init(server);
  141. // Socket.io middleware for authentication
  142. socketio.use(async (socket, next) => {
  143. if (socket.handshake.auth.auth_token) {
  144. try {
  145. let auth_res = await jwt_auth(socket);
  146. // successful auth
  147. socket.user = auth_res.user;
  148. socket.token = auth_res.token;
  149. // join user room
  150. socket.join(socket.user.id);
  151. next();
  152. } catch (e) {
  153. console.log('socket auth err', e);
  154. }
  155. }
  156. });
  157. socketio.on('connection', (socket) => {
  158. socket.on('disconnect', () => {
  159. });
  160. socket.on('trash.is_empty', (msg) => {
  161. socket.broadcast.to(socket.user.id).emit('trash.is_empty', msg);
  162. });
  163. });
  164. }
  165. async _init () {
  166. const app = express();
  167. this.app = app;
  168. app.set('services', this.services);
  169. this._register_commands(this.services.get('commands'));
  170. this.middlewares = { auth };
  171. const require = this.require;
  172. const config = this.global_config;
  173. new ContextExpressMiddleware({
  174. parent: globalThis.root_context.sub({
  175. puter_environment: Context.create({
  176. env: config.env,
  177. version: require('../../package.json').version,
  178. }),
  179. }, 'mw')
  180. }).install(app);
  181. app.use(async (req, res, next) => {
  182. req.services = this.services;
  183. next();
  184. });
  185. // Instrument logging to use our log service
  186. {
  187. const morgan = require('morgan');
  188. const stream = {
  189. write: (message) => {
  190. const [method, url, status, responseTime] = message.split(' ')
  191. const fields = {
  192. method,
  193. url,
  194. status: parseInt(status, 10),
  195. responseTime: parseFloat(responseTime),
  196. };
  197. if ( url.includes('android-icon') ) return;
  198. // remove `puter.auth.*` query params
  199. const safe_url = (u => {
  200. // We need to prepend an arbitrary domain to the URL
  201. const url = new URL('https://example.com' + u);
  202. const search = url.searchParams;
  203. for ( const key of search.keys() ) {
  204. if ( key.startsWith('puter.auth.') ) search.delete(key);
  205. }
  206. return url.pathname + '?' + search.toString();
  207. })(fields.url);
  208. fields.url = safe_url;
  209. // re-write message
  210. message = [
  211. fields.method, fields.url,
  212. fields.status, fields.responseTime,
  213. ].join(' ');
  214. const log = this.services.get('log-service').create('morgan');
  215. log.info(message, fields);
  216. }
  217. };
  218. app.use(morgan(':method :url :status :response-time', { stream }));
  219. }
  220. app.use((() => {
  221. // const router = express.Router();
  222. // router.get('/wut', express.json(), (req, res, next) => {
  223. // return res.status(500).send('Internal Error');
  224. // });
  225. // return router;
  226. return eggspress('/wut', {
  227. allowedMethods: ['GET'],
  228. }, async (req, res, next) => {
  229. // throw new Error('throwy error');
  230. return res.status(200).send('test endpoint');
  231. });
  232. })());
  233. (() => {
  234. const onFinished = require('on-finished');
  235. app.use((req, res, next) => {
  236. onFinished(res, () => {
  237. if ( res.statusCode !== 500 ) return;
  238. if ( req.__error_handled ) return;
  239. const alarm = this.services.get('alarm');
  240. alarm.create('responded-500', 'server sent a 500 response', {
  241. error: req.__error_source,
  242. url: req.url,
  243. method: req.method,
  244. body: req.body,
  245. headers: req.headers,
  246. });
  247. });
  248. next();
  249. });
  250. })();
  251. app.use(async function(req, res, next) {
  252. // Express does not document that this can be undefined.
  253. // The browser likely doesn't follow the HTTP/1.1 spec
  254. // (bot client?) and express is handling this badly by
  255. // not setting the header at all. (that's my theory)
  256. if( req.hostname === undefined ) {
  257. res.status(400).send(
  258. 'Please verify your browser is up-to-date.'
  259. );
  260. return;
  261. }
  262. return next();
  263. });
  264. // Validate host header against allowed domains to prevent host header injection
  265. // https://www.owasp.org/index.php/Host_Header_Injection
  266. app.use((req, res, next)=>{
  267. const allowedDomains = [config.domain.toLowerCase(), config.static_hosting_domain.toLowerCase()];
  268. // Retrieve the Host header and ensure it's in a valid format
  269. const hostHeader = req.headers.host;
  270. if (!hostHeader) {
  271. return res.status(400).send('Missing Host header.');
  272. }
  273. // Parse the Host header to isolate the hostname (strip out port if present)
  274. const hostName = hostHeader.split(':')[0].trim().toLowerCase();
  275. // Check if the hostname matches any of the allowed domains or is a subdomain of an allowed domain
  276. if (allowedDomains.some(allowedDomain => hostName === allowedDomain || hostName.endsWith('.' + allowedDomain))) {
  277. next(); // Proceed if the host is valid
  278. } else {
  279. return res.status(400).send('Invalid Host header.');
  280. }
  281. })
  282. app.use(express.json({limit: '50mb'}));
  283. const cookieParser = require('cookie-parser');
  284. app.use(cookieParser({limit: '50mb'}));
  285. // gzip compression for all requests
  286. const compression = require('compression');
  287. app.use(compression());
  288. // Helmet and other security
  289. const helmet = require('helmet');
  290. app.use(helmet.noSniff());
  291. app.use(helmet.hsts());
  292. app.use(helmet.ieNoOpen());
  293. app.use(helmet.permittedCrossDomainPolicies());
  294. app.use(helmet.xssFilter());
  295. // app.use(helmet.referrerPolicy());
  296. app.disable('x-powered-by');
  297. app.use(function (req, res, next) {
  298. const origin = req.headers.origin;
  299. if ( req.path === '/signup' || req.path === '/login' ) {
  300. res.setHeader('Access-Control-Allow-Origin', origin ?? '*');
  301. }
  302. // Website(s) to allow to connect
  303. if (
  304. config.experimental_no_subdomain ||
  305. req.subdomains[req.subdomains.length-1] === 'api'
  306. ) {
  307. res.setHeader('Access-Control-Allow-Origin', origin ?? '*');
  308. }
  309. // Request methods to allow
  310. res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  311. const allowed_headers = [
  312. "Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization",
  313. ];
  314. // Request headers to allow
  315. res.header("Access-Control-Allow-Headers", allowed_headers.join(', '));
  316. // Set to true if you need the website to include cookies in the requests sent
  317. // to the API (e.g. in case you use sessions)
  318. // res.setHeader('Access-Control-Allow-Credentials', true);
  319. //needed for SharedArrayBuffer
  320. // res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  321. // res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
  322. res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
  323. // Pass to next layer of middleware
  324. // disable iframes on the main domain
  325. if ( req.hostname === config.domain ) {
  326. // disable iframes
  327. res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  328. }
  329. next();
  330. });
  331. // Options for all requests (for CORS)
  332. app.options('/*', (_, res) => {
  333. return res.sendStatus(200);
  334. });
  335. }
  336. _register_commands (commands) {
  337. commands.registerCommands('web', [
  338. {
  339. id: 'dismiss',
  340. description: 'Dismiss the startup message',
  341. handler: async (_, log) => {
  342. if ( ! this.startup_widget ) return;
  343. const svc_devConsole = this.services.get('dev-console', { optional: true });
  344. if ( svc_devConsole ) svc_devConsole.remove_widget(this.startup_widget);
  345. const lines = this.startup_widget();
  346. for ( const line of lines ) log.log(line);
  347. this.startup_widget = null;
  348. }
  349. }
  350. ]);
  351. }
  352. print_puter_logo_() {
  353. if ( this.global_config.env !== 'dev' ) return;
  354. const logos = require('../fun/logos.js');
  355. let last_logo = undefined;
  356. for ( const logo of logos ) {
  357. if ( logo.sz <= (process.stdout.columns ?? 0) ) {
  358. last_logo = logo;
  359. } else break;
  360. }
  361. if ( last_logo ) {
  362. const lines = last_logo.txt.split('\n');
  363. const width = process.stdout.columns;
  364. const pad = (width - last_logo.sz) / 2;
  365. const asymmetrical = pad % 1 !== 0;
  366. const pad_left = Math.floor(pad);
  367. const pad_right = Math.ceil(pad);
  368. for ( let i = 0 ; i < lines.length ; i++ ) {
  369. lines[i] = ' '.repeat(pad_left) + lines[i] + ' '.repeat(pad_right);
  370. }
  371. const txt = lines.join('\n');
  372. console.log('\n\x1B[34;1m' + txt + '\x1B[0m\n');
  373. }
  374. }
  375. }
  376. module.exports = WebServerService;