diff --git a/CHANGES b/CHANGES index d660ed6f9..326dd65d2 100644 --- a/CHANGES +++ b/CHANGES @@ -2103,4 +2103,5 @@ * firewall: fix race condition where blocklist was not added in correct position in the FORWARD chain * services: fix issue where services where scaled up/down too fast * turn: realm variable was not updated properly on dashboard change +* nginx: add splash pages for IP based browser access diff --git a/setup/start/nginx/nginx.conf b/setup/start/nginx/nginx.conf index 44a5a262e..b0bf1bda9 100644 --- a/setup/start/nginx/nginx.conf +++ b/setup/start/nginx/nginx.conf @@ -6,7 +6,7 @@ worker_processes auto; # this is 4096 by default. See /proc//limits and /etc/security/limits.conf # usually twice the worker_connections (one for uptsream, one for downstream) # see also LimitNOFILE=16384 in systemd drop-in -worker_rlimit_nofile 8192; +worker_rlimit_nofile 8192; pid /run/nginx.pid; @@ -43,23 +43,5 @@ http { # zones for rate limiting limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second - - # default http server that returns 404 for any domain we are not listening on - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name does_not_match_anything; - - # acme challenges (for app installation and re-configure when the vhost config does not exist) - location /.well-known/acme-challenge/ { - default_type text/plain; - alias /home/yellowtent/platformdata/acme/; - } - - location / { - return 404; - } - } - include applications/*.conf; } diff --git a/src/cloudron.js b/src/cloudron.js index 6ef893ec7..d14b4e544 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -78,7 +78,7 @@ function onActivated(callback) { // 1. mail bounces can now be sent to the cloudron owner // 2. the restore code path can run without sudo (since mail/ is non-root) async.series([ - reverseProxy.removeDefaultConfig, // remove IP based nginx config once user is created + (done) => reverseProxy.writeDefaultConfig({ activated :true }, done), // update IP based nginx config once user is created platform.start, cron.startJobs, function checkBackupConfiguration(callback) { @@ -141,7 +141,7 @@ function runStartupTasks() { // we remove the config as a simple security measure to not expose IP <-> domain if (!activated) { debug('runStartupTasks: not activated. generating IP based redirection config'); - return reverseProxy.writeDefaultConfig(callback); + return reverseProxy.writeDefaultConfig({ activated: false }, callback); } onActivated(callback); diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index ff2e26a63..c0d07bca7 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -6,51 +6,57 @@ map $http_upgrade $connection_upgrade { # http server server { - listen 80; - server_tokens off; # hide version +<% if (endpoint === 'ip' || endpoint === 'setup') { -%> + listen 80 default_server; + server_name _; <% if (hasIPv6) { -%> - listen [::]:80; + listen [::]:80 default_server; <% } -%> - -<% if (vhost) { -%> - server_name <%= vhost %>; <% } else { -%> - # IP based access for initial cloudron setup. TODO: match the IPv6 address - server_name "~^\d+\.\d+\.\d+\.\d+$"; + listen 80; + server_name <%= vhost %>; +<% if (hasIPv6) { -%> + listen [::]:80; +<% } -%> <% } -%> - # acme challenges (for cert renewal where the vhost config exists) + server_tokens off; # hide version + + # acme challenges location /.well-known/acme-challenge/ { default_type text/plain; alias /home/yellowtent/platformdata/acme/; } + # for default server, serve the splash page. for other endpoints, redirect to HTTPS location / { - # redirect everything to HTTPS -<% if ( endpoint === 'admin' ) { %> +<% if ( endpoint === 'admin' || endpoint === 'setup' ) { %> return 301 https://$host$request_uri; <% } else if ( endpoint === 'app' ) { %> return 301 https://$host$request_uri; <% } else if ( endpoint === 'redirect' ) { %> return 301 https://<%= redirectTo %>$request_uri; +<% } else if ( endpoint === 'ip' ) { %> + root <%= sourceDir %>/dashboard/dist; + try_files /splash.html =404; <% } %> - } } # https server server { -<% if (vhost) { -%> - server_name <%= vhost %>; - listen 443 ssl http2; -<% if (hasIPv6) { -%> - listen [::]:443 ssl http2; -<% } -%> -<% } else { -%> +<% if (endpoint === 'ip' || endpoint === 'setup') { -%> listen 443 ssl http2 default_server; + server_name _; <% if (hasIPv6) { -%> listen [::]:443 ssl http2 default_server; <% } -%> +<% } else { -%> + listen 443 ssl http2; + server_name <%= vhost %>; +<% if (hasIPv6) { -%> + listen [::]:443 ssl http2; +<% } -%> <% } -%> server_tokens off; # hide version @@ -94,7 +100,7 @@ server { # enable for proxied requests as well gzip_proxied any; -<% if ( endpoint === 'admin' ) { -%> +<% if ( endpoint === 'admin' || endpoint === 'ip' || endpoint === 'setup' ) { -%> # CSP headers for the admin/dashboard resources add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';"; <% } else { %> @@ -171,7 +177,7 @@ server { } <% } %> -<% if ( endpoint === 'admin' ) { %> +<% if ( endpoint === 'admin' || endpoint === 'setup' ) { %> location /api/ { proxy_pass http://127.0.0.1:3000; client_max_body_size 1m; @@ -216,6 +222,11 @@ server { # redirect everything to the app. this is temporary because there is no way # to clear a permanent redirect on the browser return 302 https://<%= redirectTo %>$request_uri; +<% } else if ( endpoint === 'ip' ) { %> + location / { + root <%= sourceDir %>/dashboard/dist; + try_files /splash.html =404; + } <% } %> } } diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 9eea535ea..3aa1a4bae 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -21,8 +21,6 @@ exports = module.exports = { // these only generate nginx config writeDefaultConfig, - removeDefaultConfig, - writeDashboardConfig, writeAppConfig, @@ -376,7 +374,7 @@ function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) { var data = { sourceDir: path.resolve(__dirname, '..'), adminOrigin: settings.adminOrigin(), - vhost: vhost, // if vhost is empty it will become the default_server + vhost: vhost, hasIPv6: sysinfo.hasIPv6(), endpoint: 'admin', certFilePath: bundle.certFilePath, @@ -648,35 +646,37 @@ function removeAppConfigs() { } } -function writeDefaultConfig(callback) { +function writeDefaultConfig(options, callback) { + assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert'); - var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key'); + const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert'); + const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key'); if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { debug('writeDefaultConfig: create new cert'); - var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy + const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) { debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`); return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error)); } } - writeDashboardNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) { - if (error) return callback(error); + const data = { + sourceDir: path.resolve(__dirname, '..'), + adminOrigin: settings.adminOrigin(), + vhost: '', + hasIPv6: sysinfo.hasIPv6(), + endpoint: options.activated ? 'ip' : 'setup', + certFilePath, + keyFilePath, + robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n') + }; + const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); + const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME); - debug('writeDefaultConfig: done'); - - reload(callback); - }); -} - -function removeDefaultConfig(callback) { - assert.strictEqual(typeof callback, 'function'); - - safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME)); + if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); reload(callback); }