mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 15:53:33 +00:00 
			
		
		
		
	Compare commits
	
		
			197 Commits
		
	
	
		
			07a4e5791f
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 08bdc23131 | ||
|  | 487fa6d31b | ||
|  | 5b6ca1bf00 | ||
|  | 5039738aa3 | ||
|  | 4451be8f1c | ||
|  | bee2fd1978 | ||
|  | c8adbdfc15 | ||
|  | aff4182ab8 | ||
|  | 8c9d2745e2 | ||
|  | 076d14b5e4 | ||
|  | 8a6d815152 | ||
|  | 54d463ac36 | ||
|  | a23dc24021 | ||
|  | 4f9df893c8 | ||
|  | 304b38e82b | ||
|  | 1b0929ade6 | ||
|  | ddbafb62a6 | ||
|  | 9a0383bc73 | ||
|  | 307cb94e84 | ||
|  | 63ae924fbc | ||
|  | 1710a263c0 | ||
|  | 1357774f21 | ||
|  | 5f54490d86 | ||
|  | c97b8a339d | ||
|  | ed1d90ee7f | ||
|  | 70894e55b8 | ||
|  | 817021a43d | ||
|  | 36e3449a56 | ||
|  | db9f25638f | ||
|  | ddd3355d95 | ||
|  | aade8b42fc | ||
|  | 3735f3c11d | ||
|  | b84762b5b9 | ||
|  | 953faeac15 | ||
|  | c58f3f3ec9 | ||
|  | 0ee4d04d5f | ||
|  | 94f6756250 | ||
|  | 27e3f73854 | ||
|  | d98f4b43dc | ||
|  | ff3116a626 | ||
|  | 7047750b04 | ||
|  | 0792fc0768 | ||
|  | 9758c12ca3 | ||
|  | ccd69c8867 | ||
|  | 23fd1fec6c | ||
|  | 6f04543744 | ||
|  | cbb1fe44ca | ||
|  | 4c23f22d5b | ||
|  | af5d3eccd6 | ||
|  | a87283b030 | ||
|  | 97dbbdd60f | ||
|  | ec81f2489a | ||
|  | d0ec8e89aa | ||
|  | 9a96fbb5f4 | ||
|  | a573450bb8 | ||
|  | 60a25ffbd5 | ||
|  | 7d2369b380 | ||
|  | 64f00e8dba | ||
|  | c99143f548 | ||
|  | cc4ee6919a | ||
|  | 8a69c65b40 | ||
|  | 95ee5ca958 | ||
|  | 40f22d30c4 | ||
|  | 30dfa9e3de | ||
|  | b873499feb | ||
|  | ef69be2036 | ||
|  | 7580e65dd4 | ||
|  | f11dc5d7c1 | ||
|  | 77061a7bd6 | ||
|  | b6afc19135 | ||
|  | 09ba400d09 | ||
|  | 0291cfc270 | ||
|  | 34267e0af9 | ||
|  | f327c1e825 | ||
|  | 6f539979ec | ||
|  | 3d8079a137 | ||
|  | 6d6d83c0d0 | ||
|  | 100a4888d0 | ||
|  | 34a46bd733 | ||
|  | 7f8adc7e50 | ||
|  | 98d118cb74 | ||
|  | 4fb93542c3 | ||
|  | 4fe305520a | ||
|  | 76be31cf76 | ||
|  | 55dadb2004 | ||
|  | d9cdb3dc2c | ||
|  | 0cab720f23 | ||
|  | f5879dff6c | ||
|  | 5e66d677f1 | ||
|  | 18830f81b0 | ||
|  | 341ac65587 | ||
|  | 078baa255a | ||
|  | bf9d9bd43b | ||
|  | a394b25e61 | ||
|  | 1c47fc2ba4 | ||
|  | 312e2ab80c | ||
|  | d147ccd88d | ||
|  | 03fd292c61 | ||
|  | 79d28f03d0 | ||
|  | b09147eca8 | ||
|  | c5a319cb20 | ||
|  | c4df89df1f | ||
|  | 34c703f8b4 | ||
|  | 0a05d8f0ad | ||
|  | 0a9141fad5 | ||
|  | 42836774b7 | ||
|  | 2a07544f58 | ||
|  | dc9d884743 | ||
|  | 0d5d2b1b7c | ||
|  | df48b835c4 | ||
|  | 8a1557154a | ||
|  | a6af5ec2c7 | ||
|  | 14d7c35fd7 | ||
|  | cfcf78aaee | ||
|  | 3a01b2c84f | ||
|  | e1c84a5c10 | ||
|  | c56c95a59a | ||
|  | 6a60627833 | ||
|  | b4793d3c16 | ||
|  | 68a7803513 | ||
|  | 2657af97cf | ||
|  | 4452f014b9 | ||
|  | cd80cc8e4d | ||
|  | ee4250d770 | ||
|  | 3dbc70faa6 | ||
|  | 3091c21cae | ||
|  | 57cd2a1919 | ||
|  | ad5936c530 | ||
|  | 498109addb | ||
|  | 3f3aacd7ec | ||
|  | bb4ecf812d | ||
|  | c05f9695d0 | ||
|  | 6343b398f0 | ||
|  | 59362b7477 | ||
|  | aedaaa18e0 | ||
|  | 080bd0b749 | ||
|  | 9687e9e450 | ||
|  | 5a234bb88c | ||
|  | 4de4b65036 | ||
|  | f1c97c7c36 | ||
|  | b4f49969d6 | ||
|  | ec12d8f9bf | ||
|  | e50e3def9d | ||
|  | 6415f284f9 | ||
|  | 98e5997f0a | ||
|  | fc30a92bd4 | ||
|  | e2011ee45c | ||
|  | 1406e75c2c | ||
|  | ca3ee98c68 | ||
|  | f90d839ebe | ||
|  | be5278f31e | ||
|  | 73110d5e1e | ||
|  | 356b98bf7e | ||
|  | 3eecf7a38b | ||
|  | 7f9240dda7 | ||
|  | f537619ffe | ||
|  | 805968aac6 | ||
|  | 2a4093c1b8 | ||
|  | ae2ac8a733 | ||
|  | 5d087f1256 | ||
|  | c6eca2578e | ||
|  | 56033bee9c | ||
|  | c6630e87bb | ||
|  | d6b98f51b0 | ||
|  | 1e322804ce | ||
|  | b3de76c945 | ||
|  | fcf4117f8e | ||
|  | d26e8c1d0c | ||
|  | 19ed4c1212 | ||
|  | 03018d252b | ||
|  | 8351dd41f6 | ||
|  | 97212f2686 | ||
|  | fe068a8b51 | ||
|  | 61e2bde98f | ||
|  | 81c9038929 | ||
|  | 4ea50ca40c | ||
|  | 53ed12bcf2 | ||
|  | cb3e4ed59c | ||
|  | b20dc5eade | ||
|  | 586afc0c91 | ||
|  | 93ea17a9bb | ||
|  | 151160a834 | ||
|  | 2075f98cad | ||
|  | 126d3d44ca | ||
|  | 20646e7bb5 | ||
|  | 87998a03ce | ||
|  | 2cee211fb0 | ||
|  | a56342c76a | ||
|  | 4c89379671 | ||
|  | 10b9a49274 | ||
|  | 595a742c40 | ||
|  | c171752137 | ||
|  | a0b26b9e98 | ||
|  | d6791f4e38 | ||
|  | 62c94f3099 | ||
|  | 5084cb7296 | ||
|  | e677bfa2e8 | 
							
								
								
									
										57
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										57
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
								
							| @@ -128,7 +128,7 @@ pipeline { | ||||
| 					sh 'docker-compose down --remove-orphans --volumes -t 30 || true' | ||||
| 				} | ||||
| 				unstable { | ||||
| 					dir(path: 'testing/results') { | ||||
| 					dir(path: 'test/results') { | ||||
| 						archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') | ||||
| 					} | ||||
| 				} | ||||
| @@ -161,7 +161,45 @@ pipeline { | ||||
| 					sh 'docker-compose down --remove-orphans --volumes -t 30 || true' | ||||
| 				} | ||||
| 				unstable { | ||||
| 					dir(path: 'testing/results') { | ||||
| 					dir(path: 'test/results') { | ||||
| 						archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		stage('Test Postgres') { | ||||
| 			environment { | ||||
| 				COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres" | ||||
| 				COMPOSE_FILE         = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml' | ||||
| 			} | ||||
| 			when { | ||||
| 				not { | ||||
| 					equals expected: 'UNSTABLE', actual: currentBuild.result | ||||
| 				} | ||||
| 			} | ||||
| 			steps { | ||||
| 				sh 'rm -rf ./test/results/junit/*' | ||||
| 				sh './scripts/ci/fulltest-cypress' | ||||
| 			} | ||||
| 			post { | ||||
| 				always { | ||||
| 					// Dumps to analyze later | ||||
| 					sh 'mkdir -p debug/postgres' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1' | ||||
| 					sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1' | ||||
|  | ||||
| 					junit 'test/results/junit/*' | ||||
| 					sh 'docker-compose down --remove-orphans --volumes -t 30 || true' | ||||
| 				} | ||||
| 				unstable { | ||||
| 					dir(path: 'test/results') { | ||||
| 						archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') | ||||
| 					} | ||||
| 				} | ||||
| @@ -203,12 +241,17 @@ pipeline { | ||||
| 					} | ||||
| 					steps { | ||||
| 						script { | ||||
| 							npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on | ||||
| [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev) | ||||
| as `nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}` | ||||
| 							npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): | ||||
| ``` | ||||
| nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER} | ||||
| ``` | ||||
|  | ||||
| **Note:** ensure you backup your NPM instance before testing this image! Especially if there are database changes | ||||
| **Note:** this is a different docker image namespace than the official image | ||||
| > [!NOTE] | ||||
| > Ensure you backup your NPM instance before testing this image! Especially if there are database changes. | ||||
| > This is a different docker image namespace than the official image. | ||||
|  | ||||
| > [!WARNING] | ||||
| > Changes and additions to DNS Providers require verification by at least 2 members of the community! | ||||
| """, true) | ||||
| 						} | ||||
| 					} | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <p align="center"> | ||||
| 	<img src="https://nginxproxymanager.com/github.png"> | ||||
| 	<br><br> | ||||
| 	<img src="https://img.shields.io/badge/version-2.12.1-green.svg?style=for-the-badge"> | ||||
| 	<img src="https://img.shields.io/badge/version-2.12.6-green.svg?style=for-the-badge"> | ||||
| 	<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> | ||||
| 		<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge"> | ||||
| 	</a> | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| const schema = require('./schema'); | ||||
| const logger = require('./logger').global; | ||||
|  | ||||
| const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== 'false'; | ||||
|  | ||||
| async function appStart () { | ||||
| 	const migrate             = require('./migrate'); | ||||
| 	const setup               = require('./setup'); | ||||
| @@ -13,7 +15,16 @@ async function appStart () { | ||||
| 	return migrate.latest() | ||||
| 		.then(setup) | ||||
| 		.then(schema.getCompiledSchema) | ||||
| 		.then(internalIpRanges.fetch) | ||||
| 		.then(() => { | ||||
| 			if (IP_RANGES_FETCH_ENABLED) { | ||||
| 				logger.info('IP Ranges fetch is enabled'); | ||||
| 				return internalIpRanges.fetch().catch((err) => { | ||||
| 					logger.error('IP Ranges fetch failed, continuing anyway:', err.message); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				logger.info('IP Ranges fetch is disabled by environment variable'); | ||||
| 			} | ||||
| 		}) | ||||
| 		.then(() => { | ||||
| 			internalCertificate.initTimer(); | ||||
| 			internalIpRanges.initTimer(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| const _                     = require('lodash'); | ||||
| const fs                    = require('fs'); | ||||
| const fs                    = require('node:fs'); | ||||
| const batchflow             = require('batchflow'); | ||||
| const logger                = require('../logger').access; | ||||
| const error                 = require('../lib/error'); | ||||
| @@ -38,7 +38,7 @@ const internalAccessList = { | ||||
| 			.then((row) => { | ||||
| 				data.id = row.id; | ||||
|  | ||||
| 				let promises = []; | ||||
| 				const promises = []; | ||||
|  | ||||
| 				// Now add the items | ||||
| 				data.items.map((item) => { | ||||
| @@ -81,7 +81,7 @@ const internalAccessList = { | ||||
|  | ||||
| 				return internalAccessList.build(row) | ||||
| 					.then(() => { | ||||
| 						if (row.proxy_host_count) { | ||||
| 						if (parseInt(row.proxy_host_count, 10)) { | ||||
| 							return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); | ||||
| 						} | ||||
| 					}) | ||||
| @@ -116,7 +116,7 @@ const internalAccessList = { | ||||
| 			.then((row) => { | ||||
| 				if (row.id !== data.id) { | ||||
| 					// Sanity check that something crazy hasn't happened | ||||
| 					throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); | ||||
| 					throw new error.InternalValidationError(`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`); | ||||
| 				} | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| @@ -135,10 +135,10 @@ const internalAccessList = { | ||||
| 			.then(() => { | ||||
| 				// Check for items and add/update/remove them | ||||
| 				if (typeof data.items !== 'undefined' && data.items) { | ||||
| 					let promises      = []; | ||||
| 					let items_to_keep = []; | ||||
| 					const promises      = []; | ||||
| 					const items_to_keep = []; | ||||
|  | ||||
| 					data.items.map(function (item) { | ||||
| 					data.items.map((item) => { | ||||
| 						if (item.password) { | ||||
| 							promises.push(accessListAuthModel | ||||
| 								.query() | ||||
| @@ -154,7 +154,7 @@ const internalAccessList = { | ||||
| 						} | ||||
| 					}); | ||||
|  | ||||
| 					let query = accessListAuthModel | ||||
| 					const query = accessListAuthModel | ||||
| 						.query() | ||||
| 						.delete() | ||||
| 						.where('access_list_id', data.id); | ||||
| @@ -175,9 +175,9 @@ const internalAccessList = { | ||||
| 			.then(() => { | ||||
| 				// Check for clients and add/update/remove them | ||||
| 				if (typeof data.clients !== 'undefined' && data.clients) { | ||||
| 					let promises = []; | ||||
| 					const promises = []; | ||||
|  | ||||
| 					data.clients.map(function (client) { | ||||
| 					data.clients.map((client) => { | ||||
| 						if (client.address) { | ||||
| 							promises.push(accessListClientModel | ||||
| 								.query() | ||||
| @@ -190,7 +190,7 @@ const internalAccessList = { | ||||
| 						} | ||||
| 					}); | ||||
|  | ||||
| 					let query = accessListClientModel | ||||
| 					const query = accessListClientModel | ||||
| 						.query() | ||||
| 						.delete() | ||||
| 						.where('access_list_id', data.id); | ||||
| @@ -223,7 +223,7 @@ const internalAccessList = { | ||||
| 			.then((row) => { | ||||
| 				return internalAccessList.build(row) | ||||
| 					.then(() => { | ||||
| 						if (row.proxy_host_count) { | ||||
| 						if (parseInt(row.proxy_host_count, 10)) { | ||||
| 							return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); | ||||
| 						} | ||||
| 					}).then(internalNginx.reload) | ||||
| @@ -249,12 +249,16 @@ const internalAccessList = { | ||||
|  | ||||
| 		return access.can('access_lists:get', data.id) | ||||
| 			.then((access_data) => { | ||||
| 				let query = accessListModel | ||||
| 				const query = accessListModel | ||||
| 					.query() | ||||
| 					.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) | ||||
| 					.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') | ||||
| 					.leftJoin('proxy_host', function() { | ||||
| 						this.on('proxy_host.access_list_id', '=', 'access_list.id') | ||||
| 							.andOn('proxy_host.is_deleted', '=', 0); | ||||
| 					}) | ||||
| 					.where('access_list.is_deleted', 0) | ||||
| 					.andWhere('access_list.id', data.id) | ||||
| 					.groupBy('access_list.id') | ||||
| 					.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]') | ||||
| 					.first(); | ||||
|  | ||||
| @@ -263,7 +267,7 @@ const internalAccessList = { | ||||
| 				} | ||||
|  | ||||
| 				if (typeof data.expand !== 'undefined' && data.expand !== null) { | ||||
| 					query.withGraphFetched('[' + data.expand.join(', ') + ']'); | ||||
| 					query.withGraphFetched(`[${data.expand.join(', ')}]`); | ||||
| 				} | ||||
|  | ||||
| 				return query.then(utils.omitRow(omissions())); | ||||
| @@ -323,7 +327,7 @@ const internalAccessList = { | ||||
| 									// 3. reconfigure those hosts, then reload nginx | ||||
|  | ||||
| 									// set the access_list_id to zero for these items | ||||
| 									row.proxy_hosts.map(function (val, idx) { | ||||
| 									row.proxy_hosts.map((_val, idx) => { | ||||
| 										row.proxy_hosts[idx].access_list_id = 0; | ||||
| 									}); | ||||
|  | ||||
| @@ -336,11 +340,11 @@ const internalAccessList = { | ||||
| 					}) | ||||
| 					.then(() => { | ||||
| 						// delete the htpasswd file | ||||
| 						let htpasswd_file = internalAccessList.getFilename(row); | ||||
| 						const htpasswd_file = internalAccessList.getFilename(row); | ||||
|  | ||||
| 						try { | ||||
| 							fs.unlinkSync(htpasswd_file); | ||||
| 						} catch (err) { | ||||
| 						} catch (_err) { | ||||
| 							// do nothing | ||||
| 						} | ||||
| 					}) | ||||
| @@ -370,10 +374,13 @@ const internalAccessList = { | ||||
| 	getAll: (access, expand, search_query) => { | ||||
| 		return access.can('access_lists:list') | ||||
| 			.then((access_data) => { | ||||
| 				let query = accessListModel | ||||
| 				const query = accessListModel | ||||
| 					.query() | ||||
| 					.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) | ||||
| 					.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') | ||||
| 					.leftJoin('proxy_host', function() { | ||||
| 						this.on('proxy_host.access_list_id', '=', 'access_list.id') | ||||
| 							.andOn('proxy_host.is_deleted', '=', 0); | ||||
| 					}) | ||||
| 					.where('access_list.is_deleted', 0) | ||||
| 					.groupBy('access_list.id') | ||||
| 					.allowGraph('[owner,items,clients]') | ||||
| @@ -386,19 +393,19 @@ const internalAccessList = { | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 					query.where(function () { | ||||
| 						this.where('name', 'like', '%' + search_query + '%'); | ||||
| 						this.where('name', 'like', `%${search_query}%`); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| 				if (typeof expand !== 'undefined' && expand !== null) { | ||||
| 					query.withGraphFetched('[' + expand.join(', ') + ']'); | ||||
| 					query.withGraphFetched(`[${expand.join(', ')}]`); | ||||
| 				} | ||||
|  | ||||
| 				return query.then(utils.omitRows(omissions())); | ||||
| 			}) | ||||
| 			.then((rows) => { | ||||
| 				if (rows) { | ||||
| 					rows.map(function (row, idx) { | ||||
| 					rows.map((row, idx) => { | ||||
| 						if (typeof row.items !== 'undefined' && row.items) { | ||||
| 							rows[idx] = internalAccessList.maskItems(row); | ||||
| 						} | ||||
| @@ -417,7 +424,7 @@ const internalAccessList = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	getCount: (user_id, visibility) => { | ||||
| 		let query = accessListModel | ||||
| 		const query = accessListModel | ||||
| 			.query() | ||||
| 			.count('id as count') | ||||
| 			.where('is_deleted', 0); | ||||
| @@ -438,7 +445,7 @@ const internalAccessList = { | ||||
| 	 */ | ||||
| 	maskItems: (list) => { | ||||
| 		if (list && typeof list.items !== 'undefined') { | ||||
| 			list.items.map(function (val, idx) { | ||||
| 			list.items.map((val, idx) => { | ||||
| 				let repeat_for = 8; | ||||
| 				let first_char = '*'; | ||||
|  | ||||
| @@ -461,7 +468,7 @@ const internalAccessList = { | ||||
| 	 * @returns {String} | ||||
| 	 */ | ||||
| 	getFilename: (list) => { | ||||
| 		return '/data/access/' + list.id; | ||||
| 		return `/data/access/${list.id}`; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| @@ -472,15 +479,15 @@ const internalAccessList = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	build: (list) => { | ||||
| 		logger.info('Building Access file #' + list.id + ' for: ' + list.name); | ||||
| 		logger.info(`Building Access file #${list.id} for: ${list.name}`); | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			let htpasswd_file = internalAccessList.getFilename(list); | ||||
| 			const htpasswd_file = internalAccessList.getFilename(list); | ||||
|  | ||||
| 			// 1. remove any existing access file | ||||
| 			try { | ||||
| 				fs.unlinkSync(htpasswd_file); | ||||
| 			} catch (err) { | ||||
| 			} catch (_err) { | ||||
| 				// do nothing | ||||
| 			} | ||||
|  | ||||
| @@ -497,12 +504,17 @@ const internalAccessList = { | ||||
| 				if (list.items.length) { | ||||
| 					return new Promise((resolve, reject) => { | ||||
| 						batchflow(list.items).sequential() | ||||
| 							.each((i, item, next) => { | ||||
| 							.each((_i, item, next) => { | ||||
| 								if (typeof item.password !== 'undefined' && item.password.length) { | ||||
| 									logger.info('Adding: ' + item.username); | ||||
| 									logger.info(`Adding: ${item.username}`); | ||||
|  | ||||
| 									utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password]) | ||||
| 										.then((/*result*/) => { | ||||
| 									utils.execFile('openssl', ['passwd', '-apr1', item.password]) | ||||
| 										.then((res) => { | ||||
| 											try { | ||||
| 												fs.appendFileSync(htpasswd_file, `${item.username}:${res}\n`, {encoding: 'utf8'}); | ||||
| 											} catch (err) { | ||||
| 												reject(err); | ||||
| 											} | ||||
| 											next(); | ||||
| 										}) | ||||
| 										.catch((err) => { | ||||
| @@ -516,7 +528,7 @@ const internalAccessList = { | ||||
| 								reject(err); | ||||
| 							}) | ||||
| 							.end((results) => { | ||||
| 								logger.success('Built Access file #' + list.id + ' for: ' + list.name); | ||||
| 								logger.success(`Built Access file #${list.id} for: ${list.name}`); | ||||
| 								resolve(results); | ||||
| 							}); | ||||
| 					}); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| const error         = require('../lib/error'); | ||||
| const auditLogModel = require('../models/audit-log'); | ||||
| const error            = require('../lib/error'); | ||||
| const auditLogModel    = require('../models/audit-log'); | ||||
| const {castJsonIfNeed} = require('../lib/helpers'); | ||||
|  | ||||
| const internalAuditLog = { | ||||
|  | ||||
| @@ -22,9 +23,9 @@ const internalAuditLog = { | ||||
| 					.allowGraph('[user]'); | ||||
|  | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 				if (typeof search_query === 'string' && search_query.length > 0) { | ||||
| 					query.where(function () { | ||||
| 						this.where('meta', 'like', '%' + search_query + '%'); | ||||
| 						this.where(castJsonIfNeed('meta'), 'like', '%' + search_query + '%'); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| const _                = require('lodash'); | ||||
| const fs               = require('fs'); | ||||
| const https            = require('https'); | ||||
| const fs               = require('node:fs'); | ||||
| const https            = require('node:https'); | ||||
| const tempWrite        = require('temp-write'); | ||||
| const moment           = require('moment'); | ||||
| const archiver         = require('archiver'); | ||||
| @@ -49,7 +49,7 @@ const internalCertificate = { | ||||
| 	processExpiringHosts: () => { | ||||
| 		if (!internalCertificate.intervalProcessing) { | ||||
| 			internalCertificate.intervalProcessing = true; | ||||
| 			logger.info('Renewing SSL certs expiring within ' + internalCertificate.renewBeforeExpirationBy[0] + ' ' + internalCertificate.renewBeforeExpirationBy[1] + ' ...'); | ||||
| 			logger.info(`Renewing SSL certs expiring within ${internalCertificate.renewBeforeExpirationBy[0]} ${internalCertificate.renewBeforeExpirationBy[1]} ...`); | ||||
|  | ||||
| 			const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); | ||||
|  | ||||
| @@ -70,7 +70,7 @@ const internalCertificate = { | ||||
| 					 */ | ||||
| 					let sequence = Promise.resolve(); | ||||
|  | ||||
| 					certificates.forEach(function (certificate) { | ||||
| 					certificates.forEach((certificate) => { | ||||
| 						sequence = sequence.then(() => | ||||
| 							internalCertificate | ||||
| 								.renew( | ||||
| @@ -202,7 +202,7 @@ const internalCertificate = { | ||||
| 						.then(() => { | ||||
| 							// At this point, the letsencrypt cert should exist on disk. | ||||
| 							// Lets get the expiry date from the file and update the row silently | ||||
| 							return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') | ||||
| 							return internalCertificate.getCertificateInfoFromFile(`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`) | ||||
| 								.then((cert_info) => { | ||||
| 									return certificateModel | ||||
| 										.query() | ||||
| @@ -263,7 +263,7 @@ const internalCertificate = { | ||||
| 			.then((row) => { | ||||
| 				if (row.id !== data.id) { | ||||
| 					// Sanity check that something crazy hasn't happened | ||||
| 					throw new error.InternalValidationError('Certificate could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); | ||||
| 					throw new error.InternalValidationError(`Certificate could not be updated, IDs do not match: ${row.id} !== ${data.id}`); | ||||
| 				} | ||||
|  | ||||
| 				return certificateModel | ||||
| @@ -308,11 +308,14 @@ const internalCertificate = { | ||||
|  | ||||
| 		return access.can('certificates:get', data.id) | ||||
| 			.then((access_data) => { | ||||
| 				let query = certificateModel | ||||
| 				const query = certificateModel | ||||
| 					.query() | ||||
| 					.where('is_deleted', 0) | ||||
| 					.andWhere('id', data.id) | ||||
| 					.allowGraph('[owner]') | ||||
| 					.allowGraph('[proxy_hosts]') | ||||
| 					.allowGraph('[redirection_hosts]') | ||||
| 					.allowGraph('[dead_hosts]') | ||||
| 					.first(); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| @@ -320,7 +323,7 @@ const internalCertificate = { | ||||
| 				} | ||||
|  | ||||
| 				if (typeof data.expand !== 'undefined' && data.expand !== null) { | ||||
| 					query.withGraphFetched('[' + data.expand.join(', ') + ']'); | ||||
| 					query.withGraphFetched(`[${data.expand.join(', ')}]`); | ||||
| 				} | ||||
|  | ||||
| 				return query.then(utils.omitRow(omissions())); | ||||
| @@ -351,17 +354,17 @@ const internalCertificate = { | ||||
| 				}) | ||||
| 				.then((certificate) => { | ||||
| 					if (certificate.provider === 'letsencrypt') { | ||||
| 						const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id; | ||||
| 						const zipDirectory = internalCertificate.getLiveCertPath(data.id); | ||||
|  | ||||
| 						if (!fs.existsSync(zipDirectory)) { | ||||
| 							throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists'); | ||||
| 							throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`); | ||||
| 						} | ||||
|  | ||||
| 						let certFiles      = fs.readdirSync(zipDirectory) | ||||
| 						const certFiles    = fs.readdirSync(zipDirectory) | ||||
| 							.filter((fn) => fn.endsWith('.pem')) | ||||
| 							.map((fn) => fs.realpathSync(path.join(zipDirectory, fn))); | ||||
| 						const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`; | ||||
| 						const opName       = '/tmp/' + downloadName; | ||||
| 						const downloadName = `npm-${data.id}-${Date.now()}.zip`; | ||||
| 						const opName       = `/tmp/${downloadName}`; | ||||
| 						internalCertificate.zipFiles(certFiles, opName) | ||||
| 							.then(() => { | ||||
| 								logger.debug('zip completed : ', opName); | ||||
| @@ -389,7 +392,7 @@ const internalCertificate = { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			source | ||||
| 				.map((fl) => { | ||||
| 					let fileName = path.basename(fl); | ||||
| 					const fileName = path.basename(fl); | ||||
| 					logger.debug(fl, 'added to certificate zip'); | ||||
| 					archive.file(fl, { name: fileName }); | ||||
| 				}); | ||||
| @@ -459,11 +462,14 @@ const internalCertificate = { | ||||
| 	getAll: (access, expand, search_query) => { | ||||
| 		return access.can('certificates:list') | ||||
| 			.then((access_data) => { | ||||
| 				let query = certificateModel | ||||
| 				const query = certificateModel | ||||
| 					.query() | ||||
| 					.where('is_deleted', 0) | ||||
| 					.groupBy('id') | ||||
| 					.allowGraph('[owner]') | ||||
| 					.allowGraph('[proxy_hosts]') | ||||
| 					.allowGraph('[redirection_hosts]') | ||||
| 					.allowGraph('[dead_hosts]') | ||||
| 					.orderBy('nice_name', 'ASC'); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| @@ -473,12 +479,12 @@ const internalCertificate = { | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 					query.where(function () { | ||||
| 						this.where('nice_name', 'like', '%' + search_query + '%'); | ||||
| 						this.where('nice_name', 'like', `%${search_query}%`); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| 				if (typeof expand !== 'undefined' && expand !== null) { | ||||
| 					query.withGraphFetched('[' + expand.join(', ') + ']'); | ||||
| 					query.withGraphFetched(`[${expand.join(', ')}]`); | ||||
| 				} | ||||
|  | ||||
| 				return query.then(utils.omitRows(omissions())); | ||||
| @@ -493,7 +499,7 @@ const internalCertificate = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	getCount: (user_id, visibility) => { | ||||
| 		let query = certificateModel | ||||
| 		const query = certificateModel | ||||
| 			.query() | ||||
| 			.count('id as count') | ||||
| 			.where('is_deleted', 0); | ||||
| @@ -515,7 +521,7 @@ const internalCertificate = { | ||||
| 	writeCustomCert: (certificate) => { | ||||
| 		logger.info('Writing Custom Certificate:', certificate); | ||||
|  | ||||
| 		const dir = '/data/custom_ssl/npm-' + certificate.id; | ||||
| 		const dir = `/data/custom_ssl/npm-${certificate.id}`; | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			if (certificate.provider === 'letsencrypt') { | ||||
| @@ -525,7 +531,7 @@ const internalCertificate = { | ||||
|  | ||||
| 			let certData = certificate.meta.certificate; | ||||
| 			if (typeof certificate.meta.intermediate_certificate !== 'undefined') { | ||||
| 				certData = certData + '\n' + certificate.meta.intermediate_certificate; | ||||
| 				certData = `${certData}\n${certificate.meta.intermediate_certificate}`; | ||||
| 			} | ||||
|  | ||||
| 			try { | ||||
| @@ -537,7 +543,7 @@ const internalCertificate = { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			fs.writeFile(dir + '/fullchain.pem', certData, function (err) { | ||||
| 			fs.writeFile(`${dir}/fullchain.pem`, certData, (err) => { | ||||
| 				if (err) { | ||||
| 					reject(err); | ||||
| 				} else { | ||||
| @@ -547,7 +553,7 @@ const internalCertificate = { | ||||
| 		}) | ||||
| 			.then(() => { | ||||
| 				return new Promise((resolve, reject) => { | ||||
| 					fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) { | ||||
| 					fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => { | ||||
| 						if (err) { | ||||
| 							reject(err); | ||||
| 						} else { | ||||
| @@ -585,7 +591,7 @@ const internalCertificate = { | ||||
| 	validate: (data) => { | ||||
| 		return new Promise((resolve) => { | ||||
| 			// Put file contents into an object | ||||
| 			let files = {}; | ||||
| 			const files = {}; | ||||
| 			_.map(data.files, (file, name) => { | ||||
| 				if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { | ||||
| 					files[name] = file.data.toString(); | ||||
| @@ -597,7 +603,7 @@ const internalCertificate = { | ||||
| 			.then((files) => { | ||||
| 				// For each file, create a temp file and write the contents to it | ||||
| 				// Then test it depending on the file type | ||||
| 				let promises = []; | ||||
| 				const promises = []; | ||||
| 				_.map(files, (content, type) => { | ||||
| 					promises.push(new Promise((resolve) => { | ||||
| 						if (type === 'certificate_key') { | ||||
| @@ -682,11 +688,11 @@ const internalCertificate = { | ||||
| 						reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.')); | ||||
| 					}, 10000); | ||||
| 					utils | ||||
| 						.exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ') | ||||
| 						.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `) | ||||
| 						.then((result) => { | ||||
| 							clearTimeout(failTimeout); | ||||
| 							if (!result.toLowerCase().includes('key is valid')) { | ||||
| 								reject(new error.ValidationError('Result Validation Error: ' + result)); | ||||
| 								reject(new error.ValidationError(`Result Validation Error: ${result}`)); | ||||
| 							} | ||||
| 							fs.unlinkSync(filepath); | ||||
| 							resolve(true); | ||||
| @@ -694,7 +700,7 @@ const internalCertificate = { | ||||
| 						.catch((err) => { | ||||
| 							clearTimeout(failTimeout); | ||||
| 							fs.unlinkSync(filepath); | ||||
| 							reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err)); | ||||
| 							reject(new error.ValidationError(`Certificate Key is not valid (${err.message})`, err)); | ||||
| 						}); | ||||
| 				}); | ||||
| 			}); | ||||
| @@ -729,9 +735,9 @@ const internalCertificate = { | ||||
| 	 * @param {Boolean} [throw_expired]  Throw when the certificate is out of date | ||||
| 	 */ | ||||
| 	getCertificateInfoFromFile: (certificate_file, throw_expired) => { | ||||
| 		let certData = {}; | ||||
| 		const certData = {}; | ||||
|  | ||||
| 		return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') | ||||
| 		return utils.execFile('openssl', ['x509', '-in', certificate_file, '-subject', '-noout']) | ||||
| 			.then((result) => { | ||||
| 				// Examples: | ||||
| 				// subject=CN = *.jc21.com | ||||
| @@ -739,11 +745,11 @@ const internalCertificate = { | ||||
| 				const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; | ||||
| 				const match = regex.exec(result); | ||||
| 				if (match && typeof match[1] !== 'undefined') { | ||||
| 					certData['cn'] = match[1]; | ||||
| 					certData.cn = match[1]; | ||||
| 				} | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| 				return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); | ||||
| 				return utils.execFile('openssl', ['x509', '-in', certificate_file, '-issuer', '-noout']); | ||||
| 			}) | ||||
|  | ||||
| 			.then((result) => { | ||||
| @@ -754,11 +760,11 @@ const internalCertificate = { | ||||
| 				const regex = /^(?:issuer=)?(.*)$/gim; | ||||
| 				const match = regex.exec(result); | ||||
| 				if (match && typeof match[1] !== 'undefined') { | ||||
| 					certData['issuer'] = match[1]; | ||||
| 					certData.issuer = match[1]; | ||||
| 				} | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| 				return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout'); | ||||
| 				return utils.execFile('openssl', ['x509', '-in', certificate_file, '-dates', '-noout']); | ||||
| 			}) | ||||
| 			.then((result) => { | ||||
| 				// notBefore=Jul 14 04:04:29 2018 GMT | ||||
| @@ -767,7 +773,7 @@ const internalCertificate = { | ||||
| 				let validTo   = null; | ||||
|  | ||||
| 				const lines = result.split('\n'); | ||||
| 				lines.map(function (str) { | ||||
| 				lines.map((str) => { | ||||
| 					const regex = /^(\S+)=(.*)$/gim; | ||||
| 					const match = regex.exec(str.trim()); | ||||
|  | ||||
| @@ -783,21 +789,21 @@ const internalCertificate = { | ||||
| 				}); | ||||
|  | ||||
| 				if (!validFrom || !validTo) { | ||||
| 					throw new error.ValidationError('Could not determine dates from certificate: ' + result); | ||||
| 					throw new error.ValidationError(`Could not determine dates from certificate: ${result}`); | ||||
| 				} | ||||
|  | ||||
| 				if (throw_expired && validTo < parseInt(moment().format('X'), 10)) { | ||||
| 					throw new error.ValidationError('Certificate has expired'); | ||||
| 				} | ||||
|  | ||||
| 				certData['dates'] = { | ||||
| 				certData.dates = { | ||||
| 					from: validFrom, | ||||
| 					to:   validTo | ||||
| 				}; | ||||
|  | ||||
| 				return certData; | ||||
| 			}).catch((err) => { | ||||
| 				throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err); | ||||
| 				throw new error.ValidationError(`Certificate is not valid (${err.message})`, err); | ||||
| 			}); | ||||
| 	}, | ||||
|  | ||||
| @@ -808,7 +814,7 @@ const internalCertificate = { | ||||
| 	 * @param   {Boolean} [remove] | ||||
| 	 * @returns {Object} | ||||
| 	 */ | ||||
| 	cleanMeta: function (meta, remove) { | ||||
| 	cleanMeta: (meta, remove) => { | ||||
| 		internalCertificate.allowedSslFiles.map((key) => { | ||||
| 			if (typeof meta[key] !== 'undefined' && meta[key]) { | ||||
| 				if (remove) { | ||||
| @@ -828,24 +834,35 @@ const internalCertificate = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	requestLetsEncryptSsl: (certificate) => { | ||||
| 		logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); | ||||
| 		logger.info(`Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
|  | ||||
| 		const cmd = `${certbotCommand} certonly ` + | ||||
| 			`--config '${letsencryptConfig}' ` + | ||||
| 			'--work-dir "/tmp/letsencrypt-lib" ' + | ||||
| 			'--logs-dir "/tmp/letsencrypt-log" ' + | ||||
| 			`--cert-name "npm-${certificate.id}" ` + | ||||
| 			'--agree-tos ' + | ||||
| 			'--authenticator webroot ' + | ||||
| 			`--email '${certificate.meta.letsencrypt_email}' ` + | ||||
| 			'--preferred-challenges "dns,http" ' + | ||||
| 			`--domains "${certificate.domain_names.join(',')}" ` + | ||||
| 			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + | ||||
| 			(letsencryptStaging && letsencryptServer === null ? '--staging ' : ''); | ||||
| 		const args = [ | ||||
| 			'certonly', | ||||
| 			'--config', | ||||
| 			letsencryptConfig, | ||||
| 			'--work-dir', | ||||
| 			'/tmp/letsencrypt-lib', | ||||
| 			'--logs-dir', | ||||
| 			'/tmp/letsencrypt-log', | ||||
| 			'--cert-name', | ||||
| 			`npm-${certificate.id}`, | ||||
| 			'--agree-tos', | ||||
| 			'--authenticator', | ||||
| 			'webroot', | ||||
| 			'--email', | ||||
| 			certificate.meta.letsencrypt_email, | ||||
| 			'--preferred-challenges', | ||||
| 			'dns,http', | ||||
| 			'--domains', | ||||
| 			certificate.domain_names.join(','), | ||||
| 		]; | ||||
|  | ||||
| 		logger.info('Command:', cmd); | ||||
| 		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id); | ||||
| 		args.push(...adds.args); | ||||
|  | ||||
| 		return utils.exec(cmd) | ||||
| 		logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); | ||||
|  | ||||
| 		return utils.execFile(certbotCommand, args, adds.opts) | ||||
| 			.then((result) => { | ||||
| 				logger.success(result); | ||||
| 				return result; | ||||
| @@ -862,50 +879,48 @@ const internalCertificate = { | ||||
| 	requestLetsEncryptSslWithDnsChallenge: async (certificate) => { | ||||
| 		await certbot.installPlugin(certificate.meta.dns_provider); | ||||
| 		const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; | ||||
| 		logger.info(`Requesting Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
| 		logger.info(`Requesting LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
|  | ||||
| 		const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; | ||||
| 		const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; | ||||
| 		fs.mkdirSync('/etc/letsencrypt/credentials', { recursive: true }); | ||||
| 		fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, {mode: 0o600}); | ||||
|  | ||||
| 		// Whether the plugin has a --<name>-credentials argument | ||||
| 		const hasConfigArg = certificate.meta.dns_provider !== 'route53'; | ||||
|  | ||||
| 		let mainCmd = certbotCommand + ' certonly ' + | ||||
| 			`--config '${letsencryptConfig}' ` + | ||||
| 			'--work-dir "/tmp/letsencrypt-lib" ' + | ||||
| 			'--logs-dir "/tmp/letsencrypt-log" ' + | ||||
| 			`--cert-name 'npm-${certificate.id}' ` + | ||||
| 			'--agree-tos ' + | ||||
| 			`--email '${certificate.meta.letsencrypt_email}' ` + | ||||
| 			`--domains '${certificate.domain_names.join(',')}' ` + | ||||
| 			`--authenticator '${dnsPlugin.full_plugin_name}' ` + | ||||
| 			( | ||||
| 				hasConfigArg | ||||
| 					? `--${dnsPlugin.full_plugin_name}-credentials '${credentialsLocation}' ` | ||||
| 					: '' | ||||
| 			) + | ||||
| 			( | ||||
| 				certificate.meta.propagation_seconds !== undefined | ||||
| 					? `--${dnsPlugin.full_plugin_name}-propagation-seconds '${certificate.meta.propagation_seconds}' ` | ||||
| 					: '' | ||||
| 			) + | ||||
| 			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + | ||||
| 			(letsencryptStaging && letsencryptServer === null ? '--staging ' : ''); | ||||
| 		const args = [ | ||||
| 			'certonly', | ||||
| 			'--config', | ||||
| 			letsencryptConfig, | ||||
| 			'--work-dir', | ||||
| 			'/tmp/letsencrypt-lib', | ||||
| 			'--logs-dir', | ||||
| 			'/tmp/letsencrypt-log', | ||||
| 			'--cert-name', | ||||
| 			`npm-${certificate.id}`, | ||||
| 			'--agree-tos', | ||||
| 			'--email', | ||||
| 			certificate.meta.letsencrypt_email, | ||||
| 			'--domains', | ||||
| 			certificate.domain_names.join(','), | ||||
| 			'--authenticator', | ||||
| 			dnsPlugin.full_plugin_name, | ||||
| 		]; | ||||
|  | ||||
| 		// Prepend the path to the credentials file as an environment variable | ||||
| 		if (certificate.meta.dns_provider === 'route53') { | ||||
| 			mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; | ||||
| 		if (hasConfigArg) { | ||||
| 			args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation); | ||||
| 		} | ||||
| 		if (certificate.meta.propagation_seconds !== undefined) { | ||||
| 			args.push(`--${dnsPlugin.full_plugin_name}-propagation-seconds`, certificate.meta.propagation_seconds.toString()); | ||||
| 		} | ||||
|  | ||||
| 		if (certificate.meta.dns_provider === 'duckdns') { | ||||
| 			mainCmd = mainCmd + ' --dns-duckdns-no-txt-restore'; | ||||
| 		} | ||||
| 		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); | ||||
| 		args.push(...adds.args); | ||||
|  | ||||
| 		logger.info('Command:', mainCmd); | ||||
| 		logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); | ||||
|  | ||||
| 		try { | ||||
| 			const result = await utils.exec(mainCmd); | ||||
| 			const result = await utils.execFile(certbotCommand, args, adds.opts); | ||||
| 			logger.info(result); | ||||
| 			return result; | ||||
| 		} catch (err) { | ||||
| @@ -933,7 +948,7 @@ const internalCertificate = { | ||||
|  | ||||
| 					return renewMethod(certificate) | ||||
| 						.then(() => { | ||||
| 							return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem'); | ||||
| 							return internalCertificate.getCertificateInfoFromFile(`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`); | ||||
| 						}) | ||||
| 						.then((cert_info) => { | ||||
| 							return certificateModel | ||||
| @@ -965,22 +980,31 @@ const internalCertificate = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	renewLetsEncryptSsl: (certificate) => { | ||||
| 		logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); | ||||
| 		logger.info(`Renewing LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
|  | ||||
| 		const cmd = certbotCommand + ' renew --force-renewal ' + | ||||
| 			`--config '${letsencryptConfig}' ` + | ||||
| 			'--work-dir "/tmp/letsencrypt-lib" ' + | ||||
| 			'--logs-dir "/tmp/letsencrypt-log" ' + | ||||
| 			`--cert-name 'npm-${certificate.id}' ` + | ||||
| 			'--preferred-challenges "dns,http" ' + | ||||
| 			'--no-random-sleep-on-renew ' + | ||||
| 			'--disable-hook-validation ' + | ||||
| 			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + | ||||
| 			(letsencryptStaging && letsencryptServer === null ? '--staging ' : ''); | ||||
| 		const args = [ | ||||
| 			'renew', | ||||
| 			'--force-renewal', | ||||
| 			'--config', | ||||
| 			letsencryptConfig, | ||||
| 			'--work-dir', | ||||
| 			'/tmp/letsencrypt-lib', | ||||
| 			'--logs-dir', | ||||
| 			'/tmp/letsencrypt-log', | ||||
| 			'--cert-name', | ||||
| 			`npm-${certificate.id}`, | ||||
| 			'--preferred-challenges', | ||||
| 			'dns,http', | ||||
| 			'--no-random-sleep-on-renew', | ||||
| 			'--disable-hook-validation', | ||||
| 		]; | ||||
|  | ||||
| 		logger.info('Command:', cmd); | ||||
| 		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); | ||||
| 		args.push(...adds.args); | ||||
|  | ||||
| 		return utils.exec(cmd) | ||||
| 		logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); | ||||
|  | ||||
| 		return utils.execFile(certbotCommand, args, adds.opts) | ||||
| 			.then((result) => { | ||||
| 				logger.info(result); | ||||
| 				return result; | ||||
| @@ -998,27 +1022,29 @@ const internalCertificate = { | ||||
| 			throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); | ||||
| 		} | ||||
|  | ||||
| 		logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
| 		logger.info(`Renewing LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
|  | ||||
| 		let mainCmd = certbotCommand + ' renew --force-renewal ' + | ||||
| 			`--config "${letsencryptConfig}" ` + | ||||
| 			'--work-dir "/tmp/letsencrypt-lib" ' + | ||||
| 			'--logs-dir "/tmp/letsencrypt-log" ' + | ||||
| 			`--cert-name 'npm-${certificate.id}' ` + | ||||
| 			'--disable-hook-validation ' + | ||||
| 			'--no-random-sleep-on-renew ' + | ||||
| 			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + | ||||
| 			(letsencryptStaging && letsencryptServer === null ? '--staging ' : ''); | ||||
| 		const args = [ | ||||
| 			'renew', | ||||
| 			'--force-renewal', | ||||
| 			'--config', | ||||
| 			letsencryptConfig, | ||||
| 			'--work-dir', | ||||
| 			'/tmp/letsencrypt-lib', | ||||
| 			'--logs-dir', | ||||
| 			'/tmp/letsencrypt-log', | ||||
| 			'--cert-name', | ||||
| 			`npm-${certificate.id}`, | ||||
| 			'--disable-hook-validation', | ||||
| 			'--no-random-sleep-on-renew', | ||||
| 		]; | ||||
|  | ||||
| 		// Prepend the path to the credentials file as an environment variable | ||||
| 		if (certificate.meta.dns_provider === 'route53') { | ||||
| 			const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; | ||||
| 			mainCmd                   = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; | ||||
| 		} | ||||
| 		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); | ||||
| 		args.push(...adds.args); | ||||
|  | ||||
| 		logger.info('Command:', mainCmd); | ||||
| 		logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); | ||||
|  | ||||
| 		return utils.exec(mainCmd) | ||||
| 		return utils.execFile(certbotCommand, args, adds.opts) | ||||
| 			.then(async (result) => { | ||||
| 				logger.info(result); | ||||
| 				return result; | ||||
| @@ -1031,25 +1057,29 @@ const internalCertificate = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	revokeLetsEncryptSsl: (certificate, throw_errors) => { | ||||
| 		logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); | ||||
| 		logger.info(`Revoking LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); | ||||
|  | ||||
| 		const mainCmd = certbotCommand + ' revoke ' + | ||||
| 			`--config '${letsencryptConfig}' ` + | ||||
| 			'--work-dir "/tmp/letsencrypt-lib" ' + | ||||
| 			'--logs-dir "/tmp/letsencrypt-log" ' + | ||||
| 			`--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` + | ||||
| 			'--delete-after-revoke ' + | ||||
| 			(letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + | ||||
| 			(letsencryptStaging && letsencryptServer === null ? '--staging ' : ''); | ||||
| 		const args = [ | ||||
| 			'revoke', | ||||
| 			'--config', | ||||
| 			letsencryptConfig, | ||||
| 			'--work-dir', | ||||
| 			'/tmp/letsencrypt-lib', | ||||
| 			'--logs-dir', | ||||
| 			'/tmp/letsencrypt-log', | ||||
| 			'--cert-path', | ||||
| 			`${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, | ||||
| 			'--delete-after-revoke', | ||||
| 		]; | ||||
|  | ||||
| 		// Don't fail command if file does not exist | ||||
| 		const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; | ||||
| 		const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id); | ||||
| 		args.push(...adds.args); | ||||
|  | ||||
| 		logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd); | ||||
| 		logger.info(`Command: ${certbotCommand} ${args ? args.join(' ') : ''}`); | ||||
|  | ||||
| 		return utils.exec(mainCmd) | ||||
| 		return utils.execFile(certbotCommand, args, adds.opts) | ||||
| 			.then(async (result) => { | ||||
| 				await utils.exec(delete_credentialsCmd); | ||||
| 				await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`); | ||||
| 				logger.info(result); | ||||
| 				return result; | ||||
| 			}) | ||||
| @@ -1067,9 +1097,8 @@ const internalCertificate = { | ||||
| 	 * @returns {Boolean} | ||||
| 	 */ | ||||
| 	hasLetsEncryptSslCerts: (certificate) => { | ||||
| 		const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id; | ||||
|  | ||||
| 		return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/privkey.pem'); | ||||
| 		const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id); | ||||
| 		return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| @@ -1081,7 +1110,7 @@ const internalCertificate = { | ||||
| 	 */ | ||||
| 	disableInUseHosts: (in_use_result) => { | ||||
| 		if (in_use_result.total_count) { | ||||
| 			let promises = []; | ||||
| 			const promises = []; | ||||
|  | ||||
| 			if (in_use_result.proxy_hosts.length) { | ||||
| 				promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts)); | ||||
| @@ -1111,7 +1140,7 @@ const internalCertificate = { | ||||
| 	 */ | ||||
| 	enableInUseHosts: (in_use_result) => { | ||||
| 		if (in_use_result.total_count) { | ||||
| 			let promises = []; | ||||
| 			const promises = []; | ||||
|  | ||||
| 			if (in_use_result.proxy_hosts.length) { | ||||
| 				promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts)); | ||||
| @@ -1144,12 +1173,12 @@ const internalCertificate = { | ||||
|  | ||||
| 		// Create a test challenge file | ||||
| 		const testChallengeDir  = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge'; | ||||
| 		const testChallengeFile = testChallengeDir + '/test-challenge'; | ||||
| 		const testChallengeFile = `${testChallengeDir}/test-challenge`; | ||||
| 		fs.mkdirSync(testChallengeDir, {recursive: true}); | ||||
| 		fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'}); | ||||
|  | ||||
| 		async function performTestForDomain (domain) { | ||||
| 			logger.info('Testing http challenge for ' + domain); | ||||
| 			logger.info(`Testing http challenge for ${domain}`); | ||||
| 			const url      = `http://${domain}/.well-known/acme-challenge/test-challenge`; | ||||
| 			const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`; | ||||
| 			const options  = { | ||||
| @@ -1163,13 +1192,16 @@ const internalCertificate = { | ||||
|  | ||||
| 			const result = await new Promise((resolve) => { | ||||
|  | ||||
| 				const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) { | ||||
| 				const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, (res) => { | ||||
| 					let responseBody = ''; | ||||
|  | ||||
| 					res.on('data', (chunk) => responseBody = responseBody + chunk); | ||||
| 					res.on('end', function () { | ||||
| 					res.on('data', (chunk) => { | ||||
| 						responseBody = responseBody + chunk; | ||||
| 					}); | ||||
|  | ||||
| 					res.on('end', () => { | ||||
| 						try { | ||||
| 							const parsedBody = JSON.parse(responseBody + ''); | ||||
| 							const parsedBody = JSON.parse(`${responseBody}`); | ||||
| 							if (res.statusCode !== 200) { | ||||
| 								logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`); | ||||
| 								resolve(undefined); | ||||
| @@ -1190,7 +1222,7 @@ const internalCertificate = { | ||||
| 				// Make sure to write the request body. | ||||
| 				req.write(formBody); | ||||
| 				req.end(); | ||||
| 				req.on('error', function (e) { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e); | ||||
| 				req.on('error', (e) => { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e); | ||||
| 					resolve(undefined); }); | ||||
| 			}); | ||||
|  | ||||
| @@ -1232,6 +1264,34 @@ const internalCertificate = { | ||||
| 		fs.unlinkSync(testChallengeFile); | ||||
|  | ||||
| 		return results; | ||||
| 	}, | ||||
|  | ||||
| 	getAdditionalCertbotArgs: (certificate_id, dns_provider) => { | ||||
| 		const args = []; | ||||
| 		if (letsencryptServer !== null) { | ||||
| 			args.push('--server', letsencryptServer); | ||||
| 		} | ||||
| 		if (letsencryptStaging && letsencryptServer === null) { | ||||
| 			args.push('--staging'); | ||||
| 		} | ||||
|  | ||||
| 		// For route53, add the credentials file as an environment variable, | ||||
| 		// inheriting the process env | ||||
| 		const opts = {}; | ||||
| 		if (certificate_id && dns_provider === 'route53') { | ||||
| 			opts.env                 = process.env; | ||||
| 			opts.env.AWS_CONFIG_FILE = `/etc/letsencrypt/credentials/credentials-${certificate_id}`; | ||||
| 		} | ||||
|  | ||||
| 		if (dns_provider === 'duckdns') { | ||||
| 			args.push('--dns-duckdns-no-txt-restore'); | ||||
| 		} | ||||
|  | ||||
| 		return {args: args, opts: opts}; | ||||
| 	}, | ||||
|  | ||||
| 	getLiveCertPath: (certificate_id) => { | ||||
| 		return `/etc/letsencrypt/live/npm-${certificate_id}`; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const internalHost        = require('./host'); | ||||
| const internalNginx       = require('./nginx'); | ||||
| const internalAuditLog    = require('./audit-log'); | ||||
| const internalCertificate = require('./certificate'); | ||||
| const {castJsonIfNeed}    = require('../lib/helpers'); | ||||
|  | ||||
| function omissions () { | ||||
| 	return ['is_deleted']; | ||||
| @@ -409,16 +410,16 @@ const internalDeadHost = { | ||||
| 					.where('is_deleted', 0) | ||||
| 					.groupBy('id') | ||||
| 					.allowGraph('[owner,certificate]') | ||||
| 					.orderBy('domain_names', 'ASC'); | ||||
| 					.orderBy(castJsonIfNeed('domain_names'), 'ASC'); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| 					query.andWhere('owner_user_id', access.token.getUserId(1)); | ||||
| 				} | ||||
|  | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 				if (typeof search_query === 'string' && search_query.length > 0) { | ||||
| 					query.where(function () { | ||||
| 						this.where('domain_names', 'like', '%' + search_query + '%'); | ||||
| 						this.where(castJsonIfNeed('domain_names'), 'like', '%' + search_query + '%'); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ const _                    = require('lodash'); | ||||
| const proxyHostModel       = require('../models/proxy_host'); | ||||
| const redirectionHostModel = require('../models/redirection_host'); | ||||
| const deadHostModel        = require('../models/dead_host'); | ||||
| const {castJsonIfNeed}     = require('../lib/helpers'); | ||||
|  | ||||
| const internalHost = { | ||||
|  | ||||
| @@ -17,7 +18,7 @@ const internalHost = { | ||||
| 	cleanSslHstsData: function (data, existing_data) { | ||||
| 		existing_data = existing_data === undefined ? {} : existing_data; | ||||
|  | ||||
| 		let combined_data = _.assign({}, existing_data, data); | ||||
| 		const combined_data = _.assign({}, existing_data, data); | ||||
|  | ||||
| 		if (!combined_data.certificate_id) { | ||||
| 			combined_data.ssl_forced    = false; | ||||
| @@ -73,7 +74,7 @@ const internalHost = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	getHostsWithDomains: function (domain_names) { | ||||
| 		let promises = [ | ||||
| 		const promises = [ | ||||
| 			proxyHostModel | ||||
| 				.query() | ||||
| 				.where('is_deleted', 0), | ||||
| @@ -125,19 +126,19 @@ const internalHost = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	isHostnameTaken: function (hostname, ignore_type, ignore_id) { | ||||
| 		let promises = [ | ||||
| 		const promises = [ | ||||
| 			proxyHostModel | ||||
| 				.query() | ||||
| 				.where('is_deleted', 0) | ||||
| 				.andWhere('domain_names', 'like', '%' + hostname + '%'), | ||||
| 				.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'), | ||||
| 			redirectionHostModel | ||||
| 				.query() | ||||
| 				.where('is_deleted', 0) | ||||
| 				.andWhere('domain_names', 'like', '%' + hostname + '%'), | ||||
| 				.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'), | ||||
| 			deadHostModel | ||||
| 				.query() | ||||
| 				.where('is_deleted', 0) | ||||
| 				.andWhere('domain_names', 'like', '%' + hostname + '%') | ||||
| 				.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%') | ||||
| 		]; | ||||
|  | ||||
| 		return Promise.all(promises) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| const _      = require('lodash'); | ||||
| const fs     = require('fs'); | ||||
| const fs     = require('node:fs'); | ||||
| const logger = require('../logger').nginx; | ||||
| const config = require('../lib/config'); | ||||
| const utils  = require('../lib/utils'); | ||||
| @@ -57,9 +57,9 @@ const internalNginx = { | ||||
| 						// It will always look like this: | ||||
| 						//   nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address) | ||||
|  | ||||
| 						let valid_lines = []; | ||||
| 						let err_lines   = err.message.split('\n'); | ||||
| 						err_lines.map(function (line) { | ||||
| 						const valid_lines = []; | ||||
| 						const err_lines   = err.message.split('\n'); | ||||
| 						err_lines.map((line) => { | ||||
| 							if (line.indexOf('/var/log/nginx/error.log') === -1) { | ||||
| 								valid_lines.push(line); | ||||
| 							} | ||||
| @@ -105,7 +105,7 @@ const internalNginx = { | ||||
| 			logger.info('Testing Nginx configuration'); | ||||
| 		} | ||||
|  | ||||
| 		return utils.exec('/usr/sbin/nginx -t -g "error_log off;"'); | ||||
| 		return utils.execFile('/usr/sbin/nginx', ['-t', '-g', 'error_log off;']); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| @@ -115,7 +115,7 @@ const internalNginx = { | ||||
| 		return internalNginx.test() | ||||
| 			.then(() => { | ||||
| 				logger.info('Reloading Nginx'); | ||||
| 				return utils.exec('/usr/sbin/nginx -s reload'); | ||||
| 				return utils.execFile('/usr/sbin/nginx', ['-s', 'reload']); | ||||
| 			}); | ||||
| 	}, | ||||
|  | ||||
| @@ -128,7 +128,7 @@ const internalNginx = { | ||||
| 		if (host_type === 'default') { | ||||
| 			return '/data/nginx/default_host/site.conf'; | ||||
| 		} | ||||
| 		return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf'; | ||||
| 		return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| @@ -141,7 +141,7 @@ const internalNginx = { | ||||
| 			let template; | ||||
|  | ||||
| 			try { | ||||
| 				template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'}); | ||||
| 				template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, {encoding: 'utf8'}); | ||||
| 			} catch (err) { | ||||
| 				reject(new error.ConfigurationError(err.message)); | ||||
| 				return; | ||||
| @@ -152,7 +152,7 @@ const internalNginx = { | ||||
|  | ||||
| 			const locationRendering = async () => { | ||||
| 				for (let i = 0; i < host.locations.length; i++) { | ||||
| 					let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id}, | ||||
| 					const locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id}, | ||||
| 						{ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits}, | ||||
| 						{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support}, | ||||
| 						{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list}, | ||||
| @@ -183,21 +183,21 @@ const internalNginx = { | ||||
| 	 */ | ||||
| 	generateConfig: (host_type, host_row) => { | ||||
| 		// Prevent modifying the original object: | ||||
| 		let host             = JSON.parse(JSON.stringify(host_row)); | ||||
| 		const host           = JSON.parse(JSON.stringify(host_row)); | ||||
| 		const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); | ||||
|  | ||||
| 		if (config.debug()) { | ||||
| 			logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2)); | ||||
| 			logger.info(`Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); | ||||
| 		} | ||||
|  | ||||
| 		const renderEngine = utils.getRenderEngine(); | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			let template = null; | ||||
| 			let filename = internalNginx.getConfigName(nice_host_type, host.id); | ||||
| 			let template   = null; | ||||
| 			const filename = internalNginx.getConfigName(nice_host_type, host.id); | ||||
|  | ||||
| 			try { | ||||
| 				template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', {encoding: 'utf8'}); | ||||
| 				template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, {encoding: 'utf8'}); | ||||
| 			} catch (err) { | ||||
| 				reject(new error.ConfigurationError(err.message)); | ||||
| 				return; | ||||
| @@ -252,7 +252,7 @@ const internalNginx = { | ||||
| 					}) | ||||
| 					.catch((err) => { | ||||
| 						if (config.debug()) { | ||||
| 							logger.warn('Could not write ' + filename + ':', err.message); | ||||
| 							logger.warn(`Could not write ${filename}:`, err.message); | ||||
| 						} | ||||
|  | ||||
| 						reject(new error.ConfigurationError(err.message)); | ||||
| @@ -277,11 +277,11 @@ const internalNginx = { | ||||
| 		const renderEngine = utils.getRenderEngine(); | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			let template = null; | ||||
| 			let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; | ||||
| 			let template   = null; | ||||
| 			const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; | ||||
|  | ||||
| 			try { | ||||
| 				template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'}); | ||||
| 				template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, {encoding: 'utf8'}); | ||||
| 			} catch (err) { | ||||
| 				reject(new error.ConfigurationError(err.message)); | ||||
| 				return; | ||||
| @@ -302,7 +302,7 @@ const internalNginx = { | ||||
| 				}) | ||||
| 				.catch((err) => { | ||||
| 					if (config.debug()) { | ||||
| 						logger.warn('Could not write ' + filename + ':', err.message); | ||||
| 						logger.warn(`Could not write ${filename}:`, err.message); | ||||
| 					} | ||||
|  | ||||
| 					reject(new error.ConfigurationError(err.message)); | ||||
| @@ -316,7 +316,7 @@ const internalNginx = { | ||||
| 	 * @param   {String}  filename | ||||
| 	 */ | ||||
| 	deleteFile: (filename) => { | ||||
| 		logger.debug('Deleting file: ' + filename); | ||||
| 		logger.debug(`Deleting file: ${filename}`); | ||||
| 		try { | ||||
| 			fs.unlinkSync(filename); | ||||
| 		} catch (err) { | ||||
| @@ -330,7 +330,7 @@ const internalNginx = { | ||||
| 	 * @returns String | ||||
| 	 */ | ||||
| 	getFileFriendlyHostType: (host_type) => { | ||||
| 		return host_type.replace(new RegExp('-', 'g'), '_'); | ||||
| 		return host_type.replace(/-/g, '_'); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| @@ -340,7 +340,7 @@ const internalNginx = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	deleteLetsEncryptRequestConfig: (certificate) => { | ||||
| 		const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; | ||||
| 		const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; | ||||
| 		return new Promise((resolve/*, reject*/) => { | ||||
| 			internalNginx.deleteFile(config_file); | ||||
| 			resolve(); | ||||
| @@ -355,7 +355,7 @@ const internalNginx = { | ||||
| 	 */ | ||||
| 	deleteConfig: (host_type, host, delete_err_file) => { | ||||
| 		const config_file     = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); | ||||
| 		const config_file_err = config_file + '.err'; | ||||
| 		const config_file_err = `${config_file}.err`; | ||||
|  | ||||
| 		return new Promise((resolve/*, reject*/) => { | ||||
| 			internalNginx.deleteFile(config_file); | ||||
| @@ -373,7 +373,7 @@ const internalNginx = { | ||||
| 	 */ | ||||
| 	renameConfigAsError: (host_type, host) => { | ||||
| 		const config_file     = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); | ||||
| 		const config_file_err = config_file + '.err'; | ||||
| 		const config_file_err = `${config_file}.err`; | ||||
|  | ||||
| 		return new Promise((resolve/*, reject*/) => { | ||||
| 			fs.unlink(config_file, () => { | ||||
| @@ -392,8 +392,8 @@ const internalNginx = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	bulkGenerateConfigs: (host_type, hosts) => { | ||||
| 		let promises = []; | ||||
| 		hosts.map(function (host) { | ||||
| 		const promises = []; | ||||
| 		hosts.map((host) => { | ||||
| 			promises.push(internalNginx.generateConfig(host_type, host)); | ||||
| 		}); | ||||
|  | ||||
| @@ -406,8 +406,8 @@ const internalNginx = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	bulkDeleteConfigs: (host_type, hosts) => { | ||||
| 		let promises = []; | ||||
| 		hosts.map(function (host) { | ||||
| 		const promises = []; | ||||
| 		hosts.map((host) => { | ||||
| 			promises.push(internalNginx.deleteConfig(host_type, host, true)); | ||||
| 		}); | ||||
|  | ||||
| @@ -418,14 +418,12 @@ const internalNginx = { | ||||
| 	 * @param   {string}  config | ||||
| 	 * @returns {boolean} | ||||
| 	 */ | ||||
| 	advancedConfigHasDefaultLocation: function (cfg) { | ||||
| 		return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); | ||||
| 	}, | ||||
| 	advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im), | ||||
|  | ||||
| 	/** | ||||
| 	 * @returns {boolean} | ||||
| 	 */ | ||||
| 	ipv6Enabled: function () { | ||||
| 	ipv6Enabled: () => { | ||||
| 		if (typeof process.env.DISABLE_IPV6 !== 'undefined') { | ||||
| 			const disabled = process.env.DISABLE_IPV6.toLowerCase(); | ||||
| 			return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes'); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const internalHost        = require('./host'); | ||||
| const internalNginx       = require('./nginx'); | ||||
| const internalAuditLog    = require('./audit-log'); | ||||
| const internalCertificate = require('./certificate'); | ||||
| const {castJsonIfNeed}    = require('../lib/helpers'); | ||||
|  | ||||
| function omissions () { | ||||
| 	return ['is_deleted', 'owner.is_deleted']; | ||||
| @@ -416,16 +417,16 @@ const internalProxyHost = { | ||||
| 					.where('is_deleted', 0) | ||||
| 					.groupBy('id') | ||||
| 					.allowGraph('[owner,access_list,certificate]') | ||||
| 					.orderBy('domain_names', 'ASC'); | ||||
| 					.orderBy(castJsonIfNeed('domain_names'), 'ASC'); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| 					query.andWhere('owner_user_id', access.token.getUserId(1)); | ||||
| 				} | ||||
|  | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 				if (typeof search_query === 'string' && search_query.length > 0) { | ||||
| 					query.where(function () { | ||||
| 						this.where('domain_names', 'like', '%' + search_query + '%'); | ||||
| 						this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const internalHost         = require('./host'); | ||||
| const internalNginx        = require('./nginx'); | ||||
| const internalAuditLog     = require('./audit-log'); | ||||
| const internalCertificate  = require('./certificate'); | ||||
| const {castJsonIfNeed}     = require('../lib/helpers'); | ||||
|  | ||||
| function omissions () { | ||||
| 	return ['is_deleted']; | ||||
| @@ -409,16 +410,16 @@ const internalRedirectionHost = { | ||||
| 					.where('is_deleted', 0) | ||||
| 					.groupBy('id') | ||||
| 					.allowGraph('[owner,certificate]') | ||||
| 					.orderBy('domain_names', 'ASC'); | ||||
| 					.orderBy(castJsonIfNeed('domain_names'), 'ASC'); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| 					query.andWhere('owner_user_id', access.token.getUserId(1)); | ||||
| 				} | ||||
|  | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 				if (typeof search_query === 'string' && search_query.length > 0) { | ||||
| 					query.where(function () { | ||||
| 						this.where('domain_names', 'like', '%' + search_query + '%'); | ||||
| 						this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| const _                = require('lodash'); | ||||
| const error            = require('../lib/error'); | ||||
| const utils            = require('../lib/utils'); | ||||
| const streamModel      = require('../models/stream'); | ||||
| const internalNginx    = require('./nginx'); | ||||
| const internalAuditLog = require('./audit-log'); | ||||
| const _                   = require('lodash'); | ||||
| const error               = require('../lib/error'); | ||||
| const utils               = require('../lib/utils'); | ||||
| const streamModel         = require('../models/stream'); | ||||
| const internalNginx       = require('./nginx'); | ||||
| const internalAuditLog    = require('./audit-log'); | ||||
| const internalCertificate = require('./certificate'); | ||||
| const internalHost        = require('./host'); | ||||
| const {castJsonIfNeed}    = require('../lib/helpers'); | ||||
|  | ||||
| function omissions () { | ||||
| 	return ['is_deleted']; | ||||
| 	return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted']; | ||||
| } | ||||
|  | ||||
| const internalStream = { | ||||
| @@ -17,6 +20,12 @@ const internalStream = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	create: (access, data) => { | ||||
| 		const create_certificate = data.certificate_id === 'new'; | ||||
|  | ||||
| 		if (create_certificate) { | ||||
| 			delete data.certificate_id; | ||||
| 		} | ||||
|  | ||||
| 		return access.can('streams:create', data) | ||||
| 			.then((/*access_data*/) => { | ||||
| 				// TODO: At this point the existing ports should have been checked | ||||
| @@ -26,16 +35,44 @@ const internalStream = { | ||||
| 					data.meta = {}; | ||||
| 				} | ||||
|  | ||||
| 				// streams aren't routed by domain name so don't store domain names in the DB | ||||
| 				let data_no_domains = structuredClone(data); | ||||
| 				delete data_no_domains.domain_names; | ||||
|  | ||||
| 				return streamModel | ||||
| 					.query() | ||||
| 					.insertAndFetch(data) | ||||
| 					.insertAndFetch(data_no_domains) | ||||
| 					.then(utils.omitRow(omissions())); | ||||
| 			}) | ||||
| 			.then((row) => { | ||||
| 				if (create_certificate) { | ||||
| 					return internalCertificate.createQuickCertificate(access, data) | ||||
| 						.then((cert) => { | ||||
| 							// update host with cert id | ||||
| 							return internalStream.update(access, { | ||||
| 								id:             row.id, | ||||
| 								certificate_id: cert.id | ||||
| 							}); | ||||
| 						}) | ||||
| 						.then(() => { | ||||
| 							return row; | ||||
| 						}); | ||||
| 				} else { | ||||
| 					return row; | ||||
| 				} | ||||
| 			}) | ||||
| 			.then((row) => { | ||||
| 				// re-fetch with cert | ||||
| 				return internalStream.get(access, { | ||||
| 					id:     row.id, | ||||
| 					expand: ['certificate', 'owner'] | ||||
| 				}); | ||||
| 			}) | ||||
| 			.then((row) => { | ||||
| 				// Configure nginx | ||||
| 				return internalNginx.configure(streamModel, 'stream', row) | ||||
| 					.then(() => { | ||||
| 						return internalStream.get(access, {id: row.id, expand: ['owner']}); | ||||
| 						return row; | ||||
| 					}); | ||||
| 			}) | ||||
| 			.then((row) => { | ||||
| @@ -59,6 +96,12 @@ const internalStream = { | ||||
| 	 * @return {Promise} | ||||
| 	 */ | ||||
| 	update: (access, data) => { | ||||
| 		const create_certificate = data.certificate_id === 'new'; | ||||
|  | ||||
| 		if (create_certificate) { | ||||
| 			delete data.certificate_id; | ||||
| 		} | ||||
|  | ||||
| 		return access.can('streams:update', data.id) | ||||
| 			.then((/*access_data*/) => { | ||||
| 				// TODO: at this point the existing streams should have been checked | ||||
| @@ -70,16 +113,32 @@ const internalStream = { | ||||
| 					throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); | ||||
| 				} | ||||
|  | ||||
| 				if (create_certificate) { | ||||
| 					return internalCertificate.createQuickCertificate(access, { | ||||
| 						domain_names: data.domain_names || row.domain_names, | ||||
| 						meta:         _.assign({}, row.meta, data.meta) | ||||
| 					}) | ||||
| 						.then((cert) => { | ||||
| 							// update host with cert id | ||||
| 							data.certificate_id = cert.id; | ||||
| 						}) | ||||
| 						.then(() => { | ||||
| 							return row; | ||||
| 						}); | ||||
| 				} else { | ||||
| 					return row; | ||||
| 				} | ||||
| 			}) | ||||
| 			.then((row) => { | ||||
| 				// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. | ||||
| 				data = _.assign({}, { | ||||
| 					domain_names: row.domain_names | ||||
| 				}, data); | ||||
|  | ||||
| 				return streamModel | ||||
| 					.query() | ||||
| 					.patchAndFetchById(row.id, data) | ||||
| 					.then(utils.omitRow(omissions())) | ||||
| 					.then((saved_row) => { | ||||
| 						return internalNginx.configure(streamModel, 'stream', saved_row) | ||||
| 							.then(() => { | ||||
| 								return internalStream.get(access, {id: row.id, expand: ['owner']}); | ||||
| 							}); | ||||
| 					}) | ||||
| 					.then((saved_row) => { | ||||
| 						// Add to audit log | ||||
| 						return internalAuditLog.add(access, { | ||||
| @@ -92,6 +151,17 @@ const internalStream = { | ||||
| 								return saved_row; | ||||
| 							}); | ||||
| 					}); | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| 				return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']}) | ||||
| 					.then((row) => { | ||||
| 						return internalNginx.configure(streamModel, 'stream', row) | ||||
| 							.then((new_meta) => { | ||||
| 								row.meta = new_meta; | ||||
| 								row      = internalHost.cleanRowCertificateMeta(row); | ||||
| 								return _.omit(row, omissions()); | ||||
| 							}); | ||||
| 					}); | ||||
| 			}); | ||||
| 	}, | ||||
|  | ||||
| @@ -114,7 +184,7 @@ const internalStream = { | ||||
| 					.query() | ||||
| 					.where('is_deleted', 0) | ||||
| 					.andWhere('id', data.id) | ||||
| 					.allowGraph('[owner]') | ||||
| 					.allowGraph('[owner,certificate]') | ||||
| 					.first(); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| @@ -131,6 +201,7 @@ const internalStream = { | ||||
| 				if (!row || !row.id) { | ||||
| 					throw new error.ItemNotFoundError(data.id); | ||||
| 				} | ||||
| 				row = internalHost.cleanRowCertificateMeta(row); | ||||
| 				// Custom omissions | ||||
| 				if (typeof data.omit !== 'undefined' && data.omit !== null) { | ||||
| 					row = _.omit(row, data.omit); | ||||
| @@ -196,14 +267,14 @@ const internalStream = { | ||||
| 			.then(() => { | ||||
| 				return internalStream.get(access, { | ||||
| 					id:     data.id, | ||||
| 					expand: ['owner'] | ||||
| 					expand: ['certificate', 'owner'] | ||||
| 				}); | ||||
| 			}) | ||||
| 			.then((row) => { | ||||
| 				if (!row || !row.id) { | ||||
| 					throw new error.ItemNotFoundError(data.id); | ||||
| 				} else if (row.enabled) { | ||||
| 					throw new error.ValidationError('Host is already enabled'); | ||||
| 					throw new error.ValidationError('Stream is already enabled'); | ||||
| 				} | ||||
|  | ||||
| 				row.enabled = 1; | ||||
| @@ -249,7 +320,7 @@ const internalStream = { | ||||
| 				if (!row || !row.id) { | ||||
| 					throw new error.ItemNotFoundError(data.id); | ||||
| 				} else if (!row.enabled) { | ||||
| 					throw new error.ValidationError('Host is already disabled'); | ||||
| 					throw new error.ValidationError('Stream is already disabled'); | ||||
| 				} | ||||
|  | ||||
| 				row.enabled = 0; | ||||
| @@ -293,11 +364,11 @@ const internalStream = { | ||||
| 	getAll: (access, expand, search_query) => { | ||||
| 		return access.can('streams:list') | ||||
| 			.then((access_data) => { | ||||
| 				let query = streamModel | ||||
| 				const query = streamModel | ||||
| 					.query() | ||||
| 					.where('is_deleted', 0) | ||||
| 					.groupBy('id') | ||||
| 					.allowGraph('[owner]') | ||||
| 					.allowGraph('[owner,certificate]') | ||||
| 					.orderBy('incoming_port', 'ASC'); | ||||
|  | ||||
| 				if (access_data.permission_visibility !== 'all') { | ||||
| @@ -305,9 +376,9 @@ const internalStream = { | ||||
| 				} | ||||
|  | ||||
| 				// Query is used for searching | ||||
| 				if (typeof search_query === 'string') { | ||||
| 				if (typeof search_query === 'string' && search_query.length > 0) { | ||||
| 					query.where(function () { | ||||
| 						this.where('incoming_port', 'like', '%' + search_query + '%'); | ||||
| 						this.where(castJsonIfNeed('incoming_port'), 'like', `%${search_query}%`); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| @@ -316,6 +387,13 @@ const internalStream = { | ||||
| 				} | ||||
|  | ||||
| 				return query.then(utils.omitRows(omissions())); | ||||
| 			}) | ||||
| 			.then((rows) => { | ||||
| 				if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { | ||||
| 					return internalHost.cleanAllRowsCertificateMeta(rows); | ||||
| 				} | ||||
|  | ||||
| 				return rows; | ||||
| 			}); | ||||
| 	}, | ||||
|  | ||||
| @@ -327,9 +405,9 @@ const internalStream = { | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	getCount: (user_id, visibility) => { | ||||
| 		let query = streamModel | ||||
| 		const query = streamModel | ||||
| 			.query() | ||||
| 			.count('id as count') | ||||
| 			.count('id AS count') | ||||
| 			.where('is_deleted', 0); | ||||
|  | ||||
| 		if (visibility !== 'all') { | ||||
|   | ||||
| @@ -11,7 +11,7 @@ const certbot = { | ||||
| 	/** | ||||
| 	 * @param {array} pluginKeys | ||||
| 	 */ | ||||
| 	installPlugins: async function (pluginKeys) { | ||||
| 	installPlugins: async (pluginKeys) => { | ||||
| 		let hasErrors = false; | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| @@ -21,7 +21,7 @@ const certbot = { | ||||
| 			} | ||||
|  | ||||
| 			batchflow(pluginKeys).sequential() | ||||
| 				.each((i, pluginKey, next) => { | ||||
| 				.each((_i, pluginKey, next) => { | ||||
| 					certbot.installPlugin(pluginKey) | ||||
| 						.then(() => { | ||||
| 							next(); | ||||
| @@ -51,7 +51,7 @@ const certbot = { | ||||
| 	 * @param   {string}  pluginKey | ||||
| 	 * @returns {Object} | ||||
| 	 */ | ||||
| 	installPlugin: async function (pluginKey) { | ||||
| 	installPlugin: async (pluginKey) => { | ||||
| 		if (typeof dnsPlugins[pluginKey] === 'undefined') { | ||||
| 			// throw Error(`Certbot plugin ${pluginKey} not found`); | ||||
| 			throw new error.ItemNotFoundError(pluginKey); | ||||
| @@ -63,8 +63,15 @@ const certbot = { | ||||
| 		plugin.version      = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); | ||||
| 		plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); | ||||
|  | ||||
| 		const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate'; | ||||
| 		return utils.exec(cmd) | ||||
| 		// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly | ||||
| 		// in new versions of Python | ||||
| 		let env = Object.assign({}, process.env, {SETUPTOOLS_USE_DISTUTILS: 'stdlib'}); | ||||
| 		if (typeof plugin.env === 'object') { | ||||
| 			env = Object.assign(env, plugin.env); | ||||
| 		} | ||||
|  | ||||
| 		const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version}  && deactivate`; | ||||
| 		return utils.exec(cmd, {env}) | ||||
| 			.then((result) => { | ||||
| 				logger.complete(`Installed ${pluginKey}`); | ||||
| 				return result; | ||||
|   | ||||
| @@ -2,7 +2,10 @@ const fs      = require('fs'); | ||||
| const NodeRSA = require('node-rsa'); | ||||
| const logger  = require('../logger').global; | ||||
|  | ||||
| const keysFile = '/data/keys.json'; | ||||
| const keysFile         = '/data/keys.json'; | ||||
| const mysqlEngine      = 'mysql2'; | ||||
| const postgresEngine   = 'pg'; | ||||
| const sqliteClientName = 'sqlite3'; | ||||
|  | ||||
| let instance = null; | ||||
|  | ||||
| @@ -14,7 +17,7 @@ const configure = () => { | ||||
| 		let configData; | ||||
| 		try { | ||||
| 			configData = require(filename); | ||||
| 		} catch (err) { | ||||
| 		} catch (_) { | ||||
| 			// do nothing | ||||
| 		} | ||||
|  | ||||
| @@ -34,7 +37,7 @@ const configure = () => { | ||||
| 		logger.info('Using MySQL configuration'); | ||||
| 		instance = { | ||||
| 			database: { | ||||
| 				engine:   'mysql2', | ||||
| 				engine:   mysqlEngine, | ||||
| 				host:     envMysqlHost, | ||||
| 				port:     process.env.DB_MYSQL_PORT || 3306, | ||||
| 				user:     envMysqlUser, | ||||
| @@ -46,13 +49,33 @@ const configure = () => { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	const envPostgresHost = process.env.DB_POSTGRES_HOST || null; | ||||
| 	const envPostgresUser = process.env.DB_POSTGRES_USER || null; | ||||
| 	const envPostgresName = process.env.DB_POSTGRES_NAME || null; | ||||
| 	if (envPostgresHost && envPostgresUser && envPostgresName) { | ||||
| 		// we have enough postgres creds to go with postgres | ||||
| 		logger.info('Using Postgres configuration'); | ||||
| 		instance = { | ||||
| 			database: { | ||||
| 				engine:   postgresEngine, | ||||
| 				host:     envPostgresHost, | ||||
| 				port:     process.env.DB_POSTGRES_PORT || 5432, | ||||
| 				user:     envPostgresUser, | ||||
| 				password: process.env.DB_POSTGRES_PASSWORD, | ||||
| 				name:     envPostgresName, | ||||
| 			}, | ||||
| 			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', | ||||
| 				client:     sqliteClientName, | ||||
| 				connection: { | ||||
| 					filename: envSqliteFile | ||||
| 				}, | ||||
| @@ -143,7 +166,27 @@ module.exports = { | ||||
| 	 */ | ||||
| 	isSqlite: function () { | ||||
| 		instance === null && configure(); | ||||
| 		return instance.database.knex && instance.database.knex.client === 'sqlite3'; | ||||
| 		return instance.database.knex && instance.database.knex.client === sqliteClientName; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Is this a mysql configuration? | ||||
| 	 * | ||||
| 	 * @returns {boolean} | ||||
| 	 */ | ||||
| 	isMysql: function () { | ||||
| 		instance === null && configure(); | ||||
| 		return instance.database.engine === mysqlEngine; | ||||
| 	}, | ||||
| 	 | ||||
| 	/** | ||||
| 		 * Is this a postgres configuration? | ||||
| 		 * | ||||
| 		 * @returns {boolean} | ||||
| 		 */ | ||||
| 	isPostgres: function () { | ||||
| 		instance === null && configure(); | ||||
| 		return instance.database.engine === postgresEngine; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| const moment = require('moment'); | ||||
| const moment       = require('moment'); | ||||
| const {isPostgres} = require('./config'); | ||||
| const {ref}        = require('objection'); | ||||
|  | ||||
| module.exports = { | ||||
|  | ||||
| @@ -45,6 +47,16 @@ module.exports = { | ||||
| 			} | ||||
| 		}); | ||||
| 		return obj; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Casts a column to json if using postgres | ||||
| 	 * | ||||
| 	 * @param {string} colName | ||||
| 	 * @returns {string|Objection.ReferenceBuilder} | ||||
| 	 */ | ||||
| 	castJsonIfNeed: function (colName) { | ||||
| 		return isPostgres() ? ref(colName).castText() : colName; | ||||
| 	} | ||||
|  | ||||
| }; | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| const _          = require('lodash'); | ||||
| const exec       = require('child_process').exec; | ||||
| const execFile   = require('child_process').execFile; | ||||
| const exec       = require('node:child_process').exec; | ||||
| const execFile   = require('node:child_process').execFile; | ||||
| const { Liquid } = require('liquidjs'); | ||||
| const logger     = require('../logger').global; | ||||
| const error      = require('./error'); | ||||
|  | ||||
| module.exports = { | ||||
|  | ||||
| 	exec: async function(cmd, options = {}) { | ||||
| 	exec: async (cmd, options = {}) => { | ||||
| 		logger.debug('CMD:', cmd); | ||||
|  | ||||
| 		const { stdout, stderr } = await new Promise((resolve, reject) => { | ||||
| @@ -29,15 +29,19 @@ module.exports = { | ||||
| 	/** | ||||
| 	 * @param   {String} cmd | ||||
| 	 * @param   {Array}  args | ||||
| 	 * @param   {Object|undefined}  options | ||||
| 	 * @returns {Promise} | ||||
| 	 */ | ||||
| 	execFile: function (cmd, args) { | ||||
| 		// logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : '')); | ||||
| 	execFile: (cmd, args, options) => { | ||||
| 		logger.debug(`CMD: ${cmd} ${args ? args.join(' ') : ''}`); | ||||
| 		if (typeof options === 'undefined') { | ||||
| 			options = {}; | ||||
| 		} | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			execFile(cmd, args, function (err, stdout, /*stderr*/) { | ||||
| 			execFile(cmd, args, options, (err, stdout, stderr) => { | ||||
| 				if (err && typeof err === 'object') { | ||||
| 					reject(err); | ||||
| 					reject(new error.CommandError(stderr, 1, err)); | ||||
| 				} else { | ||||
| 					resolve(stdout.trim()); | ||||
| 				} | ||||
| @@ -51,7 +55,7 @@ module.exports = { | ||||
| 	 * @param   {Array}  omissions | ||||
| 	 * @returns {Function} | ||||
| 	 */ | ||||
| 	omitRow: function (omissions) { | ||||
| 	omitRow: (omissions) => { | ||||
| 		/** | ||||
| 		 * @param   {Object} row | ||||
| 		 * @returns {Object} | ||||
| @@ -67,7 +71,7 @@ module.exports = { | ||||
| 	 * @param   {Array}  omissions | ||||
| 	 * @returns {Function} | ||||
| 	 */ | ||||
| 	omitRows: function (omissions) { | ||||
| 	omitRows: (omissions) => { | ||||
| 		/** | ||||
| 		 * @param   {Array} rows | ||||
| 		 * @returns {Object} | ||||
| @@ -83,9 +87,9 @@ module.exports = { | ||||
| 	/** | ||||
| 	 * @returns {Object} Liquid render engine | ||||
| 	 */ | ||||
| 	getRenderEngine: function () { | ||||
| 	getRenderEngine: () => { | ||||
| 		const renderEngine = new Liquid({ | ||||
| 			root: __dirname + '/../templates/' | ||||
| 			root: `${__dirname}/../templates/` | ||||
| 		}); | ||||
|  | ||||
| 		/** | ||||
|   | ||||
							
								
								
									
										38
									
								
								backend/migrations/20240427161436_stream_ssl.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								backend/migrations/20240427161436_stream_ssl.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| const migrate_name = 'stream_ssl'; | ||||
| const logger       = require('../logger').migrate; | ||||
|  | ||||
| /** | ||||
|  * Migrate | ||||
|  * | ||||
|  * @see http://knexjs.org/#Schema | ||||
|  * | ||||
|  * @param   {Object} knex | ||||
|  * @returns {Promise} | ||||
|  */ | ||||
| exports.up = function (knex) { | ||||
| 	logger.info('[' + migrate_name + '] Migrating Up...'); | ||||
|  | ||||
| 	return knex.schema.table('stream', (table) => { | ||||
| 		table.integer('certificate_id').notNull().unsigned().defaultTo(0); | ||||
| 	}) | ||||
| 		.then(function () { | ||||
| 			logger.info('[' + migrate_name + '] stream Table altered'); | ||||
| 		}); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Undo Migrate | ||||
|  * | ||||
|  * @param   {Object} knex | ||||
|  * @returns {Promise} | ||||
|  */ | ||||
| exports.down = function (knex) { | ||||
| 	logger.info('[' + migrate_name + '] Migrating Down...'); | ||||
|  | ||||
| 	return knex.schema.table('stream', (table) => { | ||||
| 		table.dropColumn('certificate_id'); | ||||
| 	}) | ||||
| 		.then(function () { | ||||
| 			logger.info('[' + migrate_name + '] stream Table altered'); | ||||
| 		}); | ||||
| }; | ||||
| @@ -4,7 +4,6 @@ | ||||
| const db      = require('../db'); | ||||
| const helpers = require('../lib/helpers'); | ||||
| const Model   = require('objection').Model; | ||||
| const User    = require('./user'); | ||||
| const now     = require('./now_helper'); | ||||
|  | ||||
| Model.knex(db); | ||||
| @@ -68,6 +67,11 @@ class Certificate extends Model { | ||||
| 	} | ||||
|  | ||||
| 	static get relationMappings () { | ||||
| 		const ProxyHost       = require('./proxy_host'); | ||||
| 		const DeadHost        = require('./dead_host'); | ||||
| 		const User            = require('./user'); | ||||
| 		const RedirectionHost = require('./redirection_host'); | ||||
|  | ||||
| 		return { | ||||
| 			owner: { | ||||
| 				relation:   Model.HasOneRelation, | ||||
| @@ -79,6 +83,39 @@ class Certificate extends Model { | ||||
| 				modify: function (qb) { | ||||
| 					qb.where('user.is_deleted', 0); | ||||
| 				} | ||||
| 			}, | ||||
| 			proxy_hosts: { | ||||
| 				relation:   Model.HasManyRelation, | ||||
| 				modelClass: ProxyHost, | ||||
| 				join:       { | ||||
| 					from: 'certificate.id', | ||||
| 					to:   'proxy_host.certificate_id' | ||||
| 				}, | ||||
| 				modify: function (qb) { | ||||
| 					qb.where('proxy_host.is_deleted', 0); | ||||
| 				} | ||||
| 			}, | ||||
| 			dead_hosts: { | ||||
| 				relation:   Model.HasManyRelation, | ||||
| 				modelClass: DeadHost, | ||||
| 				join:       { | ||||
| 					from: 'certificate.id', | ||||
| 					to:   'dead_host.certificate_id' | ||||
| 				}, | ||||
| 				modify: function (qb) { | ||||
| 					qb.where('dead_host.is_deleted', 0); | ||||
| 				} | ||||
| 			}, | ||||
| 			redirection_hosts: { | ||||
| 				relation:   Model.HasManyRelation, | ||||
| 				modelClass: RedirectionHost, | ||||
| 				join:       { | ||||
| 					from: 'certificate.id', | ||||
| 					to:   'redirection_host.certificate_id' | ||||
| 				}, | ||||
| 				modify: function (qb) { | ||||
| 					qb.where('redirection_host.is_deleted', 0); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
|   | ||||
| @@ -12,7 +12,11 @@ Model.knex(db); | ||||
|  | ||||
| const boolFields = [ | ||||
| 	'is_deleted', | ||||
| 	'ssl_forced', | ||||
| 	'http2_support', | ||||
| 	'enabled', | ||||
| 	'hsts_enabled', | ||||
| 	'hsts_subdomains', | ||||
| ]; | ||||
|  | ||||
| class DeadHost extends Model { | ||||
|   | ||||
| @@ -17,6 +17,9 @@ const boolFields = [ | ||||
| 	'preserve_path', | ||||
| 	'ssl_forced', | ||||
| 	'block_exploits', | ||||
| 	'hsts_enabled', | ||||
| 	'hsts_subdomains', | ||||
| 	'http2_support', | ||||
| ]; | ||||
|  | ||||
| class RedirectionHost extends Model { | ||||
|   | ||||
| @@ -1,16 +1,15 @@ | ||||
| // Objection Docs: | ||||
| // http://vincit.github.io/objection.js/ | ||||
|  | ||||
| const db      = require('../db'); | ||||
| const helpers = require('../lib/helpers'); | ||||
| const Model   = require('objection').Model; | ||||
| const User    = require('./user'); | ||||
| const now     = require('./now_helper'); | ||||
| const Model       = require('objection').Model; | ||||
| const db          = require('../db'); | ||||
| const helpers     = require('../lib/helpers'); | ||||
| const User        = require('./user'); | ||||
| const Certificate = require('./certificate'); | ||||
| const now         = require('./now_helper'); | ||||
|  | ||||
| Model.knex(db); | ||||
|  | ||||
| const boolFields = [ | ||||
| 	'is_deleted', | ||||
| 	'enabled', | ||||
| 	'tcp_forwarding', | ||||
| 	'udp_forwarding', | ||||
| ]; | ||||
| @@ -64,6 +63,17 @@ class Stream extends Model { | ||||
| 				modify: function (qb) { | ||||
| 					qb.where('user.is_deleted', 0); | ||||
| 				} | ||||
| 			}, | ||||
| 			certificate: { | ||||
| 				relation:   Model.HasOneRelation, | ||||
| 				modelClass: Certificate, | ||||
| 				join:       { | ||||
| 					from: 'stream.certificate_id', | ||||
| 					to:   'certificate.id' | ||||
| 				}, | ||||
| 				modify: function (qb) { | ||||
| 					qb.where('certificate.is_deleted', 0); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| 		"node-rsa": "^1.0.8", | ||||
| 		"objection": "3.0.1", | ||||
| 		"path": "^0.12.7", | ||||
| 		"pg": "^8.13.1", | ||||
| 		"signale": "1.4.0", | ||||
| 		"sqlite3": "5.1.6", | ||||
| 		"temp-write": "^4.0.0" | ||||
|   | ||||
| @@ -6,7 +6,7 @@ const apiValidator        = require('../../lib/validator/api'); | ||||
| const internalCertificate = require('../../internal/certificate'); | ||||
| const schema              = require('../../schema'); | ||||
|  | ||||
| let router = express.Router({ | ||||
| const router = express.Router({ | ||||
| 	caseSensitive: true, | ||||
| 	strict:        true, | ||||
| 	mergeParams:   true | ||||
| @@ -231,7 +231,7 @@ router | ||||
|  */ | ||||
| router | ||||
| 	.route('/:certificate_id/download') | ||||
| 	.options((req, res) => { | ||||
| 	.options((_req, res) => { | ||||
| 		res.sendStatus(204); | ||||
| 	}) | ||||
| 	.all(jwtdecode()) | ||||
|   | ||||
| @@ -181,7 +181,7 @@ router | ||||
| 				return internalUser.setPassword(res.locals.access, payload); | ||||
| 			}) | ||||
| 			.then((result) => { | ||||
| 				res.status(201) | ||||
| 				res.status(200) | ||||
| 					.send(result); | ||||
| 			}) | ||||
| 			.catch(next); | ||||
| @@ -212,7 +212,7 @@ router | ||||
| 				return internalUser.setPermissions(res.locals.access, payload); | ||||
| 			}) | ||||
| 			.then((result) => { | ||||
| 				res.status(201) | ||||
| 				res.status(200) | ||||
| 					.send(result); | ||||
| 			}) | ||||
| 			.catch(next); | ||||
| @@ -238,7 +238,7 @@ router | ||||
| 	.post((req, res, next) => { | ||||
| 		internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)}) | ||||
| 			.then((result) => { | ||||
| 				res.status(201) | ||||
| 				res.status(200) | ||||
| 					.send(result); | ||||
| 			}) | ||||
| 			.catch(next); | ||||
|   | ||||
| @@ -110,6 +110,11 @@ | ||||
| 		"caching_enabled": { | ||||
| 			"description": "Should we cache assets", | ||||
| 			"type": "boolean" | ||||
| 		}, | ||||
| 		"email": { | ||||
| 			"description": "Email address", | ||||
| 			"type": "string", | ||||
| 			"pattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ | ||||
| 					"type": "object" | ||||
| 				}, | ||||
| 				"letsencrypt_email": { | ||||
| 					"type": "string" | ||||
| 					"$ref": "../common.json#/properties/email" | ||||
| 				}, | ||||
| 				"propagation_seconds": { | ||||
| 					"type": "integer", | ||||
|   | ||||
| @@ -22,8 +22,7 @@ | ||||
| 		"enabled", | ||||
| 		"locations", | ||||
| 		"hsts_enabled", | ||||
| 		"hsts_subdomains", | ||||
| 		"certificate" | ||||
| 		"hsts_subdomains" | ||||
| 	], | ||||
| 	"additionalProperties": false, | ||||
| 	"properties": { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
| 	"type": "array", | ||||
| 	"description": "Proxy Hosts list", | ||||
| 	"description": "Streams list", | ||||
| 	"items": { | ||||
| 		"$ref": "./proxy-host-object.json" | ||||
| 		"$ref": "./stream-object.json" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -53,8 +53,24 @@ | ||||
| 		"enabled": { | ||||
| 			"$ref": "../common.json#/properties/enabled" | ||||
| 		}, | ||||
| 		"certificate_id": { | ||||
| 			"$ref": "../common.json#/properties/certificate_id" | ||||
| 		}, | ||||
| 		"meta": { | ||||
| 			"type": "object" | ||||
| 		}, | ||||
| 		"owner": { | ||||
| 			"$ref": "./user-object.json" | ||||
| 		}, | ||||
| 		"certificate": { | ||||
| 			"oneOf": [ | ||||
| 				{ | ||||
| 					"type": "null" | ||||
| 				}, | ||||
| 				{ | ||||
| 					"$ref": "./certificate-object.json" | ||||
| 				} | ||||
| 			] | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,10 +5,9 @@ | ||||
| 	"additionalProperties": false, | ||||
| 	"properties": { | ||||
| 		"expires": { | ||||
| 			"description": "Token Expiry Unix Time", | ||||
| 			"example": 1566540249, | ||||
| 			"minimum": 1, | ||||
| 			"type": "number" | ||||
| 			"description": "Token Expiry ISO Time String", | ||||
| 			"example": "2025-02-04T20:40:46.340Z", | ||||
| 			"type": "string" | ||||
| 		}, | ||||
| 		"token": { | ||||
| 			"description": "JWT Token", | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| 			"description": "Expansions", | ||||
| 			"schema": { | ||||
| 				"type": "string", | ||||
| 				"enum": ["access_list", "owner", "certificate"] | ||||
| 				"enum": ["owner", "certificate"] | ||||
| 			} | ||||
| 		} | ||||
| 	], | ||||
| @@ -40,7 +40,8 @@ | ||||
| 										"nginx_online": true, | ||||
| 										"nginx_err": null | ||||
| 									}, | ||||
| 									"enabled": true | ||||
| 									"enabled": true, | ||||
| 									"certificate_id": 0 | ||||
| 								} | ||||
| 							] | ||||
| 						} | ||||
|   | ||||
| @@ -32,6 +32,9 @@ | ||||
| 						"udp_forwarding": { | ||||
| 							"$ref": "../../../components/stream-object.json#/properties/udp_forwarding" | ||||
| 						}, | ||||
| 						"certificate_id": { | ||||
| 							"$ref": "../../../components/stream-object.json#/properties/certificate_id" | ||||
| 						}, | ||||
| 						"meta": { | ||||
| 							"$ref": "../../../components/stream-object.json#/properties/meta" | ||||
| 						} | ||||
| @@ -73,7 +76,8 @@ | ||||
| 									"nickname": "Admin", | ||||
| 									"avatar": "", | ||||
| 									"roles": ["admin"] | ||||
| 								} | ||||
| 								}, | ||||
| 								"certificate_id": 0 | ||||
| 							} | ||||
| 						} | ||||
| 					}, | ||||
|   | ||||
| @@ -40,7 +40,8 @@ | ||||
| 									"nginx_online": true, | ||||
| 									"nginx_err": null | ||||
| 								}, | ||||
| 								"enabled": true | ||||
| 								"enabled": true, | ||||
| 								"certificate_id": 0 | ||||
| 							} | ||||
| 						} | ||||
| 					}, | ||||
|   | ||||
| @@ -29,56 +29,26 @@ | ||||
| 					"additionalProperties": false, | ||||
| 					"minProperties": 1, | ||||
| 					"properties": { | ||||
| 						"domain_names": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/domain_names" | ||||
| 						"incoming_port": { | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/incoming_port" | ||||
| 						}, | ||||
| 						"forward_scheme": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme" | ||||
| 						"forwarding_host": { | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/forwarding_host" | ||||
| 						}, | ||||
| 						"forward_host": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/forward_host" | ||||
| 						"forwarding_port": { | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/forwarding_port" | ||||
| 						}, | ||||
| 						"forward_port": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/forward_port" | ||||
| 						"tcp_forwarding": { | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding" | ||||
| 						}, | ||||
| 						"udp_forwarding": { | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/udp_forwarding" | ||||
| 						}, | ||||
| 						"certificate_id": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id" | ||||
| 						}, | ||||
| 						"ssl_forced": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced" | ||||
| 						}, | ||||
| 						"hsts_enabled": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled" | ||||
| 						}, | ||||
| 						"hsts_subdomains": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains" | ||||
| 						}, | ||||
| 						"http2_support": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support" | ||||
| 						}, | ||||
| 						"block_exploits": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits" | ||||
| 						}, | ||||
| 						"caching_enabled": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled" | ||||
| 						}, | ||||
| 						"allow_websocket_upgrade": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" | ||||
| 						}, | ||||
| 						"access_list_id": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id" | ||||
| 						}, | ||||
| 						"advanced_config": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config" | ||||
| 						}, | ||||
| 						"enabled": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/enabled" | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/certificate_id" | ||||
| 						}, | ||||
| 						"meta": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/meta" | ||||
| 						}, | ||||
| 						"locations": { | ||||
| 							"$ref": "../../../../components/proxy-host-object.json#/properties/locations" | ||||
| 							"$ref": "../../../../components/stream-object.json#/properties/meta" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| @@ -94,42 +64,32 @@ | ||||
| 						"default": { | ||||
| 							"value": { | ||||
| 								"id": 1, | ||||
| 								"created_on": "2024-10-08T23:23:03.000Z", | ||||
| 								"modified_on": "2024-10-08T23:26:37.000Z", | ||||
| 								"created_on": "2024-10-09T02:33:45.000Z", | ||||
| 								"modified_on": "2024-10-09T02:33:45.000Z", | ||||
| 								"owner_user_id": 1, | ||||
| 								"domain_names": ["test.example.com"], | ||||
| 								"forward_host": "192.168.0.10", | ||||
| 								"forward_port": 8989, | ||||
| 								"access_list_id": 0, | ||||
| 								"certificate_id": 0, | ||||
| 								"ssl_forced": false, | ||||
| 								"caching_enabled": false, | ||||
| 								"block_exploits": false, | ||||
| 								"advanced_config": "", | ||||
| 								"incoming_port": 9090, | ||||
| 								"forwarding_host": "router.internal", | ||||
| 								"forwarding_port": 80, | ||||
| 								"tcp_forwarding": true, | ||||
| 								"udp_forwarding": false, | ||||
| 								"meta": { | ||||
| 									"nginx_online": true, | ||||
| 									"nginx_err": null | ||||
| 								}, | ||||
| 								"allow_websocket_upgrade": false, | ||||
| 								"http2_support": false, | ||||
| 								"forward_scheme": "http", | ||||
| 								"enabled": true, | ||||
| 								"hsts_enabled": false, | ||||
| 								"hsts_subdomains": false, | ||||
| 								"owner": { | ||||
| 									"id": 1, | ||||
| 									"created_on": "2024-10-07T22:43:55.000Z", | ||||
| 									"modified_on": "2024-10-08T12:52:54.000Z", | ||||
| 									"created_on": "2024-10-09T02:33:16.000Z", | ||||
| 									"modified_on": "2024-10-09T02:33:16.000Z", | ||||
| 									"is_deleted": false, | ||||
| 									"is_disabled": false, | ||||
| 									"email": "admin@example.com", | ||||
| 									"name": "Administrator", | ||||
| 									"nickname": "some guy", | ||||
| 									"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", | ||||
| 									"nickname": "Admin", | ||||
| 									"avatar": "", | ||||
| 									"roles": ["admin"] | ||||
| 								}, | ||||
| 								"certificate": null, | ||||
| 								"access_list": null | ||||
| 								"certificate_id": 0 | ||||
| 							} | ||||
| 						} | ||||
| 					}, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
| 					"examples": { | ||||
| 						"default": { | ||||
| 							"value": { | ||||
| 								"expires": 1566540510, | ||||
| 								"expires": "2025-02-04T20:40:46.340Z", | ||||
| 								"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" | ||||
| 							} | ||||
| 						} | ||||
|   | ||||
| @@ -38,7 +38,7 @@ | ||||
| 						"default": { | ||||
| 							"value": { | ||||
| 								"result": { | ||||
| 									"expires": 1566540510, | ||||
| 									"expires": "2025-02-04T20:40:46.340Z", | ||||
| 									"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" | ||||
| 								} | ||||
| 							} | ||||
|   | ||||
| @@ -9,6 +9,15 @@ | ||||
| 			"url": "http://127.0.0.1:81/api" | ||||
| 		} | ||||
| 	], | ||||
| 	"components": { | ||||
| 		"securitySchemes": { | ||||
| 			"bearerAuth": { | ||||
| 				"type": "http", | ||||
| 				"scheme": "bearer", | ||||
| 				"bearerFormat": "JWT" | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	"paths": { | ||||
| 		"/": { | ||||
| 			"get": { | ||||
|   | ||||
| @@ -15,18 +15,18 @@ const certbot             = require('./lib/certbot'); | ||||
| const setupDefaultUser = () => { | ||||
| 	return userModel | ||||
| 		.query() | ||||
| 		.select(userModel.raw('COUNT(`id`) as `count`')) | ||||
| 		.select('id', ) | ||||
| 		.where('is_deleted', 0) | ||||
| 		.first() | ||||
| 		.then((row) => { | ||||
| 			if (!row.count) { | ||||
| 			if (!row || !row.id) { | ||||
| 				// Create a new user and set password | ||||
| 				let email    = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com'; | ||||
| 				let password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme'; | ||||
| 				 | ||||
| 				logger.info('Creating a new user: ' + email + ' with password: ' + password); | ||||
| 				const email    = (process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com').toLowerCase(); | ||||
| 				const password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme'; | ||||
|  | ||||
| 				let data = { | ||||
| 				logger.info(`Creating a new user: ${email} with password: ${password}`); | ||||
|  | ||||
| 				const data = { | ||||
| 					is_deleted: 0, | ||||
| 					email:      email, | ||||
| 					name:       'Administrator', | ||||
| @@ -77,11 +77,11 @@ const setupDefaultUser = () => { | ||||
| const setupDefaultSettings = () => { | ||||
| 	return settingModel | ||||
| 		.query() | ||||
| 		.select(settingModel.raw('COUNT(`id`) as `count`')) | ||||
| 		.select('id') | ||||
| 		.where({id: 'default-site'}) | ||||
| 		.first() | ||||
| 		.then((row) => { | ||||
| 			if (!row.count) { | ||||
| 			if (!row || !row.id) { | ||||
| 				settingModel | ||||
| 					.query() | ||||
| 					.insert({ | ||||
| @@ -113,20 +113,20 @@ const setupCertbotPlugins = () => { | ||||
| 		.andWhere('provider', 'letsencrypt') | ||||
| 		.then((certificates) => { | ||||
| 			if (certificates && certificates.length) { | ||||
| 				let plugins  = []; | ||||
| 				let promises = []; | ||||
| 				const plugins  = []; | ||||
| 				const promises = []; | ||||
|  | ||||
| 				certificates.map(function (certificate) { | ||||
| 				certificates.map((certificate) => { | ||||
| 					if (certificate.meta && certificate.meta.dns_challenge === true) { | ||||
| 						if (plugins.indexOf(certificate.meta.dns_provider) === -1) { | ||||
| 							plugins.push(certificate.meta.dns_provider); | ||||
| 						} | ||||
|  | ||||
| 						// Make sure credentials file exists | ||||
| 						const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id; | ||||
| 						const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; | ||||
| 						// Escape single quotes and backslashes | ||||
| 						const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); | ||||
| 						const credentials_cmd    = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }'; | ||||
| 						const credentials_cmd    = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`; | ||||
| 						promises.push(utils.exec(credentials_cmd)); | ||||
| 					} | ||||
| 				}); | ||||
| @@ -136,7 +136,7 @@ const setupCertbotPlugins = () => { | ||||
| 						if (promises.length) { | ||||
| 							return Promise.all(promises) | ||||
| 								.then(() => { | ||||
| 									logger.info('Added Certbot plugins ' + plugins.join(', ')); | ||||
| 									logger.info(`Added Certbot plugins ${plugins.join(', ')}`); | ||||
| 								}); | ||||
| 						} | ||||
| 					}); | ||||
| @@ -165,9 +165,7 @@ const setupLogrotation = () => { | ||||
| 	return runLogrotate(); | ||||
| }; | ||||
|  | ||||
| module.exports = function () { | ||||
| 	return setupDefaultUser() | ||||
| 		.then(setupDefaultSettings) | ||||
| 		.then(setupCertbotPlugins) | ||||
| 		.then(setupLogrotation); | ||||
| }; | ||||
| module.exports = () => setupDefaultUser() | ||||
| 	.then(setupDefaultSettings) | ||||
| 	.then(setupCertbotPlugins) | ||||
| 	.then(setupLogrotation); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| {% if certificate.provider == "letsencrypt" %} | ||||
|   # Let's Encrypt SSL | ||||
|   include conf.d/include/letsencrypt-acme-challenge.conf; | ||||
|   include conf.d/include/ssl-cache.conf; | ||||
|   include conf.d/include/ssl-ciphers.conf; | ||||
|   ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; | ||||
|   ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; | ||||
|   | ||||
							
								
								
									
										13
									
								
								backend/templates/_certificates_stream.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/templates/_certificates_stream.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| {% if certificate and certificate_id > 0 %} | ||||
| {% if certificate.provider == "letsencrypt" %} | ||||
|   # Let's Encrypt SSL | ||||
|   include conf.d/include/ssl-cache-stream.conf; | ||||
|   include conf.d/include/ssl-ciphers.conf; | ||||
|   ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; | ||||
|   ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; | ||||
| {%- else %} | ||||
|   # Custom SSL | ||||
|   ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem; | ||||
|   ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem; | ||||
| {%- endif -%} | ||||
| {%- endif -%} | ||||
| @@ -22,5 +22,7 @@ server { | ||||
|   } | ||||
| {% endif %} | ||||
|  | ||||
|   # Custom | ||||
|   include /data/nginx/custom/server_dead[.]conf; | ||||
| } | ||||
| {% endif %} | ||||
|   | ||||
| @@ -5,12 +5,10 @@ | ||||
| {% if enabled %} | ||||
| {% if tcp_forwarding == 1 or tcp_forwarding == true -%} | ||||
| server { | ||||
|   listen {{ incoming_port }}; | ||||
| {% if ipv6 -%} | ||||
|   listen [::]:{{ incoming_port }}; | ||||
| {% else -%} | ||||
|   #listen [::]:{{ incoming_port }}; | ||||
| {% endif %} | ||||
|   listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %}; | ||||
|   {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %}; | ||||
|  | ||||
|   {%- include "_certificates_stream.conf" %} | ||||
|  | ||||
|   proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; | ||||
|  | ||||
| @@ -19,14 +17,12 @@ server { | ||||
|   include /data/nginx/custom/server_stream_tcp[.]conf; | ||||
| } | ||||
| {% endif %} | ||||
| {% if udp_forwarding == 1 or udp_forwarding == true %} | ||||
|  | ||||
| {% if udp_forwarding == 1 or udp_forwarding == true -%} | ||||
| server { | ||||
|   listen {{ incoming_port }} udp; | ||||
| {% if ipv6 -%} | ||||
|   listen [::]:{{ incoming_port }} udp; | ||||
| {% else -%} | ||||
|   #listen [::]:{{ incoming_port }} udp; | ||||
| {% endif %} | ||||
|   {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp; | ||||
|  | ||||
|   proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; | ||||
|  | ||||
|   # Custom | ||||
|   | ||||
| @@ -492,9 +492,9 @@ boxen@^4.2.0: | ||||
|     widest-line "^3.1.0" | ||||
|  | ||||
| brace-expansion@^1.1.7: | ||||
|   version "1.1.11" | ||||
|   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" | ||||
|   integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== | ||||
|   version "1.1.12" | ||||
|   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" | ||||
|   integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== | ||||
|   dependencies: | ||||
|     balanced-match "^1.0.0" | ||||
|     concat-map "0.0.1" | ||||
| @@ -830,9 +830,9 @@ crc32-stream@^4.0.2: | ||||
|     readable-stream "^3.4.0" | ||||
|  | ||||
| cross-spawn@^7.0.2: | ||||
|   version "7.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" | ||||
|   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== | ||||
|   version "7.0.6" | ||||
|   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" | ||||
|   integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== | ||||
|   dependencies: | ||||
|     path-key "^3.1.0" | ||||
|     shebang-command "^2.0.0" | ||||
| @@ -2735,11 +2735,67 @@ path@^0.12.7: | ||||
|     process "^0.11.1" | ||||
|     util "^0.10.3" | ||||
|  | ||||
| pg-cloudflare@^1.1.1: | ||||
|   version "1.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" | ||||
|   integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== | ||||
|  | ||||
| pg-connection-string@2.5.0: | ||||
|   version "2.5.0" | ||||
|   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" | ||||
|   integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== | ||||
|  | ||||
| pg-connection-string@^2.7.0: | ||||
|   version "2.7.0" | ||||
|   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37" | ||||
|   integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA== | ||||
|  | ||||
| pg-int8@1.0.1: | ||||
|   version "1.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" | ||||
|   integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== | ||||
|  | ||||
| pg-pool@^3.7.0: | ||||
|   version "3.7.0" | ||||
|   resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec" | ||||
|   integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g== | ||||
|  | ||||
| pg-protocol@^1.7.0: | ||||
|   version "1.7.0" | ||||
|   resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93" | ||||
|   integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ== | ||||
|  | ||||
| pg-types@^2.1.0: | ||||
|   version "2.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" | ||||
|   integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== | ||||
|   dependencies: | ||||
|     pg-int8 "1.0.1" | ||||
|     postgres-array "~2.0.0" | ||||
|     postgres-bytea "~1.0.0" | ||||
|     postgres-date "~1.0.4" | ||||
|     postgres-interval "^1.1.0" | ||||
|  | ||||
| pg@^8.13.1: | ||||
|   version "8.13.1" | ||||
|   resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.1.tgz#6498d8b0a87ff76c2df7a32160309d3168c0c080" | ||||
|   integrity sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ== | ||||
|   dependencies: | ||||
|     pg-connection-string "^2.7.0" | ||||
|     pg-pool "^3.7.0" | ||||
|     pg-protocol "^1.7.0" | ||||
|     pg-types "^2.1.0" | ||||
|     pgpass "1.x" | ||||
|   optionalDependencies: | ||||
|     pg-cloudflare "^1.1.1" | ||||
|  | ||||
| pgpass@1.x: | ||||
|   version "1.0.5" | ||||
|   resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" | ||||
|   integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== | ||||
|   dependencies: | ||||
|     split2 "^4.1.0" | ||||
|  | ||||
| picomatch@^2.0.4, picomatch@^2.2.1: | ||||
|   version "2.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" | ||||
| @@ -2758,6 +2814,28 @@ pkg-conf@^2.1.0: | ||||
|     find-up "^2.0.0" | ||||
|     load-json-file "^4.0.0" | ||||
|  | ||||
| postgres-array@~2.0.0: | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" | ||||
|   integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== | ||||
|  | ||||
| postgres-bytea@~1.0.0: | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" | ||||
|   integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== | ||||
|  | ||||
| postgres-date@~1.0.4: | ||||
|   version "1.0.7" | ||||
|   resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" | ||||
|   integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== | ||||
|  | ||||
| postgres-interval@^1.1.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" | ||||
|   integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== | ||||
|   dependencies: | ||||
|     xtend "^4.0.0" | ||||
|  | ||||
| prelude-ls@^1.2.1: | ||||
|   version "1.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" | ||||
| @@ -3194,6 +3272,11 @@ socks@^2.6.2: | ||||
|     ip "^2.0.0" | ||||
|     smart-buffer "^4.2.0" | ||||
|  | ||||
| split2@^4.1.0: | ||||
|   version "4.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" | ||||
|   integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== | ||||
|  | ||||
| sprintf-js@~1.0.2: | ||||
|   version "1.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" | ||||
| @@ -3665,6 +3748,11 @@ xdg-basedir@^4.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" | ||||
|   integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== | ||||
|  | ||||
| xtend@^4.0.0: | ||||
|   version "4.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" | ||||
|   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== | ||||
|  | ||||
| y18n@^4.0.0: | ||||
|   version "4.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" | ||||
|   | ||||
							
								
								
									
										8
									
								
								docker/ci.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docker/ci.env
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0 | ||||
| AUTHENTIK_REDIS__HOST=authentik-redis | ||||
| AUTHENTIK_POSTGRESQL__HOST=db-postgres | ||||
| AUTHENTIK_POSTGRESQL__USER=authentik | ||||
| AUTHENTIK_POSTGRESQL__NAME=authentik | ||||
| AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj | ||||
| AUTHENTIK_BOOTSTRAP_PASSWORD=admin | ||||
| AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com | ||||
							
								
								
									
										
											BIN
										
									
								
								docker/ci/postgres/authentik.sql.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docker/ci/postgres/authentik.sql.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -29,7 +29,8 @@ COPY scripts/install-s6 /tmp/install-s6 | ||||
| RUN rm -f /etc/nginx/conf.d/production.conf \ | ||||
| 	&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \ | ||||
| 	&& /tmp/install-s6 "${TARGETPLATFORM}" \ | ||||
| 	&& rm -f /tmp/install-s6 | ||||
| 	&& rm -f /tmp/install-s6 \ | ||||
| 	&& chmod 644 -R /root/.cache | ||||
|  | ||||
| # Certs for testing purposes | ||||
| COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem | ||||
|   | ||||
| @@ -18,6 +18,7 @@ services: | ||||
|       MYSQL_DATABASE: 'npm' | ||||
|       MYSQL_USER: 'npm' | ||||
|       MYSQL_PASSWORD: 'npmpass' | ||||
|       MARIADB_AUTO_UPGRADE: '1' | ||||
|     volumes: | ||||
|       - mysql_vol:/var/lib/mysql | ||||
|     networks: | ||||
|   | ||||
							
								
								
									
										78
									
								
								docker/docker-compose.ci.postgres.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								docker/docker-compose.ci.postgres.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| # WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. | ||||
| services: | ||||
|  | ||||
|   cypress: | ||||
|     environment: | ||||
|       CYPRESS_stack: 'postgres' | ||||
|  | ||||
|   fullstack: | ||||
|     environment: | ||||
|       DB_POSTGRES_HOST: 'db-postgres' | ||||
|       DB_POSTGRES_PORT: '5432' | ||||
|       DB_POSTGRES_USER: 'npm' | ||||
|       DB_POSTGRES_PASSWORD: 'npmpass' | ||||
|       DB_POSTGRES_NAME: 'npm' | ||||
|     depends_on: | ||||
|       - db-postgres | ||||
|       - authentik | ||||
|       - authentik-worker | ||||
|       - authentik-ldap | ||||
|  | ||||
|   db-postgres: | ||||
|     image: postgres:latest | ||||
|     environment: | ||||
|       POSTGRES_USER: 'npm' | ||||
|       POSTGRES_PASSWORD: 'npmpass' | ||||
|       POSTGRES_DB: 'npm' | ||||
|     volumes: | ||||
|       - psql_vol:/var/lib/postgresql/data | ||||
|       - ./ci/postgres:/docker-entrypoint-initdb.d | ||||
|     networks: | ||||
|       - fulltest | ||||
|  | ||||
|   authentik-redis: | ||||
|     image: 'redis:alpine' | ||||
|     command: --save 60 1 --loglevel warning | ||||
|     restart: unless-stopped | ||||
|     healthcheck: | ||||
|       test: ['CMD-SHELL', 'redis-cli ping | grep PONG'] | ||||
|       start_period: 20s | ||||
|       interval: 30s | ||||
|       retries: 5 | ||||
|       timeout: 3s | ||||
|     volumes: | ||||
|       - redis_vol:/data | ||||
|  | ||||
|   authentik: | ||||
|     image: ghcr.io/goauthentik/server:2024.10.1 | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     env_file: | ||||
|       - ci.env | ||||
|     depends_on: | ||||
|       - authentik-redis | ||||
|       - db-postgres | ||||
|  | ||||
|   authentik-worker: | ||||
|     image: ghcr.io/goauthentik/server:2024.10.1 | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     env_file: | ||||
|       - ci.env | ||||
|     depends_on: | ||||
|       - authentik-redis | ||||
|       - db-postgres | ||||
|  | ||||
|   authentik-ldap: | ||||
|     image: ghcr.io/goauthentik/ldap:2024.10.1 | ||||
|     environment: | ||||
|       AUTHENTIK_HOST: 'http://authentik:9000' | ||||
|       AUTHENTIK_INSECURE: 'true' | ||||
|       AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp' | ||||
|     restart: unless-stopped | ||||
|     depends_on: | ||||
|       - authentik | ||||
|  | ||||
| volumes: | ||||
|   psql_vol: | ||||
|   redis_vol: | ||||
| @@ -22,6 +22,10 @@ services: | ||||
|       test: ["CMD", "/usr/bin/check-health"] | ||||
|       interval: 10s | ||||
|       timeout: 3s | ||||
|     expose: | ||||
|       - '80-81/tcp' | ||||
|       - '443/tcp' | ||||
|       - '1500-1503/tcp' | ||||
|     networks: | ||||
|       fulltest: | ||||
|         aliases: | ||||
| @@ -40,7 +44,7 @@ services: | ||||
|           - ca.internal | ||||
|  | ||||
|   pdns: | ||||
|     image: pschiffe/pdns-mysql | ||||
|     image: pschiffe/pdns-mysql:4.8 | ||||
|     volumes: | ||||
|       - '/etc/localtime:/etc/localtime:ro' | ||||
|     environment: | ||||
| @@ -97,7 +101,7 @@ services: | ||||
|       HTTP_PROXY: 'squid:3128' | ||||
|       HTTPS_PROXY: 'squid:3128' | ||||
|     volumes: | ||||
|       - 'cypress_logs:/results' | ||||
|       - 'cypress_logs:/test/results' | ||||
|       - './dev/resolv.conf:/etc/resolv.conf:ro' | ||||
|       - '/etc/localtime:/etc/localtime:ro' | ||||
|     command: cypress run --browser chrome --config-file=cypress/config/ci.js | ||||
|   | ||||
| @@ -2,8 +2,8 @@ | ||||
| services: | ||||
|  | ||||
|   fullstack: | ||||
|     image: nginxproxymanager:dev | ||||
|     container_name: npm_core | ||||
|     image: npm2dev:core | ||||
|     container_name: npm2dev.core | ||||
|     build: | ||||
|       context: ./ | ||||
|       dockerfile: ./dev/Dockerfile | ||||
| @@ -26,11 +26,17 @@ services: | ||||
|       DEVELOPMENT: 'true' | ||||
|       LE_STAGING: 'true' | ||||
|       # db: | ||||
|       DB_MYSQL_HOST: 'db' | ||||
|       DB_MYSQL_PORT: '3306' | ||||
|       DB_MYSQL_USER: 'npm' | ||||
|       DB_MYSQL_PASSWORD: 'npm' | ||||
|       DB_MYSQL_NAME: 'npm' | ||||
|       # DB_MYSQL_HOST: 'db' | ||||
|       # DB_MYSQL_PORT: '3306' | ||||
|       # DB_MYSQL_USER: 'npm' | ||||
|       # DB_MYSQL_PASSWORD: 'npm' | ||||
|       # DB_MYSQL_NAME: 'npm' | ||||
|       # db-postgres: | ||||
|       DB_POSTGRES_HOST: 'db-postgres' | ||||
|       DB_POSTGRES_PORT: '5432' | ||||
|       DB_POSTGRES_USER: 'npm' | ||||
|       DB_POSTGRES_PASSWORD: 'npmpass' | ||||
|       DB_POSTGRES_NAME: 'npm' | ||||
|       # DB_SQLITE_FILE: "/data/database.sqlite" | ||||
|       # DISABLE_IPV6: "true" | ||||
|       # Required for DNS Certificate provisioning testing: | ||||
| @@ -49,11 +55,15 @@ services: | ||||
|       timeout: 3s | ||||
|     depends_on: | ||||
|       - db | ||||
|       - db-postgres | ||||
|       - authentik | ||||
|       - authentik-worker | ||||
|       - authentik-ldap | ||||
|     working_dir: /app | ||||
|  | ||||
|   db: | ||||
|     image: jc21/mariadb-aria | ||||
|     container_name: npm_db | ||||
|     container_name: npm2dev.db | ||||
|     ports: | ||||
|       - 33306:3306 | ||||
|     networks: | ||||
| @@ -66,8 +76,22 @@ services: | ||||
|     volumes: | ||||
|       - db_data:/var/lib/mysql | ||||
|  | ||||
|   db-postgres: | ||||
|     image: postgres:latest | ||||
|     container_name: npm2dev.db-postgres | ||||
|     networks: | ||||
|       - nginx_proxy_manager | ||||
|     environment: | ||||
|       POSTGRES_USER: 'npm' | ||||
|       POSTGRES_PASSWORD: 'npmpass' | ||||
|       POSTGRES_DB: 'npm' | ||||
|     volumes: | ||||
|       - psql_data:/var/lib/postgresql/data | ||||
|       - ./ci/postgres:/docker-entrypoint-initdb.d | ||||
|  | ||||
|   stepca: | ||||
|     image: jc21/testca | ||||
|     container_name: npm2dev.stepca | ||||
|     volumes: | ||||
|       - './dev/resolv.conf:/etc/resolv.conf:ro' | ||||
|       - '/etc/localtime:/etc/localtime:ro' | ||||
| @@ -78,6 +102,7 @@ services: | ||||
|  | ||||
|   dnsrouter: | ||||
|     image: jc21/dnsrouter | ||||
|     container_name: npm2dev.dnsrouter | ||||
|     volumes: | ||||
|       - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro | ||||
|     networks: | ||||
| @@ -85,7 +110,7 @@ services: | ||||
|  | ||||
|   swagger: | ||||
|     image: swaggerapi/swagger-ui:latest | ||||
|     container_name: npm_swagger | ||||
|     container_name: npm2dev.swagger | ||||
|     ports: | ||||
|       - 3082:80 | ||||
|     environment: | ||||
| @@ -96,7 +121,7 @@ services: | ||||
|  | ||||
|   squid: | ||||
|     image: ubuntu/squid | ||||
|     container_name: npm_squid | ||||
|     container_name: npm2dev.squid | ||||
|     volumes: | ||||
|       - './dev/squid.conf:/etc/squid/squid.conf:ro' | ||||
|       - './dev/resolv.conf:/etc/resolv.conf:ro' | ||||
| @@ -107,7 +132,8 @@ services: | ||||
|       - 8128:3128 | ||||
|  | ||||
|   pdns: | ||||
|     image: pschiffe/pdns-mysql | ||||
|     image: pschiffe/pdns-mysql:4.8 | ||||
|     container_name: npm2dev.pdns | ||||
|     volumes: | ||||
|       - '/etc/localtime:/etc/localtime:ro' | ||||
|     environment: | ||||
| @@ -136,6 +162,7 @@ services: | ||||
|  | ||||
|   pdns-db: | ||||
|     image: mariadb | ||||
|     container_name: npm2dev.pdns-db | ||||
|     environment: | ||||
|       MYSQL_ROOT_PASSWORD: 'pdns' | ||||
|       MYSQL_DATABASE: 'pdns' | ||||
| @@ -149,7 +176,8 @@ services: | ||||
|       - nginx_proxy_manager | ||||
|  | ||||
|   cypress: | ||||
|     image: "npm_dev_cypress" | ||||
|     image: npm2dev:cypress | ||||
|     container_name: npm2dev.cypress | ||||
|     build: | ||||
|       context: ../ | ||||
|       dockerfile: test/cypress/Dockerfile | ||||
| @@ -164,16 +192,77 @@ services: | ||||
|     networks: | ||||
|       - nginx_proxy_manager | ||||
|  | ||||
|   authentik-redis: | ||||
|     image: 'redis:alpine' | ||||
|     container_name: npm2dev.authentik-redis | ||||
|     command: --save 60 1 --loglevel warning | ||||
|     networks: | ||||
|       - nginx_proxy_manager | ||||
|     restart: unless-stopped | ||||
|     healthcheck: | ||||
|       test: ['CMD-SHELL', 'redis-cli ping | grep PONG'] | ||||
|       start_period: 20s | ||||
|       interval: 30s | ||||
|       retries: 5 | ||||
|       timeout: 3s | ||||
|     volumes: | ||||
|       - redis_data:/data | ||||
|  | ||||
|   authentik: | ||||
|     image: ghcr.io/goauthentik/server:2024.10.1 | ||||
|     container_name: npm2dev.authentik | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     networks: | ||||
|       - nginx_proxy_manager | ||||
|     env_file: | ||||
|       - ci.env | ||||
|     ports: | ||||
|       - 9000:9000 | ||||
|     depends_on: | ||||
|       - authentik-redis | ||||
|       - db-postgres | ||||
|  | ||||
|   authentik-worker: | ||||
|     image: ghcr.io/goauthentik/server:2024.10.1 | ||||
|     container_name: npm2dev.authentik-worker | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     networks: | ||||
|       - nginx_proxy_manager | ||||
|     env_file: | ||||
|       - ci.env | ||||
|     depends_on: | ||||
|       - authentik-redis | ||||
|       - db-postgres | ||||
|  | ||||
|   authentik-ldap: | ||||
|     image: ghcr.io/goauthentik/ldap:2024.10.1 | ||||
|     container_name: npm2dev.authentik-ldap | ||||
|     networks: | ||||
|       - nginx_proxy_manager | ||||
|     environment: | ||||
|       AUTHENTIK_HOST: 'http://authentik:9000' | ||||
|       AUTHENTIK_INSECURE: 'true' | ||||
|       AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp' | ||||
|     restart: unless-stopped | ||||
|     depends_on: | ||||
|       - authentik | ||||
|  | ||||
| volumes: | ||||
|   npm_data: | ||||
|     name: npm_core_data | ||||
|     name: npm2dev_core_data | ||||
|   le_data: | ||||
|     name: npm_le_data | ||||
|     name: npm2dev_le_data | ||||
|   db_data: | ||||
|     name: npm_db_data | ||||
|     name: npm2dev_db_data | ||||
|   pdns_mysql: | ||||
|     name: npm_pdns_mysql | ||||
|     name: npnpm2dev_pdns_mysql | ||||
|   psql_data: | ||||
|     name: npm2dev_psql_data | ||||
|   redis_data: | ||||
|     name: npm2dev_redis_data | ||||
|  | ||||
| networks: | ||||
|   nginx_proxy_manager: | ||||
|     name: npm_network | ||||
|     name: npm2dev_network | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|eot|ttf|svg|ico|css\.map|js\.map)$ { | ||||
| location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ { | ||||
| 	if_modified_since off; | ||||
|  | ||||
| 	# use the public cache | ||||
|   | ||||
| @@ -0,0 +1,2 @@ | ||||
| ssl_session_timeout 5m; | ||||
| ssl_session_cache shared:SSL_stream:50m; | ||||
							
								
								
									
										2
									
								
								docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| ssl_session_timeout 5m; | ||||
| ssl_session_cache shared:SSL:50m; | ||||
| @@ -1,6 +1,3 @@ | ||||
| ssl_session_timeout 5m; | ||||
| ssl_session_cache shared:SSL:50m; | ||||
|  | ||||
| # intermediate configuration. tweak to your needs. | ||||
| ssl_protocols TLSv1.2 TLSv1.3; | ||||
| ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; | ||||
|   | ||||
| @@ -8,21 +8,53 @@ log_info 'Setting ownership ...' | ||||
| # root | ||||
| chown root /tmp/nginx | ||||
|  | ||||
| # npm user and group | ||||
| chown -R "$PUID:$PGID" /data | ||||
| chown -R "$PUID:$PGID" /etc/letsencrypt | ||||
| chown -R "$PUID:$PGID" /run/nginx | ||||
| chown -R "$PUID:$PGID" /tmp/nginx | ||||
| chown -R "$PUID:$PGID" /var/cache/nginx | ||||
| chown -R "$PUID:$PGID" /var/lib/logrotate | ||||
| chown -R "$PUID:$PGID" /var/lib/nginx | ||||
| chown -R "$PUID:$PGID" /var/log/nginx | ||||
| locations=( | ||||
| 	"/data" | ||||
| 	"/etc/letsencrypt" | ||||
| 	"/run/nginx" | ||||
| 	"/tmp/nginx" | ||||
| 	"/var/cache/nginx" | ||||
| 	"/var/lib/logrotate" | ||||
| 	"/var/lib/nginx" | ||||
| 	"/var/log/nginx" | ||||
| 	"/etc/nginx/nginx" | ||||
| 	"/etc/nginx/nginx.conf" | ||||
| 	"/etc/nginx/conf.d" | ||||
| ) | ||||
|  | ||||
| # Don't chown entire /etc/nginx folder as this causes crashes on some systems | ||||
| chown -R "$PUID:$PGID" /etc/nginx/nginx | ||||
| chown -R "$PUID:$PGID" /etc/nginx/nginx.conf | ||||
| chown -R "$PUID:$PGID" /etc/nginx/conf.d | ||||
| chownit() { | ||||
| 	local dir="$1" | ||||
| 	local recursive="${2:-true}" | ||||
|  | ||||
| # Prevents errors when installing python certbot plugins when non-root | ||||
| chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin | ||||
| find /opt/certbot/lib/python*/site-packages -not -user "$PUID" -execdir chown "$PUID:$PGID" {} \+ | ||||
| 	local have | ||||
| 	have="$(stat -c '%u:%g' "$dir")" | ||||
| 	echo "- $dir ... " | ||||
|  | ||||
| 	if [ "$have" != "$PUID:$PGID" ]; then | ||||
| 		if [ "$recursive" = 'true' ] && [ -d "$dir" ]; then | ||||
| 			chown -R "$PUID:$PGID" "$dir" | ||||
| 		else | ||||
| 			chown "$PUID:$PGID" "$dir" | ||||
| 		fi | ||||
| 		echo "    DONE" | ||||
| 	else | ||||
| 		echo "    SKIPPED" | ||||
| 	fi | ||||
| } | ||||
|  | ||||
| for loc in "${locations[@]}"; do | ||||
| 	chownit "$loc" | ||||
| done | ||||
|  | ||||
| if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then | ||||
| 	log_info 'Skipping ownership change of certbot directories' | ||||
| else | ||||
| 	log_info 'Changing ownership of certbot directories, this may take some time ...' | ||||
| 	chownit "/opt/certbot" false | ||||
| 	chownit "/opt/certbot/bin" false | ||||
|  | ||||
| 	# Handle all site-packages directories efficiently | ||||
| 	find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do | ||||
| 		chownit "$SITE_PACKAGES_DIR" | ||||
| 	done | ||||
| fi | ||||
|   | ||||
| @@ -5,12 +5,9 @@ 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 | ||||
| if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; 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 | ||||
|   | ||||
| @@ -8,14 +8,11 @@ set -e | ||||
|  | ||||
| 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 | ||||
| 	if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then | ||||
| 		# IPV6 is disabled | ||||
| 		echo "Disabling IPV6 in hosts in: $1" | ||||
| 		SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g' | ||||
|   | ||||
| @@ -56,3 +56,13 @@ get_group_id () { | ||||
| 		getent group "$1" | cut -d: -f3 | ||||
| 	fi | ||||
| } | ||||
|  | ||||
| # param $1: value | ||||
| is_true () { | ||||
| 	VAL=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') | ||||
| 	if [ "$VAL" == 'true' ] || [ "$VAL" == 'on' ] || [ "$VAL" == '1' ] || [ "$VAL" == 'yes' ]; then | ||||
| 		echo '1' | ||||
| 	else | ||||
| 		echo '0' | ||||
| 	fi | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ BLUE='\E[1;34m' | ||||
| GREEN='\E[1;32m' | ||||
| RESET='\E[0m' | ||||
|  | ||||
| S6_OVERLAY_VERSION=3.1.5.0 | ||||
| S6_OVERLAY_VERSION=3.2.1.0 | ||||
| TARGETPLATFORM=${1:-linux/amd64} | ||||
|  | ||||
| # Determine the correct binary file for the architecture given | ||||
|   | ||||
| @@ -50,7 +50,6 @@ networks: | ||||
| Let's look at a Portainer example: | ||||
|  | ||||
| ```yml | ||||
| version: '3.8' | ||||
| services: | ||||
|  | ||||
|   portainer: | ||||
| @@ -92,8 +91,6 @@ This image supports the use of Docker secrets to import from files and keep sens | ||||
| You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name. | ||||
|  | ||||
| ```yml | ||||
| version: '3.8' | ||||
|  | ||||
| secrets: | ||||
|   # Secrets are single-line text files where the sole content is the secret | ||||
|   # Paths in this example assume that secrets are kept in local folder called ".secrets" | ||||
| @@ -164,6 +161,14 @@ The easy fix is to add a Docker environment variable to the Nginx Proxy Manager | ||||
|       DISABLE_IPV6: 'true' | ||||
| ``` | ||||
|  | ||||
| ## Disabling IP Ranges Fetch | ||||
|  | ||||
| By default, NPM fetches IP ranges from CloudFront and Cloudflare during application startup. In environments with limited internet access or to speed up container startup, this fetch can be disabled: | ||||
|  | ||||
| ```yml | ||||
|     environment: | ||||
|       IP_RANGES_FETCH_ENABLED: 'false' | ||||
| ``` | ||||
|  | ||||
| ## Custom Nginx Configurations | ||||
|  | ||||
| @@ -184,6 +189,7 @@ You can add your custom configuration snippet files at `/data/nginx/custom` as f | ||||
|  - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block | ||||
|  - `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block | ||||
|  - `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block | ||||
|  - `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block | ||||
|  | ||||
| Every file is optional. | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,6 @@ outline: deep | ||||
| Create a `docker-compose.yml` file: | ||||
|  | ||||
| ```yml | ||||
| version: '3.8' | ||||
| services: | ||||
|   app: | ||||
|     image: 'jc21/nginx-proxy-manager:latest' | ||||
| @@ -22,8 +21,7 @@ services: | ||||
|       # Add any other Stream port you want to expose | ||||
|       # - '21:21' # FTP | ||||
|  | ||||
|     # Uncomment the next line if you uncomment anything in the section | ||||
|     # environment: | ||||
|     #environment: | ||||
|       # Uncomment this if you want to change the location of | ||||
|       # the SQLite DB file within the container | ||||
|       # DB_SQLITE_FILE: "/data/database.sqlite" | ||||
| @@ -55,7 +53,6 @@ are going to use. | ||||
| Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container: | ||||
|  | ||||
| ```yml | ||||
| version: '3.8' | ||||
| services: | ||||
|   app: | ||||
|     image: 'jc21/nginx-proxy-manager:latest' | ||||
| @@ -101,6 +98,53 @@ Please note, that `DB_MYSQL_*` environment variables will take precedent over `D | ||||
|  | ||||
| ::: | ||||
|  | ||||
| ## Using Postgres database | ||||
|  | ||||
| Similar to the MySQL server setup: | ||||
|  | ||||
| ```yml | ||||
| services: | ||||
|   app: | ||||
|     image: 'jc21/nginx-proxy-manager:latest' | ||||
|     restart: unless-stopped | ||||
|     ports: | ||||
|       # These ports are in format <host-port>:<container-port> | ||||
|       - '80:80' # Public HTTP Port | ||||
|       - '443:443' # Public HTTPS Port | ||||
|       - '81:81' # Admin Web Port | ||||
|       # Add any other Stream port you want to expose | ||||
|       # - '21:21' # FTP | ||||
|     environment: | ||||
|       # Postgres parameters: | ||||
|       DB_POSTGRES_HOST: 'db' | ||||
|       DB_POSTGRES_PORT: '5432' | ||||
|       DB_POSTGRES_USER: 'npm' | ||||
|       DB_POSTGRES_PASSWORD: 'npmpass' | ||||
|       DB_POSTGRES_NAME: 'npm' | ||||
|       # Uncomment this if IPv6 is not enabled on your host | ||||
|       # DISABLE_IPV6: 'true' | ||||
|     volumes: | ||||
|       - ./data:/data | ||||
|       - ./letsencrypt:/etc/letsencrypt | ||||
|     depends_on: | ||||
|       - db | ||||
|  | ||||
|   db: | ||||
|     image: postgres:latest | ||||
|     environment: | ||||
|       POSTGRES_USER: 'npm' | ||||
|       POSTGRES_PASSWORD: 'npmpass' | ||||
|       POSTGRES_DB: 'npm' | ||||
|     volumes: | ||||
|       - ./postgres:/var/lib/postgresql/data | ||||
| ``` | ||||
|  | ||||
| ::: warning | ||||
|  | ||||
| Custom Postgres schema is not supported, as such `public` will be used. | ||||
|  | ||||
| ::: | ||||
|  | ||||
| ## Running on Raspberry PI / ARM devices | ||||
|  | ||||
| The docker images support the following architectures: | ||||
|   | ||||
							
								
								
									
										1
									
								
								docs/src/third-party/index.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								docs/src/third-party/index.md
									
									
									
									
										vendored
									
									
								
							| @@ -12,6 +12,7 @@ Known integrations: | ||||
| - [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager) | ||||
| - [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager) | ||||
| - [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) | ||||
| - [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager) | ||||
| - [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -873,9 +873,9 @@ mitt@^3.0.1: | ||||
|   integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== | ||||
|  | ||||
| nanoid@^3.3.7: | ||||
|   version "3.3.7" | ||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" | ||||
|   integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== | ||||
|   version "3.3.8" | ||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" | ||||
|   integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== | ||||
|  | ||||
| oniguruma-to-js@0.4.3: | ||||
|   version "0.4.3" | ||||
| @@ -1065,9 +1065,9 @@ vfile@^6.0.0: | ||||
|     vfile-message "^4.0.0" | ||||
|  | ||||
| vite@^5.4.8: | ||||
|   version "5.4.8" | ||||
|   resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8" | ||||
|   integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ== | ||||
|   version "5.4.21" | ||||
|   resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027" | ||||
|   integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw== | ||||
|   dependencies: | ||||
|     esbuild "^0.21.3" | ||||
|     postcss "^8.4.43" | ||||
|   | ||||
| @@ -4,444 +4,438 @@ const Tokens   = require('./tokens'); | ||||
|  | ||||
| module.exports = { | ||||
|  | ||||
|     /** | ||||
|      * @param {String} route | ||||
|      * @param {Object} [options] | ||||
|      * @returns {Boolean} | ||||
|      */ | ||||
|     navigate: function (route, options) { | ||||
|         options = options || {}; | ||||
|         Backbone.history.navigate(route.toString(), options); | ||||
|         return true; | ||||
|     }, | ||||
| 	/** | ||||
| 	 * @param {String} route | ||||
| 	 * @param {Object} [options] | ||||
| 	 * @returns {Boolean} | ||||
| 	 */ | ||||
| 	navigate: function (route, options) { | ||||
| 		options = options || {}; | ||||
| 		Backbone.history.navigate(route.toString(), options); | ||||
| 		return true; | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Login | ||||
|      */ | ||||
|     showLogin: function () { | ||||
|         window.location = '/login'; | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Login | ||||
| 	 */ | ||||
| 	showLogin: function () { | ||||
| 		window.location = '/login'; | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Users | ||||
|      */ | ||||
|     showUsers: function () { | ||||
|         let controller = this; | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './users/main'], (App, View) => { | ||||
|                 controller.navigate('/users'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } else { | ||||
|             this.showDashboard(); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Users | ||||
| 	 */ | ||||
| 	showUsers: function () { | ||||
| 		const controller = this; | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			require(['./main', './users/main'], (App, View) => { | ||||
| 				controller.navigate('/users'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			this.showDashboard(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * User Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showUserForm: function (model) { | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './user/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * User Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showUserForm: function (model) { | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			require(['./main', './user/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * User Permissions Form | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showUserPermissions: function (model) { | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './user/permissions'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * User Permissions Form | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showUserPermissions: function (model) { | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			require(['./main', './user/permissions'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * User Password Form | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showUserPasswordForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) { | ||||
|             require(['./main', './user/password'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * User Password Form | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showUserPasswordForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) { | ||||
| 			require(['./main', './user/password'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * User Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showUserDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) { | ||||
|             require(['./main', './user/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * User Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showUserDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) { | ||||
| 			require(['./main', './user/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Dashboard | ||||
|      */ | ||||
|     showDashboard: function () { | ||||
|         let controller = this; | ||||
| 	/** | ||||
| 	 * Dashboard | ||||
| 	 */ | ||||
| 	showDashboard: function () { | ||||
| 		const controller = this; | ||||
| 		require(['./main', './dashboard/main'], (App, View) => { | ||||
| 			controller.navigate('/'); | ||||
| 			App.UI.showAppContent(new View()); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
|         require(['./main', './dashboard/main'], (App, View) => { | ||||
|             controller.navigate('/'); | ||||
|             App.UI.showAppContent(new View()); | ||||
|         }); | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Proxy Hosts | ||||
| 	 */ | ||||
| 	showNginxProxy: function () { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) { | ||||
| 			const controller = this; | ||||
|  | ||||
|     /** | ||||
|      * Nginx Proxy Hosts | ||||
|      */ | ||||
|     showNginxProxy: function () { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) { | ||||
|             let controller = this; | ||||
| 			require(['./main', './nginx/proxy/main'], (App, View) => { | ||||
| 				controller.navigate('/nginx/proxy'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|             require(['./main', './nginx/proxy/main'], (App, View) => { | ||||
|                 controller.navigate('/nginx/proxy'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Proxy Host Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showNginxProxyForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { | ||||
| 			require(['./main', './nginx/proxy/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Proxy Host Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxProxyForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { | ||||
|             require(['./main', './nginx/proxy/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Proxy Host Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxProxyDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { | ||||
| 			require(['./main', './nginx/proxy/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Proxy Host Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxProxyDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { | ||||
|             require(['./main', './nginx/proxy/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Redirection Hosts | ||||
| 	 */ | ||||
| 	showNginxRedirection: function () { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) { | ||||
| 			const controller = this; | ||||
| 			require(['./main', './nginx/redirection/main'], (App, View) => { | ||||
| 				controller.navigate('/nginx/redirection'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Redirection Hosts | ||||
|      */ | ||||
|     showNginxRedirection: function () { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) { | ||||
|             let controller = this; | ||||
| 	/** | ||||
| 	 * Nginx Redirection Host Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showNginxRedirectionForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { | ||||
| 			require(['./main', './nginx/redirection/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|             require(['./main', './nginx/redirection/main'], (App, View) => { | ||||
|                 controller.navigate('/nginx/redirection'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Proxy Redirection Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxRedirectionDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { | ||||
| 			require(['./main', './nginx/redirection/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Redirection Host Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxRedirectionForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { | ||||
|             require(['./main', './nginx/redirection/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Stream Hosts | ||||
| 	 */ | ||||
| 	showNginxStream: function () { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canView('streams')) { | ||||
| 			const controller = this; | ||||
| 			require(['./main', './nginx/stream/main'], (App, View) => { | ||||
| 				controller.navigate('/nginx/stream'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Proxy Redirection Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxRedirectionDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { | ||||
|             require(['./main', './nginx/redirection/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Stream Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showNginxStreamForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { | ||||
| 			require(['./main', './nginx/stream/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Stream Hosts | ||||
|      */ | ||||
|     showNginxStream: function () { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canView('streams')) { | ||||
|             let controller = this; | ||||
| 	/** | ||||
| 	 * Stream Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxStreamDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { | ||||
| 			require(['./main', './nginx/stream/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|             require(['./main', './nginx/stream/main'], (App, View) => { | ||||
|                 controller.navigate('/nginx/stream'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Dead Hosts | ||||
| 	 */ | ||||
| 	showNginxDead: function () { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) { | ||||
| 			const controller = this; | ||||
| 			require(['./main', './nginx/dead/main'], (App, View) => { | ||||
| 				controller.navigate('/nginx/404'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Stream Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxStreamForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { | ||||
|             require(['./main', './nginx/stream/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Dead Host Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showNginxDeadForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { | ||||
| 			require(['./main', './nginx/dead/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Stream Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxStreamDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { | ||||
|             require(['./main', './nginx/stream/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Dead Host Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxDeadDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { | ||||
| 			require(['./main', './nginx/dead/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Dead Hosts | ||||
|      */ | ||||
|     showNginxDead: function () { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) { | ||||
|             let controller = this; | ||||
| 	/** | ||||
| 	 * Help Dialog | ||||
| 	 * | ||||
| 	 * @param {String}  title | ||||
| 	 * @param {String}  content | ||||
| 	 */ | ||||
| 	showHelp: function (title, content) { | ||||
| 		require(['./main', './help/main'], function (App, View) { | ||||
| 			App.UI.showModalDialog(new View({title: title, content: content})); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
|             require(['./main', './nginx/dead/main'], (App, View) => { | ||||
|                 controller.navigate('/nginx/404'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Access | ||||
| 	 */ | ||||
| 	showNginxAccess: function () { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) { | ||||
| 			const controller = this; | ||||
| 			require(['./main', './nginx/access/main'], (App, View) => { | ||||
| 				controller.navigate('/nginx/access'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Dead Host Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxDeadForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { | ||||
|             require(['./main', './nginx/dead/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Access List Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showNginxAccessListForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { | ||||
| 			require(['./main', './nginx/access/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Dead Host Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxDeadDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { | ||||
|             require(['./main', './nginx/dead/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Access List Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxAccessListDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { | ||||
| 			require(['./main', './nginx/access/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Help Dialog | ||||
|      * | ||||
|      * @param {String}  title | ||||
|      * @param {String}  content | ||||
|      */ | ||||
|     showHelp: function (title, content) { | ||||
|         require(['./main', './help/main'], function (App, View) { | ||||
|             App.UI.showModalDialog(new View({title: title, content: content})); | ||||
|         }); | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Nginx Certificates | ||||
| 	 */ | ||||
| 	showNginxCertificates: function () { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canView('certificates')) { | ||||
| 			const controller = this; | ||||
| 			require(['./main', './nginx/certificates/main'], (App, View) => { | ||||
| 				controller.navigate('/nginx/certificates'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Access | ||||
|      */ | ||||
|     showNginxAccess: function () { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) { | ||||
|             let controller = this; | ||||
| 	/** | ||||
| 	 * Nginx Certificate Form | ||||
| 	 * | ||||
| 	 * @param [model] | ||||
| 	 */ | ||||
| 	showNginxCertificateForm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
| 			require(['./main', './nginx/certificates/form'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|             require(['./main', './nginx/access/main'], (App, View) => { | ||||
|                 controller.navigate('/nginx/access'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Certificate Renew | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxCertificateRenew: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
| 			require(['./main', './nginx/certificates/renew'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Access List Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxAccessListForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { | ||||
|             require(['./main', './nginx/access/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Certificate Delete Confirm | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxCertificateDeleteConfirm: function (model) { | ||||
| 		if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
| 			require(['./main', './nginx/certificates/delete'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Access List Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxAccessListDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { | ||||
|             require(['./main', './nginx/access/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Certificate Test Reachability | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showNginxCertificateTestReachability: function (model) { | ||||
| 	  if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
| 		require(['./main', './nginx/certificates/test'], function (App, View) { | ||||
| 		  App.UI.showModalDialog(new View({model: model})); | ||||
| 		}); | ||||
| 	  } | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Certificates | ||||
|      */ | ||||
|     showNginxCertificates: function () { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canView('certificates')) { | ||||
|             let controller = this; | ||||
| 	/** | ||||
| 	 * Audit Log | ||||
| 	 */ | ||||
| 	showAuditLog: function () { | ||||
| 		const controller = this; | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			require(['./main', './audit-log/main'], (App, View) => { | ||||
| 				controller.navigate('/audit-log'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			this.showDashboard(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|             require(['./main', './nginx/certificates/main'], (App, View) => { | ||||
|                 controller.navigate('/nginx/certificates'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Audit Log Metadata | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showAuditMeta: function (model) { | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			require(['./main', './audit-log/meta'], function (App, View) { | ||||
| 				App.UI.showModalDialog(new View({model: model})); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Nginx Certificate Form | ||||
|      * | ||||
|      * @param [model] | ||||
|      */ | ||||
|     showNginxCertificateForm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
|             require(['./main', './nginx/certificates/form'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Settings | ||||
| 	 */ | ||||
| 	showSettings: function () { | ||||
| 		const controller = this; | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			require(['./main', './settings/main'], (App, View) => { | ||||
| 				controller.navigate('/settings'); | ||||
| 				App.UI.showAppContent(new View()); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			this.showDashboard(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Certificate Renew | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxCertificateRenew: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
|             require(['./main', './nginx/certificates/renew'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * Settings Item Form | ||||
| 	 * | ||||
| 	 * @param model | ||||
| 	 */ | ||||
| 	showSettingForm: function (model) { | ||||
| 		if (Cache.User.isAdmin()) { | ||||
| 			if (model.get('id') === 'default-site') { | ||||
| 				require(['./main', './settings/default-site/main'], function (App, View) { | ||||
| 					App.UI.showModalDialog(new View({model: model})); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     /** | ||||
|      * Certificate Delete Confirm | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxCertificateDeleteConfirm: function (model) { | ||||
|         if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
|             require(['./main', './nginx/certificates/delete'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Certificate Test Reachability | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showNginxCertificateTestReachability: function (model) { | ||||
|       if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { | ||||
|         require(['./main', './nginx/certificates/test'], function (App, View) { | ||||
|           App.UI.showModalDialog(new View({model: model})); | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Audit Log | ||||
|      */ | ||||
|     showAuditLog: function () { | ||||
|         let controller = this; | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './audit-log/main'], (App, View) => { | ||||
|                 controller.navigate('/audit-log'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } else { | ||||
|             this.showDashboard(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Audit Log Metadata | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showAuditMeta: function (model) { | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './audit-log/meta'], function (App, View) { | ||||
|                 App.UI.showModalDialog(new View({model: model})); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Settings | ||||
|      */ | ||||
|     showSettings: function () { | ||||
|         let controller = this; | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             require(['./main', './settings/main'], (App, View) => { | ||||
|                 controller.navigate('/settings'); | ||||
|                 App.UI.showAppContent(new View()); | ||||
|             }); | ||||
|         } else { | ||||
|             this.showDashboard(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Settings Item Form | ||||
|      * | ||||
|      * @param model | ||||
|      */ | ||||
|     showSettingForm: function (model) { | ||||
|         if (Cache.User.isAdmin()) { | ||||
|             if (model.get('id') === 'default-site') { | ||||
|                 require(['./main', './settings/default-site/main'], function (App, View) { | ||||
|                     App.UI.showModalDialog(new View({model: model})); | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Logout | ||||
|      */ | ||||
|     logout: function () { | ||||
|         Tokens.dropTopToken(); | ||||
|         this.showLogin(); | ||||
|     } | ||||
| 	/** | ||||
| 	 * Logout | ||||
| 	 */ | ||||
| 	logout: function () { | ||||
| 		Tokens.dropTopToken(); | ||||
| 		this.showLogin(); | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -6,87 +6,85 @@ const Helpers    = require('../../lib/helpers'); | ||||
| const template   = require('./main.ejs'); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template: template, | ||||
|     id:       'dashboard', | ||||
|     columns:  0, | ||||
| 	template: template, | ||||
| 	id:       'dashboard', | ||||
| 	columns:  0, | ||||
|  | ||||
|     stats: {}, | ||||
| 	stats: {}, | ||||
|  | ||||
|     ui: { | ||||
|         links: 'a' | ||||
|     }, | ||||
| 	ui: { | ||||
| 		links: 'a' | ||||
| 	}, | ||||
|  | ||||
|     events: { | ||||
|         'click @ui.links': function (e) { | ||||
|             e.preventDefault(); | ||||
|             Controller.navigate($(e.currentTarget).attr('href'), true); | ||||
|         } | ||||
|     }, | ||||
| 	events: { | ||||
| 		'click @ui.links': function (e) { | ||||
| 			e.preventDefault(); | ||||
| 			Controller.navigate($(e.currentTarget).attr('href'), true); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|     templateContext: function () { | ||||
|         let view = this; | ||||
| 	templateContext: function () { | ||||
| 		const view = this; | ||||
|  | ||||
|         return { | ||||
|             getUserName: function () { | ||||
|                 return Cache.User.get('nickname') || Cache.User.get('name'); | ||||
|             }, | ||||
| 		return { | ||||
| 			getUserName: function () { | ||||
| 				return Cache.User.get('nickname') || Cache.User.get('name'); | ||||
| 			}, | ||||
|  | ||||
|             getHostStat: function (type) { | ||||
|                 if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') { | ||||
|                     return Helpers.niceNumber(view.stats.hosts[type]); | ||||
|                 } | ||||
| 			getHostStat: function (type) { | ||||
| 				if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') { | ||||
| 					return Helpers.niceNumber(view.stats.hosts[type]); | ||||
| 				} | ||||
|  | ||||
|                 return '-'; | ||||
|             }, | ||||
| 				return '-'; | ||||
| 			}, | ||||
|  | ||||
|             canShow: function (perm) { | ||||
|                 return Cache.User.isAdmin() || Cache.User.canView(perm); | ||||
|             }, | ||||
| 			canShow: function (perm) { | ||||
| 				return Cache.User.isAdmin() || Cache.User.canView(perm); | ||||
| 			}, | ||||
|  | ||||
|             columns: view.columns | ||||
|         }; | ||||
|     }, | ||||
| 			columns: view.columns | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
|     onRender: function () { | ||||
|         let view = this; | ||||
| 	onRender: function () { | ||||
| 		const view = this; | ||||
| 		if (typeof view.stats.hosts === 'undefined') { | ||||
| 			Api.Reports.getHostStats() | ||||
| 				.then(response => { | ||||
| 					if (!view.isDestroyed()) { | ||||
| 						view.stats.hosts = response; | ||||
| 						view.render(); | ||||
| 					} | ||||
| 				}) | ||||
| 				.catch(err => { | ||||
| 					console.log(err); | ||||
| 				}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|         if (typeof view.stats.hosts === 'undefined') { | ||||
|             Api.Reports.getHostStats() | ||||
|                 .then(response => { | ||||
|                     if (!view.isDestroyed()) { | ||||
|                         view.stats.hosts = response; | ||||
|                         view.render(); | ||||
|                     } | ||||
|                 }) | ||||
|                 .catch(err => { | ||||
|                     console.log(err); | ||||
|                 }); | ||||
|         } | ||||
|     }, | ||||
| 	/** | ||||
| 	 * @param {Object}  [model] | ||||
| 	 */ | ||||
| 	preRender: function (model) { | ||||
| 		this.columns = 0; | ||||
|  | ||||
|     /** | ||||
|      * @param {Object}  [model] | ||||
|      */ | ||||
|     preRender: function (model) { | ||||
|         this.columns = 0; | ||||
| 		// calculate the available columns based on permissions for the objects | ||||
| 		// and store as a variable | ||||
| 		const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts']; | ||||
|  | ||||
|         // calculate the available columns based on permissions for the objects | ||||
|         // and store as a variable | ||||
|         //let view = this; | ||||
|         let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts']; | ||||
| 		perms.map(perm => { | ||||
| 			this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0; | ||||
| 		}); | ||||
|  | ||||
|         perms.map(perm => { | ||||
|             this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0; | ||||
|         }); | ||||
| 		// Prevent double rendering on initial calls | ||||
| 		if (typeof model !== 'undefined') { | ||||
| 			this.render(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|         // Prevent double rendering on initial calls | ||||
|         if (typeof model !== 'undefined') { | ||||
|             this.render(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     initialize: function () { | ||||
|         this.preRender(); | ||||
|         this.listenTo(Cache.User, 'change', this.preRender); | ||||
|     } | ||||
| 	initialize: function () { | ||||
| 		this.preRender(); | ||||
| 		this.listenTo(Cache.User, 'change', this.preRender); | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>"> | ||||
|         <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>"> | ||||
|         <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
| @@ -33,6 +33,13 @@ | ||||
| <td class="<%- isExpired() ? 'text-danger' : '' %>"> | ||||
|     <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> | ||||
| </td> | ||||
| <td> | ||||
|     <% if (active_domain_names().length > 0) { %> | ||||
|         <span class="status-icon bg-success"></span> <%- i18n('certificates', 'in-use') %> | ||||
|     <% } else { %> | ||||
|         <span class="status-icon bg-danger"></span> <%- i18n('certificates', 'inactive') %> | ||||
|     <% } %> | ||||
| </td> | ||||
| <% if (canManage) { %> | ||||
| <td class="text-right"> | ||||
|     <div class="item-action dropdown"> | ||||
| @@ -48,7 +55,14 @@ | ||||
|                 <div class="dropdown-divider"></div> | ||||
|             <% } %> | ||||
|             <a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a> | ||||
|             <% if (active_domain_names().length > 0) { %> | ||||
|                 <div class="dropdown-divider"></div> | ||||
|                 <span class="dropdown-header"><%- i18n('certificates', 'active-domain_names') %></span> | ||||
|                 <% active_domain_names().forEach(function(host) { %> | ||||
|                     <a href="https://<%- host %>" class="dropdown-item" target="_blank"><%- host %></a> | ||||
|                 <% }); %> | ||||
|             <% } %> | ||||
|         </div> | ||||
|     </div> | ||||
| </td> | ||||
| <% } %> | ||||
| <% } %> | ||||
| @@ -44,14 +44,24 @@ module.exports = Mn.View.extend({ | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         canManage: App.Cache.User.canManage('certificates'), | ||||
|         isExpired: function () { | ||||
|             return moment(this.expires_on).isBefore(moment()); | ||||
|         }, | ||||
|         dns_providers: dns_providers | ||||
|     templateContext: function () { | ||||
|         return { | ||||
|             canManage: App.Cache.User.canManage('certificates'), | ||||
|             isExpired: function () { | ||||
|                 return moment(this.expires_on).isBefore(moment()); | ||||
|             }, | ||||
|             dns_providers: dns_providers, | ||||
|             active_domain_names: function () { | ||||
|                 const { proxy_hosts = [], redirect_hosts = [], dead_hosts = [] } = this; | ||||
|                 return [...proxy_hosts, ...redirect_hosts, ...dead_hosts].reduce((acc, host) => { | ||||
|                     acc.push(...(host.domain_names || [])); | ||||
|                     return acc; | ||||
|                 }, []); | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|  | ||||
|     initialize: function () { | ||||
|         this.listenTo(this.model, 'change', this.render); | ||||
|     } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|     <th><%- i18n('str', 'name') %></th> | ||||
|     <th><%- i18n('all-hosts', 'cert-provider') %></th> | ||||
|     <th><%- i18n('str', 'expires') %></th> | ||||
|     <th><%- i18n('str', 'status') %></th> | ||||
|     <% if (canManage) { %> | ||||
|     <th> </th> | ||||
|     <% } %> | ||||
|   | ||||
| @@ -74,7 +74,7 @@ module.exports = Mn.View.extend({ | ||||
|             e.preventDefault(); | ||||
|             let query = this.ui.query.val(); | ||||
|  | ||||
|             this.fetch(['owner'], query) | ||||
|             this.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'], query) | ||||
|                 .then(response => this.showData(response)) | ||||
|                 .catch(err => { | ||||
|                     this.showError(err); | ||||
| @@ -89,7 +89,7 @@ module.exports = Mn.View.extend({ | ||||
|     onRender: function () { | ||||
|         let view = this; | ||||
|  | ||||
|         view.fetch(['owner']) | ||||
|         view.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts']) | ||||
|             .then(response => { | ||||
|                 if (!view.isDestroyed()) { | ||||
|                     if (response && response.length) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>"> | ||||
|         <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>"> | ||||
|         <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>"> | ||||
|         <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|   | ||||
| @@ -3,48 +3,187 @@ | ||||
|         <h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5> | ||||
|         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|     <div class="modal-body has-tabs"> | ||||
|         <div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div> | ||||
|         <form> | ||||
|             <div class="row"> | ||||
|                 <div class="col-sm-12 col-md-12"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label> | ||||
|                         <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required> | ||||
|             <ul class="nav nav-tabs" role="tablist"> | ||||
|                 <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li> | ||||
|                 <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li> | ||||
|             </ul> | ||||
|             <div class="tab-content"> | ||||
|                 <!-- Details --> | ||||
|                 <div role="tabpanel" class="tab-pane active" id="details"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label> | ||||
|                                 <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-8 col-md-8"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label> | ||||
|                                 <input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-4 col-md-4"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label> | ||||
|                                 <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-6 col-md-6"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-6 col-md-6"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-8 col-md-8"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label> | ||||
|                         <input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required> | ||||
|  | ||||
|                 <!-- SSL --> | ||||
|                 <div role="tabpanel" class="tab-pane" id="ssl-options"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-12 col-md-12"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('streams', 'ssl-certificate') %></label> | ||||
|                                 <select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>"> | ||||
|                                     <option selected value="0" data-data="{"id":0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option> | ||||
|                                     <option selected value="new" data-data="{"id":"new"}"><%- i18n('all-hosts', 'new-cert') %></option> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- DNS challenge --> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label> | ||||
|                                 <input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>"> | ||||
|                             </div> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input | ||||
|                                             type="checkbox" | ||||
|                                             class="custom-switch-input" | ||||
|                                             name="meta[dns_challenge]" | ||||
|                                             value="1" | ||||
|                                             checked | ||||
|                                             disabled | ||||
|                                     > | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%= i18n('ssl', 'dns-challenge') %></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt"> | ||||
|                             <fieldset class="form-fieldset dns-challenge"> | ||||
|                                 <div class="text-red mb-4"><i class="fe fe-alert-triangle"></i> <%= i18n('ssl', 'certbot-warning') %></div> | ||||
|  | ||||
|                                 <!-- Certbot DNS plugin selection --> | ||||
|                                 <div class="row"> | ||||
|                                     <div class="col-sm-12 col-md-12"> | ||||
|                                         <div class="form-group"> | ||||
|                                             <label class="form-label"><%- i18n('ssl', 'dns-provider') %> <span class="form-required">*</span></label> | ||||
|                                             <select | ||||
|                                                     name="meta[dns_provider]" | ||||
|                                                     id="dns_provider" | ||||
|                                                     class="form-control custom-select" | ||||
|                                             > | ||||
|                                                 <option | ||||
|                                                         value="" | ||||
|                                                         disabled | ||||
|                                                         hidden | ||||
|                                                         <%- getDnsProvider() === null ? 'selected' : '' %> | ||||
|                                                 >Please Choose...</option> | ||||
|                                                 <% _.each(dns_plugins, function(plugin_info, plugin_name){ %> | ||||
|                                                     <option | ||||
|                                                             value="<%- plugin_name %>" | ||||
|                                                             <%- getDnsProvider() === plugin_name ? 'selected' : '' %> | ||||
|                                                     ><%- plugin_info.name %></option> | ||||
|                                                 <% }); %> | ||||
|                                             </select> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- Certbot credentials file content --> | ||||
|                                 <div class="row credentials-file-content"> | ||||
|                                     <div class="col-sm-12 col-md-12"> | ||||
|                                         <div class="form-group"> | ||||
|                                             <label class="form-label"><%- i18n('ssl', 'credentials-file-content') %> <span class="form-required">*</span></label> | ||||
|                                             <textarea | ||||
|                                                     name="meta[dns_provider_credentials]" | ||||
|                                                     class="form-control text-monospace" | ||||
|                                                     id="dns_provider_credentials" | ||||
|                                             ><%- getDnsProviderCredentials() %></textarea> | ||||
|                                             <div class="text-secondary small"> | ||||
|                                                 <i class="fe fe-info"></i> | ||||
|                                                 <%= i18n('ssl', 'credentials-file-content-info') %> | ||||
|                                             </div> | ||||
|                                             <div class="text-red small"> | ||||
|                                                 <i class="fe fe-alert-triangle"></i> | ||||
|                                                 <%= i18n('ssl', 'stored-as-plaintext-info') %> | ||||
|                                             </div> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- DNS propagation delay --> | ||||
|                                 <div class="row"> | ||||
|                                     <div class="col-sm-12 col-md-12"> | ||||
|                                         <div class="form-group mb-0"> | ||||
|                                             <label class="form-label"><%- i18n('ssl', 'propagation-seconds') %></label> | ||||
|                                             <input | ||||
|                                                     type="number" | ||||
|                                                     min="0" | ||||
|                                                     name="meta[propagation_seconds]" | ||||
|                                                     class="form-control" | ||||
|                                                     id="propagation_seconds" | ||||
|                                                     value="<%- getPropagationSeconds() %>" | ||||
|                                             > | ||||
|                                             <div class="text-secondary small"> | ||||
|                                                 <i class="fe fe-info"></i> | ||||
|                                                 <%= i18n('ssl', 'propagation-seconds-info') %> | ||||
|                                             </div> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </fieldset> | ||||
|                         </div> | ||||
|  | ||||
|                         <!-- Lets encrypt --> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label> | ||||
|                                 <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12 col-md-12 letsencrypt"> | ||||
|                             <div class="form-group"> | ||||
|                                 <label class="custom-switch"> | ||||
|                                     <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled> | ||||
|                                     <span class="custom-switch-indicator"></span> | ||||
|                                     <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-4 col-md-4"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label> | ||||
|                         <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-6 col-md-6"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="custom-switch"> | ||||
|                             <input type="checkbox" class="custom-switch-input" name="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>> | ||||
|                             <span class="custom-switch-indicator"></span> | ||||
|                             <span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span> | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-6 col-md-6"> | ||||
|                     <div class="form-group"> | ||||
|                         <label class="custom-switch"> | ||||
|                             <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>> | ||||
|                             <span class="custom-switch-indicator"></span> | ||||
|                             <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span> | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-sm-12 col-md-12"> | ||||
|                     <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
|   | ||||
| @@ -1,24 +1,38 @@ | ||||
| const Mn          = require('backbone.marionette'); | ||||
| const App         = require('../../main'); | ||||
| const StreamModel = require('../../../models/stream'); | ||||
| const template    = require('./form.ejs'); | ||||
| const Mn            = require('backbone.marionette'); | ||||
| const App           = require('../../main'); | ||||
| const StreamModel   = require('../../../models/stream'); | ||||
| const template      = require('./form.ejs'); | ||||
| const dns_providers = require('../../../../../global/certbot-dns-plugins'); | ||||
|  | ||||
| require('jquery-serializejson'); | ||||
| require('jquery-mask-plugin'); | ||||
| require('selectize'); | ||||
| const Helpers = require("../../../lib/helpers"); | ||||
| const certListItemTemplate = require("../certificates-list-item.ejs"); | ||||
| const i18n = require("../../i18n"); | ||||
|  | ||||
| module.exports = Mn.View.extend({ | ||||
|     template:  template, | ||||
|     className: 'modal-dialog', | ||||
|  | ||||
|     ui: { | ||||
|         form:       'form', | ||||
|         forwarding_host: 'input[name="forwarding_host"]', | ||||
|         type_error: '.forward-type-error', | ||||
|         buttons:    '.modal-footer button', | ||||
|         switches:   '.custom-switch-input', | ||||
|         cancel:     'button.cancel', | ||||
|         save:       'button.save' | ||||
|         form:                     'form', | ||||
|         forwarding_host:          'input[name="forwarding_host"]', | ||||
|         type_error:               '.forward-type-error', | ||||
|         buttons:                  '.modal-footer button', | ||||
|         switches:                 '.custom-switch-input', | ||||
|         cancel:                   'button.cancel', | ||||
|         save:                     'button.save', | ||||
|         le_error_info:            '#le-error-info', | ||||
|         certificate_select:       'select[name="certificate_id"]', | ||||
|         domain_names:             'input[name="domain_names"]', | ||||
|         dns_challenge_switch:     'input[name="meta[dns_challenge]"]', | ||||
|         dns_challenge_content:    '.dns-challenge', | ||||
|         dns_provider:             'select[name="meta[dns_provider]"]', | ||||
|         credentials_file_content: '.credentials-file-content', | ||||
|         dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', | ||||
|         propagation_seconds:      'input[name="meta[propagation_seconds]"]', | ||||
|         letsencrypt:              '.letsencrypt' | ||||
|     }, | ||||
|  | ||||
|     events: { | ||||
| @@ -48,6 +62,35 @@ module.exports = Mn.View.extend({ | ||||
|             data.tcp_forwarding  = !!data.tcp_forwarding; | ||||
|             data.udp_forwarding  = !!data.udp_forwarding; | ||||
|  | ||||
|             if (typeof data.meta === 'undefined') data.meta = {}; | ||||
|             data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; | ||||
|             data.meta.dns_challenge = true; | ||||
|  | ||||
|             if (data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; | ||||
|  | ||||
|             if (typeof data.domain_names === 'string' && data.domain_names) { | ||||
|                 data.domain_names = data.domain_names.split(','); | ||||
|             } | ||||
|  | ||||
|             // Check for any domain names containing wildcards, which are not allowed with letsencrypt | ||||
|             if (data.certificate_id === 'new') { | ||||
|                 let domain_err = false; | ||||
|                 if (!data.meta.dns_challenge) { | ||||
|                     data.domain_names.map(function (name) { | ||||
|                         if (name.match(/\*/im)) { | ||||
|                             domain_err = true; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 if (domain_err) { | ||||
|                     alert(i18n('ssl', 'no-wildcard-without-dns')); | ||||
|                     return; | ||||
|                 } | ||||
|             } else { | ||||
|                 data.certificate_id = parseInt(data.certificate_id, 10); | ||||
|             } | ||||
|  | ||||
|             let method = App.Api.Nginx.Streams.create; | ||||
|             let is_new = true; | ||||
|  | ||||
| @@ -70,10 +113,108 @@ module.exports = Mn.View.extend({ | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(err => { | ||||
|                     alert(err.message); | ||||
|                     let more_info = ''; | ||||
|                     if (err.code === 500 && err.debug) { | ||||
|                         try { | ||||
|                             more_info = JSON.parse(err.debug).debug.stack.join("\n"); | ||||
|                         } catch (e) { | ||||
|                         } | ||||
|                     } | ||||
|                     this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>` : ''}`; | ||||
|                     this.ui.le_error_info.show(); | ||||
|                     this.ui.le_error_info[0].scrollIntoView(); | ||||
|                     this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); | ||||
|                     this.ui.save.removeClass('btn-loading'); | ||||
|                 }); | ||||
|         } | ||||
|         }, | ||||
|  | ||||
|         'change @ui.certificate_select': function () { | ||||
|             let id = this.ui.certificate_select.val(); | ||||
|             if (id === 'new') { | ||||
|                 this.ui.letsencrypt.show().find('input').prop('disabled', false); | ||||
|                 this.ui.domain_names.prop('required', 'required'); | ||||
|  | ||||
|                 this.ui.dns_challenge_switch | ||||
|                     .prop('disabled', true) | ||||
|                     .parents('.form-group') | ||||
|                     .css('opacity', 0.5); | ||||
|  | ||||
|                 this.ui.dns_provider.prop('required', 'required'); | ||||
|                 const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; | ||||
|                 if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { | ||||
|                     this.ui.dns_provider_credentials.prop('required', 'required'); | ||||
|                 } | ||||
|                 this.ui.dns_challenge_content.show(); | ||||
|             } else { | ||||
|                 this.ui.letsencrypt.hide().find('input').prop('disabled', true); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         'change @ui.dns_provider': function () { | ||||
|             const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; | ||||
|             if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { | ||||
|                 this.ui.dns_provider_credentials.prop('required', 'required'); | ||||
|                 this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; | ||||
|                 this.ui.credentials_file_content.show(); | ||||
|             } else { | ||||
|                 this.ui.dns_provider_credentials.prop('required', false); | ||||
|                 this.ui.credentials_file_content.hide(); | ||||
|             } | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     templateContext: { | ||||
|         getLetsencryptEmail: function () { | ||||
|             return App.Cache.User.get('email'); | ||||
|         }, | ||||
|         getDnsProvider: function () { | ||||
|             return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; | ||||
|         }, | ||||
|         getDnsProviderCredentials: function () { | ||||
|             return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; | ||||
|         }, | ||||
|         getPropagationSeconds: function () { | ||||
|             return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; | ||||
|         }, | ||||
|         dns_plugins: dns_providers, | ||||
|     }, | ||||
|  | ||||
|     onRender: function () { | ||||
|         let view = this; | ||||
|  | ||||
|         // Certificates | ||||
|         this.ui.le_error_info.hide(); | ||||
|         this.ui.dns_challenge_content.hide(); | ||||
|         this.ui.credentials_file_content.hide(); | ||||
|         this.ui.letsencrypt.hide(); | ||||
|         this.ui.certificate_select.selectize({ | ||||
|             valueField:       'id', | ||||
|             labelField:       'nice_name', | ||||
|             searchField:      ['nice_name', 'domain_names'], | ||||
|             create:           false, | ||||
|             preload:          true, | ||||
|             allowEmptyOption: true, | ||||
|             render:           { | ||||
|                 option: function (item) { | ||||
|                     item.i18n         = App.i18n; | ||||
|                     item.formatDbDate = Helpers.formatDbDate; | ||||
|                     return certListItemTemplate(item); | ||||
|                 } | ||||
|             }, | ||||
|             load:             function (query, callback) { | ||||
|                 App.Api.Nginx.Certificates.getAll() | ||||
|                     .then(rows => { | ||||
|                         callback(rows); | ||||
|                     }) | ||||
|                     .catch(err => { | ||||
|                         console.error(err); | ||||
|                         callback(); | ||||
|                     }); | ||||
|             }, | ||||
|             onLoad:           function () { | ||||
|                 view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); | ||||
|             } | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     initialize: function (options) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <td class="text-center"> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> | ||||
|         <span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> | ||||
|     <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>"> | ||||
|         <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
| @@ -16,7 +16,10 @@ | ||||
| </td> | ||||
| <td> | ||||
|     <div> | ||||
|         <% if (tcp_forwarding) { %> | ||||
|         <% if (certificate) { %> | ||||
|             <span class="tag"><%- i18n('streams', 'tcp+ssl') %></span> | ||||
|         <% } | ||||
|         else if (tcp_forwarding) { %> | ||||
|             <span class="tag"><%- i18n('streams', 'tcp') %></span> | ||||
|         <% } | ||||
|         if (udp_forwarding) { %> | ||||
| @@ -24,6 +27,9 @@ | ||||
|         <% } %> | ||||
|     </div> | ||||
| </td> | ||||
| <td> | ||||
|     <div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('all-hosts', 'none') %></div> | ||||
| </td> | ||||
| <td> | ||||
|     <% | ||||
|     var o = isOnline(); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|     <th><%- i18n('streams', 'incoming-port') %></th> | ||||
|     <th><%- i18n('str', 'destination') %></th> | ||||
|     <th><%- i18n('streams', 'protocol') %></th> | ||||
|     <th><%- i18n('str', 'ssl') %></th> | ||||
|     <th><%- i18n('str', 'status') %></th> | ||||
|     <% if (canManage) { %> | ||||
|     <th> </th> | ||||
|   | ||||
| @@ -88,7 +88,7 @@ module.exports = Mn.View.extend({ | ||||
|     onRender: function () { | ||||
|         let view = this; | ||||
|  | ||||
|         view.fetch(['owner']) | ||||
|         view.fetch(['owner', 'certificate']) | ||||
|             .then(response => { | ||||
|                 if (!view.isDestroyed()) { | ||||
|                     if (response && response.length) { | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="modal-header"> | ||||
|         <h5 class="modal-title"><%- i18n('users', 'form-title', {id: id}) %></h5> | ||||
|         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|         <form> | ||||
|     <form> | ||||
|         <div class="modal-header"> | ||||
|             <h5 class="modal-title"><%- i18n('users', 'form-title', {id: id}) %></h5> | ||||
|             <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button> | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div class="row"> | ||||
|                 <div class="col-sm-6 col-md-6"> | ||||
|                     <div class="form-group"> | ||||
| @@ -49,10 +49,10 @@ | ||||
|                 </div> | ||||
|                 <% } %> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|         <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button> | ||||
|         <button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button> | ||||
|     </div> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|             <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button> | ||||
|             <button type="submit" class="btn btn-teal save"><%- i18n('str', 'save') %></button> | ||||
|         </div> | ||||
|     </form> | ||||
| </div> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ module.exports = Mn.View.extend({ | ||||
|  | ||||
|     events: { | ||||
|  | ||||
|         'click @ui.save': function (e) { | ||||
|         'submit @ui.form': function (e) { | ||||
|             e.preventDefault(); | ||||
|             this.ui.error.hide(); | ||||
|             let view = this; | ||||
|   | ||||
| @@ -60,7 +60,7 @@ | ||||
|     }, | ||||
|     "footer": { | ||||
|       "fork-me": "Fork me on Github", | ||||
|       "copy": "© 2024 <a href=\"{url}\" target=\"_blank\">jc21.com</a>.", | ||||
|       "copy": "© 2025 <a href=\"{url}\" target=\"_blank\">jc21.com</a>.", | ||||
|       "theme": "Theme by <a href=\"{url}\" target=\"_blank\">Tabler</a>" | ||||
|     }, | ||||
|     "dashboard": { | ||||
| @@ -179,7 +179,9 @@ | ||||
|       "delete-confirm": "Are you sure you want to delete this Stream?", | ||||
|       "help-title": "What is a Stream?", | ||||
|       "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.", | ||||
|       "search": "Search Incoming Port…" | ||||
|       "search": "Search Incoming Port…", | ||||
|       "ssl-certificate": "SSL Certificate for TCP Forwarding", | ||||
|       "tcp+ssl": "TCP+SSL" | ||||
|     }, | ||||
|     "certificates": { | ||||
|       "title": "SSL Certificates", | ||||
| @@ -206,7 +208,10 @@ | ||||
|       "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", | ||||
|       "download": "Download", | ||||
|       "renew-title": "Renew Let's Encrypt Certificate", | ||||
|       "search": "Search Certificate…" | ||||
|       "search": "Search Certificate…", | ||||
|       "in-use"  : "In use", | ||||
|       "inactive": "Inactive", | ||||
|       "active-domain_names": "Active domain names" | ||||
|     }, | ||||
|     "access-lists": { | ||||
|       "title": "Access Lists", | ||||
|   | ||||
| @@ -15,8 +15,11 @@ const model = Backbone.Model.extend({ | ||||
|             udp_forwarding:  false, | ||||
|             enabled:         true, | ||||
|             meta:            {}, | ||||
|             certificate_id:  0, | ||||
|             domain_names:    [], | ||||
|             // The following are expansions: | ||||
|             owner:           null | ||||
|             owner:           null, | ||||
|             certificate:     null | ||||
|         }; | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -26,8 +26,8 @@ | ||||
|     "messageformat": "^2.3.0", | ||||
|     "messageformat-loader": "^0.8.1", | ||||
|     "mini-css-extract-plugin": "^0.9.0", | ||||
|     "moment": "^2.29.4", | ||||
|     "node-sass": "^9.0.0", | ||||
|     "moment": "^2.30.1", | ||||
|     "sass": "^1.92.1", | ||||
|     "nodemon": "^2.0.2", | ||||
|     "numeral": "^2.0.6", | ||||
|     "sass-loader": "^10.0.0", | ||||
|   | ||||
| @@ -167,4 +167,5 @@ $pink: #f66d9b; | ||||
|  | ||||
| textarea.form-control.text-monospace { | ||||
|     font-size: 12px; | ||||
|     font-family: monospace; | ||||
| } | ||||
|   | ||||
							
								
								
									
										1491
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										1491
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -7,18 +7,18 @@ | ||||
| 		"credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json", | ||||
| 		"full_plugin_name": "dns-acmedns" | ||||
| 	}, | ||||
| 	"active24":{ | ||||
| 	"active24": { | ||||
| 		"name": "Active24", | ||||
| 		"package_name": "certbot-dns-active24", | ||||
| 		"version": "~=1.5.1", | ||||
| 		"version": "~=2.0.0", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_active24_token=\"TOKEN\"", | ||||
| 		"credentials": "dns_active24_api_key = <identifier>\ndns_active24_secret = <secret>", | ||||
| 		"full_plugin_name": "dns-active24" | ||||
| 	}, | ||||
| 	"aliyun": { | ||||
| 		"name": "Aliyun", | ||||
| 		"package_name": "certbot-dns-aliyun", | ||||
| 		"version": "~=0.38.1", | ||||
| 		"version": "~=2.0.0", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef", | ||||
| 		"full_plugin_name": "dns-aliyun" | ||||
| @@ -31,6 +31,22 @@ | ||||
| 		"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2", | ||||
| 		"full_plugin_name": "dns-azure" | ||||
| 	}, | ||||
| 	"baidu": { | ||||
| 		"name": "baidu", | ||||
| 		"package_name": "certbot-dns-baidu", | ||||
| 		"version": "~=0.1.1", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_baidu_access_key = 12345678\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef", | ||||
| 		"full_plugin_name": "dns-baidu" | ||||
| 	}, | ||||
| 	"beget": { | ||||
| 		"name":"Beget", | ||||
| 		"package_name": "certbot-beget-plugin", | ||||
| 		"version": "~=1.0.0.dev9", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "# Beget API credentials used by Certbot\nbeget_plugin_username = username\nbeget_plugin_password = password", | ||||
| 		"full_plugin_name": "beget-plugin" | ||||
| 	}, | ||||
| 	"bunny": { | ||||
| 		"name": "bunny.net", | ||||
| 		"package_name": "certbot-dns-bunny", | ||||
| @@ -39,11 +55,19 @@ | ||||
| 		"credentials": "# Bunny API token used by Certbot (see https://dash.bunny.net/account/settings)\ndns_bunny_api_key = xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx", | ||||
| 		"full_plugin_name": "dns-bunny" | ||||
| 	}, | ||||
| 	"cdmon": { | ||||
| 		"name": "cdmon", | ||||
| 		"package_name": "certbot-dns-cdmon", | ||||
| 		"version": "~=0.4.1", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_cdmon_api_key=your-cdmon-api-token\ndns_cdmon_domain=your_domain_is_optional", | ||||
| 		"full_plugin_name": "dns-cdmon" | ||||
| 	}, | ||||
| 	"cloudflare": { | ||||
| 		"name": "Cloudflare", | ||||
| 		"package_name": "certbot-dns-cloudflare", | ||||
| 		"version": "=={{certbot-version}}", | ||||
| 		"dependencies": "cloudflare==2.19.* acme=={{certbot-version}}", | ||||
| 		"dependencies": "acme=={{certbot-version}}", | ||||
| 		"credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567", | ||||
| 		"full_plugin_name": "dns-cloudflare" | ||||
| 	}, | ||||
| @@ -82,11 +106,19 @@ | ||||
| 	"cpanel": { | ||||
| 		"name": "cPanel", | ||||
| 		"package_name": "certbot-dns-cpanel", | ||||
| 		"version": "~=0.2.2", | ||||
| 		"version": "~=0.4.0", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = user\ncpanel_password = hunter2", | ||||
| 		"credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = your_username\ncpanel_password = your_password\ncpanel_token = your_api_token", | ||||
| 		"full_plugin_name": "cpanel" | ||||
| 	}, | ||||
| 	"ddnss": { | ||||
| 		"name": "DDNSS", | ||||
| 		"package_name": "certbot-dns-ddnss", | ||||
| 		"version": "~=1.1.0", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_ddnss_token = YOUR_DDNSS_API_TOKEN", | ||||
| 		"full_plugin_name": "dns-ddnss" | ||||
| 	}, | ||||
| 	"desec": { | ||||
| 		"name": "deSEC", | ||||
| 		"package_name": "certbot-dns-desec", | ||||
| @@ -153,11 +185,11 @@ | ||||
| 	}, | ||||
| 	"domainoffensive": { | ||||
| 		"name": "DomainOffensive (do.de)", | ||||
| 		"package_name": "certbot-dns-do", | ||||
| 		"version": "~=0.31.0", | ||||
| 		"package_name": "certbot-dns-domainoffensive", | ||||
| 		"version": "~=2.0.0", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_do_api_token = YOUR_DO_DE_AUTH_TOKEN", | ||||
| 		"full_plugin_name": "dns-do" | ||||
| 		"credentials": "dns_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN", | ||||
| 		"full_plugin_name": "dns-domainoffensive" | ||||
| 	}, | ||||
| 	"domeneshop": { | ||||
| 		"name": "Domeneshop", | ||||
| @@ -191,6 +223,14 @@ | ||||
| 		"credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy", | ||||
| 		"full_plugin_name": "dns-eurodns" | ||||
| 	}, | ||||
| 	"firstdomains": { | ||||
|                 "name": "First Domains", | ||||
|                 "package_name": "certbot-dns-firstdomains", | ||||
|                 "version": ">=1.0", | ||||
|                 "dependencies": "", | ||||
|                 "credentials": "dns_firstdomains_username = myremoteuser\ndns_firstdomains_password = verysecureremoteuserpassword", | ||||
|                 "full_plugin_name": "dns-firstdomains" | ||||
|         }, | ||||
| 	"freedns": { | ||||
| 		"name": "FreeDNS", | ||||
| 		"package_name": "certbot-dns-freedns", | ||||
| @@ -201,12 +241,20 @@ | ||||
| 	}, | ||||
| 	"gandi": { | ||||
| 		"name": "Gandi Live DNS", | ||||
| 		"package_name": "certbot_plugin_gandi", | ||||
| 		"version": "~=1.5.0", | ||||
| 		"package_name": "certbot-dns-gandi", | ||||
| 		"version": "~=1.6.1", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN", | ||||
| 		"full_plugin_name": "dns-gandi" | ||||
| 	}, | ||||
| 	"gcore": { | ||||
| 		"name": "Gcore DNS", | ||||
| 		"package_name": "certbot-dns-gcore", | ||||
| 		"version": "~=0.1.8", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567", | ||||
| 		"full_plugin_name": "dns-gcore" | ||||
| 	}, | ||||
| 	"godaddy": { | ||||
| 		"name": "GoDaddy", | ||||
| 		"package_name": "certbot-dns-godaddy", | ||||
| @@ -247,6 +295,14 @@ | ||||
| 		"credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef", | ||||
| 		"full_plugin_name": "dns-hetzner" | ||||
| 	}, | ||||
| 	"hostingnl": { | ||||
| 		"name": "Hosting.nl", | ||||
| 		"package_name": "certbot-dns-hostingnl", | ||||
| 		"version": "~=0.1.5", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef", | ||||
| 		"full_plugin_name": "dns-hostingnl" | ||||
| 	}, | ||||
| 	"hover": { | ||||
| 		"name": "Hover", | ||||
| 		"package_name": "certbot-dns-hover", | ||||
| @@ -340,7 +396,7 @@ | ||||
| 		"package_name": "certbot-dns-mijn-host", | ||||
| 		"version": "~=0.0.4", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns-mijn-host-credentials = /etc/letsencrypt/mijnhost-credentials.ini", | ||||
| 		"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef", | ||||
| 		"full_plugin_name": "dns-mijn-host" | ||||
| 	}, | ||||
| 	"namecheap": { | ||||
| @@ -359,6 +415,14 @@ | ||||
| 		"credentials": "dns_netcup_customer_id  = 123456\ndns_netcup_api_key      = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123", | ||||
| 		"full_plugin_name": "dns-netcup" | ||||
| 	}, | ||||
| 	"nicru": { | ||||
| 		"name": "nic.ru", | ||||
| 		"package_name": "certbot-dns-nicru", | ||||
| 		"version": "~=1.0.3", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_nicru_client_id = application-id\ndns_nicru_client_secret = application-token\ndns_nicru_username = 0001110/NIC-D\ndns_nicru_password = password\ndns_nicru_scope = .+:.+/zones/example.com(/.+)?\ndns_nicru_service = DNS_SERVICE_NAME\ndns_nicru_zone = example.com", | ||||
| 		"full_plugin_name": "dns-nicru" | ||||
| 	}, | ||||
| 	"njalla": { | ||||
| 		"name": "Njalla", | ||||
| 		"package_name": "certbot-dns-njalla", | ||||
| @@ -402,7 +466,7 @@ | ||||
| 	"porkbun": { | ||||
| 		"name": "Porkbun", | ||||
| 		"package_name": "certbot-dns-porkbun", | ||||
| 		"version": "~=0.2", | ||||
| 		"version": "~=0.9", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret", | ||||
| 		"full_plugin_name": "dns-porkbun" | ||||
| @@ -447,14 +511,30 @@ | ||||
| 		"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", | ||||
| 		"full_plugin_name": "dns-route53" | ||||
| 	}, | ||||
| 	"spaceship": { | ||||
| 		"name": "Spaceship", | ||||
| 		"package_name": "certbot-dns-spaceship", | ||||
| 		"version": "~=1.0.4", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "[spaceship]\napi_key=your_api_key\napi_secret=your_api_secret", | ||||
| 		"full_plugin_name": "dns-spaceship" | ||||
| 	}, | ||||
| 	"strato": { | ||||
| 		"name": "Strato", | ||||
| 		"package_name": "certbot-dns-strato", | ||||
| 		"version": "~=0.2.1", | ||||
| 		"version": "~=0.2.2", | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "dns_strato_username = user\ndns_strato_password = pass\n# uncomment if youre using two factor authentication:\n# dns_strato_totp_devicename = 2fa_device\n# dns_strato_totp_secret = 2fa_secret\n#\n# uncomment if domain name contains special characters\n# insert domain display name as seen on your account page here\n# dns_strato_domain_display_name = my-punicode-url.de\n#\n# if youre not using strato.de or another special endpoint you can customise it below\n# you will probably only need to adjust the host, but you can also change the complete endpoint url\n# dns_strato_custom_api_scheme = https\n# dns_strato_custom_api_host = www.strato.de\n# dns_strato_custom_api_port = 443\n# dns_strato_custom_api_path = \"/apps/CustomerService\"", | ||||
| 		"full_plugin_name": "dns-strato" | ||||
| 	}, | ||||
| 	        "selectelv2": { | ||||
|                 "name": "Selectel api v2", | ||||
|                 "package_name": "certbot-dns-selectel-api-v2", | ||||
|                 "version": "~=0.3.0", | ||||
|                 "dependencies": "", | ||||
|                 "credentials": "dns_selectel_api_v2_account_id = your_account_id\ndns_selectel_api_v2_project_name = your_project\ndns_selectel_api_v2_username = your_username\ndns_selectel_api_v2_password = your_password", | ||||
|                 "full_plugin_name": "dns-selectel-api-v2" | ||||
|         }, | ||||
| 	"timeweb": { | ||||
| 		"name": "Timeweb Cloud", | ||||
| 		"package_name": "certbot-dns-timeweb", | ||||
| @@ -495,7 +575,7 @@ | ||||
| 		"credentials": "dns_websupport_identifier = <api_key>\ndns_websupport_secret_key = <secret>", | ||||
| 		"full_plugin_name": "dns-websupport" | ||||
| 	}, | ||||
| 	"wedos":{ | ||||
| 	"wedos": { | ||||
| 		"name": "Wedos", | ||||
| 		"package_name": "certbot-dns-wedos", | ||||
| 		"version": "~=2.2", | ||||
| @@ -510,5 +590,13 @@ | ||||
| 		"dependencies": "", | ||||
| 		"credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf", | ||||
| 		"full_plugin_name": "edgedns" | ||||
| 	} | ||||
| 	}, | ||||
| 	"zoneedit": { | ||||
| 		"name": "ZoneEdit", | ||||
| 		"package_name": "certbot-dns-zoneedit", | ||||
| 		"version": "~=0.3.2", | ||||
| 		"dependencies": "--no-deps dnspython", | ||||
| 		"credentials": "dns_zoneedit_user = <login-user-id>\ndns_zoneedit_token = <dyn-authentication-token>", | ||||
| 		"full_plugin_name": "dns-zoneedit" | ||||
|  	} | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ YELLOW='\E[1;33m' | ||||
| export BLUE CYAN GREEN RED RESET YELLOW | ||||
|  | ||||
| # Docker Compose | ||||
| COMPOSE_PROJECT_NAME="npmdev" | ||||
| COMPOSE_PROJECT_NAME="npm2dev" | ||||
| COMPOSE_FILE="docker/docker-compose.dev.yml" | ||||
|  | ||||
| export COMPOSE_FILE COMPOSE_PROJECT_NAME | ||||
|   | ||||
| @@ -67,6 +67,8 @@ printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}" | ||||
| # bring up all remaining containers, except cypress! | ||||
| docker-compose up -d --remove-orphans stepca squid | ||||
| docker-compose pull db-mysql || true # ok to fail | ||||
| docker-compose pull db-postgres || true # ok to fail | ||||
| docker-compose pull authentik authentik-redis authentik-ldap || true # ok to fail | ||||
| docker-compose up -d --remove-orphans --pull=never fullstack | ||||
|  | ||||
| # wait for main container to be healthy | ||||
|   | ||||
| @@ -36,12 +36,11 @@ if hash docker-compose 2>/dev/null; then | ||||
|  | ||||
| 	# bring up all remaining containers, except cypress! | ||||
| 	docker-compose up -d --remove-orphans stepca squid | ||||
| 	docker-compose pull db | ||||
| 	docker-compose up -d --remove-orphans --pull=never fullstack | ||||
| 	docker-compose pull db db-postgres authentik-redis authentik authentik-worker authentik-ldap | ||||
| 	docker-compose build --pull --parallel fullstack | ||||
| 	docker-compose up -d --remove-orphans fullstack | ||||
| 	docker-compose up -d --remove-orphans swagger | ||||
|  | ||||
| 	# docker-compose up -d --remove-orphans --force-recreate --build | ||||
|  | ||||
| 	# wait for main container to be healthy | ||||
| 	bash "$DIR/wait-healthy" "$(docker-compose ps --all -q fullstack)" 120 | ||||
|  | ||||
| @@ -53,10 +52,10 @@ if hash docker-compose 2>/dev/null; then | ||||
|  | ||||
| 	if [ "$1" == "-f" ]; then | ||||
| 		echo -e "${BLUE}❯ ${YELLOW}Following Backend Container:${RESET}" | ||||
| 		docker logs -f npm_core | ||||
| 		docker logs -f npm2dev.core | ||||
| 	else | ||||
| 		echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:" | ||||
| 		echo "  docker logs -f npm_core" | ||||
| 		echo "  docker logs -f npm2dev.core" | ||||
| 	fi | ||||
| else | ||||
| 	echo -e "${RED}❯ docker-compose command is not available${RESET}" | ||||
|   | ||||
| @@ -1,11 +1,22 @@ | ||||
| FROM cypress/included:13.9.0 | ||||
|  | ||||
| COPY --chown=1000 ./test /test | ||||
| FROM cypress/included:14.0.1 | ||||
|  | ||||
| # Disable Cypress CLI colors | ||||
| ENV FORCE_COLOR=0 | ||||
| ENV NO_COLOR=1 | ||||
|  | ||||
| # testssl.sh and mkcert | ||||
| RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \ | ||||
| 	&& tar -xzf /tmp/testssl.tgz -C /tmp \ | ||||
| 	&& mv /tmp/testssl.sh-3.2rc4 /testssl \ | ||||
| 	&& rm /tmp/testssl.tgz \ | ||||
| 	&& apt-get update \ | ||||
| 	&& apt-get install -y bsdmainutils curl dnsutils \ | ||||
| 	&& apt-get clean \ | ||||
| 	&& rm -rf /var/lib/apt/lists/* \ | ||||
| 	&& wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \ | ||||
| 	&& chmod +x /bin/mkcert | ||||
|  | ||||
| COPY --chown=1000 ./test /test | ||||
| WORKDIR /test | ||||
| RUN yarn install && yarn cache clean | ||||
| ENTRYPOINT [] | ||||
|   | ||||
| @@ -10,7 +10,7 @@ describe('Certificates endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Validate custom certificate', function() { | ||||
| 	it('Validate custom certificate', () => { | ||||
| 		cy.task('backendApiPostFiles', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/certificates/validate', | ||||
| @@ -25,7 +25,7 @@ describe('Certificates endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Custom certificate lifecycle', function() { | ||||
| 	it('Custom certificate lifecycle', () => { | ||||
| 		// Create custom cert | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| @@ -73,7 +73,7 @@ describe('Certificates endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() { | ||||
| 	it('Request Certificate - CVE-2024-46256/CVE-2024-46257', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/certificates', | ||||
| @@ -96,4 +96,28 @@ describe('Certificates endpoints', () => { | ||||
| 			expect(data.error.message).to.contain('data/domain_names/0 must match pattern'); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Request Certificate - LE Email Escaped', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/certificates', | ||||
| 			data:  { | ||||
| 				domain_names: ['test.com"||echo hello-world||\\\\n test.com"'], | ||||
| 				meta:         { | ||||
| 					dns_challenge:     false, | ||||
| 					letsencrypt_agree: true, | ||||
| 					letsencrypt_email: "admin@example.com' --version;echo hello-world", | ||||
| 				}, | ||||
| 				provider: 'letsencrypt', | ||||
| 			}, | ||||
| 			returnOnError: true, | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data); | ||||
| 			expect(data).to.have.property('error'); | ||||
| 			expect(data.error).to.have.property('message'); | ||||
| 			expect(data.error).to.have.property('code'); | ||||
| 			expect(data.error.code).to.equal(400); | ||||
| 			expect(data.error.message).to.contain('data/meta/letsencrypt_email must match pattern'); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										25
									
								
								test/cypress/e2e/api/Dashboard.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								test/cypress/e2e/api/Dashboard.cy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| /// <reference types="cypress" /> | ||||
|  | ||||
| describe('Dashboard endpoints', () => { | ||||
| 	let token; | ||||
|  | ||||
| 	before(() => { | ||||
| 		cy.getToken().then((tok) => { | ||||
| 			token = tok; | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to get host counts', () => { | ||||
| 		cy.task('backendApiGet', { | ||||
| 			token: token, | ||||
| 			path:  '/api/reports/hosts' | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('get', 200, '/reports/hosts', data); | ||||
| 			expect(data).to.have.property('dead'); | ||||
| 			expect(data).to.have.property('proxy'); | ||||
| 			expect(data).to.have.property('redirection'); | ||||
| 			expect(data).to.have.property('stream'); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
| @@ -9,7 +9,7 @@ describe('Full Certificate Provisions', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create new http certificate', function() { | ||||
| 	it('Should be able to create new http certificate', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/certificates', | ||||
| @@ -32,7 +32,7 @@ describe('Full Certificate Provisions', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create new DNS certificate with Powerdns', function() { | ||||
| 	it('Should be able to create new DNS certificate with Powerdns', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/certificates', | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| /// <reference types="cypress" /> | ||||
|  | ||||
| describe('Basic API checks', () => { | ||||
| 	it('Should return a valid health payload', function () { | ||||
| 	it('Should return a valid health payload', () => { | ||||
| 		cy.task('backendApiGet', { | ||||
| 			path: '/api/', | ||||
| 		}).then((data) => { | ||||
| @@ -10,9 +10,9 @@ describe('Basic API checks', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should return a valid schema payload', function () { | ||||
| 	it('Should return a valid schema payload', () => { | ||||
| 		cy.task('backendApiGet', { | ||||
| 			path: '/api/schema?ts=' + Date.now(), | ||||
| 			path: `/api/schema?ts=${Date.now()}`, | ||||
| 		}).then((data) => { | ||||
| 			expect(data.openapi).to.be.equal('3.1.0'); | ||||
| 		}); | ||||
|   | ||||
							
								
								
									
										64
									
								
								test/cypress/e2e/api/Ldap.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								test/cypress/e2e/api/Ldap.cy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| /// <reference types="cypress" /> | ||||
|  | ||||
| describe('LDAP with Authentik', () => { | ||||
| 	let _token; | ||||
| 	if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { | ||||
|  | ||||
| 		before(() => { | ||||
| 			cy.getToken().then((tok) => { | ||||
| 				_token = tok; | ||||
|  | ||||
| 				// cy.task('backendApiPut', { | ||||
| 				// 	token: token, | ||||
| 				// 	path:  '/api/settings/ldap-auth', | ||||
| 				// 	data:  { | ||||
| 				// 		value: { | ||||
| 				// 			host: 'authentik-ldap:3389', | ||||
| 				// 			base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io', | ||||
| 				// 			user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io', | ||||
| 				// 			email_property: 'mail', | ||||
| 				// 			name_property: 'sn', | ||||
| 				// 			self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))', | ||||
| 				// 			auto_create_user: true | ||||
| 				// 		} | ||||
| 				// 	} | ||||
| 				// }).then((data) => { | ||||
| 				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); | ||||
| 				// 	expect(data.result).to.have.property('id'); | ||||
| 				// 	expect(data.result.id).to.be.greaterThan(0); | ||||
| 				// }); | ||||
|  | ||||
| 				// cy.task('backendApiPut', { | ||||
| 				// 	token: token, | ||||
| 				// 	path:  '/api/settings/auth-methods', | ||||
| 				// 	data:  { | ||||
| 				// 		value: [ | ||||
| 				// 			'local', | ||||
| 				// 			'ldap' | ||||
| 				// 		] | ||||
| 				// 	} | ||||
| 				// }).then((data) => { | ||||
| 				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); | ||||
| 				// 	expect(data.result).to.have.property('id'); | ||||
| 				// 	expect(data.result.id).to.be.greaterThan(0); | ||||
| 				// }); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		it.skip('Should log in with LDAP', () => { | ||||
| 			// cy.task('backendApiPost', { | ||||
| 			// 	token: token, | ||||
| 			// 	path:  '/api/auth', | ||||
| 			// 	data:  { | ||||
| 			// 		// Authentik LDAP creds: | ||||
| 			// 		type: 'ldap', | ||||
| 			// 		identity: 'cypress', | ||||
| 			// 		secret: 'fqXBfUYqHvYqiwBHWW7f' | ||||
| 			// 	} | ||||
| 			// }).then((data) => { | ||||
| 			// 	cy.validateSwaggerSchema('post', 200, '/auth', data); | ||||
| 			// 	expect(data.result).to.have.property('token'); | ||||
| 			// }); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										97
									
								
								test/cypress/e2e/api/OAuth.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								test/cypress/e2e/api/OAuth.cy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| /// <reference types="cypress" /> | ||||
|  | ||||
| describe('OAuth with Authentik', () => { | ||||
| 	let _token; | ||||
| 	if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { | ||||
|  | ||||
| 		before(() => { | ||||
| 			cy.getToken().then((tok) => { | ||||
| 				_token = tok; | ||||
|  | ||||
| 				// cy.task('backendApiPut', { | ||||
| 				// 	token: token, | ||||
| 				// 	path:  '/api/settings/oauth-auth', | ||||
| 				// 	data:  { | ||||
| 				// 		value: { | ||||
| 				// 			client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU', | ||||
| 				// 			client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq', | ||||
| 				// 			authorization_url: 'http://authentik:9000/application/o/authorize/', | ||||
| 				// 			resource_url: 'http://authentik:9000/application/o/userinfo/', | ||||
| 				// 			token_url: 'http://authentik:9000/application/o/token/', | ||||
| 				// 			logout_url: 'http://authentik:9000/application/o/npm/end-session/', | ||||
| 				// 			identifier: 'preferred_username', | ||||
| 				// 			scopes: [], | ||||
| 				// 			auto_create_user: true | ||||
| 				// 		} | ||||
| 				// 	} | ||||
| 				// }).then((data) => { | ||||
| 				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); | ||||
| 				// 	expect(data.result).to.have.property('id'); | ||||
| 				// 	expect(data.result.id).to.be.greaterThan(0); | ||||
| 				// }); | ||||
|  | ||||
| 				// cy.task('backendApiPut', { | ||||
| 				// 	token: token, | ||||
| 				// 	path:  '/api/settings/auth-methods', | ||||
| 				// 	data:  { | ||||
| 				// 		value: [ | ||||
| 				// 			'local', | ||||
| 				// 			'oauth' | ||||
| 				// 		] | ||||
| 				// 	} | ||||
| 				// }).then((data) => { | ||||
| 				// 	cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); | ||||
| 				// 	expect(data.result).to.have.property('id'); | ||||
| 				// 	expect(data.result.id).to.be.greaterThan(0); | ||||
| 				// }); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		it.skip('Should log in with OAuth', () => { | ||||
| 			// cy.task('backendApiGet', { | ||||
| 			// 	path:  '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')), | ||||
| 			// }).then((data) => { | ||||
| 			// 	expect(data).to.have.property('result'); | ||||
|  | ||||
| 			// 	cy.origin('http://authentik:9000', {args: data.result}, (url) => { | ||||
| 			// 		cy.visit(url); | ||||
| 			// 		cy.get('ak-flow-executor') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('ak-stage-identification') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('input[name="uidField"]', { visible: true }) | ||||
| 			// 		.type('cypress'); | ||||
|  | ||||
| 			// 	cy.get('ak-flow-executor') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('ak-stage-identification') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('button[type="submit"]', { visible: true }) | ||||
| 			// 		.click(); | ||||
|  | ||||
| 			// 	cy.get('ak-flow-executor') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('ak-stage-password') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('input[name="password"]', { visible: true }) | ||||
| 			// 		.type('fqXBfUYqHvYqiwBHWW7f'); | ||||
|  | ||||
| 			// 	cy.get('ak-flow-executor') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('ak-stage-password') | ||||
| 			// 		.shadow() | ||||
| 			// 		.find('button[type="submit"]', { visible: true }) | ||||
| 			// 		.click(); | ||||
| 			// 	}) | ||||
|  | ||||
| 			// 	// we should be logged in | ||||
| 			// 	cy.get('#root p.chakra-text') | ||||
| 			// 		.first() | ||||
| 			// 		.should('have.text', 'Nginx Proxy Manager'); | ||||
|  | ||||
| 			// 	// logout: | ||||
| 			// 	cy.clearLocalStorage(); | ||||
| 			// }); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| @@ -9,7 +9,7 @@ describe('Proxy Hosts endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create a http host', function() { | ||||
| 	it('Should be able to create a http host', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/proxy-hosts', | ||||
|   | ||||
| @@ -9,7 +9,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Get all settings', function() { | ||||
| 	it('Get all settings', () => { | ||||
| 		cy.task('backendApiGet', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings', | ||||
| @@ -19,7 +19,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Get default-site setting', function() { | ||||
| 	it('Get default-site setting', () => { | ||||
| 		cy.task('backendApiGet', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings/default-site', | ||||
| @@ -30,7 +30,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Default Site congratulations', function() { | ||||
| 	it('Default Site congratulations', () => { | ||||
| 		cy.task('backendApiPut', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings/default-site', | ||||
| @@ -46,7 +46,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Default Site 404', function() { | ||||
| 	it('Default Site 404', () => { | ||||
| 		cy.task('backendApiPut', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings/default-site', | ||||
| @@ -62,7 +62,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Default Site 444', function() { | ||||
| 	it('Default Site 444', () => { | ||||
| 		cy.task('backendApiPut', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings/default-site', | ||||
| @@ -78,7 +78,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Default Site redirect', function() { | ||||
| 	it('Default Site redirect', () => { | ||||
| 		cy.task('backendApiPut', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings/default-site', | ||||
| @@ -100,7 +100,7 @@ describe('Settings endpoints', () => { | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Default Site html', function() { | ||||
| 	it('Default Site html', () => { | ||||
| 		cy.task('backendApiPut', { | ||||
| 			token: token, | ||||
| 			path:  '/api/settings/default-site', | ||||
|   | ||||
							
								
								
									
										225
									
								
								test/cypress/e2e/api/Streams.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								test/cypress/e2e/api/Streams.cy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | ||||
| /// <reference types="cypress" /> | ||||
|  | ||||
| describe('Streams', () => { | ||||
| 	let token; | ||||
|  | ||||
| 	before(() => { | ||||
| 		cy.getToken().then((tok) => { | ||||
| 			token = tok; | ||||
| 			// Set default site content | ||||
| 			cy.task('backendApiPut', { | ||||
| 				token: token, | ||||
| 				path:  '/api/settings/default-site', | ||||
| 				data: { | ||||
| 					value: 'html', | ||||
| 					meta: { | ||||
| 						html: '<p>yay it works</p>' | ||||
| 					}, | ||||
| 				}, | ||||
| 			}).then((data) => { | ||||
| 				cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		// Create a custom cert pair | ||||
| 		cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => { | ||||
| 			expect(result.code).to.eq(0); | ||||
| 			// Install CA | ||||
| 			cy.exec('mkcert -install').then((result) => { | ||||
| 				expect(result.code).to.eq(0); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		cy.exec('rm -f /test/results/testssl.json'); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create TCP Stream', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/streams', | ||||
| 			data:  { | ||||
| 				incoming_port: 1500, | ||||
| 				forwarding_host: '127.0.0.1', | ||||
| 				forwarding_port: 80, | ||||
| 				certificate_id: 0, | ||||
| 				meta: { | ||||
| 					dns_provider_credentials: "", | ||||
| 					letsencrypt_agree: false, | ||||
| 					dns_challenge: true | ||||
| 				}, | ||||
| 				tcp_forwarding: true, | ||||
| 				udp_forwarding: false | ||||
| 			} | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); | ||||
| 			expect(data).to.have.property('id'); | ||||
| 			expect(data.id).to.be.greaterThan(0); | ||||
| 			expect(data).to.have.property('enabled', true); | ||||
| 			expect(data).to.have.property('tcp_forwarding', true); | ||||
| 			expect(data).to.have.property('udp_forwarding', false); | ||||
|  | ||||
| 			cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => { | ||||
| 				expect(result.code).to.eq(0); | ||||
| 				expect(result.stdout).to.contain('yay it works'); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create UDP Stream', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/streams', | ||||
| 			data:  { | ||||
| 				incoming_port: 1501, | ||||
| 				forwarding_host: '127.0.0.1', | ||||
| 				forwarding_port: 80, | ||||
| 				certificate_id: 0, | ||||
| 				meta: { | ||||
| 					dns_provider_credentials: "", | ||||
| 					letsencrypt_agree: false, | ||||
| 					dns_challenge: true | ||||
| 				}, | ||||
| 				tcp_forwarding: false, | ||||
| 				udp_forwarding: true | ||||
| 			} | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); | ||||
| 			expect(data).to.have.property('id'); | ||||
| 			expect(data.id).to.be.greaterThan(0); | ||||
| 			expect(data).to.have.property('enabled', true); | ||||
| 			expect(data).to.have.property('tcp_forwarding', false); | ||||
| 			expect(data).to.have.property('udp_forwarding', true); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create TCP/UDP Stream', () => { | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/streams', | ||||
| 			data:  { | ||||
| 				incoming_port: 1502, | ||||
| 				forwarding_host: '127.0.0.1', | ||||
| 				forwarding_port: 80, | ||||
| 				certificate_id: 0, | ||||
| 				meta: { | ||||
| 					dns_provider_credentials: "", | ||||
| 					letsencrypt_agree: false, | ||||
| 					dns_challenge: true | ||||
| 				}, | ||||
| 				tcp_forwarding: true, | ||||
| 				udp_forwarding: true | ||||
| 			} | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); | ||||
| 			expect(data).to.have.property('id'); | ||||
| 			expect(data.id).to.be.greaterThan(0); | ||||
| 			expect(data).to.have.property('enabled', true); | ||||
| 			expect(data).to.have.property('tcp_forwarding', true); | ||||
| 			expect(data).to.have.property('udp_forwarding', true); | ||||
|  | ||||
| 			cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => { | ||||
| 				expect(result.code).to.eq(0); | ||||
| 				expect(result.stdout).to.contain('yay it works'); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to create SSL TCP Stream', () => { | ||||
| 		let certID = 0; | ||||
|  | ||||
| 		// Create custom cert | ||||
| 		cy.task('backendApiPost', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/certificates', | ||||
| 			data:  { | ||||
| 				provider: "other", | ||||
| 				nice_name: "Custom Certificate for SSL Stream", | ||||
| 			}, | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data); | ||||
| 			expect(data).to.have.property('id'); | ||||
| 			certID = data.id; | ||||
|  | ||||
| 			// Upload files | ||||
| 			cy.task('backendApiPostFiles', { | ||||
| 				token: token, | ||||
| 				path:  `/api/nginx/certificates/${certID}/upload`, | ||||
| 				files:  { | ||||
| 					certificate: 'website1.pem', | ||||
| 					certificate_key: 'website1.key.pem', | ||||
| 				}, | ||||
| 			}).then((data) => { | ||||
| 				cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data); | ||||
| 				expect(data).to.have.property('certificate'); | ||||
| 				expect(data).to.have.property('certificate_key'); | ||||
|  | ||||
| 				// Create the stream | ||||
| 				cy.task('backendApiPost', { | ||||
| 					token: token, | ||||
| 					path:  '/api/nginx/streams', | ||||
| 					data:  { | ||||
| 						incoming_port: 1503, | ||||
| 						forwarding_host: '127.0.0.1', | ||||
| 						forwarding_port: 80, | ||||
| 						certificate_id: certID, | ||||
| 						meta: { | ||||
| 							dns_provider_credentials: "", | ||||
| 							letsencrypt_agree: false, | ||||
| 							dns_challenge: true | ||||
| 						}, | ||||
| 						tcp_forwarding: true, | ||||
| 						udp_forwarding: false | ||||
| 					} | ||||
| 				}).then((data) => { | ||||
| 					cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); | ||||
| 					expect(data).to.have.property('id'); | ||||
| 					expect(data.id).to.be.greaterThan(0); | ||||
| 					expect(data).to.have.property("enabled", true); | ||||
| 					expect(data).to.have.property('tcp_forwarding', true); | ||||
| 					expect(data).to.have.property('udp_forwarding', false); | ||||
| 					expect(data).to.have.property('certificate_id', certID); | ||||
|  | ||||
| 					// Check the ssl termination | ||||
| 					cy.task('log', '[testssl.sh] Running ...'); | ||||
| 					cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', { | ||||
| 						timeout: 120000, // 2 minutes | ||||
| 					}).then((result) => { | ||||
| 						cy.task('log', `[testssl.sh] ${result.stdout}`); | ||||
|  | ||||
| 						const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"]; | ||||
| 						const ignoredIDs = [ | ||||
| 							'cert_chain_of_trust', | ||||
| 							'cert_extlifeSpan', | ||||
| 							'cert_revocation', | ||||
| 							'overall_grade', | ||||
| 						]; | ||||
|  | ||||
| 						cy.readFile('/test/results/testssl.json').then((data) => { | ||||
| 							// Parse each array item | ||||
| 							for (let i = 0; i < data.length; i++) { | ||||
| 								const item = data[i]; | ||||
| 								if (ignoredIDs.includes(item.id)) { | ||||
| 									continue; | ||||
| 								} | ||||
| 								expect(item.severity).to.be.oneOf(allowedSeverities); | ||||
| 							} | ||||
| 						}); | ||||
| 					}); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('Should be able to List Streams', () => { | ||||
| 		cy.task('backendApiGet', { | ||||
| 			token: token, | ||||
| 			path:  '/api/nginx/streams?expand=owner,certificate', | ||||
| 		}).then((data) => { | ||||
| 			cy.validateSwaggerSchema('get', 200, '/nginx/streams', data); | ||||
| 			expect(data.length).to.be.greaterThan(0); | ||||
| 			expect(data[0]).to.have.property('id'); | ||||
| 			expect(data[0]).to.have.property('enabled'); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user