Compare commits

..

7 Commits

Author SHA1 Message Date
Johannes Zellner 51a69cce41 Ensure notifications are sorted by time descending
(cherry picked from commit 63310c44c0)
2019-05-14 09:49:26 -07:00
Johannes Zellner 8686832bd1 4.0.3 changes 2019-05-14 16:57:45 +02:00
Girish Ramakrishnan c93c06ba88 gcs: fix crash
(cherry picked from commit 05d3f8a667)
2019-05-12 18:14:17 -07:00
Girish Ramakrishnan 36d64b3566 4.0.2 changes
(cherry picked from commit 3fa45ea728)
2019-05-12 14:04:26 -07:00
Girish Ramakrishnan e0c531564c Add option to skip backup before update
(cherry picked from commit a7d2098f09)
2019-05-12 13:59:07 -07:00
Girish Ramakrishnan 1e0ec75f0a gcdns: fix crash
(cherry picked from commit e1ecb49d59)
2019-05-12 12:57:06 -07:00
Girish Ramakrishnan 36ac02d29f Fix crash because params was undefined
(cherry picked from commit 800e25a7a7)
2019-05-10 13:08:26 -07:00
36 changed files with 1120 additions and 646 deletions
-9
View File
@@ -1600,12 +1600,3 @@
[4.0.3]
* Fix dashboard issue for non-admins
[4.1.0]
* Remove password requirement for uninstalling apps and users
* Hosting provider edition
* Enforce limits in mail container
* Fix crash when using unauthenticated relay
* Fix domain and tag filtering
* Customizable app icons
* Remove obsolete X-Frame-Options from nginx configs
+6 -1
View File
@@ -41,7 +41,12 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
## Installing
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
You can install the Cloudron platform on your own server or get a managed server
from cloudron.io. In either case, the Cloudron platform will keep your server and
apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
+260 -59
View File
@@ -129,6 +129,42 @@
"execa": "^1.0.0"
}
},
"@sinonjs/commons": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
"integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/formatio": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
"integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1",
"@sinonjs/samsam": "^3.1.0"
}
},
"@sinonjs/samsam": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz",
"integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.0.2",
"array-from": "^2.1.1",
"lodash": "^4.17.11"
}
},
"@sinonjs/text-encoding": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
"@types/caseless": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
@@ -224,7 +260,7 @@
},
"amdefine": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
@@ -283,9 +319,15 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-from": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
"dev": true
},
"arrify": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
},
"asn1": {
@@ -298,7 +340,7 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
@@ -359,7 +401,7 @@
},
"backoff": {
"version": "2.5.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
@@ -401,6 +443,14 @@
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
"integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
@@ -497,7 +547,7 @@
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
@@ -512,7 +562,7 @@
},
"bunyan": {
"version": "1.8.12",
"resolved": false,
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
@@ -651,7 +701,7 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color-convert": {
@@ -697,7 +747,7 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
@@ -884,7 +934,7 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
@@ -973,7 +1023,7 @@
},
"dashdash": {
"version": "1.14.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
@@ -1048,7 +1098,7 @@
},
"decamelize": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"deep-eql": {
@@ -1352,7 +1402,7 @@
},
"ent": {
"version": "2.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
},
"error-ex": {
@@ -1449,7 +1499,7 @@
},
"expect.js": {
"version": "0.3.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
"dev": true
},
@@ -1574,7 +1624,7 @@
},
"extsprintf": {
"version": "1.3.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"eyes": {
@@ -1605,6 +1655,11 @@
"pend": "~1.2.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"final-fs": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/final-fs/-/final-fs-1.6.1.tgz",
@@ -1722,7 +1777,7 @@
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
@@ -1734,7 +1789,7 @@
},
"rimraf": {
"version": "2.2.8",
"resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
}
@@ -2081,6 +2136,11 @@
}
}
},
"hoek": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"hosted-git-info": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
@@ -2193,7 +2253,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
@@ -2202,7 +2262,7 @@
},
"inherits": {
"version": "2.0.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
@@ -2222,7 +2282,7 @@
},
"is-arrayish": {
"version": "0.2.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
@@ -2268,7 +2328,7 @@
},
"is-stream": {
"version": "1.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-stream-ended": {
@@ -2298,12 +2358,20 @@
},
"isarray": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isemail": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
"requires": {
"punycode": "2.x.x"
}
},
"isexe": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isstream": {
@@ -2321,6 +2389,23 @@
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"joi": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz",
"integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==",
"requires": {
"hoek": "5.x.x",
"isemail": "3.x.x",
"topo": "3.x.x"
},
"dependencies": {
"hoek": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz",
"integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w=="
}
}
},
"js-base64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
@@ -2380,7 +2465,7 @@
},
"jsonfile": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
"dev": true
},
@@ -2400,6 +2485,12 @@
"verror": "1.10.0"
}
},
"just-extend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
"dev": true
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -2434,7 +2525,7 @@
},
"ldap-filter": {
"version": "0.2.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
@@ -2442,7 +2533,7 @@
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": false,
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
@@ -2548,6 +2639,12 @@
"chalk": "^2.0.1"
}
},
"lolex": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz",
"integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==",
"dev": true
},
"loud-rejection": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
@@ -2672,19 +2769,19 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": false,
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -2911,7 +3008,7 @@
},
"mv": {
"version": "2.1.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
@@ -2922,7 +3019,7 @@
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": false,
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
@@ -2935,13 +3032,13 @@
},
"ncp": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"rimraf": {
"version": "2.4.5",
"resolved": false,
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
@@ -2961,6 +3058,14 @@
"sqlstring": "2.3.1"
}
},
"namecheap": {
"version": "github:joshuakarjala/node-namecheap#464a9528b7ded3ee2520c2688bc98cbffb08e603",
"from": "github:joshuakarjala/node-namecheap#464a952",
"requires": {
"request": "*",
"xml2json": "*"
}
},
"nan": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
@@ -2981,6 +3086,42 @@
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"nise": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz",
"integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==",
"dev": true,
"requires": {
"@sinonjs/formatio": "^3.1.0",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"lolex": "^2.3.2",
"path-to-regexp": "^1.7.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"lolex": {
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
"dev": true
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"requires": {
"isarray": "0.0.1"
}
}
}
},
"nock": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz",
@@ -3030,6 +3171,15 @@
}
}
},
"node-expat": {
"version": "2.3.17",
"resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.17.tgz",
"integrity": "sha512-mNTxY/GMiZGayqdKZXyf6lJR7OM1JqyL0EISjE4XF7Ov7+X4zJjmlnfxCi6Gml90IEOyiYBcyJg9MHDsDp6YHw==",
"requires": {
"bindings": "^1.2.1",
"nan": "^2.10.0"
}
},
"node-fetch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
@@ -3203,7 +3353,7 @@
},
"nopt": {
"version": "3.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true,
"requires": {
@@ -3252,7 +3402,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
@@ -3403,7 +3553,7 @@
},
"p-finally": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-is-promise": {
@@ -3442,7 +3592,7 @@
},
"parse-json": {
"version": "2.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
@@ -3512,7 +3662,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
@@ -3607,7 +3757,7 @@
},
"precond": {
"version": "0.2.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"process-nextick-args": {
@@ -3659,7 +3809,7 @@
},
"pseudomap": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"psl": {
@@ -3888,12 +4038,12 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
},
"resolve": {
@@ -4177,7 +4327,7 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
@@ -4187,7 +4337,7 @@
},
"shebang-command": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"requires": {
"shebang-regex": "^1.0.0"
@@ -4195,7 +4345,7 @@
},
"shebang-regex": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"showdown": {
@@ -4364,9 +4514,35 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"sinon": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz",
"integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.4.0",
"@sinonjs/formatio": "^3.2.1",
"@sinonjs/samsam": "^3.3.1",
"diff": "^3.5.0",
"lolex": "^4.0.1",
"nise": "^1.4.10",
"supports-color": "^5.5.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"smtp-connection": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
@@ -4449,7 +4625,7 @@
},
"sprintf-js": {
"version": "1.0.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
@@ -4527,7 +4703,7 @@
},
"stream-shift": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
},
"streamsearch": {
@@ -4576,7 +4752,7 @@
},
"strip-eof": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-indent": {
@@ -4595,7 +4771,7 @@
},
"stubs": {
"version": "3.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
@@ -4784,6 +4960,21 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"topo": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
"requires": {
"hoek": "6.x.x"
},
"dependencies": {
"hoek": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz",
"integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q=="
}
}
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
@@ -4880,7 +5071,7 @@
},
"typedarray": {
"version": "0.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
@@ -4957,7 +5148,7 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utile": {
@@ -5017,7 +5208,7 @@
},
"vasync": {
"version": "1.6.4",
"resolved": false,
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
@@ -5025,12 +5216,12 @@
"dependencies": {
"extsprintf": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"verror": {
"version": "1.6.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
@@ -5040,7 +5231,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
@@ -5063,7 +5254,7 @@
},
"which-module": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
@@ -5118,7 +5309,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"requires": {
"string-width": "^1.0.1",
@@ -5160,7 +5351,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
@@ -5195,6 +5386,16 @@
"xmlbuilder": "~9.0.1"
}
},
"xml2json": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.11.2.tgz",
"integrity": "sha512-ZJpHpPOL0T5lOvAHMnWm59iQOPqNtam5t2TMUllWZ1k5Wm8L5YyvQnkeaVnRKCvDwY5EumqXWyOjjMdQVz272A==",
"requires": {
"hoek": "^4.2.1",
"joi": "^13.1.2",
"node-expat": "^2.3.15"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
@@ -5208,7 +5409,7 @@
},
"xtend": {
"version": "4.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"y18n": {
+4 -4
View File
@@ -40,13 +40,13 @@
"js-yaml": "^3.13.1",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.11",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.2",
"moment-timezone": "^0.5.25",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
"mysql": "^2.17.1",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^6.1.1",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
@@ -78,8 +78,7 @@
"uuid": "^3.3.2",
"valid-url": "^1.0.9",
"validator": "^10.11.0",
"ws": "^6.2.1",
"xml2js": "^0.4.19"
"ws": "^6.2.1"
},
"devDependencies": {
"expect.js": "*",
@@ -89,7 +88,8 @@
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.11.0",
"recursive-readdir": "^2.2.2"
"recursive-readdir": "^2.2.2",
"sinon": "^7.3.2"
},
"scripts": {
"test": "./runTests",
+25 -34
View File
@@ -1,7 +1,5 @@
#!/bin/bash
set -eu -o pipefail
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
@@ -19,7 +17,7 @@ This script collects diagnostic information to help debug server related issues
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root. Run with sudo"
echo "This script should be run as root." > /dev/stderr
exit 1
fi
@@ -60,6 +58,21 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
if [[ $SUDO_USER == "" ]]; then
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
else
ssh_user="$SUDO_USER"
ssh_folder="/home/$SUDO_USER/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
@@ -91,42 +104,20 @@ iptables -L &>> $OUT
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
ssh_user="root"
keys_file="/root/.ssh/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
echo "Key file: ${keys_file}" >> $OUT
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
touch "${keys_file}" # required for concat to work
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
chmod 600 "${keys_file}"
chown "${ssh_user}" "${keys_file}"
fi
echo "Done"
fi
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
fi
echo ""
echo "Please email the following link to support@cloudron.io"
echo ""
+7 -22
View File
@@ -1,31 +1,16 @@
# add customizations here
# after making changes run "sudo systemctl restart box"
# backups:
# configurable: true
#
# domains:
# features:
# configureBackup: true
# dynamicDns: true
# changeDashboardDomain: true
#
# subscription:
# configurable: true
#
# subscription: true
# remoteSupport: true
#
# support:
# email: support@cloudron.io
# remoteSupport: true
#
# ticketFormBody: |
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
# * [Forum](https://forum.cloudron.io/)
#
# submitTickets: true
#
#
# alerts:
# email: support@cloudron.io
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
+4
View File
@@ -72,6 +72,10 @@ server {
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
proxy_hide_header X-Frame-Options;
# https://github.com/twitter/secureheaders
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
+8 -4
View File
@@ -69,7 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -121,6 +121,9 @@ function postProcess(result) {
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
// TODO remove later once all apps have this attribute
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
@@ -276,6 +279,7 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const xFrameOptions = data.xFrameOptions || '';
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
const sso = 'sso' in data ? data.sso : null;
@@ -289,10 +293,10 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions,'
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson,
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
});
+35 -27
View File
@@ -90,6 +90,7 @@ var appdb = require('./appdb.js'),
taskmanager = require('./taskmanager.js'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
url = require('url'),
util = require('util'),
uuid = require('uuid'),
validator = require('validator'),
@@ -240,6 +241,20 @@ function validateMemoryLimit(manifest, memoryLimit) {
return null;
}
// https://tools.ietf.org/html/rfc7034
function validateXFrameOptions(xFrameOptions) {
assert.strictEqual(typeof xFrameOptions, 'string');
if (xFrameOptions === 'DENY') return null;
if (xFrameOptions === 'SAMEORIGIN') return null;
var parts = xFrameOptions.split(' ');
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
var uri = url.parse(parts[1]);
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
}
function validateDebugMode(debugMode) {
assert.strictEqual(typeof debugMode, 'object');
@@ -357,7 +372,7 @@ function getAppConfig(app) {
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
@@ -374,7 +389,7 @@ function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
@@ -387,15 +402,8 @@ function removeRestrictedFields(app) {
}
function getIconUrlSync(app) {
const iconUrl = '/api/v1/apps/' + app.id + '/icon';
const userIconPath = `${paths.APP_ICONS_DIR}/${app.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return iconUrl;
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${app.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return iconUrl;
return null;
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
function postProcess(app, domainObjectMap) {
@@ -570,6 +578,7 @@ function install(data, user, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
@@ -604,6 +613,9 @@ function install(data, user, auditSource, callback) {
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
error = validateXFrameOptions(xFrameOptions);
if (error) return callback(error);
error = validateDebugMode(debugMode);
if (error) return callback(error);
@@ -660,6 +672,7 @@ function install(data, user, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
@@ -741,6 +754,12 @@ function configure(appId, data, user, auditSource, callback) {
if (error) return callback(error);
}
if ('xFrameOptions' in data) {
values.xFrameOptions = data.xFrameOptions;
error = validateXFrameOptions(values.xFrameOptions);
if (error) return callback(error);
}
if ('debugMode' in data) {
values.debugMode = data.debugMode;
error = validateDebugMode(values.debugMode);
@@ -794,18 +813,6 @@ function configure(appId, data, user, auditSource, callback) {
values.tags = data.tags;
}
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
}
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -829,7 +836,7 @@ function configure(appId, data, user, auditSource, callback) {
values.oldConfig = getAppConfig(app);
debug(`configure: id:${appId}`);
debug('Will configure app with id:%s values:%j', appId, values);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
@@ -857,7 +864,7 @@ function update(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`update: id:${appId}`);
debug('Will update app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
@@ -894,11 +901,11 @@ function update(appId, data, auditSource, callback) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
@@ -1112,6 +1119,7 @@ function clone(appId, data, user, auditSource, callback) {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
+3 -4
View File
@@ -387,15 +387,14 @@ function registerCloudron(data, callback) {
});
}
function registerWithLicense(license, domain, callback) {
function registerWithLicense(license, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
registerCloudron({ license, domain }, callback);
registerCloudron({ license }, callback);
});
}
@@ -447,7 +446,7 @@ function createTicket(info, callback) {
let url = config.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.spec().support.email; // destination address for tickets
info.supportEmail = custom.supportEmail(); // destination address for tickets
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
+5 -15
View File
@@ -420,15 +420,10 @@ function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', safe.error);
}
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.user.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove user icon : %s', safe.error);
}
callback(null);
fs.unlink(path.join(paths.APP_ICONS_DIR, app.id + '.png'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
callback(null);
});
}
function cleanupLogs(app, callback) {
@@ -702,8 +697,6 @@ function update(app, callback) {
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
const FORCED_UPDATE = (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE);
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
@@ -711,7 +704,7 @@ function update(app, callback) {
verifyManifest.bind(null, app.updateConfig.manifest),
function (next) {
if (FORCED_UPDATE) return next(null);
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
@@ -798,9 +791,6 @@ function update(app, callback) {
debugApp(app, 'Error updating app: %s', error);
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
} else {
// do not spam the notifcation view
if (FORCED_UPDATE) return callback();
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback);
}
});
+2 -1
View File
@@ -174,7 +174,8 @@ function getConfig(callback) {
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
features: custom.features(),
supportEmail: custom.supportEmail()
});
});
}
+27 -46
View File
@@ -1,63 +1,44 @@
'use strict';
let config = require('./config.js'),
debug = require('debug')('box:features'),
lodash = require('lodash'),
let debug = require('debug')('box:features'),
paths = require('./paths.js'),
safe = require('safetydance'),
yaml = require('js-yaml');
exports = module.exports = {
uiSpec: uiSpec,
spec: spec
features: features,
supportEmail: supportEmail,
alertsEmail: alertsEmail,
sendAlertsToCloudronAdmins: sendAlertsToCloudronAdmins
};
const DEFAULT_SPEC = {
backups: {
configurable: true
},
domains: {
dynamicDns: true,
changeDashboardDomain: true
},
subscription: {
configurable: true
},
support: {
email: 'support@cloudron.io',
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
alerts: {
email: '',
notifyCloudronAdmins: false
},
footer: {
body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
}
};
const gSpec = (function () {
const gCustom = (function () {
try {
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return DEFAULT_SPEC;
const c = yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
return lodash.merge({}, DEFAULT_SPEC, c);
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return {};
return yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
} catch (e) {
debug(`Error loading features file from ${paths.CUSTOM_FILE} : ${e.message}`);
return DEFAULT_SPEC;
return {};
}
})();
// flags sent to the UI. this is separate because we have values that are secret to the backend
function uiSpec() {
return gSpec;
function features() {
return {
dynamicDns: safe.query(gCustom, 'features.dynamicDns', true),
remoteSupport: safe.query(gCustom, 'features.remoteSupport', true),
subscription: safe.query(gCustom, 'features.subscription', true),
configureBackup: safe.query(gCustom, 'features.configureBackup', true)
};
}
function spec() {
return gSpec;
}
function supportEmail() {
return safe.query(gCustom, 'support.email', 'support@cloudron.io');
}
function alertsEmail() {
return safe.query(gCustom, 'alerts.email', '');
}
function sendAlertsToCloudronAdmins() {
return safe.query(gCustom, 'alerts.notifyCloudronAdmins', true);
}
+41 -66
View File
@@ -15,14 +15,14 @@ var assert = require('assert'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
Namecheap = require('namecheap'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
xml2js = require('xml2js');
waitForDns = require('./waitfordns.js');
const ENDPOINT = 'https://api.namecheap.com/xml.response';
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
@@ -33,19 +33,37 @@ function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getQuery(dnsConfig, callback) {
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
function mapHosts(hosts) {
return hosts.map(function (host) {
let tmp = {};
tmp.TTL = '300';
tmp.RecordType = host.RecordType || host.Type;
tmp.HostName = host.HostName || host.Name;
tmp.Address = host.Address;
if (tmp.RecordType === 'MX') {
tmp.EmailType = 'MX';
if (host.MXPref) tmp.MXPref = host.MXPref;
}
return tmp;
});
}
function getApi(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback(null, {
ApiUser: dnsConfig.username,
ApiKey: dnsConfig.token,
UserName: dnsConfig.username,
ClientIp: ip
});
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
namecheap.setUsername(dnsConfig.username);
callback(null, namecheap);
});
}
@@ -56,31 +74,15 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getQuery(dnsConfig, function (error, query) {
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
query.Command = 'namecheap.domains.dns.getHosts';
query.SLD = zoneName.split('.')[0];
query.TLD = zoneName.split('.')[1];
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
superagent.get(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
debug('entire getInternal response: %j', result);
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
return h['$'];
});
callback(null, hosts);
});
return callback(null, result['DomainDNSGetHostsResult']['host']);
});
});
}
@@ -91,42 +93,15 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
assert(Array.isArray(hosts));
assert.strictEqual(typeof callback, 'function');
getQuery(dnsConfig, function (error, query) {
let mappedHosts = mapHosts(hosts);
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
query.Command = 'namecheap.domains.dns.setHosts';
query.SLD = zoneName.split('.')[0];
query.TLD = zoneName.split('.')[1];
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
hosts.forEach(function (host, i) {
var n = i+1; // api starts with 1 not 0
query['TTL' + n] = '300'; // keep it low
query['HostName' + n] = host.HostName || host.Name;
query['RecordType' + n] = host.RecordType || host.Type;
query['Address' + n] = host.Address;
if (host.Type === 'MX') {
query['EmailType' + n] = 'MX';
if (host.MXPref) query['MXPref' + n] = host.MXPref;
}
});
superagent.post(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
callback(null);
});
return callback(null, result);
});
});
}
+1 -1
View File
@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.0@sha256:5118cd2cc755a53661485a1c7507cbb546f765642fc83a82f0351d646221342f' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.2.0@sha256:20e4d2508dcf712eb56481067993ae39bf541d793d44f99f6a41d630ad941d9e' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}
+6 -19
View File
@@ -289,25 +289,13 @@ function checkMx(domain, mailFqdn, callback) {
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
}
if (mx.status) return callback(null, mx); // MX record is "my."
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
dns.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS, function (error, mxIps) {
if (error || mxIps.length !== 1) return callback(null, mx);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(null, mx);
mx.status = mxIps[0] === ip;
callback(null, mx);
});
});
callback(null, mx);
});
}
@@ -608,14 +596,13 @@ function createMailConfig(mailFqdn, callback) {
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
host = relay.host || '',
port = relay.port || 25,
authType = relay.username ? 'plain' : '',
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
});
+1 -1
View File
@@ -289,7 +289,7 @@ function appDied(mailTo, app) {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.spec().support.email, format: 'text' })
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.supportEmail(), format: 'text' })
};
sendMail(mailOptions);
+10 -10
View File
@@ -193,8 +193,8 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
message = 'The container has been restarted automatically. Consider increasing the [memory limit](https://docs.docker.com/v17.09/edge/engine/reference/commandline/update/#update-a-containers-kernel-memory-constraints)';
}
if (custom.spec().alerts.email) mailer.oomEvent(custom.spec().alerts.email, program, event);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.oomEvent(custom.alertsEmail(), program, event);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.oomEvent(admin.email, program, event);
@@ -208,8 +208,8 @@ function appUp(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.appUp(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.appUp(custom.alertsEmail(), app);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.appUp(admin.email, app);
@@ -222,8 +222,8 @@ function appDied(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.appDied(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.appDied(custom.alertsEmail(), app);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
@@ -254,8 +254,8 @@ function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.certificateRenewalError(custom.spec().alerts.email, vhost, errorMessage);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.certificateRenewalError(custom.alertsEmail(), vhost, errorMessage);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
@@ -269,8 +269,8 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.backupFailed(custom.alertsEmail(), errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
+3 -5
View File
@@ -78,7 +78,6 @@ ProvisionError.BAD_STATE = 'Bad State';
ProvisionError.ALREADY_SETUP = 'Already Setup';
ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.LICENSE_ERROR = 'License Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function setProgress(task, message, callback) {
@@ -109,8 +108,7 @@ function autoprovision(autoconf, callback) {
});
}
function autoRegister(domain, callback) {
assert.strictEqual(typeof domain, 'string');
function autoRegister(callback) {
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
@@ -120,7 +118,7 @@ function autoRegister(domain, callback) {
debug('Auto-registering cloudron');
appstore.registerWithLicense(license.trim(), domain, function (error) {
appstore.registerWithLicense(license.trim(), function (error) {
if (error && error.reason !== AppstoreError.ALREADY_REGISTERED) {
debug('Failed to auto-register cloudron', error);
return callback(new ProvisionError(ProvisionError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
@@ -190,7 +188,7 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
callback(); // now that args are validated run the task in the background
async.series([
autoRegister.bind(null, domain),
autoRegister,
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
+5 -2
View File
@@ -392,6 +392,7 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -456,7 +457,8 @@ function writeAppNginxConfig(app, bundle, callback) {
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -485,7 +487,8 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: null
robotsTxtQuoted: null,
xFrameOptions: 'SAMEORIGIN'
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
+11 -10
View File
@@ -31,7 +31,9 @@ var apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
auditSource = require('../auditsource.js'),
config = require('../config.js'),
debug = require('debug')('box:routes/apps'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
@@ -65,15 +67,11 @@ function getApps(req, res, next) {
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
if (!req.query.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath);
}
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath);
return next(new HttpError(404, 'No such icon'));
var iconPath = paths.APP_ICONS_DIR + '/' + req.params.id + '.png';
fs.exists(iconPath, function (exists) {
if (!exists) return next(new HttpError(404, 'No such icon'));
res.sendFile(iconPath);
});
}
function installApp(req, res, next) {
@@ -109,6 +107,8 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
@@ -166,6 +166,7 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
@@ -188,7 +189,6 @@ function configureApp(req, res, next) {
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
@@ -326,6 +326,7 @@ function updateApp(req, res, next) {
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
-5
View File
@@ -20,7 +20,6 @@ let assert = require('assert'),
auditSource = require('../auditsource.js'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
@@ -153,8 +152,6 @@ function getLogStream(req, res, next) {
function setDashboardAndMailDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -166,8 +163,6 @@ function setDashboardAndMailDomain(req, res, next) {
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
+2 -2
View File
@@ -65,7 +65,7 @@ function setup(req, res, next) {
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
next(new HttpSuccess(200));
});
}
@@ -118,7 +118,7 @@ function restore(req, res, next) {
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
next(new HttpSuccess(200));
});
}
+4 -4
View File
@@ -131,8 +131,8 @@ function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
// used by the UI to figure if backups are disabled
if (!custom.features().configureBackup) {
return next(new HttpSuccess(200, { provider: config.provider }));
}
@@ -143,7 +143,7 @@ function getBackupConfig(req, res, next) {
function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().backups.configurable) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.features().configureBackup) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
@@ -207,7 +207,7 @@ function getDynamicDnsConfig(req, res, next) {
function setDynamicDnsConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().domains.dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.features().dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
+2 -4
View File
@@ -18,8 +18,6 @@ var appstore = require('../appstore.js'),
function createTicket(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (!custom.spec().support.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
@@ -29,7 +27,7 @@ function createTicket(req, res, next) {
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.supportEmail()}`));
next(new HttpSuccess(201, {}));
});
@@ -38,7 +36,7 @@ function createTicket(req, res, next) {
function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().support.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.features().remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
+25
View File
@@ -56,6 +56,8 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
const CLOUDRON_ID = 'somecloudronid';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_LOCATION_2 = 'appslocationtwo';
@@ -537,6 +539,7 @@ describe('App API', function () {
it('cannot uninstall invalid app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -544,8 +547,28 @@ describe('App API', function () {
});
});
it('cannot uninstall app without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot uninstall app with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('non admin cannot uninstall app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
@@ -558,6 +581,7 @@ describe('App API', function () {
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -1124,6 +1148,7 @@ describe('App installation', function () {
}
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
+24
View File
@@ -253,6 +253,7 @@ describe('Domains API', function () {
it('cannot delete locked domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
@@ -261,9 +262,31 @@ describe('Domains API', function () {
});
describe('delete', function () {
it('fails without password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails for non-existing domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -274,6 +297,7 @@ describe('Domains API', function () {
it('succeeds', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
+2
View File
@@ -250,6 +250,7 @@ describe('Groups API', function () {
it('can remove empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + group1Object.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
@@ -259,6 +260,7 @@ describe('Groups API', function () {
it('can remove non-empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + groupObject.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
+29
View File
@@ -190,9 +190,29 @@ describe('Mail API', function () {
});
});
it('cannot delete domain without password', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot delete domain with wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('cannot delete non-existing domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
@@ -201,6 +221,7 @@ describe('Mail API', function () {
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -210,6 +231,7 @@ describe('Mail API', function () {
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -267,6 +289,7 @@ describe('Mail API', function () {
dns.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -516,6 +539,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -567,6 +591,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -637,6 +662,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -714,6 +740,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -819,6 +846,7 @@ describe('Mail API', function () {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -944,6 +972,7 @@ describe('Mail API', function () {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
+1 -1
View File
@@ -248,7 +248,7 @@ describe('Profile API', function () {
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.statusCode).to.equal(403);
done();
});
});
+33
View File
@@ -529,6 +529,7 @@ describe('Users API', function () {
it('remove random user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/randomid')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
@@ -538,15 +539,46 @@ describe('Users API', function () {
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('admin cannot remove normal user without giving a password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('admin cannot remove normal user with empty password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('admin cannot remove normal user with giving wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('admin removes normal user', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
@@ -556,6 +588,7 @@ describe('Users API', function () {
it('admin removes himself should not be allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
+1 -1
View File
@@ -132,7 +132,7 @@ function verifyPassword(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
users.verifyWithUsername(req.user.username, req.body.password, function (error) {
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(400, 'Password incorrect'));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect')); // not 401 intentionally since the UI redirects for 401
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
+10 -8
View File
@@ -18,6 +18,7 @@ var accesscontrol = require('./accesscontrol.js'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
provision = require('./provision.js'),
routes = require('./routes/index.js'),
ws = require('ws');
@@ -167,7 +168,7 @@ function initializeExpressSync() {
router.get ('/api/v1/users', usersReadScope, routes.users.list);
router.post('/api/v1/users', usersManageScope, routes.users.create);
router.get ('/api/v1/users/:userId', usersManageScope, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.remove);
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
router.post('/api/v1/users/:userId/password', usersManageScope, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
@@ -181,7 +182,7 @@ function initializeExpressSync() {
router.get ('/api/v1/groups/:groupId', usersManageScope, routes.groups.get);
router.put ('/api/v1/groups/:groupId/members', usersManageScope, routes.groups.updateMembers);
router.post('/api/v1/groups/:groupId', usersManageScope, routes.groups.update);
router.del ('/api/v1/groups/:groupId', usersManageScope, routes.groups.remove);
router.del ('/api/v1/groups/:groupId', usersManageScope, routes.users.verifyPassword, routes.groups.remove);
// form based login routes used by oauth2 frame
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
@@ -221,13 +222,13 @@ function initializeExpressSync() {
// app routes
router.get ('/api/v1/apps', appsReadScope, routes.apps.getApps);
router.get ('/api/v1/apps/:id', appsManageScope, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', appsReadScope, routes.apps.getAppIcon);
router.get ('/api/v1/apps/:id/icon', routes.apps.getAppIcon);
router.post('/api/v1/apps/install', appsManageScope, routes.apps.installApp);
router.post('/api/v1/apps/:id/uninstall', appsManageScope, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/uninstall', appsManageScope, routes.users.verifyPassword, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/configure', appsManageScope, routes.apps.configureApp);
router.post('/api/v1/apps/:id/update', appsManageScope, routes.apps.updateApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.users.verifyPassword, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/backup', appsManageScope, routes.apps.backupApp);
router.get ('/api/v1/apps/:id/backups', appsManageScope, routes.apps.listBackups);
router.post('/api/v1/apps/:id/stop', appsManageScope, routes.apps.stopApp);
@@ -251,8 +252,8 @@ function initializeExpressSync() {
// email routes
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
router.post('/api/v1/mail', mailScope, routes.mail.addDomain);
router.get ('/api/v1/mail/:domain/stats', mailScope, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', mailScope, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/stats', mailScope, routes.users.verifyPassword, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', mailScope, routes.users.verifyPassword, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/status', mailScope, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', mailScope, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', mailScope, routes.mail.setCatchAllAddress);
@@ -284,7 +285,7 @@ function initializeExpressSync() {
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.del);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
// addon routes
router.get ('/api/v1/services', cloudronScope, routes.services.getAll);
@@ -369,6 +370,7 @@ function start(callback) {
async.series([
routes.accesscontrol.initialize, // hooks up authentication strategies into passport
database.initialize,
provision.autoRegister,
cloudron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
-1
View File
@@ -14,7 +14,6 @@ let assert = require('assert'),
path = require('path'),
util = require('util');
// the logic here is also used in the cloudron-support tool
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
AUTHORIZED_KEYS_USER = config.TEST ? process.getuid() : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'),
AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
+3
View File
@@ -412,6 +412,7 @@ describe('database', function () {
oldConfig: null,
newConfig: null,
memoryLimit: 4294967296,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -992,6 +993,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 4294967296,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -1026,6 +1028,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 0,
xFrameOptions: 'SAMEORIGIN',
sso: true,
debugMode: null,
robotsTxt: null,
+520 -280
View File
@@ -2,19 +2,20 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global beforeEach:false */
/* global after:false */
'use strict';
var async = require('async'),
AWS = require('aws-sdk'),
GCDNS = require('@google-cloud/dns').DNS,
GCDNS = require('@google-cloud/dns'),
config = require('../config.js'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
namecheap = require('namecheap'),
nock = require('nock'),
sinon = require('sinon'),
util = require('util');
var DOMAIN_0 = {
@@ -595,282 +596,465 @@ describe('dns provider', function () {
});
});
describe('namecheap', function () {
const NAMECHEAP_ENDPOINT = 'https://api.namecheap.com';
const username = 'namecheapuser';
const token = 'namecheaptoken';
xdescribe('namecheap', function () {
let sandbox = require('sinon').createSandbox();
// the success answer is always the same
const SET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.sethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.setHosts">
<DomainDNSSetHostsResult Domain="cloudron.space" IsSuccess="true">
<Warnings />
</DomainDNSSetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT03</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.408</ExecutionTime>
</ApiResponse>`;
let username = 'namecheapuser';
let apiKey = 'API_KEY';
before(function (done) {
DOMAIN_0.provider = 'namecheap';
DOMAIN_0.config = {
username: username,
token: token
username,
apiKey
};
domains.update(DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE, done);
});
beforeEach(function () {
nock.cleanAll();
after(function() {
sandbox.restore();
});
it('upsert non-existing record succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="@" Type="TXT" Address="v=spf1 a:my.nebulon.space ~all" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'A',
HostName: 'test',
Address: '1.2.3.4'
}
];
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL2: '300',
HostName2: '@',
RecordType2: 'TXT',
Address2: 'v=spf1 a:my.nebulon.space ~all',
TTL3: '300',
HostName3: 'test',
RecordType3: 'A',
Address3: '1.2.3.4',
})
.reply(200, SET_HOSTS_RETURN);
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert multiple non-existing records succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="@" Type="TXT" Address="v=spf1 a:my.nebulon.space ~all" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '1.2.3.4'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '2.3.4.5'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '3.4.5.6'
}
];
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL2: '300',
HostName2: '@',
RecordType2: 'TXT',
Address2: 'v=spf1 a:my.nebulon.space ~all',
TTL3: '300',
HostName3: 'test',
RecordType3: 'TXT',
Address3: '1.2.3.4',
TTL4: '300',
HostName4: 'test',
RecordType4: 'TXT',
Address4: '2.3.4.5',
TTL5: '300',
HostName5: 'test',
RecordType5: 'TXT',
Address5: '3.4.5.6',
})
.reply(200, SET_HOSTS_RETURN);
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'TXT', ['1.2.3.4', '2.3.4.5', '3.4.5.6'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert multiple non-existing MX records succeeds', function (done) {
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '1.2.3.4',
MXPref: '10'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '2.3.4.5',
MXPref: '20'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '3.4.5.6',
MXPref: '30'
}
];
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'MX', ['10 1.2.3.4', '20 2.3.4.5', '30 3.4.5.6'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert existing record succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': DOMAIN_0.domain,
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
];
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL2: '300',
HostName2: 'www',
RecordType2: 'CNAME',
Address2: '1.2.3.4'
})
.reply(200, SET_HOSTS_RETURN);
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('get succeeds', function(done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="test" Type="A" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400613" Name="test" Type="A" Address="2.3.4.5" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': 'test',
'Type': 'A',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'FriendlyName': 'A Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614431',
'Name': 'test',
'Type': 'A',
'Address': '2.3.4.5',
'MXPref': '10',
'TTL': '1800',
'FriendlyName': 'A Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let mockObj = {
dns: {
getHosts: getHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(result).to.be.an(Array);
expect(result.length).to.eql(2);
expect(getHostsFake.calledOnce).to.eql(true);
expect(result).to.eql(['1.2.3.4', '2.3.4.5']);
done();
@@ -878,74 +1062,130 @@ describe('dns provider', function () {
});
it('del succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let setInternalExpect = [
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
];
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.removeDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('del succeeds with non-existing domain', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
it('del succeeds w/ non-present host', function (done) {
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(setHostsFake.notCalled).to.eql(true);
done();
});
@@ -1135,7 +1375,7 @@ describe('dns provider', function () {
});
});
describe('gcdns', function () {
xdescribe('gcdns', function () {
var HOSTED_ZONES = [];
var zoneQueue = [];
var _OriginalGCDNS;
@@ -1173,7 +1413,7 @@ describe('dns provider', function () {
}
function fakeZone(name, ns, recordQueue) {
var zone = new GCDNS().zone(name.replace('.', '-'));
var zone = GCDNS().zone(name.replace('.', '-'));
zone.metadata.dnsName = name + '.';
zone.metadata.nameServers = ns || ['8.8.8.8', '8.8.4.4'];
zone.getRecords = mockery(recordQueue || zoneQueue);
@@ -1210,7 +1450,7 @@ describe('dns provider', function () {
it('upsert existing record succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, { id: '2' }]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
@@ -1236,7 +1476,7 @@ describe('dns provider', function () {
it('get succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
@@ -1251,7 +1491,7 @@ describe('dns provider', function () {
it('del succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, { id: '5' }]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {