Files
puter/packages/backend/src/services/WebServerService.js
T
Sam Atkins 60523dc7a7 chore: Fix some eslint issues in services
/puter/packages/backend/src/services/ContextInitService.js
  57:18  error  'async_factory' is not defined  no-undef

/puter/packages/backend/src/services/StorageService.js
  22:5  error  Expected to call 'super()'  constructor-super

/puter/packages/backend/src/services/WebServerService.js
  258:35  error  'services' is not defined  no-undef

/puter/packages/backend/src/services/auth/AuthService.js
  52:13  error  Unreachable code  no-unreachable

/puter/packages/backend/src/services/drivers/implementations/BaseImplementation.js
   64:25  error  'services' is not defined  no-undef
   75:39  error  'services' is not defined  no-undef
  117:39  error  'services' is not defined  no-undef
  123:42  error  'services' is not defined  no-undef
  149:42  error  'services' is not defined  no-undef
  168:38  error  'services' is not defined  no-undef

/puter/packages/backend/src/services/drivers/implementations/PuterDriverProxy.js
  43:5  error  Expected to call 'super()'              constructor-super
  44:9  error  'this' is not allowed before 'super()'  no-this-before-super

/puter/packages/backend/src/services/drivers/meta/Construct.js
  125:9  error  Unreachable code  no-unreachable

/puter/packages/backend/src/services/runtime-analysis/PagerService.js
  49:41  error  'util' is not defined  no-undef
2024-05-02 11:21:50 +01:00

420 lines
15 KiB
JavaScript

/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require('express');
const eggspress = require("../api/eggspress");
const { Context, ContextExpressMiddleware } = require("../util/context");
const BaseService = require("./BaseService");
const config = require('../config');
const https = require('https')
var http = require('http');
const fs = require('fs');
const auth = require('../middleware/auth');
const { osclink } = require('../util/strutil');
const { surrounding_box, es_import_promise } = require('../fun/dev-console-ui-utils');
class WebServerService extends BaseService {
static MODULES = {
https: require('https'),
http: require('http'),
fs: require('fs'),
express: require('express'),
helmet: require('helmet'),
cookieParser: require('cookie-parser'),
compression: require('compression'),
['on-finished']: require('on-finished'),
morgan: require('morgan'),
};
async ['__on_start.webserver'] () {
await es_import_promise;
// error handling middleware goes last, as per the
// expressjs documentation:
// https://expressjs.com/en/guide/error-handling.html
this.app.use(require('../api/api_error_handler'));
const path = require('path')
const { jwt_auth } = require('../helpers');
config.http_port = process.env.PORT ?? config.http_port;
globalThis.deployment_type =
config.http_port === 5101 ? 'green' :
config.http_port === 5102 ? 'blue' :
'not production';
let server;
const auto_port = config.http_port === 'auto';
let ports_to_try = auto_port ? (() => {
const ports = [];
for ( let i = 0 ; i < 20 ; i++ ) {
ports.push(4100 + i);
}
return ports;
})() : [Number.parseInt(config.http_port)];
for ( let i = 0 ; i < ports_to_try.length ; i++ ) {
const port = ports_to_try[i];
const is_last_port = i === ports_to_try.length - 1;
if ( auto_port ) this.log.info('trying port: ' + port);
try {
server = http.createServer(this.app).listen(port);
server.timeout = 1000 * 60 * 60 * 2; // 2 hours
let should_continue = false;
await new Promise((rslv, rjct) => {
server.on('error', e => {
if ( e.code === 'EADDRINUSE' ) {
if ( ! is_last_port && e.code === 'EADDRINUSE' ) {
this.log.info('port in use: ' + port);
should_continue = true;
}
rslv();
} else {
rjct(e);
}
});
server.on('listening', () => {
rslv();
})
})
if ( should_continue ) continue;
} catch (e) {
if ( ! is_last_port && e.code === 'EADDRINUSE' ) {
this.log.info('port in use:' + port);
continue;
}
throw e;
}
config.http_port = port;
break;
}
ports_to_try = null; // GC
const url = config.origin;
this.startup_widget = () => {
const link = `\x1B[34;1m${osclink(url)}\x1B[0m`;
const lines = [
"",
`Puter is now live at: ${link}`,
`Type web:dismiss to dismiss this message`,
"",
];
const lengths = [
0,
(`Puter is now live at: `).length + url.length,
lines[2].length,
0,
];
surrounding_box('34;1', lines, lengths);
return lines;
};
{
const svc_devConsole = this.services.get('dev-console', { optional: true });
if ( svc_devConsole ) svc_devConsole.add_widget(this.startup_widget);
}
this.print_puter_logo_();
server.timeout = 1000 * 60 * 60 * 2; // 2 hours
server.requestTimeout = 1000 * 60 * 60 * 2; // 2 hours
server.headersTimeout = 1000 * 60 * 60 * 2; // 2 hours
// server.keepAliveTimeout = 1000 * 60 * 60 * 2; // 2 hours
// Socket.io server instance
const socketio = require('../socketio.js').init(server);
// Socket.io middleware for authentication
socketio.use(async (socket, next) => {
if (socket.handshake.query.auth_token) {
try {
let auth_res = await jwt_auth(socket);
// successful auth
socket.user = auth_res.user;
socket.token = auth_res.token;
// join user room
socket.join(socket.user.id);
next();
} catch (e) {
console.log('socket auth err');
}
}
});
socketio.on('connection', (socket) => {
socket.on('disconnect', () => {
});
socket.on('trash.is_empty', (msg) => {
socket.broadcast.to(socket.user.id).emit('trash.is_empty', msg);
});
});
}
async _init () {
const app = express();
this.app = app;
app.set('services', this.services);
this._register_commands(this.services.get('commands'));
this.middlewares = { auth };
const require = this.require;
const config = this.global_config;
new ContextExpressMiddleware({
parent: globalThis.root_context.sub({
puter_environment: Context.create({
env: config.env,
version: require('../../package.json').version,
}),
}, 'mw')
}).install(app);
app.use(async (req, res, next) => {
req.services = this.services;
next();
});
// Instrument logging to use our log service
{
const morgan = require('morgan');
const stream = {
write: (message) => {
const [method, url, status, responseTime] = message.split(' ')
const fields = {
method,
url,
status: parseInt(status, 10),
responseTime: parseFloat(responseTime),
};
if ( url.includes('android-icon') ) return;
// remove `puter.auth.*` query params
const safe_url = (u => {
// We need to prepend an arbitrary domain to the URL
const url = new URL('https://example.com' + u);
const search = url.searchParams;
for ( const key of search.keys() ) {
if ( key.startsWith('puter.auth.') ) search.delete(key);
}
return url.pathname + '?' + search.toString();
})(fields.url);
fields.url = safe_url;
// re-write message
message = [
fields.method, fields.url,
fields.status, fields.responseTime,
].join(' ');
const log = this.services.get('log-service').create('morgan');
log.info(message, fields);
}
};
app.use(morgan(':method :url :status :response-time', { stream }));
}
app.use((() => {
// const router = express.Router();
// router.get('/wut', express.json(), (req, res, next) => {
// return res.status(500).send('Internal Error');
// });
// return router;
return eggspress('/wut', {
allowedMethods: ['GET'],
}, async (req, res, next) => {
// throw new Error('throwy error');
return res.status(200).send('test endpoint');
});
})());
(() => {
const onFinished = require('on-finished');
app.use((req, res, next) => {
onFinished(res, () => {
if ( res.statusCode !== 500 ) return;
if ( req.__error_handled ) return;
const alarm = this.services.get('alarm');
alarm.create('responded-500', 'server sent a 500 response', {
error: req.__error_source,
url: req.url,
method: req.method,
body: req.body,
headers: req.headers,
});
});
next();
});
})();
app.use(async function(req, res, next) {
// Express does not document that this can be undefined.
// The browser likely doesn't follow the HTTP/1.1 spec
// (bot client?) and express is handling this badly by
// not setting the header at all. (that's my theory)
if( req.hostname === undefined ) {
res.status(400).send(
'Please verify your browser is up-to-date.'
);
return;
}
return next();
});
// Validate host header against allowed domains to prevent host header injection
// https://www.owasp.org/index.php/Host_Header_Injection
app.use((req, res, next)=>{
const allowedDomains = [config.domain.toLowerCase(), config.static_hosting_domain.toLowerCase()];
// Retrieve the Host header and ensure it's in a valid format
const hostHeader = req.headers.host;
if (!hostHeader) {
return res.status(400).send('Missing Host header.');
}
// Parse the Host header to isolate the hostname (strip out port if present)
const hostName = hostHeader.split(':')[0].trim().toLowerCase();
// Check if the hostname matches any of the allowed domains or is a subdomain of an allowed domain
if (allowedDomains.some(allowedDomain => hostName === allowedDomain || hostName.endsWith('.' + allowedDomain))) {
next(); // Proceed if the host is valid
} else {
return res.status(400).send('Invalid Host header.');
}
})
app.use(express.json({limit: '50mb'}));
const cookieParser = require('cookie-parser');
app.use(cookieParser({limit: '50mb'}));
// gzip compression for all requests
const compression = require('compression');
app.use(compression());
// Helmet and other security
const helmet = require('helmet');
app.use(helmet.noSniff());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.xssFilter());
// app.use(helmet.referrerPolicy());
app.disable('x-powered-by');
app.use(function (req, res, next) {
const origin = req.headers.origin;
if ( req.path === '/signup' || req.path === '/login' ) {
res.setHeader('Access-Control-Allow-Origin', origin ?? '*');
}
// Website(s) to allow to connect
if (
config.experimental_no_subdomain ||
req.subdomains[req.subdomains.length-1] === 'api'
) {
res.setHeader('Access-Control-Allow-Origin', origin ?? '*');
}
// Request methods to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
const allowed_headers = [
"Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization",
];
// Request headers to allow
res.header("Access-Control-Allow-Headers", allowed_headers.join(', '));
// Set to true if you need the website to include cookies in the requests sent
// to the API (e.g. in case you use sessions)
// res.setHeader('Access-Control-Allow-Credentials', true);
//needed for SharedArrayBuffer
// res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
// res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
// Pass to next layer of middleware
// disable iframes on the main domain
if ( req.hostname === config.domain ) {
// disable iframes
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
}
next();
});
// Options for all requests (for CORS)
app.options('/*', (_, res) => {
return res.sendStatus(200);
});
}
_register_commands (commands) {
commands.registerCommands('web', [
{
id: 'dismiss',
description: 'Dismiss the startup message',
handler: async (_, log) => {
if ( ! this.startup_widget ) return;
const svc_devConsole = this.services.get('dev-console', { optional: true });
if ( svc_devConsole ) svc_devConsole.remove_widget(this.startup_widget);
const lines = this.startup_widget();
for ( const line of lines ) log.log(line);
this.startup_widget = null;
}
}
]);
}
print_puter_logo_() {
if ( this.global_config.env !== 'dev' ) return;
const logos = require('../fun/logos.js');
let last_logo = undefined;
for ( const logo of logos ) {
if ( logo.sz <= (process.stdout.columns ?? 0) ) {
last_logo = logo;
} else break;
}
if ( last_logo ) {
const lines = last_logo.txt.split('\n');
const width = process.stdout.columns;
const pad = (width - last_logo.sz) / 2;
const asymmetrical = pad % 1 !== 0;
const pad_left = Math.floor(pad);
const pad_right = Math.ceil(pad);
for ( let i = 0 ; i < lines.length ; i++ ) {
lines[i] = ' '.repeat(pad_left) + lines[i] + ' '.repeat(pad_right);
}
const txt = lines.join('\n');
console.log('\n\x1B[34;1m' + txt + '\x1B[0m\n');
}
}
}
module.exports = WebServerService;