From 49b0805014a5847da8bac9fbf9c3bd2c27ea3b72 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 9 May 2025 15:27:25 -0400 Subject: [PATCH] dev: custom domain routing (the easy part) With this commit alone, any user can specify "domain" on their subdomains. This means they could affect the route for domains they don't control, so this is not production-ready without further changes. --- .../src/modules/web/WebServerService.js | 6 +++++- src/backend/src/om/mappings/subdomain.js | 13 ++++++++++++ src/backend/src/routers/hosting/puter-site.js | 20 ++++++++++++++----- src/backend/src/services/PuterSiteService.js | 7 +++++-- .../database/SqliteDatabaseAccessService.js | 3 +++ .../sqlite_setup/0038_custom-domains.sql | 2 ++ 6 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/backend/src/services/database/sqlite_setup/0038_custom-domains.sql diff --git a/src/backend/src/modules/web/WebServerService.js b/src/backend/src/modules/web/WebServerService.js index dbb4a3c51..c59bfcc70 100644 --- a/src/backend/src/modules/web/WebServerService.js +++ b/src/backend/src/modules/web/WebServerService.js @@ -500,7 +500,11 @@ class WebServerService extends BaseService { 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.'); + if ( ! config.custom_domains_enabled ) { + return res.status(400).send('Invalid Host header.'); + } + req.is_custom_domain = true; + next(); } }) diff --git a/src/backend/src/om/mappings/subdomain.js b/src/backend/src/om/mappings/subdomain.js index a8f159832..5b823f02c 100644 --- a/src/backend/src/om/mappings/subdomain.js +++ b/src/backend/src/om/mappings/subdomain.js @@ -54,6 +54,19 @@ module.exports = { } } }, + domain: { + type: 'string', + maxlen: 253, + + // It turns out validating domain names kind of sucks + // source: https://stackoverflow.com/questions/10306690 + regex: '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$', + + // TODO: can this 'adapt' be data instead? + async adapt (value) { + return value.toLowerCase(); + }, + }, root_dir: { type: 'puter-node', fs_permission: 'read', diff --git a/src/backend/src/routers/hosting/puter-site.js b/src/backend/src/routers/hosting/puter-site.js index 97ee33c57..56e2d1ee7 100644 --- a/src/backend/src/routers/hosting/puter-site.js +++ b/src/backend/src/routers/hosting/puter-site.js @@ -41,10 +41,17 @@ class PuterSiteMiddleware extends AdvancedBase { app.use(this.run.bind(this)); } async run (req, res, next) { - if ( - ! req.hostname.endsWith(config.static_hosting_domain) - && ( req.subdomains[0] !== 'devtest' ) - ) return next(); + + ! req.hostname.endsWith(config.static_hosting_domain) + && ( req.subdomains[0] !== 'devtest' ) + + const is_subdomain = + req.hostname.endsWith(config.static_hosting_domain) + || + req.subdomains[0] === 'devtest' + ; + + if ( ! is_subdomain && ! req.is_custom_domain ) return next(); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -64,6 +71,7 @@ class PuterSiteMiddleware extends AdvancedBase { } async run_ (req, res, next) { const subdomain = + req.is_custom_domain ? req.hostname : req.subdomains[0] === 'devtest' ? 'devtest' : req.hostname.slice(0, -1 * (config.static_hosting_domain.length + 1)); @@ -100,7 +108,9 @@ class PuterSiteMiddleware extends AdvancedBase { await get_username_site() || await (async () => { const svc_puterSite = services.get('puter-site'); - const site = await svc_puterSite.get_subdomain(subdomain); + const site = await svc_puterSite.get_subdomain(subdomain, { + is_custom_domain: req.is_custom_domain, + }); return site; })(); diff --git a/src/backend/src/services/PuterSiteService.js b/src/backend/src/services/PuterSiteService.js index 590cd2224..45746c23e 100644 --- a/src/backend/src/services/PuterSiteService.js +++ b/src/backend/src/services/PuterSiteService.js @@ -113,15 +113,18 @@ class PuterSiteService extends BaseService { * @returns {Promise} Returns an object with subdomain details or null if not found. * @note In development environment, 'devtest' subdomain returns hardcoded values. */ - async get_subdomain (subdomain) { + async get_subdomain (subdomain, options) { if ( subdomain === 'devtest' && this.global_config.env === 'dev' ) { return { user_id: null, root_dir_id: this.config.devtest_directory, }; } + console.log('???', subdomain, options); const rows = await this.db.read( - `SELECT * FROM subdomains WHERE subdomain = ? LIMIT 1`, + `SELECT * FROM subdomains WHERE ${ + options.is_custom_domain ? 'domain' : 'subdomain' + } = ? LIMIT 1`, [subdomain] ); if ( rows.length === 0 ) return null; diff --git a/src/backend/src/services/database/SqliteDatabaseAccessService.js b/src/backend/src/services/database/SqliteDatabaseAccessService.js index 85e880d02..fbada0773 100644 --- a/src/backend/src/services/database/SqliteDatabaseAccessService.js +++ b/src/backend/src/services/database/SqliteDatabaseAccessService.js @@ -161,6 +161,9 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { [33, [ '0036_dev-to-app.sql', ]], + [34, [ + '0038_custom-domains.sql', + ]], ]; // Database upgrade logic diff --git a/src/backend/src/services/database/sqlite_setup/0038_custom-domains.sql b/src/backend/src/services/database/sqlite_setup/0038_custom-domains.sql new file mode 100644 index 000000000..04df78f10 --- /dev/null +++ b/src/backend/src/services/database/sqlite_setup/0038_custom-domains.sql @@ -0,0 +1,2 @@ +ALTER TABLE `subdomains` ADD COLUMN `domain` varchar(256) DEFAULT NULL; +-- reminder: add index \ No newline at end of file