diff --git a/README.md b/README.md index d80b3d44..29e4adfd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@



- + @@ -66,12 +66,17 @@ so that the barrier for entry here is low. - Passwort reset (only sqlite) (`docker exec -it nginx-proxy-manager password-reset.js USER_EMAIL PASSWORD`) ## Soon +- disabling IPv4/IPv6 +- MariaDB/MySQL TLS support (if requested) +- support changing the PUID/PGID (maybe) - more ## migration - **NOTE: migrating back to the original is not possible**, so make first a **backup** before migration, so you can use the backup to switch back - if you use custom certificates, you need to upload the CA/Intermediate Certificate (file name: `chain.pem`) in the `/opt/npm/tls/custom/npm-[certificate-id]` folder - some buttons have changed, check if they are still correct +- please delete all dnspod certs and recreate them +- changing the PUID/PGID is not supported (since it would break running in network_mode host) # Use as webserver diff --git a/backend/app.js b/backend/app.js index ca6d6fba..e528a0bb 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2,6 +2,7 @@ const express = require('express'); const bodyParser = require('body-parser'); const fileUpload = require('express-fileupload'); const compression = require('compression'); +const config = require('./lib/config'); const log = require('./logger').express; /** @@ -24,7 +25,7 @@ app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); app.enable('strict routing'); // pretty print JSON when not live -if (process.env.NODE_ENV !== 'production') { +if (config.debug()) { app.set('json spaces', 2); } @@ -65,7 +66,7 @@ app.use(function (err, req, res, next) { } }; - if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) { + if (config.debug() || (req.baseUrl + req.path).includes('nginx/certificates')) { payload.debug = { stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null, previous: err.previous @@ -74,7 +75,7 @@ app.use(function (err, req, res, next) { // Not every error is worth logging - but this is good for now until it gets annoying. if (typeof err.stack !== 'undefined' && err.stack) { - if (process.env.NODE_ENV === 'development' || process.env.DEBUG) { + if (config.debug()) { log.debug(err.stack); } else if (typeof err.public == 'undefined' || !err.public) { log.warn(err.message); diff --git a/backend/db.js b/backend/db.js index b1444a0a..1a8b1634 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,44 +1,27 @@ -const config = require('config'); -const fs = require('fs'); +const config = require('./lib/config'); if (!config.has('database')) { - throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md'); + throw new Error('Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/'); } function generateDbConfig() { - if (config.database.engine === 'knex-native') { - return config.database.knex; - } else { - let newConfig = { - client: config.database.engine, - connection: { - host: config.database.host, - user: config.database.user, - password: config.database.password, - database: config.database.name, - port: config.database.port - }, - migrations: { - tableName: 'migrations' - } - }; - - if (process.env.DB_MYSQL_CA) { - newConfig.connection.ssl = { - ca: fs.readFileSync(process.env.DB_MYSQL_CA), - rejectUnauthorized: true - }; - } - - return newConfig; + const cfg = config.get('database'); + if (cfg.engine === 'knex-native') { + return cfg.knex; } + return { + client: cfg.engine, + connection: { + host: cfg.host, + user: cfg.user, + password: cfg.password, + database: cfg.name, + port: cfg.port + }, + migrations: { + tableName: 'migrations' + } + }; } - -let data = generateDbConfig(); - -if (typeof config.database.version !== 'undefined') { - data.version = config.database.version; -} - -module.exports = require('knex')(data); \ No newline at end of file +module.exports = require('knex')(generateDbConfig()); diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json index c6eda485..596642bd 100644 --- a/backend/doc/api.swagger.json +++ b/backend/doc/api.swagger.json @@ -40,6 +40,210 @@ } } }, + "/nginx/proxy-hosts": { + "get": { + "operationId": "getProxyHosts", + "summary": "Get all proxy hosts", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["users"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["access_list", "owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2023-03-30T01:12:23.000Z", + "modified_on": "2023-03-30T02:15:40.000Z", + "owner_user_id": 1, + "domain_names": ["aasdasdad"], + "forward_host": "asdasd", + "forward_port": 80, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": 0, + "caching_enabled": 0, + "block_exploits": 0, + "advanced_config": "sdfsdfsdf", + "meta": { + "letsencrypt_agree": false, + "dns_challenge": false, + "nginx_online": false, + "nginx_err": "Command failed: /usr/sbin/nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /etc/nginx/nginx.conf test failed\n" + }, + "allow_websocket_upgrade": 0, + "http2_support": 0, + "forward_scheme": "http", + "enabled": 1, + "locations": [], + "hsts_enabled": 0, + "hsts_subdomains": 0, + "owner": { + "id": 1, + "created_on": "2023-03-30T01:11:50.000Z", + "modified_on": "2023-03-30T01:11:50.000Z", + "is_deleted": 0, + "is_disabled": 0, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "access_list": null, + "certificate": null + }, + { + "id": 2, + "created_on": "2023-03-30T02:11:49.000Z", + "modified_on": "2023-03-30T02:11:49.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_host": "1.1.1.1", + "forward_port": 80, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": 0, + "caching_enabled": 0, + "block_exploits": 0, + "advanced_config": "", + "meta": { + "letsencrypt_agree": false, + "dns_challenge": false, + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": 0, + "http2_support": 0, + "forward_scheme": "http", + "enabled": 1, + "locations": [], + "hsts_enabled": 0, + "hsts_subdomains": 0, + "owner": { + "id": 1, + "created_on": "2023-03-30T01:11:50.000Z", + "modified_on": "2023-03-30T01:11:50.000Z", + "is_deleted": 0, + "is_disabled": 0, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "access_list": null, + "certificate": null + } + ] + } + }, + "schema": { + "$ref": "#/components/schemas/ProxyHostsList" + } + } + } + } + } + }, + "post": { + "operationId": "createProxyHost", + "summary": "Create a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["users"] + } + ], + "parameters": [ + { + "in": "body", + "name": "proxyhost", + "description": "Proxy Host Payload", + "required": true, + "schema": { + "$ref": "#/components/schemas/ProxyHostObject" + } + } + ], + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 3, + "created_on": "2023-03-30T02:31:27.000Z", + "modified_on": "2023-03-30T02:31:27.000Z", + "owner_user_id": 1, + "domain_names": ["test2.example.com"], + "forward_host": "1.1.1.1", + "forward_port": 80, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": 0, + "caching_enabled": 0, + "block_exploits": 0, + "advanced_config": "", + "meta": { + "letsencrypt_agree": false, + "dns_challenge": false + }, + "allow_websocket_upgrade": 0, + "http2_support": 0, + "forward_scheme": "http", + "enabled": 1, + "locations": [], + "hsts_enabled": 0, + "hsts_subdomains": 0, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2023-03-30T01:11:50.000Z", + "modified_on": "2023-03-30T01:11:50.000Z", + "is_deleted": 0, + "is_disabled": 0, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "access_list": null, + "use_default_location": true, + "ipv6": true + } + } + }, + "schema": { + "$ref": "#/components/schemas/ProxyHostObject" + } + } + } + } + } + } + }, "/schema": { "get": { "operationId": "schema", @@ -55,14 +259,10 @@ "get": { "operationId": "refreshToken", "summary": "Refresh your access token", - "tags": [ - "Tokens" - ], + "tags": ["Tokens"], "security": [ { - "BearerAuth": [ - "tokens" - ] + "BearerAuth": ["tokens"] } ], "responses": { @@ -104,19 +304,14 @@ "scope": { "minLength": 1, "type": "string", - "enum": [ - "user" - ] + "enum": ["user"] }, "secret": { "minLength": 1, "type": "string" } }, - "required": [ - "identity", - "secret" - ], + "required": ["identity", "secret"], "type": "object" } } @@ -144,23 +339,17 @@ } }, "summary": "Request a new access token from credentials", - "tags": [ - "Tokens" - ] + "tags": ["Tokens"] } }, "/settings": { "get": { "operationId": "getSettings", "summary": "Get all settings", - "tags": [ - "Settings" - ], + "tags": ["Settings"], "security": [ { - "BearerAuth": [ - "settings" - ] + "BearerAuth": ["settings"] } ], "responses": { @@ -194,14 +383,10 @@ "get": { "operationId": "getSetting", "summary": "Get a setting", - "tags": [ - "Settings" - ], + "tags": ["Settings"], "security": [ { - "BearerAuth": [ - "settings" - ] + "BearerAuth": ["settings"] } ], "parameters": [ @@ -244,14 +429,10 @@ "put": { "operationId": "updateSetting", "summary": "Update a setting", - "tags": [ - "Settings" - ], + "tags": ["Settings"], "security": [ { - "BearerAuth": [ - "settings" - ] + "BearerAuth": ["settings"] } ], "parameters": [ @@ -305,14 +486,10 @@ "get": { "operationId": "getUsers", "summary": "Get all users", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -322,9 +499,7 @@ "description": "Expansions", "schema": { "type": "string", - "enum": [ - "permissions" - ] + "enum": ["permissions"] } } ], @@ -345,9 +520,7 @@ "name": "Jamie Curnow", "nickname": "James", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] + "roles": ["admin"] } ] }, @@ -362,9 +535,7 @@ "name": "Jamie Curnow", "nickname": "James", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], + "roles": ["admin"], "permissions": { "visibility": "all", "proxy_hosts": "manage", @@ -389,14 +560,10 @@ "post": { "operationId": "createUser", "summary": "Create a User", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -426,9 +593,7 @@ "name": "Jamie Curnow", "nickname": "James", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], + "roles": ["admin"], "permissions": { "visibility": "all", "proxy_hosts": "manage", @@ -454,14 +619,10 @@ "get": { "operationId": "getUser", "summary": "Get a user", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -501,9 +662,7 @@ "name": "Jamie Curnow", "nickname": "James", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] + "roles": ["admin"] } } }, @@ -518,14 +677,10 @@ "put": { "operationId": "updateUser", "summary": "Update a User", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -574,9 +729,7 @@ "name": "Jamie Curnow", "nickname": "James", "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] + "roles": ["admin"] } } }, @@ -591,14 +744,10 @@ "delete": { "operationId": "deleteUser", "summary": "Delete a User", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -637,14 +786,10 @@ "put": { "operationId": "updateUserAuth", "summary": "Update a User's Authentication", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -700,14 +845,10 @@ "put": { "operationId": "updateUserPermissions", "summary": "Update a User's Permissions", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -755,14 +896,10 @@ "put": { "operationId": "loginAsUser", "summary": "Login as this user", - "tags": [ - "Users" - ], + "tags": ["Users"], "security": [ { - "BearerAuth": [ - "users" - ] + "BearerAuth": ["users"] } ], "parameters": [ @@ -797,9 +934,7 @@ "name": "Jamie Curnow", "nickname": "James", "avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm", - "roles": [ - "admin" - ] + "roles": ["admin"] } } } @@ -807,11 +942,7 @@ "schema": { "type": "object", "description": "Login object", - "required": [ - "expires", - "token", - "user" - ], + "required": ["expires", "token", "user"], "additionalProperties": false, "properties": { "expires": { @@ -840,14 +971,10 @@ "get": { "operationId": "reportsHosts", "summary": "Report on Host Statistics", - "tags": [ - "Reports" - ], + "tags": ["Reports"], "security": [ { - "BearerAuth": [ - "reports" - ] + "BearerAuth": ["reports"] } ], "responses": { @@ -878,14 +1005,10 @@ "get": { "operationId": "getAuditLog", "summary": "Get Audit Log", - "tags": [ - "Audit Log" - ], + "tags": ["Audit Log"], "security": [ { - "BearerAuth": [ - "audit-log" - ] + "BearerAuth": ["audit-log"] } ], "responses": { @@ -925,10 +1048,7 @@ "type": "object", "description": "Health object", "additionalProperties": false, - "required": [ - "status", - "version" - ], + "required": ["status", "version"], "properties": { "status": { "type": "string", @@ -944,11 +1064,7 @@ "revision": 0 }, "additionalProperties": false, - "required": [ - "major", - "minor", - "revision" - ], + "required": ["major", "minor", "revision"], "properties": { "major": { "type": "integer", @@ -969,10 +1085,7 @@ "TokenObject": { "type": "object", "description": "Token object", - "required": [ - "expires", - "token" - ], + "required": ["expires", "token"], "additionalProperties": false, "properties": { "expires": { @@ -988,16 +1101,147 @@ } } }, + "ProxyHostObject": { + "type": "object", + "description": "Proxy Host object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "domain_names", + "forward_host", + "forward_port", + "access_list_id", + "certificate_id", + "ssl_forced", + "caching_enabled", + "block_exploits", + "advanced_config", + "meta", + "allow_websocket_upgrade", + "http2_support", + "forward_scheme", + "enabled", + "locations", + "hsts_enabled", + "hsts_subdomains", + "certificate", + "use_default_location", + "ipv6" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "description": "Proxy Host ID", + "minimum": 1, + "example": 1 + }, + "created_on": { + "type": "string", + "description": "Created Date", + "example": "2020-01-30T09:36:08.000Z" + }, + "modified_on": { + "type": "string", + "description": "Modified Date", + "example": "2020-01-30T09:41:04.000Z" + }, + "owner_user_id": { + "type": "integer", + "minimum": 1, + "example": 1 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "forward_host": { + "type": "string", + "minLength": 1 + }, + "forward_port": { + "type": "integer", + "minimum": 1 + }, + "access_list_id": { + "type": "integer" + }, + "certificate_id": { + "type": "integer" + }, + "ssl_forced": { + "type": "integer" + }, + "caching_enabled": { + "type": "integer" + }, + "block_exploits": { + "type": "integer" + }, + "advanced_config": { + "type": "string" + }, + "meta": { + "type": "object" + }, + "allow_websocket_upgrade": { + "type": "integer" + }, + "http2_support": { + "type": "integer" + }, + "forward_scheme": { + "type": "string" + }, + "enabled": { + "type": "integer" + }, + "locations": { + "type": "array" + }, + "hsts_enabled": { + "type": "integer" + }, + "hsts_subdomains": { + "type": "integer" + }, + "certificate": { + "type": "object", + "nullable": true + }, + "owner": { + "type": "object", + "nullable": true + }, + "access_list": { + "type": "object", + "nullable": true + }, + "use_default_location": { + "type": "boolean" + }, + "ipv6": { + "type": "boolean" + } + } + }, + "ProxyHostsList": { + "type": "array", + "description": "Proxyn Hosts list", + "items": { + "$ref": "#/components/schemas/ProxyHostObject" + } + }, "SettingObject": { "type": "object", "description": "Setting object", - "required": [ - "id", - "name", - "description", - "value", - "meta" - ], + "required": ["id", "name", "description", "value", "meta"], "additionalProperties": false, "properties": { "id": { @@ -1057,17 +1301,7 @@ "UserObject": { "type": "object", "description": "User object", - "required": [ - "id", - "created_on", - "modified_on", - "is_disabled", - "email", - "name", - "nickname", - "avatar", - "roles" - ], + "required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"], "additionalProperties": false, "properties": { "id": { @@ -1117,9 +1351,7 @@ }, "roles": { "description": "Roles applied", - "example": [ - "admin" - ], + "example": ["admin"], "type": "array", "items": { "type": "string" @@ -1137,10 +1369,7 @@ "AuthObject": { "type": "object", "description": "Authentication Object", - "required": [ - "type", - "secret" - ], + "required": ["type", "secret"], "properties": { "type": { "type": "string", @@ -1167,64 +1396,37 @@ "visibility": { "type": "string", "description": "Visibility Type", - "enum": [ - "all", - "user" - ] + "enum": ["all", "user"] }, "access_lists": { "type": "string", "description": "Access Lists Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] + "enum": ["hidden", "view", "manage"] }, "dead_hosts": { "type": "string", "description": "404 Hosts Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] + "enum": ["hidden", "view", "manage"] }, "proxy_hosts": { "type": "string", "description": "Proxy Hosts Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] + "enum": ["hidden", "view", "manage"] }, "redirection_hosts": { "type": "string", "description": "Redirection Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] + "enum": ["hidden", "view", "manage"] }, "streams": { "type": "string", "description": "Streams Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] + "enum": ["hidden", "view", "manage"] }, "certificates": { "type": "string", "description": "Certificates Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] + "enum": ["hidden", "view", "manage"] } } }, diff --git a/backend/index.js b/backend/index.js index dcdc6866..4bb2f1da 100755 --- a/backend/index.js +++ b/backend/index.js @@ -3,9 +3,6 @@ const logger = require('./logger').global; async function appStart () { - // Create config file db settings if environment variables have been set - await createDbConfigFromEnvironment(); - const migrate = require('./migrate'); const setup = require('./setup'); const app = require('./app'); @@ -42,90 +39,6 @@ async function appStart () { }); } -async function createDbConfigFromEnvironment() { - return new Promise((resolve, reject) => { - const envMysqlHost = process.env.DB_MYSQL_HOST || null; - const envMysqlPort = process.env.DB_MYSQL_PORT || null; - const envMysqlUser = process.env.DB_MYSQL_USER || null; - const envMysqlName = process.env.DB_MYSQL_NAME || null; - let envSqliteFile = process.env.DB_SQLITE_FILE || null; - - const fs = require('fs'); - const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json'; - let configData = {}; - - try { - configData = require(filename); - } catch (err) { - // do nothing - } - - if (configData.database && configData.database.engine && !configData.database.fromEnv) { - logger.info('Manual db configuration already exists, skipping config creation from environment variables'); - resolve(); - return; - } - - if ((!envMysqlHost || !envMysqlPort || !envMysqlUser || !envMysqlName) && !envSqliteFile){ - envSqliteFile = '/data/database.sqlite'; - logger.info(`No valid environment variables for database provided, using default SQLite file '${envSqliteFile}'`); - } - - if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) { - const newConfig = { - fromEnv: true, - engine: 'mysql', - host: envMysqlHost, - port: envMysqlPort, - user: envMysqlUser, - password: process.env.DB_MYSQL_PASSWORD, - name: envMysqlName, - }; - - if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) { - // Config is unchanged, skip overwrite - resolve(); - return; - } - - logger.info('Generating MySQL knex configuration from environment variables'); - configData.database = newConfig; - - } else { - const newConfig = { - fromEnv: true, - engine: 'knex-native', - knex: { - client: 'sqlite3', - connection: { - filename: envSqliteFile - }, - useNullAsDefault: true - } - }; - if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) { - // Config is unchanged, skip overwrite - resolve(); - return; - } - - logger.info('Generating SQLite knex configuration'); - configData.database = newConfig; - } - - // Write config - fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => { - if (err) { - logger.error('Could not write db config to config file: ' + filename); - reject(err); - } else { - logger.debug('Wrote db configuration to config file: ' + filename); - resolve(); - } - }); - }); -} - try { appStart(); } catch (err) { diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 44f7e0ff..09949fbc 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -4,6 +4,7 @@ const https = require('https'); const tempWrite = require('temp-write'); const moment = require('moment'); const logger = require('../logger').ssl; +const config = require('../lib/config'); const error = require('../lib/error'); const utils = require('../lib/utils'); const certificateModel = require('../models/certificate'); @@ -14,8 +15,9 @@ const internalHost = require('./host'); const archiver = require('archiver'); const path = require('path'); const { isArray } = require('lodash'); -const certbotConfig = '/data/tls/certbot/config.ini'; -const certbotCommand = 'certbot --config-dir /data/tls/certbot'; + +const letsencryptConfig = '/data/tls/certbot/config.ini'; +const certbotCommand = 'certbot --config-dir /data/tls/certbot'; function omissions() { return ['is_deleted']; @@ -874,7 +876,7 @@ const internalCertificate = { // Escape single quotes and backslashes const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); const credentialsCmd = 'mkdir -p /data/tls/certbot/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\''; - let prepareCmd = 'pip install ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies; + const prepareCmd = 'pip install --no-cache-dir ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies; // Whether the plugin has a ---credentials argument const hasConfigArg = certificate.meta.dns_provider !== 'route53'; diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 13619df8..04bf421b 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -1,9 +1,9 @@ -const _ = require('lodash'); -const fs = require('fs'); -const logger = require('../logger').nginx; -const utils = require('../lib/utils'); -const error = require('../lib/error'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; +const _ = require('lodash'); +const fs = require('fs'); +const logger = require('../logger').nginx; +const config = require('../lib/config'); +const utils = require('../lib/utils'); +const error = require('../lib/error'); const internalNginx = { @@ -65,7 +65,7 @@ const internalNginx = { } }); - if (debug_mode) { + if (config.debug()) { logger.error('Nginx test failed:', valid_lines.join('\n')); } @@ -101,7 +101,7 @@ const internalNginx = { * @returns {Promise} */ test: () => { - if (debug_mode) { + if (config.debug()) { logger.info('Testing Nginx configuration'); } @@ -184,7 +184,7 @@ const internalNginx = { generateConfig: (host_type, host) => { const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); - if (debug_mode) { + if (config.debug()) { logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2)); } @@ -239,7 +239,7 @@ const internalNginx = { .then((config_text) => { fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - if (debug_mode) { + if (config.debug()) { logger.success('Wrote config:', filename, config_text); } @@ -249,7 +249,7 @@ const internalNginx = { resolve(true); }) .catch((err) => { - if (debug_mode) { + if (config.debug()) { logger.warn('Could not write ' + filename + ':', err.message); } @@ -268,7 +268,7 @@ const internalNginx = { * @returns {Promise} */ generateLetsEncryptRequestConfig: (certificate) => { - if (debug_mode) { + if (config.debug()) { logger.info('Generating certbot Request Config:', certificate); } @@ -292,14 +292,14 @@ const internalNginx = { .then((config_text) => { fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - if (debug_mode) { + if (config.debug()) { logger.success('Wrote config:', filename, config_text); } resolve(true); }) .catch((err) => { - if (debug_mode) { + if (config.debug()) { logger.warn('Could not write ' + filename + ':', err.message); } @@ -416,8 +416,8 @@ const internalNginx = { * @param {string} config * @returns {boolean} */ - advancedConfigHasDefaultLocation: function (config) { - return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); + advancedConfigHasDefaultLocation: function (cfg) { + return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); }, /** diff --git a/backend/lib/config.js b/backend/lib/config.js new file mode 100644 index 00000000..caa57fcf --- /dev/null +++ b/backend/lib/config.js @@ -0,0 +1,184 @@ +const fs = require('fs'); +const NodeRSA = require('node-rsa'); +const logger = require('../logger').global; + +const keysFile = '/data/keys.json'; + +let instance = null; + +// 1. Load from config file first (not recommended anymore) +// 2. Use config env variables next +const configure = () => { + const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json'; + if (fs.existsSync(filename)) { + let configData; + try { + configData = require(filename); + } catch (err) { + // do nothing + } + + if (configData && configData.database) { + logger.info(`Using configuration from file: ${filename}`); + instance = configData; + instance.keys = getKeys(); + return; + } + } + + const envMysqlHost = process.env.DB_MYSQL_HOST || null; + const envMysqlUser = process.env.DB_MYSQL_USER || null; + const envMysqlName = process.env.DB_MYSQL_NAME || null; + if (envMysqlHost && envMysqlUser && envMysqlName) { + // we have enough mysql creds to go with mysql + logger.info('Using MySQL configuration'); + instance = { + database: { + engine: 'mysql', + host: envMysqlHost, + port: process.env.DB_MYSQL_PORT || 3306, + user: envMysqlUser, + password: process.env.DB_MYSQL_PASSWORD, + name: envMysqlName, + }, + keys: getKeys(), + }; + return; + } + + const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite'; + logger.info(`Using Sqlite: ${envSqliteFile}`); + instance = { + database: { + engine: 'knex-native', + knex: { + client: 'sqlite3', + connection: { + filename: envSqliteFile + }, + useNullAsDefault: true + } + }, + keys: getKeys(), + }; +}; + +const getKeys = () => { + // Get keys from file + if (!fs.existsSync(keysFile)) { + generateKeys(); + } else if (process.env.DEBUG) { + logger.info('Keys file exists OK'); + } + try { + return require(keysFile); + } catch (err) { + logger.error('Could not read JWT key pair from config file: ' + keysFile, err); + process.exit(1); + } +}; + +const generateKeys = () => { + logger.info('Creating a new JWT key pair...'); + // Now create the keys and save them in the config. + const key = new NodeRSA({ b: 2048 }); + key.generateKeyPair(); + + const keys = { + key: key.exportKey('private').toString(), + pub: key.exportKey('public').toString(), + }; + + // Write keys config + try { + fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2)); + } catch (err) { + logger.error('Could not write JWT key pair to config file: ' + keysFile + ': ' . err.message); + process.exit(1); + } + logger.info('Wrote JWT key pair to config file: ' + keysFile); +}; + +module.exports = { + + /** + * + * @param {string} key ie: 'database' or 'database.engine' + * @returns {boolean} + */ + has: function(key) { + instance === null && configure(); + const keys = key.split('.'); + let level = instance; + let has = true; + keys.forEach((keyItem) =>{ + if (typeof level[keyItem] === 'undefined') { + has = false; + } else { + level = level[keyItem]; + } + }); + + return has; + }, + + /** + * Gets a specific key from the top level + * + * @param {string} key + * @returns {*} + */ + get: function (key) { + instance === null && configure(); + if (key && typeof instance[key] !== 'undefined') { + return instance[key]; + } + return instance; + }, + + /** + * Is this a sqlite configuration? + * + * @returns {boolean} + */ + isSqlite: function () { + instance === null && configure(); + return instance.database.knex && instance.database.knex.client === 'sqlite3'; + }, + + /** + * Are we running in debug mdoe? + * + * @returns {boolean} + */ + debug: function () { + return !!process.env.DEBUG; + }, + + /** + * Returns a public key + * + * @returns {string} + */ + getPublicKey: function () { + instance === null && configure(); + return instance.keys.pub; + }, + + /** + * Returns a private key + * + * @returns {string} + */ + getPrivateKey: function () { + instance === null && configure(); + return instance.keys.key; + }, + + /** + * @returns {boolean} + */ + useLetsencryptStaging: function () { + return !!process.env.LE_STAGING; + } +}; diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js index fca6f4bf..d09c9be5 100644 --- a/backend/lib/validator/index.js +++ b/backend/lib/validator/index.js @@ -5,7 +5,7 @@ const definitions = require('../../schema/definitions.json'); RegExp.prototype.toJSON = RegExp.prototype.toString; const ajv = require('ajv')({ - verbose: true, //process.env.NODE_ENV === 'development', + verbose: true, allErrors: true, format: 'full', // strict regexes for format checks coerceTypes: true, diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js index 11c31a88..dec70c3d 100644 --- a/backend/models/now_helper.js +++ b/backend/models/now_helper.js @@ -1,11 +1,11 @@ const db = require('../db'); -const config = require('config'); +const config = require('../lib/config'); const Model = require('objection').Model; Model.knex(db); module.exports = function () { - if (config.database.knex && config.database.knex.client === 'sqlite3') { + if (config.isSqlite()) { // eslint-disable-next-line return Model.raw("datetime('now','localtime')"); } diff --git a/backend/models/token.js b/backend/models/token.js index 37d53144..7cf11e03 100644 --- a/backend/models/token.js +++ b/backend/models/token.js @@ -6,44 +6,36 @@ const _ = require('lodash'); const jwt = require('jsonwebtoken'); const crypto = require('crypto'); +const config = require('../lib/config'); const error = require('../lib/error'); +const logger = require('../logger').global; const ALGO = 'RS256'; -let public_key = null; -let private_key = null; - -function checkJWTKeyPair() { - if (!public_key || !private_key) { - let config = require('config'); - public_key = config.get('jwt.pub'); - private_key = config.get('jwt.key'); - } -} - module.exports = function () { let token_data = {}; - let self = { + const self = { /** * @param {Object} payload * @returns {Promise} */ create: (payload) => { + if (!config.getPrivateKey()) { + logger.error('Private key is empty!'); + } // sign with RSA SHA256 - let options = { + const options = { algorithm: ALGO, expiresIn: payload.expiresIn || '1d' }; payload.jti = crypto.randomBytes(12) .toString('base64') - .substr(-8); - - checkJWTKeyPair(); + .substring(-8); return new Promise((resolve, reject) => { - jwt.sign(payload, private_key, options, (err, token) => { + jwt.sign(payload, config.getPrivateKey(), options, (err, token) => { if (err) { reject(err); } else { @@ -62,13 +54,15 @@ module.exports = function () { * @returns {Promise} */ load: function (token) { + if (!config.getPublicKey()) { + logger.error('Public key is empty!'); + } return new Promise((resolve, reject) => { - checkJWTKeyPair(); try { if (!token || token === null || token === 'null') { reject(new error.AuthError('Empty token')); } else { - jwt.verify(token, public_key, {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => { + jwt.verify(token, config.getPublicKey(), {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => { if (err) { if (err.name === 'TokenExpiredError') { @@ -132,7 +126,7 @@ module.exports = function () { * @returns {Integer} */ getUserId: (default_value) => { - let attrs = self.get('attrs'); + const attrs = self.get('attrs'); if (attrs && typeof attrs.id !== 'undefined' && attrs.id) { return attrs.id; } diff --git a/backend/package.json b/backend/package.json index 7248173e..abde33e7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,6 @@ "bcrypt": "5.1.0", "body-parser": "1.20.2", "compression": "1.7.4", - "config": "3.3.9", "express": "4.18.2", "express-fileupload": "1.4.0", "gravatar": "1.8.2", @@ -22,7 +21,6 @@ "moment": "2.29.4", "mysql": "2.18.1", "node-rsa": "1.1.1", - "nodemon": "2.0.22", "objection": "3.0.1", "path": "0.12.7", "signale": "1.4.0", diff --git a/backend/setup.js b/backend/setup.js index 212c5a09..68dc0a59 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -1,6 +1,4 @@ -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const config = require('config'); +const config = require('./lib/config'); const logger = require('./logger').setup; const certificateModel = require('./models/certificate'); const userModel = require('./models/user'); @@ -9,62 +7,6 @@ const utils = require('./lib/utils'); const authModel = require('./models/auth'); const settingModel = require('./models/setting'); const dns_plugins = require('./global/certbot-dns-plugins'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; - -/** - * Creates a new JWT RSA Keypair if not alread set on the config - * - * @returns {Promise} - */ -const setupJwt = () => { - return new Promise((resolve, reject) => { - // Now go and check if the jwt gpg keys have been created and if not, create them - if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) { - logger.info('Creating a new JWT key pair...'); - - // jwt keys are not configured properly - const filename = config.util.getEnv('NODE_CONFIG_DIR') + '/' + (config.util.getEnv('NODE_ENV') || 'default') + '.json'; - let config_data = {}; - - try { - config_data = require(filename); - } catch (err) { - // do nothing - if (debug_mode) { - logger.debug(filename + ' config file could not be required'); - } - } - - // Now create the keys and save them in the config. - let key = new NodeRSA({ b: 2048 }); - key.generateKeyPair(); - - config_data.jwt = { - key: key.exportKey('private').toString(), - pub: key.exportKey('public').toString(), - }; - - // Write config - fs.writeFile(filename, JSON.stringify(config_data, null, 2), (err) => { - if (err) { - logger.error('Could not write JWT key pair to config file: ' + filename); - reject(err); - } else { - logger.info('Wrote JWT key pair to config file: ' + filename); - delete require.cache[require.resolve('config')]; - resolve(); - } - }); - } else { - // JWT key pair exists - if (debug_mode) { - logger.debug('JWT Keypair already exists'); - } - - resolve(); - } - }); -}; /** * Creates a default admin users if one doesn't already exist in the database @@ -119,8 +61,8 @@ const setupDefaultUser = () => { .then(() => { logger.info('Initial admin setup completed'); }); - } else if (debug_mode) { - logger.debug('Admin user setup not required'); + } else if (config.debug()) { + logger.info('Admin user setup not required'); } }); }; @@ -151,8 +93,8 @@ const setupDefaultSettings = () => { logger.info('Default settings added'); }); } - if (debug_mode) { - logger.debug('Default setting setup not required'); + if (config.debug()) { + logger.info('Default setting setup not required'); } }); }; @@ -189,7 +131,7 @@ const setupCertbotPlugins = () => { }); if (plugins.length) { - const install_cmd = 'pip install ' + plugins.join(' '); + const install_cmd = 'pip install --no-cache-dir ' + plugins.join(' '); promises.push(utils.exec(install_cmd)); } @@ -204,8 +146,7 @@ const setupCertbotPlugins = () => { }; module.exports = function () { - return setupJwt() - .then(setupDefaultUser) + return setupDefaultUser() .then(setupDefaultSettings) .then(setupCertbotPlugins); }; diff --git a/docker/rootfs/bin/common.sh b/docker/rootfs/bin/common.sh new file mode 100644 index 00000000..0bc6468d --- /dev/null +++ b/docker/rootfs/bin/common.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e + +CYAN='\E[1;36m' +BLUE='\E[1;34m' +YELLOW='\E[1;33m' +RED='\E[1;31m' +RESET='\E[0m' +export CYAN BLUE YELLOW RED RESET + +PUID=${PUID:-0} +PGID=${PGID:-0} + +if [[ "$PUID" -ne '0' ]] && [ "$PGID" = '0' ]; then + # set group id to same as user id, + # the user probably forgot to specify the group id and + # it would be rediculous to intentionally use the root group + # for a non-root user + PGID=$PUID +fi + +export PUID PGID + +log_info () { + echo -e "${BLUE}❯ ${CYAN}$1${RESET}" +} + +log_error () { + echo -e "${RED}❯ $1${RESET}" +} + +# The `run` file will only execute 1 line so this helps keep things +# logically separated + +log_fatal () { + echo -e "${RED}--------------------------------------${RESET}" + echo -e "${RED}ERROR: $1${RESET}" + echo -e "${RED}--------------------------------------${RESET}" + /run/s6/basedir/bin/halt + exit 1 +} diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh new file mode 100755 index 00000000..1d5899e4 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh @@ -0,0 +1,18 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /bin/common.sh + +if [ "$(id -u)" != "0" ]; then + log_fatal "This docker container must be run as root, do not specify a user.\nYou can specify PUID and PGID env vars to run processes as that user and group after initialization." +fi + +. /etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh +. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh +. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh +. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh +. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh +. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh +. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh new file mode 100755 index 00000000..c5cf5435 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh @@ -0,0 +1,20 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Configuring npmuser ...' + +if id -u npmuser; then + # user already exists + usermod -u "$PUID" npmuser || exit 1 +else + # Add npmuser user + useradd -o -u "$PUID" -U -d /tmp/npmuserhome -s /bin/false npmuser || exit 1 +fi + +usermod -G "$PGID" npmuser || exit 1 +groupmod -o -g "$PGID" npmuser || exit 1 +# Home for npmuser +mkdir -p /tmp/npmuserhome +chown -R "$PUID:$PGID" /tmp/npmuserhome diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh new file mode 100755 index 00000000..2f59ef41 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh @@ -0,0 +1,41 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Checking paths ...' + +# Ensure /data is mounted +if [ ! -d '/data' ]; then + log_fatal '/data is not mounted! Check your docker configuration.' +fi +# Ensure /etc/letsencrypt is mounted +if [ ! -d '/etc/letsencrypt' ]; then + log_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.' +fi + +# Create required folders +mkdir -p \ + /data/nginx \ + /data/custom_ssl \ + /data/logs \ + /data/access \ + /data/nginx/default_host \ + /data/nginx/default_www \ + /data/nginx/proxy_host \ + /data/nginx/redirection_host \ + /data/nginx/stream \ + /data/nginx/dead_host \ + /data/nginx/temp \ + /data/letsencrypt-acme-challenge \ + /run/nginx \ + /tmp/nginx/body \ + /var/log/nginx \ + /var/lib/nginx/cache/public \ + /var/lib/nginx/cache/private \ + /var/cache/nginx/proxy_temp + +touch /var/log/nginx/error.log || true +chmod 777 /var/log/nginx/error.log || true +chmod -R 777 /var/cache/nginx || true +chmod 644 /etc/logrotate.d/nginx-proxy-manager diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh new file mode 100755 index 00000000..684166e1 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh @@ -0,0 +1,24 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Setting ownership ...' + +# root +chown root /tmp/nginx + +# npmuser +chown -R "$PUID:$PGID" /data \ + /etc/letsencrypt \ + /run/nginx \ + /tmp/nginx \ + /var/cache/nginx \ + /var/lib/logrotate \ + /var/lib/nginx \ + /var/log/nginx + +# Don't chown entire /etc/nginx folder as this causes crashes on some systems +chown -R "$PUID:$PGID" /etc/nginx/nginx \ + /etc/nginx/nginx.conf \ + /etc/nginx/conf.d diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh new file mode 100755 index 00000000..0cb9f126 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh @@ -0,0 +1,17 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Dynamic resolvers ...' + +DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]') + +# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` +# thanks @tfmm +if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; +then + echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf +else + echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh new file mode 100755 index 00000000..bc27eb14 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# This command reads the `DISABLE_IPV6` env var and will either enable +# or disable ipv6 in all nginx configs based on this setting. + +log_info 'IPv6 ...' + +# Lowercase +DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]') + +process_folder () { + FILES=$(find "$1" -type f -name "*.conf") + SED_REGEX= + + if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then + # IPV6 is disabled + echo "Disabling IPV6 in hosts in: $1" + SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g' + else + # IPV6 is enabled + echo "Enabling IPV6 in hosts in: $1" + SED_REGEX='s/^(\s*)#listen \[::\]/\1listen [::]/g' + fi + + for FILE in $FILES + do + echo "- ${FILE}" + sed -E -i "$SED_REGEX" "$FILE" + done + + # ensure the files are still owned by the npmuser + chown -R "$PUID:$PGID" "$1" +} + +process_folder /etc/nginx/conf.d +process_folder /data/nginx diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh new file mode 100755 index 00000000..faa22acc --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +# in s6, environmental variables are written as text files for s6 to monitor +# search through full-path filenames for files ending in "__FILE" +log_info 'Docker secrets ...' + +for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do + echo "[secret-init] Evaluating ${FILENAME##*/} ..." + + # set SECRETFILE to the contents of the full-path textfile + SECRETFILE=$(cat "${FILENAME}") + # if SECRETFILE exists / is not null + if [[ -f "${SECRETFILE}" ]]; then + # strip the appended "__FILE" from environmental variable name ... + STRIPFILE=$(echo "${FILENAME}" | sed "s/__FILE//g") + # echo "[secret-init] Set STRIPFILE to ${STRIPFILE}" # DEBUG - rm for prod! + + # ... and set value to contents of secretfile + # since s6 uses text files, this is effectively "export ..." + printf $(cat "${SECRETFILE}") > "${STRIPFILE}" + # echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})" # DEBUG - rm for prod!" + echo "Success: ${STRIPFILE##*/} set from ${FILENAME##*/}" + + else + echo "Cannot find secret in ${FILENAME}" + fi +done diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh new file mode 100755 index 00000000..7991ddf4 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh @@ -0,0 +1,17 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +echo " +------------------------------------- + _ _ ____ __ __ +| \ | | _ \| \/ | +| \| | |_) | |\/| | +| |\ | __/| | | | +|_| \_|_| |_| |_| +------------------------------------- +User ID: $PUID +Group ID: $PGID +------------------------------------- +" diff --git a/global/.version b/global/.version index 93d20b2d..c6436a85 100644 --- a/global/.version +++ b/global/.version @@ -1 +1 @@ -2.9.22 +2.10.2 diff --git a/test/cypress/integration/api/Hosts.spec.js b/test/cypress/integration/api/Hosts.spec.js new file mode 100644 index 00000000..4652c8e0 --- /dev/null +++ b/test/cypress/integration/api/Hosts.spec.js @@ -0,0 +1,48 @@ +/// + +describe('Hosts endpoints', () => { + let token; + + before(() => { + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to create a http host', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/proxy-hosts', + data: { + domain_names: ['test.example.com'], + forward_scheme: 'http', + forward_host: '1.1.1.1', + forward_port: 80, + access_list_id: '0', + certificate_id: 0, + meta: { + letsencrypt_agree: false, + dns_challenge: false + }, + advanced_config: '', + locations: [], + block_exploits: false, + caching_enabled: false, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + ssl_forced: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled'); + expect(data.enabled).to.be.greaterThan(0); + expect(data).to.have.property('meta'); + expect(typeof data.meta.nginx_online).to.be.equal('undefined'); + }); + }); + +});