WebServerService.js 15 KB

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