Compare commits
549 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10f1ad5cfe | |||
| fd11eb8da0 | |||
| 62d5e99802 | |||
| 48305f0e95 | |||
| 8170b490f2 | |||
| 072962bbc3 | |||
| 33bc1cf7d9 | |||
| 85df9d1472 | |||
| 109ba3bf56 | |||
| 8083362e71 | |||
| 9b4c385a64 | |||
| ee9c8ba4eb | |||
| 000a64d54a | |||
| eba74d77a6 | |||
| 714a1bcb1d | |||
| 02d17dc2e4 | |||
| 4b54e776cc | |||
| ba6f05b119 | |||
| 1d9ae120dc | |||
| 3ce841e050 | |||
| 436fc2ba13 | |||
| 77d652fc2b | |||
| ac3681296e | |||
| 5254d3325f | |||
| ce0a24a95d | |||
| 1bb596bf58 | |||
| c384ac6080 | |||
| 61c2ce0f47 | |||
| 7a71315d33 | |||
| 0a658e5862 | |||
| 5f8c99aa0e | |||
| 4c6f1e4b4a | |||
| 226ae627f9 | |||
| 27a02aa918 | |||
| 3c43503df8 | |||
| 35c926d504 | |||
| ea18ca5c60 | |||
| 55a56355d5 | |||
| dc83ba2686 | |||
| 62615dfd0f | |||
| a6998550a7 | |||
| 3b199170be | |||
| 1f93787a63 | |||
| 199c5b926a | |||
| d9ad7085c3 | |||
| df12f31800 | |||
| ad205da3db | |||
| 34aab65db3 | |||
| 63c06a508e | |||
| a2899c9c65 | |||
| ff6d5e9efc | |||
| f48fe0a7c0 | |||
| 5f6c8ca520 | |||
| 0eaa3a8d94 | |||
| 8ad190fa83 | |||
| 70f096c820 | |||
| 2840251862 | |||
| b43966df22 | |||
| cc22285beb | |||
| b72d48b49f | |||
| 3a6b9c23c6 | |||
| b2da364345 | |||
| de7a6abc50 | |||
| 10f74349ca | |||
| 05a771c365 | |||
| cfa2089d7b | |||
| d56abd94a9 | |||
| 2f20ff8def | |||
| 9706daf330 | |||
| a246b3e90c | |||
| e28e1b239f | |||
| 4aead483de | |||
| f8cc6e471e | |||
| 6b9ed9472d | |||
| a763b08c41 | |||
| 178f904143 | |||
| bb88fa3620 | |||
| 1e1249d8e0 | |||
| bcb0e61bfc | |||
| 022ff89836 | |||
| b9d4b8f6e8 | |||
| 0f5ce651cc | |||
| 6b8d5f92de | |||
| 55e556c725 | |||
| 19bb0a6ec2 | |||
| 290132f432 | |||
| 4a8be8e62d | |||
| 23b61aef0c | |||
| 24cc433a3d | |||
| e014b7de81 | |||
| 0895a2bdea | |||
| 03ca4887ba | |||
| 9eeb17c397 | |||
| 6a5da2745a | |||
| e1111ba2bb | |||
| d186084835 | |||
| 06c2ba9fa9 | |||
| b82e5fd8c6 | |||
| 6e1f96a832 | |||
| f68135c7aa | |||
| f48cbb457b | |||
| 8d192dc992 | |||
| b70324aa24 | |||
| 390afaf614 | |||
| 5112322e7d | |||
| 2cb498d500 | |||
| 2bd6e02cdc | |||
| 85423cbc20 | |||
| 1c0d027bd3 | |||
| 5a8a023039 | |||
| 196b059cfb | |||
| 2d930b9c3d | |||
| a5ba3faa49 | |||
| 02ba91f1bb | |||
| bfa917e057 | |||
| 909dd0725a | |||
| 74860f2d16 | |||
| 132ebb4e74 | |||
| 698158cd93 | |||
| 5bfc684f1b | |||
| c944c9b65b | |||
| d61698b894 | |||
| a4d32009ad | |||
| 3007875e35 | |||
| b4aad138fc | |||
| 8df7eb2acb | |||
| 18cab6f861 | |||
| b2071c65d8 | |||
| 402dba096e | |||
| abf0c81de4 | |||
| 613985a17c | |||
| bfc9801699 | |||
| ee705eb979 | |||
| 67b94c7fde | |||
| 77e5d3f4bb | |||
| 30618b8644 | |||
| 57a2613286 | |||
| e15bd89ba2 | |||
| d2ed816f44 | |||
| e51234928b | |||
| 3aa668aea3 | |||
| 870edab78a | |||
| ebc9d9185d | |||
| 093150d4e3 | |||
| de80a6692d | |||
| c28f564a47 | |||
| eb6a09c2bd | |||
| 19f404e092 | |||
| 55799ebb2d | |||
| fdf4d8fdcf | |||
| 6dc11edafe | |||
| c82ca1c69d | |||
| 7ef3d55cbf | |||
| 44e4f53827 | |||
| 643e490cbb | |||
| e61498c3b6 | |||
| bb6b61d810 | |||
| cff173c2e6 | |||
| 226501d103 | |||
| c5b8b0e3db | |||
| 46878e4363 | |||
| f77682365e | |||
| d9850fa660 | |||
| 9258585746 | |||
| e635aaaa58 | |||
| d0d6725df5 | |||
| 61f4fea9c3 | |||
| 66d59c1d6c | |||
| f9725965e2 | |||
| 4629739a14 | |||
| e9b3a1e99c | |||
| 8ac27b9dc7 | |||
| 2edd434474 | |||
| bebf480321 | |||
| 10c09d9def | |||
| 6ce6b96e5c | |||
| 16a9cae80e | |||
| e865e2ae6d | |||
| 06363a43f9 | |||
| 09a88e6a1c | |||
| 28baef8929 | |||
| 9b061a4c7c | |||
| 0b542dfbdf | |||
| d3b039ebd8 | |||
| c22924eed7 | |||
| 033ccb121f | |||
| ecd91e8f2a | |||
| 1cdb954967 | |||
| f309f87f55 | |||
| 989ab3094d | |||
| 70ac18d139 | |||
| 8f43236e2e | |||
| 38416d46a6 | |||
| fb96b00922 | |||
| 0601ea2f39 | |||
| a92d4f2af7 | |||
| 658305c969 | |||
| 8aed2be19b | |||
| 263f6e49d8 | |||
| d822e38016 | |||
| 6cf78f19bb | |||
| 4a3319406c | |||
| cb4418b973 | |||
| 5a797124b3 | |||
| 63430fbce6 | |||
| bc2085139e | |||
| f98c710f5b | |||
| eb7101deff | |||
| 826f50da7e | |||
| 4e94c8ea56 | |||
| 3120eca721 | |||
| 26c9bcbc28 | |||
| 7a2e73a5d6 | |||
| cd35ab5932 | |||
| efaacdb534 | |||
| 5eb3c208f1 | |||
| 8347b62c1b | |||
| 48c3c7b4dc | |||
| faefe078af | |||
| 66eb0481b5 | |||
| 0a0fc130d4 | |||
| 44afd7b657 | |||
| 165b572a5f | |||
| 1a30e622cc | |||
| aec3238e42 | |||
| 249868dba7 | |||
| f82e714b3c | |||
| baa7eae77c | |||
| c69542a34d | |||
| 9d45892603 | |||
| e65f247b99 | |||
| 600f061e47 | |||
| 8ac0e9e751 | |||
| 772787fc22 | |||
| 985b33b65b | |||
| ef7c5c2d2b | |||
| 0d49aafb54 | |||
| 98aae5ddc6 | |||
| 070e8606fa | |||
| 511b2848c3 | |||
| 7dd96d9a75 | |||
| 7d80f69ee8 | |||
| 85491cb7b5 | |||
| 3c0b88a1ee | |||
| 33c072f544 | |||
| a8d08bca3f | |||
| d7ddc56ab3 | |||
| b562cd5c73 | |||
| 2549a41eb3 | |||
| 464f0fc231 | |||
| 5914fd9fb7 | |||
| 2e03217551 | |||
| e0dc974f87 | |||
| fdf1ed829d | |||
| ba90490ad9 | |||
| 910be97f54 | |||
| e162582045 | |||
| cfd197d26c | |||
| aa0486bc2b | |||
| 97a1fc62ae | |||
| fd9dcd065a | |||
| 59997560eb | |||
| 026e71cc6e | |||
| 98ecc24425 | |||
| 3886006343 | |||
| 6d539c9203 | |||
| 5f778e61dd | |||
| 9860489f05 | |||
| 21ca8ac883 | |||
| ec93becb17 | |||
| 0319445888 | |||
| 7cba9f50c8 | |||
| 9483c3afbc | |||
| e518976534 | |||
| 3626cc2394 | |||
| 3a61fc7181 | |||
| 4a95fa5e87 | |||
| 8e3d1422f3 | |||
| 640a0b2627 | |||
| 30e0cb6515 | |||
| e3253aacdb | |||
| 32f49d2122 | |||
| b4bef44135 | |||
| ce38742caf | |||
| a7d39cc8d4 | |||
| cb7eb660b9 | |||
| bad28c60ae | |||
| ab4c04085c | |||
| 761002f39d | |||
| 996f9c7f5d | |||
| a6eca44a0d | |||
| 1820751801 | |||
| 030faaa5d1 | |||
| 95e1947352 | |||
| 7deb11a0a6 | |||
| 41c597801f | |||
| 128a138e74 | |||
| ca74b5740a | |||
| 9afcbe1565 | |||
| 9e531a05e1 | |||
| 7c3562cea2 | |||
| 1edddf79d2 | |||
| 114f03e434 | |||
| 3ee1487985 | |||
| a9eda2176e | |||
| bb90bafb62 | |||
| cea8783fec | |||
| 789d1fef84 | |||
| d83939b165 | |||
| 3a4ec5c86a | |||
| 013c14530b | |||
| ebf1cfc113 | |||
| ec4d04c338 | |||
| 584b7790e4 | |||
| ef25f66107 | |||
| 3060b34bdd | |||
| 3fb5c682f8 | |||
| bb5dfa13ee | |||
| 9e391941c5 | |||
| 8b1d3e5fba | |||
| 07e322df96 | |||
| a8959cbf26 | |||
| d793e5bae5 | |||
| 29954fa9e8 | |||
| 0384fa9a51 | |||
| 75b19d3883 | |||
| c15f84da08 | |||
| 8539d4caf1 | |||
| 3eb1fe5e4b | |||
| 16b88b697e | |||
| 08ba6ac831 | |||
| 49710618ff | |||
| b4ba001617 | |||
| 87e0876cce | |||
| 87f5e3f102 | |||
| e921d0db6e | |||
| 2e93bc2e1d | |||
| 5ac15c8c49 | |||
| 98c05f3614 | |||
| 05af56defc | |||
| ce48a2fc12 | |||
| 01e910af79 | |||
| 3e2ce9e94c | |||
| 9db602b274 | |||
| a2d0ac7ee3 | |||
| 62bd3f6e83 | |||
| 20ac2ff6e7 | |||
| aa7c9e06a4 | |||
| 0c2fb7c0d9 | |||
| 72cc318607 | |||
| b533d325a4 | |||
| 9dad7ff563 | |||
| b389d30728 | |||
| 606885b23c | |||
| bc7b8aadc4 | |||
| d136b2065f | |||
| 3b2683463d | |||
| 989730d402 | |||
| 50f7209ba2 | |||
| 44b728c660 | |||
| 9abc5bbf96 | |||
| 56dd936e9c | |||
| e982281cd4 | |||
| a6b7b5fa94 | |||
| ef00114aab | |||
| ba4edc5c0e | |||
| dae2d81764 | |||
| cee9cd14c0 | |||
| f1ec110673 | |||
| 7104a3b738 | |||
| 114951b18c | |||
| 3c85a602a4 | |||
| a6415b8689 | |||
| b37670de84 | |||
| c9053bb0bc | |||
| 5362102be6 | |||
| bf4601470b | |||
| bd6274282b | |||
| 331b4d8524 | |||
| 1e19f68cb5 | |||
| c286b491d6 | |||
| c7acdbf20d | |||
| 3cd0cc01c4 | |||
| 87d109727a | |||
| 47b6819ec8 | |||
| 00ee89a693 | |||
| ac14b08af4 | |||
| db97d7e836 | |||
| 5a0c80611e | |||
| 4e872865a3 | |||
| aea39a83b6 | |||
| b9a3c508c9 | |||
| 9ae49e7169 | |||
| 7a1cdd62a4 | |||
| a242881101 | |||
| 44ca59ac70 | |||
| 398dfce698 | |||
| 0ebe6bde3d | |||
| 15f686fc69 | |||
| 6a0b8e0722 | |||
| bb040eb5a8 | |||
| 51a0ad70aa | |||
| 71faa5f89e | |||
| 6cf0554e23 | |||
| fe0c1745e1 | |||
| 2cdf73adab | |||
| 31125dedc8 | |||
| a2e2d70660 | |||
| 7fe1b02ccd | |||
| 04e27496bd | |||
| 51e2e5ec9c | |||
| cd9602e641 | |||
| 8303991217 | |||
| 12eae2c002 | |||
| 99489e5e77 | |||
| 5c9ff468cc | |||
| bf18307168 | |||
| b3fa76f8c5 | |||
| 8a77242072 | |||
| c453df55d6 | |||
| 75d69050d5 | |||
| 390285d9e5 | |||
| 8ef15df7c0 | |||
| 109f9567ea | |||
| 748eadd225 | |||
| b8e115ddf6 | |||
| 11d4df4f7d | |||
| 0c285f21c1 | |||
| c9bf017637 | |||
| 0d78150f10 | |||
| f36946a8aa | |||
| 5c51619798 | |||
| a022bdb30d | |||
| 2cfb91d0ce | |||
| 5885d76b89 | |||
| 5cb1a2d120 | |||
| 53fa339363 | |||
| 5f0bb0c6ce | |||
| 3dec6ac9f1 | |||
| 4c9ec582dc | |||
| 28b000c820 | |||
| 30320e0ac6 | |||
| 88b682a317 | |||
| 6c5a5c0882 | |||
| 1d27fffe44 | |||
| a3383b1f98 | |||
| f9c2b0acd1 | |||
| a9444ed879 | |||
| 8d5a3ecd69 | |||
| 44ff676eef | |||
| 4bb017b740 | |||
| 0f2435c308 | |||
| 0cd56f4d4c | |||
| e328ec2382 | |||
| 4b5ac67993 | |||
| cb73218dfe | |||
| 5523c2d34a | |||
| 01889c45a2 | |||
| e89b4a151e | |||
| ec235eafe8 | |||
| d99720258a | |||
| 3f064322e4 | |||
| 11592279e2 | |||
| 31b4923eb2 | |||
| cfdfb9a907 | |||
| e7a21c821e | |||
| 255422d5be | |||
| 02b4990cb1 | |||
| 11bf39374c | |||
| 04cf382de5 | |||
| 38884bc0e6 | |||
| a4d0394d1a | |||
| 8292e78ef2 | |||
| 6243404d1d | |||
| 6fe67c93fe | |||
| 422b65d934 | |||
| 2fa3a3c47e | |||
| 27e4810239 | |||
| cbdae3547b | |||
| a5d122c0b3 | |||
| 47b662be09 | |||
| 1ee09825a0 | |||
| 0a679da968 | |||
| 59d174004e | |||
| d0d0d95475 | |||
| b08a6840f5 | |||
| 77ada9c151 | |||
| 222e6b6611 | |||
| 70c93c7be7 | |||
| b73fc70ecf | |||
| eab33150ad | |||
| 21c16d2009 | |||
| 56413ecce6 | |||
| e94f2a95de | |||
| c2a43b69a9 | |||
| c90e0fd21e | |||
| 6744621415 | |||
| a6680a775f | |||
| 5a67be2292 | |||
| c8b2b34138 | |||
| af8f4676ba | |||
| b51cb9d84a | |||
| ec7a61021f | |||
| 8d0d19132e | |||
| d2bde5f0b1 | |||
| 3f9ae5d6bf | |||
| 9b97e26b58 | |||
| 219032bbbb | |||
| f0fd4ea45c | |||
| 23a5a275f8 | |||
| 3a1bfa91d1 | |||
| a4f77dfcd0 | |||
| 795ba3e365 | |||
| 83ef4234bc | |||
| b12b464462 | |||
| 04726ba697 | |||
| 4d607ada9d | |||
| 2c7cf9faa1 | |||
| 7efa2fd072 | |||
| 9adf5167c9 | |||
| 4978984d75 | |||
| 3b7ef4615a | |||
| af8f4b64c0 | |||
| 93042d862d | |||
| ba5424c250 | |||
| afdde9b032 | |||
| a033480500 | |||
| 14333e2910 | |||
| 20df96b6ba | |||
| a7729e1597 | |||
| 29c3233375 | |||
| d0eab70974 | |||
| f3cbb91527 | |||
| 9772cfe1f2 | |||
| 3557e8a125 | |||
| 9e0bb6ca34 | |||
| bb74eef601 | |||
| d1db38ba8e | |||
| e9af9fb16b | |||
| fd74be8848 | |||
| 926fafd7f6 | |||
| c51c715cee | |||
| aa80210075 | |||
| 768654ae63 | |||
| 6efb291449 | |||
| 7a431b9b83 | |||
| c78d09df66 | |||
| 7d30d9e867 | |||
| d3fb244cef |
+4
-3
@@ -1,6 +1,7 @@
|
||||
# Skip files when using git archive
|
||||
# following files are skipped when exporting using git archive
|
||||
/release export-ignore
|
||||
/admin export-ignore
|
||||
test export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
/scripts export-ignore
|
||||
test export-ignore
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ coverage/
|
||||
docs/
|
||||
webadmin/dist/
|
||||
setup/splash/website/
|
||||
installer/src/certs/server.key
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
[0.0.1]
|
||||
- Hot Chocolate
|
||||
|
||||
[0.0.2]
|
||||
- Hotfix appstore ui in webadim
|
||||
|
||||
[0.0.3]
|
||||
- Tall Pike
|
||||
|
||||
[0.0.4]
|
||||
- This will be 0.0.4 changes
|
||||
|
||||
[0.0.5]
|
||||
- App install/configure route fixes
|
||||
|
||||
[0.0.6]
|
||||
- Not sure what happenned here
|
||||
|
||||
[0.0.7]
|
||||
- resetToken is now sent as part of create user
|
||||
- Same as 0.0.7 which got released by mistake
|
||||
|
||||
[0.0.8]
|
||||
- Manifest changes
|
||||
|
||||
[0.0.9]
|
||||
- Fix app restore
|
||||
- Fix backup issues
|
||||
|
||||
[0.0.10]
|
||||
- Unknown orchestra
|
||||
|
||||
[0.0.11]
|
||||
- Add ldap addon
|
||||
|
||||
[0.0.12]
|
||||
- Support OAuth2 state
|
||||
|
||||
[0.0.13]
|
||||
- Use docker image from cloudron repository
|
||||
|
||||
[0.0.14]
|
||||
- Improve setup flow
|
||||
|
||||
[0.0.15]
|
||||
- Improved Appstore view
|
||||
|
||||
[0.0.16]
|
||||
- Improved Backup approach
|
||||
|
||||
[0.0.17]
|
||||
- Upgrade testing
|
||||
- App auto updates
|
||||
- Usage graphs
|
||||
|
||||
[0.0.18]
|
||||
- Rework backups and updates
|
||||
|
||||
[0.0.19]
|
||||
- Graphite fixes
|
||||
- Avatar and Cloudron name support
|
||||
|
||||
[0.0.20]
|
||||
- Apptask fixes
|
||||
- Chrome related fixes
|
||||
|
||||
[0.0.21]
|
||||
- Increase nginx hostname size to 64
|
||||
|
||||
[0.0.22]
|
||||
- Testing the e2e tests
|
||||
|
||||
[0.0.23]
|
||||
- Better error status page
|
||||
- Fix updater and backup progress reporting
|
||||
- New avatar set
|
||||
- Improved setup wizard
|
||||
|
||||
[0.0.24]
|
||||
- Hotfix the ldap support
|
||||
|
||||
[0.0.25]
|
||||
- Add support page
|
||||
- Really fix ldap issues
|
||||
|
||||
[0.0.26]
|
||||
- Add configurePath support
|
||||
|
||||
[0.0.27]
|
||||
- Improved log collector
|
||||
|
||||
[0.0.28]
|
||||
- Improve app feedback
|
||||
- Restyle login page
|
||||
|
||||
[0.0.29]
|
||||
- Update to ubuntu 15.04
|
||||
|
||||
[0.0.30]
|
||||
- Move to docker 1.7
|
||||
|
||||
[0.0.31]
|
||||
- WARNING: This update restarts your containers
|
||||
- System processes are prioritized over apps
|
||||
- Add ldap group support
|
||||
|
||||
[0.0.32]
|
||||
- MySQL addon update
|
||||
|
||||
[0.0.33]
|
||||
- Fix graphs
|
||||
- Fix MySQL 5.6 memory usage
|
||||
|
||||
[0.0.34]
|
||||
- Correctly mark apps pending for approval
|
||||
|
||||
[0.0.35]
|
||||
- Fix ldap admin group username
|
||||
|
||||
[0.0.36]
|
||||
- Fix restore without backup
|
||||
- Optimize image deletion during updates
|
||||
- Add memory accounting
|
||||
- Restrict access to metadata from containers
|
||||
|
||||
[0.0.37]
|
||||
- Prepare for Selfhosting 1. part
|
||||
- Use userData instead of provisioning calls
|
||||
|
||||
[0.0.38]
|
||||
- Account for Ext4 reserved block when partitioning disk
|
||||
|
||||
[0.0.39]
|
||||
- Move subdomain management to the cloudron
|
||||
|
||||
[0.0.40]
|
||||
- Add journal limit
|
||||
- Fix reprovisioning on reboot
|
||||
- Fix subdomain management during startup
|
||||
|
||||
[0.0.41]
|
||||
- Finally bring things to a sane state
|
||||
|
||||
[0.0.42]
|
||||
- Parallel apptask
|
||||
|
||||
[0.0.43]
|
||||
- Move to systemd
|
||||
|
||||
[0.0.44]
|
||||
- Fix apptask concurrency bug
|
||||
|
||||
[0.0.45]
|
||||
- Retry subdomain registration
|
||||
|
||||
[0.0.46]
|
||||
- Fix app update email notification
|
||||
|
||||
[0.0.47]
|
||||
- Ensure box code quits within 5 seconds
|
||||
|
||||
[0.0.48]
|
||||
- Styling fixes
|
||||
- Improved session handling
|
||||
|
||||
[0.0.49]
|
||||
- Fix app autoupdate logic
|
||||
|
||||
[0.0.50]
|
||||
- Use domainmanagement via CaaS
|
||||
|
||||
[0.0.51]
|
||||
- Fix memory management
|
||||
|
||||
[0.0.52]
|
||||
- Restrict addons memory
|
||||
- Get nofication about container OOMs
|
||||
|
||||
[0.0.53]
|
||||
- Restrict addons memory
|
||||
- Get notification about container OOMs
|
||||
- Add retry to subdomain logic
|
||||
|
||||
[0.0.54]
|
||||
- OAuth Proxy now uses internal port forwarding
|
||||
|
||||
[0.0.55]
|
||||
- Setup cloudron timezone based on droplet region
|
||||
|
||||
[0.0.56]
|
||||
- Use correct timezone in updater
|
||||
|
||||
[0.0.57]
|
||||
- Fix systemd logging issues
|
||||
|
||||
[0.0.58]
|
||||
- Ensure backups of failed apps are retained across archival cycles
|
||||
|
||||
[0.0.59]
|
||||
- Installer API fixes
|
||||
|
||||
[0.0.60]
|
||||
- Do full box backup on updates
|
||||
|
||||
[0.0.61]
|
||||
- Track update notifications to inform admin only once
|
||||
|
||||
[0.0.62]
|
||||
- Export bind dn and password from LDAP addon
|
||||
|
||||
[0.0.63]
|
||||
- Fix creation of TXT records
|
||||
|
||||
[0.0.64]
|
||||
- Stop apps in a retired cloudron
|
||||
- Retry downloading application on failure
|
||||
|
||||
[0.0.65]
|
||||
- Do not send crash mails for apps in development
|
||||
|
||||
[0.0.66]
|
||||
- Readonly application and addon containers
|
||||
|
||||
[0.0.67]
|
||||
- Fix email notifications
|
||||
- Fix bug when restoring from certain backups
|
||||
|
||||
[0.0.68]
|
||||
- Update graphite image
|
||||
- Add simpleauth addon support
|
||||
|
||||
[0.0.69]
|
||||
- Support newer manifest format
|
||||
- Fix app listing rendering in chrome
|
||||
- Fix redis backup across upgrades
|
||||
|
||||
[0.0.70]
|
||||
- Retry app download on error
|
||||
|
||||
[0.0.71]
|
||||
- Fix oauth and simple auth login
|
||||
|
||||
[0.0.72]
|
||||
- Cleanup application volumes periodically
|
||||
- New application logging design
|
||||
|
||||
[0.0.73]
|
||||
- Update SSL certificate
|
||||
|
||||
[0.0.74]
|
||||
- Support singleUser apps
|
||||
|
||||
[0.0.75]
|
||||
- scheduler addon
|
||||
|
||||
[0.0.76]
|
||||
- DNS Sync fixes
|
||||
- Show warning to user when memory limit reached
|
||||
|
||||
[0.0.77]
|
||||
- Do not set hostname in app containers
|
||||
|
||||
[0.0.78]
|
||||
- Support custom domains
|
||||
|
||||
[0.0.79]
|
||||
- Move SSH Port
|
||||
|
||||
[0.0.80]
|
||||
- Use journalctl for container logs
|
||||
|
||||
[0.1.0]
|
||||
- Wait for configuration changes before starting Cloudron
|
||||
|
||||
[0.1.1]
|
||||
- Ensure dns config for all cloudrons
|
||||
|
||||
[0.1.2]
|
||||
- Make email work again
|
||||
- Add DKIM keys for custom domains
|
||||
|
||||
[0.1.3]
|
||||
- Storage backend
|
||||
|
||||
[0.1.4]
|
||||
- CaaS Backup configuration fix
|
||||
|
||||
[0.1.5]
|
||||
- Use correct tokens for DNS backend
|
||||
|
||||
[0.1.6]
|
||||
- Add hook to determine the api server of the box
|
||||
- Fix crash notification
|
||||
|
||||
[0.2.0]
|
||||
- New cloudron exec implementation
|
||||
|
||||
[0.2.1]
|
||||
- Update to node 4.1.1
|
||||
- Fix certification installation with custom domains
|
||||
|
||||
[0.2.2]
|
||||
- Better debug output
|
||||
- Retry more times if docker registry goes down
|
||||
|
||||
[0.3.0]
|
||||
- Update SSH keys
|
||||
- Allow bigger manifest files
|
||||
|
||||
[0.4.0]
|
||||
- Update to docker 1.9.0
|
||||
|
||||
[0.4.1]
|
||||
- Fix scheduler crash
|
||||
- Crucial OAuth fixes
|
||||
|
||||
[0.4.2]
|
||||
- Fix crash when reporting backup error
|
||||
- Allow larger manifests
|
||||
|
||||
[0.4.3]
|
||||
- Fix cloudron exec
|
||||
|
||||
[0.4.4]
|
||||
- Initial Lets Encrypt integration
|
||||
|
||||
[0.4.5]
|
||||
- Fixup nginx configuration to allow dynamic certificates
|
||||
|
||||
[0.4.6]
|
||||
- LetsEncrypt integration for custom domains
|
||||
- Rate limit crash emails
|
||||
|
||||
[0.5.0]
|
||||
- Enable staging Lets Encrypt Integration
|
||||
|
||||
[0.5.1]
|
||||
- Display error dialog for app installation errors
|
||||
- Enable prod Lets Encrypt Integration
|
||||
- Handle apptask crashes correctly
|
||||
|
||||
[0.5.2]
|
||||
- Fix apphealthtask crash
|
||||
- Use cgroup fs driver instead of systemd cgroup driver in docker
|
||||
|
||||
[0.5.3]
|
||||
- Changes for e2e testing
|
||||
|
||||
[0.5.4]
|
||||
- Fix bug in LE server selection
|
||||
|
||||
[0.5.5]
|
||||
- Scheduler redesign
|
||||
- Fix journalctl logging
|
||||
|
||||
[0.5.6]
|
||||
- Prepare for selfhosting option
|
||||
|
||||
[0.5.7]
|
||||
- Move app images off the btrfs subvolume
|
||||
|
||||
[0.6.0]
|
||||
- Consolidate code repositories
|
||||
|
||||
[0.6.1]
|
||||
- Use no-reply as email from address for apps in naked domains
|
||||
- Update Lets Encrypt account with owner email when available
|
||||
- Fix email templates to indicate auto update
|
||||
- Add notification UI
|
||||
|
||||
[0.6.2]
|
||||
- Fix `cloudron exec` container to have same namespaces as app
|
||||
- Add developmentMode to manifest
|
||||
|
||||
[0.6.3]
|
||||
- Make sending invite for new users optional
|
||||
|
||||
[0.6.4]
|
||||
- Add support for display names
|
||||
- Send invite links to admins for user setup
|
||||
- Enforce stronger passwords
|
||||
|
||||
[0.6.5]
|
||||
- Finalize stronger password requirement
|
||||
|
||||
[0.7.0]
|
||||
- Upgrade to 15.10
|
||||
- Do not remove docker images when in use by another container
|
||||
- Fix sporadic error when reconfiguring apps
|
||||
- Handle journald crashes gracefully
|
||||
|
||||
[0.7.1]
|
||||
- Allow admins to edit users
|
||||
- Fix graphs
|
||||
- Support more LDAP cases
|
||||
- Allow appstore deep linking
|
||||
|
||||
[0.7.2]
|
||||
- Fix 5xx errors when password does not meet requirements
|
||||
- Improved box update management using prereleases
|
||||
- Less aggressive disk space checks
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
The Box
|
||||
=======
|
||||
Cloudron a Smart Server
|
||||
=======================
|
||||
|
||||
Development setup
|
||||
-----------------
|
||||
* sudo useradd -m yellowtent
|
||||
** Add admin-localhost as 127.0.0.1 in /etc/hosts
|
||||
** All apps will be installed as hypened-subdomains of localhost. You should add
|
||||
hyphened-subdomains of your apps into /etc/hosts
|
||||
|
||||
|
||||
Selfhost Instructions
|
||||
---------------------
|
||||
|
||||
The smart server currently relies on an AWS account with access to Route53 and S3 and is tested on DigitalOcean and EC2.
|
||||
|
||||
First create a virtual private server with Ubuntu 15.04 and run the following commands in an ssh session to initialize the base image:
|
||||
|
||||
```
|
||||
curl https://s3.amazonaws.com/prod-cloudron-releases/installer.sh -o installer.sh
|
||||
chmod +x installer.sh
|
||||
./installer.sh <domain> <aws access key> <aws acccess secret> <backup bucket> <provider> <release sha1>
|
||||
```
|
||||
|
||||
Executable
+186
@@ -0,0 +1,186 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
provider="digitalocean"
|
||||
installer_revision=$(git rev-parse HEAD)
|
||||
box_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="dev"
|
||||
|
||||
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
|
||||
# brew install gnu-getopt to get the GNU getopt on OS X
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "provider:,revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) installer_revision="$2"; shift 2;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--name) box_name="$2"; destroy_server="no"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Creating image using ${provider}"
|
||||
if [[ "${provider}" == "digitalocean" ]]; then
|
||||
if [[ "${deploy_env}" == "staging" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
|
||||
elif [[ "${deploy_env}" == "dev" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
|
||||
elif [[ "${deploy_env}" == "prod" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
|
||||
else
|
||||
echo "No such env ${deploy_env}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
|
||||
else
|
||||
echo "Unknown provider : ${provider}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
|
||||
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${installer_revision}")
|
||||
|
||||
if [[ -z "${box_name}" ]]; then
|
||||
# if you change this, change the regexp is appstore/janitor.js
|
||||
box_name="box-${deploy_env}-${pretty_revision}-${now}" # remove slashes
|
||||
|
||||
# create a new server if no name given
|
||||
if ! caas_ssh_key_id=$($vps get_ssh_key_id "caas"); then
|
||||
echo "Could not query caas ssh key"
|
||||
exit 1
|
||||
fi
|
||||
echo "Detected caas ssh key id: ${caas_ssh_key_id}"
|
||||
|
||||
echo "Creating Server with name [${box_name}]"
|
||||
if ! server_id=$($vps create ${caas_ssh_key_id} ${box_name}); then
|
||||
echo "Failed to create server"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created server with id: ${server_id}"
|
||||
|
||||
# If we run scripts overenthusiastically without the wait, setup script randomly fails
|
||||
echo -n "Waiting 120 seconds for server creation"
|
||||
for i in $(seq 1 24); do
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
echo ""
|
||||
else
|
||||
if ! server_id=$($vps get_id "${box_name}"); then
|
||||
echo "Could not determine id from name"
|
||||
exit 1
|
||||
fi
|
||||
echo "Reusing server with id: ${server_id}"
|
||||
|
||||
$vps power_on "${server_id}"
|
||||
fi
|
||||
|
||||
# Query until we get an IP
|
||||
while true; do
|
||||
echo "Trying to get the server IP"
|
||||
if server_ip=$($vps get_ip "${server_id}"); then
|
||||
echo "Server IP : [${server_ip}]"
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 10 seconds"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
while true; do
|
||||
echo "Trying to copy init script to server"
|
||||
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" root@${server_ip}:.; then
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 30 seconds"
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "Copying INFRA_VERSION"
|
||||
$scp22 "${SCRIPT_DIR}/../setup/INFRA_VERSION" root@${server_ip}:.
|
||||
|
||||
echo "Copying box source"
|
||||
cd "${SOURCE_DIR}"
|
||||
git archive --format=tar HEAD | $ssh22 "root@${server_ip}" "cat - > /tmp/box.tar.gz"
|
||||
|
||||
echo "Executing init script"
|
||||
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh ${installer_revision}"; then
|
||||
echo "Init script failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shutting down server with id : ${server_id}"
|
||||
$ssh202 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
|
||||
|
||||
# wait 10 secs for actual shutdown
|
||||
echo "Waiting for 10 seconds for server to shutdown"
|
||||
sleep 30
|
||||
|
||||
echo "Powering off server"
|
||||
if ! $vps power_off "${server_id}"; then
|
||||
echo "Could not power off server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
snapshot_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
echo "Snapshotting as ${snapshot_name}"
|
||||
if ! image_id=$($vps snapshot "${server_id}" "${snapshot_name}"); then
|
||||
echo "Could not snapshot and get image id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "Destroying server"
|
||||
if ! $vps destroy "${server_id}"; then
|
||||
echo "Could not destroy server"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Skipping server destroy"
|
||||
fi
|
||||
|
||||
echo "Transferring image ${image_id} to other regions"
|
||||
$vps transfer_image_to_all_regions "${image_id}"
|
||||
|
||||
echo "Done."
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ -z "${DIGITAL_OCEAN_TOKEN}" ]]; then
|
||||
echo "Script requires DIGITAL_OCEAN_TOKEN env to be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${JSON}" ]]; then
|
||||
echo "Script requires JSON env to be set to path of JSON binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly CURL="curl -s -u ${DIGITAL_OCEAN_TOKEN}:"
|
||||
|
||||
function debug() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
function get_ssh_key_id() {
|
||||
id=$($CURL "https://api.digitalocean.com/v2/account/keys" \
|
||||
| $JSON ssh_keys \
|
||||
| $JSON -c "this.name === \"$1\"" \
|
||||
| $JSON 0.id)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function create_droplet() {
|
||||
local ssh_key_id="$1"
|
||||
local box_name="$2"
|
||||
|
||||
local image_region="sfo1"
|
||||
local ubuntu_image_slug="ubuntu-15-10-x64"
|
||||
local box_size="512mb"
|
||||
|
||||
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
|
||||
|
||||
id=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets" | $JSON droplet.id)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function get_droplet_ip() {
|
||||
local droplet_id="$1"
|
||||
ip=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}" | $JSON "droplet.networks.v4[0].ip_address")
|
||||
[[ -z "$ip" ]] && exit 1
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
function get_droplet_id() {
|
||||
local droplet_name="$1"
|
||||
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=100" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function power_off_droplet() {
|
||||
local droplet_id="$1"
|
||||
local data='{"type":"power_off"}'
|
||||
local response=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions")
|
||||
local event_id=`echo "${response}" | $JSON action.id`
|
||||
|
||||
if [[ -z "${event_id}" ]]; then
|
||||
debug "Got no event id, assuming already powered off."
|
||||
debug "Response: ${response}"
|
||||
return
|
||||
fi
|
||||
|
||||
debug "Powered off droplet. Event id: ${event_id}"
|
||||
debug -n "Waiting for droplet to power off"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function power_on_droplet() {
|
||||
local droplet_id="$1"
|
||||
local data='{"type":"power_on"}'
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||
|
||||
debug "Powered on droplet. Event id: ${event_id}"
|
||||
|
||||
if [[ -z "${event_id}" ]]; then
|
||||
debug "Got no event id, assuming already powered on"
|
||||
return
|
||||
fi
|
||||
|
||||
debug -n "Waiting for droplet to power on"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function get_image_id() {
|
||||
local snapshot_name="$1"
|
||||
local image_id=""
|
||||
|
||||
image_id=$($CURL "https://api.digitalocean.com/v2/images?per_page=100" \
|
||||
| $JSON images \
|
||||
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id)
|
||||
|
||||
if [[ -n "${image_id}" ]]; then
|
||||
echo "${image_id}"
|
||||
fi
|
||||
}
|
||||
|
||||
function snapshot_droplet() {
|
||||
local droplet_id="$1"
|
||||
local snapshot_name="$2"
|
||||
local data="{\"type\":\"snapshot\",\"name\":\"${snapshot_name}\"}"
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||
|
||||
debug "Droplet snapshotted as ${snapshot_name}. Event id: ${event_id}"
|
||||
debug -n "Waiting for snapshot to complete"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
|
||||
get_image_id "${snapshot_name}"
|
||||
}
|
||||
|
||||
function destroy_droplet() {
|
||||
local droplet_id="$1"
|
||||
# TODO: check for 204 status
|
||||
$CURL -X DELETE "https://api.digitalocean.com/v2/droplets/${droplet_id}"
|
||||
debug "Droplet destroyed"
|
||||
debug ""
|
||||
}
|
||||
|
||||
function transfer_image() {
|
||||
local image_id="$1"
|
||||
local region_slug="$2"
|
||||
local data="{\"type\":\"transfer\",\"region\":\"${region_slug}\"}"
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/images/${image_id}/actions" | $JSON action.id`
|
||||
echo "${event_id}"
|
||||
}
|
||||
|
||||
function wait_for_image_event() {
|
||||
local image_id="$1"
|
||||
local event_id="$2"
|
||||
|
||||
debug -n "Waiting for ${event_id}"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/images/${image_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function transfer_image_to_all_regions() {
|
||||
local image_id="$1"
|
||||
|
||||
xfer_events=()
|
||||
image_regions=(ams3) ## sfo1 is where the image is created
|
||||
for image_region in ${image_regions[@]}; do
|
||||
xfer_event=$(transfer_image ${image_id} ${image_region})
|
||||
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
|
||||
xfer_events+=("${xfer_event}")
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Image transfer initiated, but they will take some time to get transferred."
|
||||
|
||||
for xfer_event in ${xfer_events[@]}; do
|
||||
$vps wait_for_image_event "${image_id}" "${xfer_event}"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
debug "<command> <params...>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
get_ssh_key_id)
|
||||
get_ssh_key_id "${@:2}"
|
||||
;;
|
||||
|
||||
create)
|
||||
create_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
get_id)
|
||||
get_droplet_id "${@:2}"
|
||||
;;
|
||||
|
||||
get_ip)
|
||||
get_droplet_ip "${@:2}"
|
||||
;;
|
||||
|
||||
power_on)
|
||||
power_on_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
power_off)
|
||||
power_off_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
snapshot)
|
||||
snapshot_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
destroy)
|
||||
destroy_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
transfer_image_to_all_regions)
|
||||
transfer_image_to_all_regions "${@:2}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command $1"
|
||||
exit 1
|
||||
esac
|
||||
@@ -0,0 +1,325 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euv -o pipefail
|
||||
|
||||
readonly USER=yellowtent
|
||||
readonly USER_HOME="/home/${USER}"
|
||||
readonly INSTALLER_SOURCE_DIR="${USER_HOME}/installer"
|
||||
readonly INSTALLER_REVISION="$1"
|
||||
readonly SELFHOSTED=$(( $# > 1 ? 1 : 0 ))
|
||||
readonly USER_DATA_FILE="/root/user_data.img"
|
||||
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
function die {
|
||||
echo $1
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
|
||||
|
||||
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
|
||||
source "${SOURCE_DIR}/INFRA_VERSION"
|
||||
else
|
||||
echo "No INFRA_VERSION found, skip pulling docker images"
|
||||
fi
|
||||
|
||||
if [ ${SELFHOSTED} == 0 ]; then
|
||||
echo "!! Initializing Ubuntu image for CaaS"
|
||||
else
|
||||
echo "!! Initializing Ubuntu image for Selfhosting"
|
||||
fi
|
||||
|
||||
echo "==== Create User ${USER} ===="
|
||||
if ! id "${USER}"; then
|
||||
useradd "${USER}" -m
|
||||
fi
|
||||
|
||||
echo "=== Yellowtent base image preparation (installer revision - ${INSTALLER_REVISION}) ==="
|
||||
|
||||
echo "=== Prepare installer source ==="
|
||||
rm -rf "${INSTALLER_SOURCE_DIR}" && mkdir -p "${INSTALLER_SOURCE_DIR}"
|
||||
rm -rf /tmp/box && mkdir -p /tmp/box
|
||||
tar xvf /tmp/box.tar.gz -C /tmp/box && rm /tmp/box.tar.gz
|
||||
cp -rf /tmp/box/installer/* "${INSTALLER_SOURCE_DIR}"
|
||||
echo "${INSTALLER_REVISION}" > "${INSTALLER_SOURCE_DIR}/REVISION"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "=== Upgrade ==="
|
||||
apt-get update
|
||||
apt-get upgrade -y
|
||||
apt-get install -y curl
|
||||
|
||||
# Setup firewall before everything. docker creates it's own chain and the -X below will remove it
|
||||
# Do NOT use iptables-persistent because it's startup ordering conflicts with docker
|
||||
echo "=== Setting up firewall ==="
|
||||
# clear tables and set default policy
|
||||
iptables -F # flush all chains
|
||||
iptables -X # delete all chains
|
||||
# default policy for filter table
|
||||
iptables -P INPUT DROP
|
||||
iptables -P FORWARD ACCEPT # TODO: disable icc and make this as reject
|
||||
iptables -P OUTPUT ACCEPT
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
if [ ${SELFHOSTED} == 0 ]; then
|
||||
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,202,443,886 -j ACCEPT
|
||||
else
|
||||
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,22,443,886 -j ACCEPT
|
||||
fi
|
||||
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
iptables -A INPUT -p udp --sport 53 -j ACCEPT
|
||||
iptables -A INPUT -s 172.17.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
|
||||
|
||||
# loopback
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
|
||||
# prevent DoS
|
||||
# iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
|
||||
|
||||
# log dropped incoming. keep this at the end of all the rules
|
||||
iptables -N LOGGING # new chain
|
||||
iptables -A INPUT -j LOGGING # last rule in INPUT chain
|
||||
iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -A LOGGING -j DROP
|
||||
|
||||
echo "==== Install btrfs tools ==="
|
||||
apt-get -y install btrfs-tools
|
||||
|
||||
echo "==== Install docker ===="
|
||||
# install docker from binary to pin it to a specific version. the current debian repo does not allow pinning
|
||||
curl https://get.docker.com/builds/Linux/x86_64/docker-1.9.1 > /usr/bin/docker
|
||||
chmod +x /usr/bin/docker
|
||||
groupadd docker
|
||||
cat > /etc/systemd/system/docker.socket <<EOF
|
||||
[Unit]
|
||||
Description=Docker Socket for the API
|
||||
PartOf=docker.service
|
||||
|
||||
[Socket]
|
||||
ListenStream=/var/run/docker.sock
|
||||
SocketMode=0660
|
||||
SocketUser=root
|
||||
SocketGroup=docker
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
EOF
|
||||
cat > /etc/systemd/system/docker.service <<EOF
|
||||
[Unit]
|
||||
Description=Docker Application Container Engine
|
||||
After=network.target docker.socket
|
||||
Requires=docker.socket
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs
|
||||
MountFlags=slave
|
||||
LimitNOFILE=1048576
|
||||
LimitNPROC=1048576
|
||||
LimitCORE=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "=== Setup btrfs data ==="
|
||||
fallocate -l "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
|
||||
mkfs.btrfs -L UserHome "${USER_DATA_FILE}"
|
||||
echo "${USER_DATA_FILE} ${USER_DATA_DIR} btrfs loop,nosuid 0 0" >> /etc/fstab
|
||||
mkdir -p "${USER_DATA_DIR}" && mount "${USER_DATA_FILE}"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
|
||||
# give docker sometime to start up and create iptables rules
|
||||
# those rules come in after docker has started, and we want to wait for them to be sure iptables-save has all of them
|
||||
sleep 10
|
||||
|
||||
# Disable forwarding to metadata route from containers
|
||||
iptables -I FORWARD -d 169.254.169.254 -j DROP
|
||||
|
||||
# ubuntu will restore iptables from this file automatically. this is here so that docker's chain is saved to this file
|
||||
mkdir /etc/iptables && iptables-save > /etc/iptables/rules.v4
|
||||
|
||||
echo "=== Enable memory accounting =="
|
||||
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
# now add the user to the docker group
|
||||
usermod "${USER}" -a -G docker
|
||||
|
||||
if [ -z $(echo "${INFRA_VERSION}") ]; then
|
||||
echo "Skip pulling base docker images"
|
||||
else
|
||||
echo "=== Pulling base docker images ==="
|
||||
docker pull "${BASE_IMAGE}"
|
||||
|
||||
echo "=== Pulling mysql addon image ==="
|
||||
docker pull "${MYSQL_IMAGE}"
|
||||
|
||||
echo "=== Pulling postgresql addon image ==="
|
||||
docker pull "${POSTGRESQL_IMAGE}"
|
||||
|
||||
echo "=== Pulling redis addon image ==="
|
||||
docker pull "${REDIS_IMAGE}"
|
||||
|
||||
echo "=== Pulling mongodb addon image ==="
|
||||
docker pull "${MONGODB_IMAGE}"
|
||||
|
||||
echo "=== Pulling graphite docker images ==="
|
||||
docker pull "${GRAPHITE_IMAGE}"
|
||||
|
||||
echo "=== Pulling mail relay ==="
|
||||
docker pull "${MAIL_IMAGE}"
|
||||
fi
|
||||
|
||||
echo "==== Install nginx ===="
|
||||
apt-get -y install nginx-full
|
||||
[[ "$(nginx -v 2>&1)" == *"nginx/1.9."* ]] || die "Expecting nginx version to be 1.9.x"
|
||||
|
||||
echo "==== Install build-essential ===="
|
||||
apt-get -y install build-essential rcconf
|
||||
|
||||
echo "==== Install mysql ===="
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||
apt-get -y install mysql-server
|
||||
[[ "$(mysqld --version 2>&1)" == *"5.6."* ]] || die "Expecting nginx version to be 5.6.x"
|
||||
|
||||
echo "==== Install pwgen ===="
|
||||
apt-get -y install pwgen
|
||||
|
||||
echo "==== Install collectd ==="
|
||||
if ! apt-get install -y collectd collectd-utils; then
|
||||
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
update-rc.d -f collectd remove
|
||||
|
||||
# this simply makes it explicit that we run logrotate via cron. it's already part of base ubuntu
|
||||
echo "==== Install logrotate ==="
|
||||
apt-get install -y cron logrotate
|
||||
systemctl enable cron
|
||||
|
||||
echo "==== Install nodejs ===="
|
||||
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
|
||||
mkdir -p /usr/local/node-4.1.1
|
||||
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
|
||||
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
|
||||
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
echo "=== Rebuilding npm packages ==="
|
||||
cd "${INSTALLER_SOURCE_DIR}" && npm install --production
|
||||
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
|
||||
|
||||
echo "==== Install installer systemd script ===="
|
||||
provisionEnv="PROVISION=digitalocean"
|
||||
if [ ${SELFHOSTED} == 1 ]; then
|
||||
provisionEnv="PROVISION=local"
|
||||
fi
|
||||
|
||||
cat > /etc/systemd/system/cloudron-installer.service <<EOF
|
||||
[Unit]
|
||||
Description=Cloudron Installer
|
||||
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||
BindsTo=systemd-journald.service
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
|
||||
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
|
||||
; kill any child (installer.sh) as well
|
||||
KillMode=control-group
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Restore iptables before docker
|
||||
echo "==== Install iptables-restore systemd script ===="
|
||||
cat > /etc/systemd/system/iptables-restore.service <<EOF
|
||||
[Unit]
|
||||
Description=IPTables Restore
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Allocate swap files
|
||||
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
|
||||
echo "==== Install box-setup systemd script ===="
|
||||
cat > /etc/systemd/system/box-setup.service <<EOF
|
||||
[Unit]
|
||||
Description=Box Setup
|
||||
Before=docker.service umount.target collectd.service
|
||||
After=do-resize.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="${INSTALLER_SOURCE_DIR}/systemd/box-setup.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable cloudron-installer
|
||||
systemctl enable iptables-restore
|
||||
systemctl enable box-setup
|
||||
|
||||
# Configure systemd
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
-i /etc/systemd/journald.conf
|
||||
|
||||
# When rotating logs, systemd kills journald too soon sometimes
|
||||
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
|
||||
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
|
||||
-i /lib/systemd/system/systemd-journald.service
|
||||
|
||||
sync
|
||||
|
||||
# Configure time
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Give user access to system logs
|
||||
apt-get -y install acl
|
||||
usermod -a -G systemd-journal ${USER}
|
||||
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
|
||||
chown root:systemd-journal /var/log/journal
|
||||
systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
if [ ${SELFHOSTED} == 0 ]; then
|
||||
echo "==== Install ssh ==="
|
||||
apt-get -y install openssh-server
|
||||
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
|
||||
sed -e 's/^#\?Port .*/Port 202/g' \
|
||||
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
|
||||
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
|
||||
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
|
||||
-i /etc/ssh/sshd_config
|
||||
|
||||
# required so we can connect to this machine since port 22 is blocked by iptables by now
|
||||
systemctl reload sshd
|
||||
fi
|
||||
+7
-11
@@ -39,7 +39,7 @@ gulp.task('3rdparty', function () {
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-update', 'js-error'], function () {});
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-update'], function () {});
|
||||
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
@@ -80,14 +80,6 @@ gulp.task('js-setup', function () {
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-error', function () {
|
||||
gulp.src(['webadmin/src/js/error.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(uglify())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-update', function () {
|
||||
gulp.src(['webadmin/src/js/update.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
@@ -102,7 +94,7 @@ gulp.task('js-update', function () {
|
||||
// HTML
|
||||
// --------------
|
||||
|
||||
gulp.task('html', ['html-views', 'html-update'], function () {
|
||||
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
|
||||
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
|
||||
});
|
||||
|
||||
@@ -114,6 +106,10 @@ gulp.task('html-views', function () {
|
||||
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
|
||||
});
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
// --------------
|
||||
@@ -143,8 +139,8 @@ gulp.task('watch', ['default'], function () {
|
||||
gulp.watch(['webadmin/src/img/*'], ['images']);
|
||||
gulp.watch(['webadmin/src/**/*.html'], ['html']);
|
||||
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
|
||||
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
|
||||
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
||||
gulp.watch(['webadmin/src/js/error.js'], ['js-error']);
|
||||
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
||||
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
||||
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
||||
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
echo ""
|
||||
echo "======== Cloudron Installer ========"
|
||||
echo ""
|
||||
|
||||
if [ $# -lt 4 ]; then
|
||||
echo "Usage: ./installer.sh <fqdn> <aws key id> <aws key secret> <bucket> <provider> <revision>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# commandline arguments
|
||||
readonly fqdn="${1}"
|
||||
readonly aws_access_key_id="${2}"
|
||||
readonly aws_access_key_secret="${3}"
|
||||
readonly aws_backup_bucket="${4}"
|
||||
readonly provider="${5}"
|
||||
readonly revision="${6}"
|
||||
|
||||
# environment specific urls
|
||||
readonly api_server_origin="https://api.dev.cloudron.io"
|
||||
readonly web_server_origin="https://dev.cloudron.io"
|
||||
readonly release_bucket_url="https://s3.amazonaws.com/dev-cloudron-releases"
|
||||
readonly versions_url="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
|
||||
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
|
||||
|
||||
# runtime consts
|
||||
readonly installer_code_file="/tmp/box.tar.gz"
|
||||
readonly installer_tmp_dir="/tmp/box"
|
||||
readonly cert_folder="/tmp/certificates"
|
||||
|
||||
# check for fqdn in /ets/hosts
|
||||
echo "[INFO] checking for hostname entry"
|
||||
readonly hostentry_found=$(grep "${fqdn}" /etc/hosts || true)
|
||||
if [[ -z $hostentry_found ]]; then
|
||||
echo "[WARNING] No entry for ${fqdn} found in /etc/hosts"
|
||||
echo "Adding an entry ..."
|
||||
|
||||
cat >> /etc/hosts <<EOF
|
||||
|
||||
# The following line was added by the Cloudron installer script
|
||||
127.0.1.1 ${fqdn} ${fqdn}
|
||||
EOF
|
||||
else
|
||||
echo "Valid hostname entry found in /etc/hosts"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "[INFO] ensure minimal dependencies ..."
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
echo ""
|
||||
|
||||
echo "[INFO] Generating certificates ..."
|
||||
rm -rf "${cert_folder}"
|
||||
mkdir -p "${cert_folder}"
|
||||
|
||||
cat > "${cert_folder}/CONFIG" <<EOF
|
||||
[ req ]
|
||||
default_bits = 1024
|
||||
default_keyfile = keyfile.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
C = DE
|
||||
ST = Berlin
|
||||
L = Berlin
|
||||
O = Cloudron UG
|
||||
OU = Cloudron
|
||||
CN = ${fqdn}
|
||||
emailAddress = cert@cloudron.io
|
||||
|
||||
[ v3_req ]
|
||||
# Extensions to add to a certificate request
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = ${fqdn}
|
||||
DNS.2 = *.${fqdn}
|
||||
EOF
|
||||
|
||||
# generate cert files
|
||||
openssl genrsa 2048 > "${cert_folder}/host.key"
|
||||
openssl req -new -out "${cert_folder}/host.csr" -key "${cert_folder}/host.key" -config "${cert_folder}/CONFIG"
|
||||
openssl x509 -req -days 3650 -in "${cert_folder}/host.csr" -signkey "${cert_folder}/host.key" -out "${cert_folder}/host.cert" -extensions v3_req -extfile "${cert_folder}/CONFIG"
|
||||
|
||||
# make them json compatible, by collapsing to one line
|
||||
tls_cert=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.cert")
|
||||
tls_key=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.key")
|
||||
echo ""
|
||||
|
||||
echo "[INFO] Fetching installer code ..."
|
||||
curl "${installer_code_url}" -o "${installer_code_file}"
|
||||
echo ""
|
||||
|
||||
echo "[INFO] Extracting installer code to ${installer_tmp_dir} ..."
|
||||
rm -rf "${installer_tmp_dir}" && mkdir -p "${installer_tmp_dir}"
|
||||
tar xvf "${installer_code_file}" -C "${installer_tmp_dir}"
|
||||
echo ""
|
||||
|
||||
echo "Creating initial provisioning config ..."
|
||||
cat > /root/provision.json <<EOF
|
||||
{
|
||||
"sourceTarballUrl": "",
|
||||
"data": {
|
||||
"apiServerOrigin": "${api_server_origin}",
|
||||
"webServerOrigin": "${web_server_origin}",
|
||||
"fqdn": "${fqdn}",
|
||||
"token": "",
|
||||
"isCustomDomain": true,
|
||||
"boxVersionsUrl": "${versions_url}",
|
||||
"version": "",
|
||||
"tlsCert": "${tls_cert}",
|
||||
"tlsKey": "${tls_key}",
|
||||
"provider": "${provider}",
|
||||
"backupConfig": {
|
||||
"provider": "s3",
|
||||
"accessKeyId": "${aws_access_key_id}",
|
||||
"secretAccessKey": "${aws_access_key_secret}",
|
||||
"bucket": "${aws_backup_bucket}",
|
||||
"prefix": "backups"
|
||||
},
|
||||
"dnsConfig": {
|
||||
"provider": "route53",
|
||||
"accessKeyId": "${aws_access_key_id}",
|
||||
"secretAccessKey": "${aws_access_key_secret}"
|
||||
},
|
||||
"tlsConfig": {
|
||||
"provider": "letsencrypt-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[INFO] Running Ubuntu initializing script ..."
|
||||
/bin/bash "${installer_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${revision}" selfhosting
|
||||
echo ""
|
||||
|
||||
echo "[INFO] Reloading systemd daemon ..."
|
||||
systemctl daemon-reload
|
||||
echo ""
|
||||
|
||||
echo "[INFO] Restart docker ..."
|
||||
systemctl restart docker
|
||||
echo ""
|
||||
|
||||
echo "[FINISHED] Now starting Cloudron init jobs ..."
|
||||
systemctl start box-setup
|
||||
|
||||
# TODO this is only for convenience we should probably just let the user do a restart
|
||||
sleep 5 && sync
|
||||
systemctl start cloudron-installer
|
||||
journalctl -u cloudron-installer.service -f
|
||||
Generated
+516
@@ -0,0 +1,516 @@
|
||||
{
|
||||
"name": "installer",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.0",
|
||||
"from": "async@>=1.5.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.14.1",
|
||||
"from": "body-parser@>=1.12.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.1.tgz",
|
||||
"dependencies": {
|
||||
"bytes": {
|
||||
"version": "2.1.0",
|
||||
"from": "bytes@2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz"
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.1",
|
||||
"from": "content-type@>=1.0.1 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.0",
|
||||
"from": "depd@>=1.1.0 <1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.3.1",
|
||||
"from": "http-errors@>=1.3.1 <1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"from": "inherits@>=2.0.1 <2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.2.1",
|
||||
"from": "statuses@>=1.0.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.12",
|
||||
"from": "iconv-lite@0.4.12",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.12.tgz"
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"dependencies": {
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"from": "ee-first@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "5.1.0",
|
||||
"from": "qs@5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz"
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.1.4",
|
||||
"from": "raw-body@>=2.1.4 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.4.tgz",
|
||||
"dependencies": {
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"from": "unpipe@1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.9",
|
||||
"from": "type-is@>=1.6.9 <1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
|
||||
"dependencies": {
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"from": "media-typer@0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.7",
|
||||
"from": "mime-types@>=2.1.7 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.19.0",
|
||||
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connect-lastmile": {
|
||||
"version": "0.0.13",
|
||||
"from": "connect-lastmile@0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.1.3",
|
||||
"from": "debug@>=2.1.0 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.0",
|
||||
"from": "ms@0.7.0",
|
||||
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.2.0",
|
||||
"from": "debug@>=2.1.1 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.1",
|
||||
"from": "ms@0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"express": {
|
||||
"version": "4.13.3",
|
||||
"from": "express@>=4.11.2 <5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.2.13",
|
||||
"from": "accepts@>=1.2.12 <1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
|
||||
"dependencies": {
|
||||
"mime-types": {
|
||||
"version": "2.1.7",
|
||||
"from": "mime-types@>=2.1.6 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.19.0",
|
||||
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.5.3",
|
||||
"from": "negotiator@0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"from": "array-flatten@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.0",
|
||||
"from": "content-disposition@0.5.0",
|
||||
"resolved": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.1",
|
||||
"from": "content-type@>=1.0.1 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.1.3",
|
||||
"from": "cookie@0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"from": "cookie-signature@1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.0.1",
|
||||
"from": "depd@>=1.0.1 <1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.2",
|
||||
"from": "escape-html@1.0.2",
|
||||
"resolved": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.7.0",
|
||||
"from": "etag@>=1.7.0 <1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "0.4.0",
|
||||
"from": "finalhandler@0.4.0",
|
||||
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
|
||||
"dependencies": {
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"from": "unpipe@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.3.0",
|
||||
"from": "fresh@0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.0",
|
||||
"from": "merge-descriptors@1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.1",
|
||||
"from": "methods@>=1.1.1 <1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.1.tgz"
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"dependencies": {
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"from": "ee-first@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.0",
|
||||
"from": "parseurl@>=1.3.0 <1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"from": "path-to-regexp@0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "1.0.8",
|
||||
"from": "proxy-addr@>=1.0.8 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.8.tgz",
|
||||
"dependencies": {
|
||||
"forwarded": {
|
||||
"version": "0.1.0",
|
||||
"from": "forwarded@>=0.1.0 <0.2.0",
|
||||
"resolved": "http://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.0.1",
|
||||
"from": "ipaddr.js@1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "4.0.0",
|
||||
"from": "qs@4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz"
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.0.3",
|
||||
"from": "range-parser@>=1.0.2 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
|
||||
},
|
||||
"send": {
|
||||
"version": "0.13.0",
|
||||
"from": "send@0.13.0",
|
||||
"resolved": "http://registry.npmjs.org/send/-/send-0.13.0.tgz",
|
||||
"dependencies": {
|
||||
"destroy": {
|
||||
"version": "1.0.3",
|
||||
"from": "destroy@1.0.3",
|
||||
"resolved": "http://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.3.1",
|
||||
"from": "http-errors@>=1.3.1 <1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"from": "inherits@>=2.0.1 <2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.3.4",
|
||||
"from": "mime@1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
|
||||
},
|
||||
"ms": {
|
||||
"version": "0.7.1",
|
||||
"from": "ms@0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.2.1",
|
||||
"from": "statuses@>=1.2.1 <1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.10.0",
|
||||
"from": "serve-static@>=1.10.0 <1.11.0",
|
||||
"resolved": "http://registry.npmjs.org/serve-static/-/serve-static-1.10.0.tgz"
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.9",
|
||||
"from": "type-is@>=1.6.9 <1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
|
||||
"dependencies": {
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"from": "media-typer@0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.7",
|
||||
"from": "mime-types@>=2.1.6 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.19.0",
|
||||
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.0",
|
||||
"from": "utils-merge@1.0.0",
|
||||
"resolved": "http://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.0.1",
|
||||
"from": "vary@>=1.0.1 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"version": "9.0.3",
|
||||
"from": "json@>=9.0.3 <10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json/-/json-9.0.3.tgz"
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.6.1",
|
||||
"from": "morgan@>=1.5.1 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz",
|
||||
"dependencies": {
|
||||
"basic-auth": {
|
||||
"version": "1.0.3",
|
||||
"from": "basic-auth@>=1.0.3 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.3.tgz"
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.0.1",
|
||||
"from": "depd@>=1.0.1 <1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"dependencies": {
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"from": "ee-first@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"on-headers": {
|
||||
"version": "1.0.1",
|
||||
"from": "on-headers@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy-middleware": {
|
||||
"version": "0.15.0",
|
||||
"from": "proxy-middleware@>=0.15.0 <0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz"
|
||||
},
|
||||
"safetydance": {
|
||||
"version": "0.0.19",
|
||||
"from": "safetydance@0.0.19",
|
||||
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.1.0",
|
||||
"from": "semver@>=5.1.0 <6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
|
||||
},
|
||||
"superagent": {
|
||||
"version": "0.21.0",
|
||||
"from": "superagent@>=0.21.0 <0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-0.21.0.tgz",
|
||||
"dependencies": {
|
||||
"component-emitter": {
|
||||
"version": "1.1.2",
|
||||
"from": "component-emitter@1.1.2",
|
||||
"resolved": "http://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz"
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.0.1",
|
||||
"from": "cookiejar@2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.1.tgz"
|
||||
},
|
||||
"extend": {
|
||||
"version": "1.2.1",
|
||||
"from": "extend@>=1.2.1 <1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
|
||||
},
|
||||
"form-data": {
|
||||
"version": "0.1.3",
|
||||
"from": "form-data@0.1.3",
|
||||
"resolved": "http://registry.npmjs.org/form-data/-/form-data-0.1.3.tgz",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "0.9.2",
|
||||
"from": "async@>=0.9.0 <0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "0.0.7",
|
||||
"from": "combined-stream@>=0.0.4 <0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
|
||||
"dependencies": {
|
||||
"delayed-stream": {
|
||||
"version": "0.0.5",
|
||||
"from": "delayed-stream@0.0.5",
|
||||
"resolved": "http://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "1.0.14",
|
||||
"from": "formidable@1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.0.1",
|
||||
"from": "methods@1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.0.1.tgz"
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.2.11",
|
||||
"from": "mime@1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz"
|
||||
},
|
||||
"qs": {
|
||||
"version": "1.2.0",
|
||||
"from": "qs@1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz"
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.0.27-1",
|
||||
"from": "readable-stream@1.0.27-1",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.27-1.tgz",
|
||||
"dependencies": {
|
||||
"core-util-is": {
|
||||
"version": "1.0.1",
|
||||
"from": "core-util-is@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz"
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"from": "inherits@>=2.0.1 <2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"from": "isarray@0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"from": "string_decoder@>=0.10.0 <0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reduce-component": {
|
||||
"version": "1.0.1",
|
||||
"from": "reduce-component@1.0.1",
|
||||
"resolved": "http://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "installer",
|
||||
"description": "Cloudron Installer",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"author": {
|
||||
"name": "Cloudron authors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git"
|
||||
},
|
||||
"engines": [
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.5.0",
|
||||
"body-parser": "^1.12.0",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"debug": "^2.1.1",
|
||||
"express": "^4.11.2",
|
||||
"json": "^9.0.3",
|
||||
"morgan": "^1.5.1",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"safetydance": "0.0.19",
|
||||
"semver": "^5.1.0",
|
||||
"superagent": "^0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.8.1",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^0.3.5",
|
||||
"lodash": "^3.2.0",
|
||||
"mocha": "^2.1.0",
|
||||
"nock": "^0.59.1",
|
||||
"sleep": "^3.0.0",
|
||||
"superagent-sync": "^0.2.0",
|
||||
"supererror": "^0.7.0",
|
||||
"yesno": "0.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
"postmerge": "/bin/true"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('installer:installer'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
InstallerError: InstallerError,
|
||||
|
||||
provision: provision,
|
||||
|
||||
_ensureVersion: ensureVersion
|
||||
};
|
||||
|
||||
var INSTALLER_CMD = path.join(__dirname, 'scripts/installer.sh'),
|
||||
SUDO = '/usr/bin/sudo';
|
||||
|
||||
function InstallerError(reason, info) {
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
this.message = !info ? reason : (typeof info === 'object' ? JSON.stringify(info) : info);
|
||||
}
|
||||
util.inherits(InstallerError, Error);
|
||||
InstallerError.INTERNAL_ERROR = 1;
|
||||
InstallerError.ALREADY_PROVISIONED = 2;
|
||||
|
||||
// system until file has KillMode=control-group to bring down child processes
|
||||
function spawn(tag, cmd, args, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof cmd, 'string');
|
||||
assert(util.isArray(args));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cp = child_process.spawn(cmd, args, { timeout: 0 });
|
||||
cp.stdout.setEncoding('utf8');
|
||||
cp.stdout.on('data', function (data) { debug('%s (stdout): %s', tag, data); });
|
||||
cp.stderr.setEncoding('utf8');
|
||||
cp.stderr.on('data', function (data) { debug('%s (stderr): %s', tag, data); });
|
||||
|
||||
cp.on('error', function (error) {
|
||||
debug('%s : child process errored %s', tag, error.message);
|
||||
callback(error);
|
||||
});
|
||||
|
||||
cp.on('exit', function (code, signal) {
|
||||
debug('%s : child process exited. code: %d signal: %d', tag, code, signal);
|
||||
if (signal) return callback(new Error('Exited with signal ' + signal));
|
||||
if (code !== 0) return callback(new Error('Exited with code ' + code));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureVersion(args, callback) {
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!args.data || !args.data.boxVersionsUrl) return callback(new Error('No boxVersionsUrl specified'));
|
||||
|
||||
if (args.sourceTarballUrl) return callback(null, args);
|
||||
|
||||
superagent.get(args.data.boxVersionsUrl).end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new Error(util.format('Bad status: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var versions = safe.JSON.parse(result.text);
|
||||
|
||||
if (!versions || typeof versions !== 'object') return callback(new Error('versions is not in valid format:' + safe.error));
|
||||
|
||||
var latestVersion = Object.keys(versions).sort(semver.compare).pop();
|
||||
debug('ensureVersion: Latest version is %s etag:%s', latestVersion, result.header['etag']);
|
||||
|
||||
if (!versions[latestVersion]) return callback(new Error('No version available'));
|
||||
if (!versions[latestVersion].sourceTarballUrl) return callback(new Error('No sourceTarballUrl specified'));
|
||||
|
||||
args.sourceTarballUrl = versions[latestVersion].sourceTarballUrl;
|
||||
args.data.version = latestVersion;
|
||||
|
||||
callback(null, args);
|
||||
});
|
||||
}
|
||||
|
||||
function provision(args, callback) {
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (process.env.NODE_ENV === 'test') return callback(null);
|
||||
|
||||
ensureVersion(args, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var pargs = [ INSTALLER_CMD ];
|
||||
pargs.push('--sourcetarballurl', result.sourceTarballUrl);
|
||||
pargs.push('--data', JSON.stringify(result.data));
|
||||
|
||||
debug('provision: calling with args %j', pargs);
|
||||
|
||||
// sudo is required for update()
|
||||
spawn('provision', SUDO, pargs, callback);
|
||||
});
|
||||
}
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly BOX_SRC_DIR=/home/yellowtent/box
|
||||
readonly DATA_DIR=/home/yellowtent/data
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly json="${script_dir}/../../node_modules/.bin/json"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 180"
|
||||
|
||||
readonly is_update=$([[ -d "${BOX_SRC_DIR}" ]] && echo "yes" || echo "no")
|
||||
|
||||
# create a provision file for testing. %q escapes args. %q is reused as much as necessary to satisfy $@
|
||||
(echo -e "#!/bin/bash\n"; printf "%q " "${script_dir}/installer.sh" "$@") > /home/yellowtent/provision.sh
|
||||
chmod +x /home/yellowtent/provision.sh
|
||||
|
||||
arg_source_tarball_url=""
|
||||
arg_data=""
|
||||
|
||||
args=$(getopt -o "" -l "sourcetarballurl:,data:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--sourcetarballurl) arg_source_tarball_url="$2";;
|
||||
--data) arg_data="$2";;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
|
||||
shift 2
|
||||
done
|
||||
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
echo "Downloading box code from ${arg_source_tarball_url} to ${box_src_tmp_dir}"
|
||||
|
||||
while true; do
|
||||
if $curl -L "${arg_source_tarball_url}" | tar -zxf - -C "${box_src_tmp_dir}"; then break; fi
|
||||
echo "Failed to download source tarball, trying again"
|
||||
sleep 5
|
||||
done
|
||||
while true; do
|
||||
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
|
||||
if cd "${box_src_tmp_dir}" && npm rebuild; then break; fi
|
||||
echo "Failed to rebuild, trying again"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
echo "Setting up update splash screen"
|
||||
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" # show splash from new code
|
||||
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
|
||||
fi
|
||||
|
||||
# switch the codes
|
||||
rm -rf "${BOX_SRC_DIR}"
|
||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
chown -R yellowtent.yellowtent "${BOX_SRC_DIR}"
|
||||
|
||||
# create a start file for testing. %q escapes args
|
||||
(echo -e "#!/bin/bash\n"; printf "%q " "${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}") > /home/yellowtent/setup_start.sh
|
||||
chmod +x /home/yellowtent/setup_start.sh
|
||||
|
||||
echo "Calling box setup script"
|
||||
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
|
||||
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('installer:server'),
|
||||
express = require('express'),
|
||||
fs = require('fs'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
installer = require('./installer.js'),
|
||||
json = require('body-parser').json,
|
||||
lastMile = require('connect-lastmile'),
|
||||
morgan = require('morgan'),
|
||||
superagent = require('superagent');
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var PROVISION_CONFIG_FILE = '/root/provision.json';
|
||||
var CLOUDRON_CONFIG_FILE = '/home/yellowtent/configs/cloudron.conf';
|
||||
|
||||
var gHttpServer = null; // update server; used for updates
|
||||
|
||||
function provisionDigitalOcean(callback) {
|
||||
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
|
||||
|
||||
superagent.get('http://169.254.169.254/metadata/v1.json').end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error('Error getting metadata', error);
|
||||
return callback(new Error('Error getting metadata'));
|
||||
}
|
||||
|
||||
var userData = JSON.parse(result.body.user_data);
|
||||
|
||||
installer.provision(userData, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function provisionLocal(callback) {
|
||||
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
|
||||
|
||||
if (!fs.existsSync(PROVISION_CONFIG_FILE)) {
|
||||
console.error('No provisioning data found at %s', PROVISION_CONFIG_FILE);
|
||||
return callback(new Error('No provisioning data found'));
|
||||
}
|
||||
|
||||
var userData = require(PROVISION_CONFIG_FILE);
|
||||
|
||||
installer.provision(userData, callback);
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.sourceTarballUrl || typeof req.body.sourceTarballUrl !== 'string') return next(new HttpError(400, 'No sourceTarballUrl provided'));
|
||||
if (!req.body.data || typeof req.body.data !== 'object') return next(new HttpError(400, 'No data provided'));
|
||||
|
||||
debug('provision: received from box %j', req.body);
|
||||
|
||||
installer.provision(req.body, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
}
|
||||
|
||||
function startUpdateServer(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Starting update server');
|
||||
|
||||
var app = express();
|
||||
|
||||
var router = new express.Router();
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') app.use(morgan('dev', { immediate: false }));
|
||||
|
||||
app.use(json({ strict: true }))
|
||||
.use(router)
|
||||
.use(lastMile());
|
||||
|
||||
router.post('/api/v1/installer/update', update);
|
||||
|
||||
gHttpServer = http.createServer(app);
|
||||
gHttpServer.on('error', console.error);
|
||||
|
||||
gHttpServer.listen(2020, '127.0.0.1', callback);
|
||||
}
|
||||
|
||||
function stopUpdateServer(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Stopping update server');
|
||||
|
||||
if (!gHttpServer) return callback(null);
|
||||
|
||||
gHttpServer.close(callback);
|
||||
gHttpServer = null;
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var actions;
|
||||
|
||||
if (process.env.PROVISION === 'local') {
|
||||
debug('Starting Installer in selfhost mode');
|
||||
|
||||
actions = [
|
||||
startUpdateServer,
|
||||
provisionLocal
|
||||
];
|
||||
} else { // current fallback, should be 'digitalocean' eventually, see initializeBaseUbuntuImage.sh
|
||||
debug('Starting Installer in managed mode');
|
||||
|
||||
actions = [
|
||||
startUpdateServer,
|
||||
provisionDigitalOcean
|
||||
];
|
||||
}
|
||||
|
||||
async.series(actions, callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
stopUpdateServer
|
||||
], callback);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
start(function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
nock = require('nock'),
|
||||
os = require('os'),
|
||||
request = require('superagent'),
|
||||
server = require('../server.js'),
|
||||
installer = require('../installer.js'),
|
||||
_ = require('lodash');
|
||||
|
||||
var EXTERNAL_SERVER_URL = 'https://localhost:4443';
|
||||
var INTERNAL_SERVER_URL = 'http://localhost:2020';
|
||||
var APPSERVER_ORIGIN = 'http://appserver';
|
||||
var FQDN = os.hostname();
|
||||
|
||||
describe('Server', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(function (done) {
|
||||
var user_data = JSON.stringify({ apiServerOrigin: APPSERVER_ORIGIN }); // user_data is a string
|
||||
var scope = nock('http://169.254.169.254')
|
||||
.persist()
|
||||
.get('/metadata/v1.json')
|
||||
.reply(200, JSON.stringify({ user_data: user_data }), { 'Content-Type': 'application/json' });
|
||||
done();
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
nock.cleanAll();
|
||||
done();
|
||||
});
|
||||
|
||||
describe('starts and stop', function () {
|
||||
it('starts', function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
it('stops', function (done) {
|
||||
server.stop(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update (internal server)', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
after(function (done) {
|
||||
server.stop(done);
|
||||
});
|
||||
|
||||
it('does not respond to provision', function (done) {
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/provision').send({ }).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not respond to restore', function (done) {
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/restore').send({ }).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var data = {
|
||||
sourceTarballUrl: "https://foo.tar.gz",
|
||||
|
||||
data: {
|
||||
token: 'sometoken',
|
||||
apiServerOrigin: APPSERVER_ORIGIN,
|
||||
webServerOrigin: 'https://somethingelse.com',
|
||||
fqdn: 'www.something.com',
|
||||
tlsKey: 'key',
|
||||
tlsCert: 'cert',
|
||||
boxVersionsUrl: 'https://versions.json',
|
||||
version: '0.1'
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(data).forEach(function (key) {
|
||||
it('fails due to missing ' + key, function (done) {
|
||||
var dataCopy = _.merge({ }, data);
|
||||
delete dataCopy[key];
|
||||
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(dataCopy).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(data).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureVersion', function () {
|
||||
before(function () {
|
||||
process.env.NODE_ENV = undefined;
|
||||
});
|
||||
|
||||
after(function () {
|
||||
process.env.NODE_ENV = 'test';
|
||||
});
|
||||
|
||||
it ('fails without data', function (done) {
|
||||
installer._ensureVersion({}, function (error) {
|
||||
expect(error).to.be.an(Error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('fails without boxVersionsUrl', function (done) {
|
||||
installer._ensureVersion({ data: {}}, function (error) {
|
||||
expect(error).to.be.an(Error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with sourceTarballUrl', function (done) {
|
||||
var data = {
|
||||
sourceTarballUrl: 'sometarballurl',
|
||||
data: {
|
||||
boxVersionsUrl: 'http://foobar/versions.json'
|
||||
}
|
||||
};
|
||||
|
||||
installer._ensureVersion(data, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result).to.eql(data);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds without sourceTarballUrl', function (done) {
|
||||
var versions = {
|
||||
'0.1.0': {
|
||||
sourceTarballUrl: 'sometarballurl1'
|
||||
},
|
||||
'0.2.0': {
|
||||
sourceTarballUrl: 'sometarballurl2'
|
||||
}
|
||||
};
|
||||
|
||||
var scope = nock('http://foobar')
|
||||
.get('/versions.json')
|
||||
.reply(200, JSON.stringify(versions), { 'Content-Type': 'application/json' });
|
||||
|
||||
var data = {
|
||||
data: {
|
||||
boxVersionsUrl: 'http://foobar/versions.json'
|
||||
}
|
||||
};
|
||||
|
||||
installer._ensureVersion(data, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result.sourceTarballUrl).to.equal(versions['0.2.0'].sourceTarballUrl);
|
||||
expect(result.data.boxVersionsUrl).to.equal(data.data.boxVersionsUrl);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly USER_HOME="/home/yellowtent"
|
||||
readonly APPS_SWAP_FILE="/apps.swap"
|
||||
readonly BACKUP_SWAP_FILE="/backup.swap" # used when doing app backups
|
||||
readonly USER_DATA_FILE="/root/user_data.img"
|
||||
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
# detect device
|
||||
if [[ -b "/dev/vda1" ]]; then
|
||||
disk_device="/dev/vda1"
|
||||
fi
|
||||
|
||||
if [[ -b "/dev/xvda1" ]]; then
|
||||
disk_device="/dev/xvda1"
|
||||
fi
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size="${physical_memory}"
|
||||
readonly app_count=$((${physical_memory} / 200)) # estimated app count
|
||||
readonly disk_size_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ print $3 }')
|
||||
readonly disk_size=$((disk_size_gb * 1024))
|
||||
readonly backup_swap_size=1024
|
||||
# readonly system_size=5120 # 5 gigs for system libs, installer, box code and tmp
|
||||
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
|
||||
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
|
||||
|
||||
echo "Disk device: ${disk_device}"
|
||||
echo "Physical memory: ${physical_memory}"
|
||||
echo "Estimated app count: ${app_count}"
|
||||
echo "Disk size: ${disk_size}"
|
||||
|
||||
# Allocate two sets of swap files - one for general app usage and another for backup
|
||||
# The backup swap is setup for swap on the fly by the backup scripts
|
||||
if [[ ! -f "${APPS_SWAP_FILE}" ]]; then
|
||||
echo "Creating Apps swap file of size ${swap_size}M"
|
||||
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
|
||||
chmod 600 "${APPS_SWAP_FILE}"
|
||||
mkswap "${APPS_SWAP_FILE}"
|
||||
swapon "${APPS_SWAP_FILE}"
|
||||
echo "${APPS_SWAP_FILE} none swap sw 0 0" >> /etc/fstab
|
||||
else
|
||||
echo "Apps Swap file already exists"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${BACKUP_SWAP_FILE}" ]]; then
|
||||
echo "Creating Backup swap file of size ${backup_swap_size}M"
|
||||
fallocate -l "${backup_swap_size}m" "${BACKUP_SWAP_FILE}"
|
||||
chmod 600 "${BACKUP_SWAP_FILE}"
|
||||
mkswap "${BACKUP_SWAP_FILE}"
|
||||
else
|
||||
echo "Backups Swap file already exists"
|
||||
fi
|
||||
|
||||
echo "Resizing data volume"
|
||||
home_data_size=$((disk_size - system_size - swap_size - backup_swap_size - ext4_reserved))
|
||||
echo "Resizing up btrfs user data to size ${home_data_size}M"
|
||||
umount "${USER_DATA_DIR}"
|
||||
fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
|
||||
mount "${USER_DATA_FILE}"
|
||||
btrfs filesystem resize max "${USER_DATA_DIR}"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN displayName VARCHAR(512) DEFAULT ""', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN displayName', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
createdAt VARCHAR(512) NOT NULL,
|
||||
modifiedAt VARCHAR(512) NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
displayName VARCHAR(512) DEFAULT '',
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
|
||||
Generated
+315
-310
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -18,7 +18,7 @@
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"bytes": "^2.1.0",
|
||||
"cloudron-manifestformat": "^2.0.0",
|
||||
"cloudron-manifestformat": "^2.2.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"connect-timeout": "^1.5.0",
|
||||
@@ -42,6 +42,7 @@
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.7.0",
|
||||
"native-dns": "^0.7.0",
|
||||
"node-df": "^0.1.1",
|
||||
"node-uuid": "^1.4.3",
|
||||
"nodemailer": "^1.3.0",
|
||||
"nodemailer-smtp-transport": "^1.0.3",
|
||||
@@ -70,6 +71,7 @@
|
||||
"devDependencies": {
|
||||
"apidoc": "*",
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"deep-extend": "^0.4.1",
|
||||
"del": "^1.1.1",
|
||||
"expect.js": "*",
|
||||
"gulp": "^3.8.11",
|
||||
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
|
||||
# brew install gnu-getopt to get the GNU getopt on OS X
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,output:,publish,no-upload" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
readonly RELEASE_TOOL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../release" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
delete_bundle="yes"
|
||||
commitish="HEAD"
|
||||
publish="no"
|
||||
upload="yes"
|
||||
bundle_file=""
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--revision) commitish="$2"; shift 2;;
|
||||
--output) bundle_file="$2"; delete_bundle="no"; shift 2;;
|
||||
--no-upload) upload="no"; shift;;
|
||||
--publish) publish="yes"; shift;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${upload}" == "no" && "${publish}" == "yes" ]]; then
|
||||
echo "Cannot publish without uploading"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
|
||||
|
||||
assertNotEmpty AWS_DEV_ACCESS_KEY
|
||||
assertNotEmpty AWS_DEV_SECRET_KEY
|
||||
|
||||
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
echo "You have local changes, stash or commit them to proceed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v4.1.1" ]]; then
|
||||
echo "This script requires node 4.1.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
|
||||
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
|
||||
|
||||
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
|
||||
echo "Checking out code [${version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
|
||||
|
||||
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
|
||||
echo "Reusing dev modules from cache"
|
||||
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
|
||||
else
|
||||
echo "Installing modules with dev dependencies"
|
||||
(cd "${bundle_dir}" && npm install)
|
||||
|
||||
echo "Caching dev dependencies"
|
||||
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
|
||||
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
|
||||
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all"
|
||||
fi
|
||||
|
||||
echo "Building webadmin assets"
|
||||
(cd "${bundle_dir}" && gulp)
|
||||
|
||||
echo "Remove intermediate files required at build-time only"
|
||||
rm -rf "${bundle_dir}/node_modules/"
|
||||
rm -rf "${bundle_dir}/webadmin/src"
|
||||
rm -rf "${bundle_dir}/gulpfile.js"
|
||||
|
||||
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
|
||||
echo "Reusing prod modules from cache"
|
||||
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
|
||||
else
|
||||
echo "Installing modules for production"
|
||||
(cd "${bundle_dir}" && npm install --production --no-optional)
|
||||
|
||||
echo "Caching prod dependencies"
|
||||
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
|
||||
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
|
||||
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod"
|
||||
fi
|
||||
|
||||
echo "Create final tarball"
|
||||
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
|
||||
echo "Cleaning up ${bundle_dir}"
|
||||
rm -rf "${bundle_dir}"
|
||||
|
||||
if [[ "${upload}" == "yes" ]]; then
|
||||
echo "Uploading bundle to S3"
|
||||
# That special header is needed to allow access with singed urls created with different aws credentials than the ones the file got uploaded
|
||||
s3cmd --multipart-chunk-size-mb=5 --ssl --acl-public --access_key="${AWS_DEV_ACCESS_KEY}" --secret_key="${AWS_DEV_SECRET_KEY}" --no-mime-magic put "${bundle_file}" "s3://dev-cloudron-releases/box-${version}.tar.gz"
|
||||
|
||||
versions_file_url="https://dev-cloudron-releases.s3.amazonaws.com/box-${version}.tar.gz"
|
||||
echo "The URL for the versions file is: ${versions_file_url}"
|
||||
|
||||
if [[ "${publish}" == "yes" ]]; then
|
||||
echo "Publishing to dev"
|
||||
${RELEASE_TOOL_DIR}/release create --env dev --code "${versions_file_url}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${delete_bundle}" == "no" ]]; then
|
||||
echo "Tarball preserved at ${bundle_file}"
|
||||
else
|
||||
rm "${bundle_file}"
|
||||
fi
|
||||
|
||||
+10
-2
@@ -19,6 +19,8 @@ arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_backup_config=""
|
||||
arg_dns_config=""
|
||||
arg_update_config=""
|
||||
arg_provider=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
@@ -31,12 +33,14 @@ while true; do
|
||||
;;
|
||||
--data)
|
||||
# only read mandatory non-empty parameters here
|
||||
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_token arg_is_custom_domain arg_box_versions_url arg_version <<EOF
|
||||
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn token isCustomDomain boxVersionsUrl version | tr '\n' ' ')
|
||||
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_is_custom_domain arg_box_versions_url arg_version <<EOF
|
||||
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
|
||||
EOF
|
||||
# read possibly empty parameters here
|
||||
arg_tls_cert=$(echo "$2" | $json tlsCert)
|
||||
arg_tls_key=$(echo "$2" | $json tlsKey)
|
||||
arg_token=$(echo "$2" | $json token)
|
||||
arg_provider=$(echo "$2" | $json provider)
|
||||
|
||||
arg_tls_config=$(echo "$2" | $json tlsConfig)
|
||||
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
|
||||
@@ -53,6 +57,9 @@ EOF
|
||||
arg_dns_config=$(echo "$2" | $json dnsConfig)
|
||||
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
|
||||
|
||||
arg_update_config=$(echo "$2" | $json updateConfig)
|
||||
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
@@ -73,3 +80,4 @@ echo "token: ${arg_token}"
|
||||
echo "tlsConfig: ${arg_tls_config}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
echo "provider: ${arg_provider}"
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ systemctl daemon-reload
|
||||
systemctl enable cloudron.target
|
||||
|
||||
########## sudoers
|
||||
rm -f /etc/sudoers.d/*
|
||||
rm -f /etc/sudoers.d/yellowtent
|
||||
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
|
||||
|
||||
########## collectd
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# sudo logging breaks journalctl output with very long urls (systemd bug)
|
||||
Defaults !syslog
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
|
||||
|
||||
@@ -27,3 +30,7 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Description=Cloudron Admin
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||
BindsTo=systemd-journald.service
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
@@ -9,7 +11,8 @@ WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
; kill apptask processes as well
|
||||
KillMode=control-group
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=200M
|
||||
|
||||
+9
-4
@@ -92,10 +92,6 @@ EOF
|
||||
|
||||
set_progress "28" "Setup collectd"
|
||||
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
||||
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
|
||||
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
|
||||
vda1_id=$(blkid -s UUID -o value /dev/vda1)
|
||||
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
|
||||
service collectd restart
|
||||
|
||||
set_progress "30" "Setup nginx"
|
||||
@@ -143,6 +139,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"isCustomDomain": ${arg_is_custom_domain},
|
||||
"boxVersionsUrl": "${arg_box_versions_url}",
|
||||
"adminEmail": "admin@${arg_fqdn}",
|
||||
"provider": "${arg_provider}",
|
||||
"database": {
|
||||
"hostname": "localhost",
|
||||
"username": "root",
|
||||
@@ -177,6 +174,14 @@ if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
# Add Update Configuration
|
||||
if [[ ! -z "${arg_update_config}" ]]; then
|
||||
echo "Add Update Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
|
||||
fi
|
||||
|
||||
# Add TLS Configuration
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
echo "Add TLS Config"
|
||||
|
||||
@@ -133,10 +133,10 @@ LoadPlugin nginx
|
||||
# Globals true
|
||||
#</LoadPlugin>
|
||||
#LoadPlugin pinba
|
||||
LoadPlugin ping
|
||||
#LoadPlugin ping
|
||||
#LoadPlugin postgresql
|
||||
#LoadPlugin powerdns
|
||||
LoadPlugin processes
|
||||
#LoadPlugin processes
|
||||
#LoadPlugin protocols
|
||||
#<LoadPlugin python>
|
||||
# Globals true
|
||||
@@ -161,7 +161,7 @@ LoadPlugin tail
|
||||
#LoadPlugin users
|
||||
#LoadPlugin uuid
|
||||
#LoadPlugin varnish
|
||||
LoadPlugin vmem
|
||||
#LoadPlugin vmem
|
||||
#LoadPlugin vserver
|
||||
#LoadPlugin wireless
|
||||
LoadPlugin write_graphite
|
||||
@@ -193,11 +193,11 @@ LoadPlugin write_graphite
|
||||
</Plugin>
|
||||
|
||||
<Plugin df>
|
||||
FSType "tmpfs"
|
||||
MountPoint "/dev"
|
||||
FSType "ext4"
|
||||
FSType "btrfs"
|
||||
|
||||
ReportByDevice true
|
||||
IgnoreSelected true
|
||||
IgnoreSelected false
|
||||
|
||||
ValuesAbsolute true
|
||||
ValuesPercentage true
|
||||
@@ -212,17 +212,6 @@ LoadPlugin write_graphite
|
||||
URL "http://127.0.0.1/nginx_status"
|
||||
</Plugin>
|
||||
|
||||
<Plugin ping>
|
||||
Host "google.com"
|
||||
Interval 1.0
|
||||
Timeout 0.9
|
||||
TTL 255
|
||||
</Plugin>
|
||||
|
||||
<Plugin processes>
|
||||
ProcessMatch "app" "node box.js"
|
||||
</Plugin>
|
||||
|
||||
<Plugin swap>
|
||||
ReportByDevice false
|
||||
ReportBytes true
|
||||
@@ -255,10 +244,6 @@ LoadPlugin write_graphite
|
||||
</File>
|
||||
</Plugin>
|
||||
|
||||
<Plugin vmem>
|
||||
Verbose false
|
||||
</Plugin>
|
||||
|
||||
<Plugin write_graphite>
|
||||
<Node "graphing">
|
||||
Host "localhost"
|
||||
|
||||
@@ -58,12 +58,17 @@ server {
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
|
||||
# graphite paths
|
||||
location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
client_max_body_size 1m;
|
||||
location ~ ^/api/v1/apps/.*/exec$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 30m;
|
||||
}
|
||||
|
||||
# graphite paths
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
location / {
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
index index.html index.htm;
|
||||
|
||||
@@ -58,6 +58,14 @@ http {
|
||||
ssl_certificate cert/host.cert;
|
||||
ssl_certificate_key cert/host.key;
|
||||
|
||||
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
|
||||
# Disable check to allow unlimited body sizes
|
||||
client_max_body_size 0;
|
||||
|
||||
error_page 404 = @fallback;
|
||||
location @fallback {
|
||||
internal;
|
||||
@@ -65,7 +73,16 @@ http {
|
||||
rewrite ^/$ /nakeddomain.html break;
|
||||
}
|
||||
|
||||
return 404;
|
||||
location / {
|
||||
internal;
|
||||
root /home/yellowtent/box/webadmin/dist;
|
||||
rewrite ^/$ /nakeddomain.html break;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
}
|
||||
|
||||
include applications/*.conf;
|
||||
|
||||
+4
-2
@@ -388,10 +388,12 @@ function setupSendMail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var username = app.location ? app.location + '-app' : 'no-reply'; // use no-reply for bare domains
|
||||
|
||||
var env = [
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
|
||||
'MAIL_SMTP_USERNAME=' + (app.location || app.id) + '-app', // use app.id for bare domains
|
||||
'MAIL_SMTP_USERNAME=' + username,
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
|
||||
@@ -764,7 +766,7 @@ function setupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var redisPassword = generatePassword(64, false /* memorable */);
|
||||
var redisPassword = generatePassword(64, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.DATA_DIR, app.id + '/redis');
|
||||
|
||||
|
||||
+1
-1
@@ -368,7 +368,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || exports.ISTATE_PENDING_CONFIGURE || installationState == exports.ISTATE_PENDING_BACKUP) {
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_CONFIGURE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else {
|
||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||
|
||||
+53
-34
@@ -22,6 +22,7 @@ exports = module.exports = {
|
||||
|
||||
backup: backup,
|
||||
backupApp: backupApp,
|
||||
listBackups: listBackups,
|
||||
|
||||
getLogs: getLogs,
|
||||
|
||||
@@ -56,7 +57,6 @@ var addons = require('./addons.js'),
|
||||
docker = require('./docker.js'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -73,8 +73,6 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
|
||||
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
@@ -288,6 +286,9 @@ function purchase(appStoreId, callback) {
|
||||
// Skip purchase if appStoreId is empty
|
||||
if (appStoreId === '') return callback(null);
|
||||
|
||||
// Skip if we don't have an appstore token
|
||||
if (config.token() === '') return callback(null);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/apps/' + appStoreId + '/purchase';
|
||||
|
||||
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
|
||||
@@ -645,42 +646,31 @@ function exec(appId, options, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var createOptions = {
|
||||
var container = docker.connection.getContainer(app.containerId);
|
||||
|
||||
var execOptions = {
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
OpenStdin: true,
|
||||
StdinOnce: false,
|
||||
Tty: true
|
||||
Tty: options.tty,
|
||||
Cmd: cmd
|
||||
};
|
||||
|
||||
docker.createSubcontainer(app, app.id + '-exec-' + Date.now(), cmd, createOptions, function (error, container) {
|
||||
container.exec(execOptions, function (error, exec) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
|
||||
var startOptions = {
|
||||
Detach: false,
|
||||
Tty: options.tty,
|
||||
stdin: true // this is a dockerode option that enabled openStdin in the modem
|
||||
};
|
||||
exec.start(startOptions, function(error, stream) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
docker.startContainer(container.id, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (options.rows && options.columns) {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}
|
||||
|
||||
if (options.rows && options.columns) {
|
||||
container.resize({ h: options.rows, w: options.columns }, NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
var deleteContainer = once(docker.deleteContainer.bind(null, container.id, NOOP_CALLBACK));
|
||||
|
||||
container.wait(function (error) {
|
||||
if (error) debug('Error waiting on container', error);
|
||||
|
||||
debug('exec: container finished', container.id);
|
||||
|
||||
deleteContainer();
|
||||
});
|
||||
|
||||
stream.close = deleteContainer;
|
||||
|
||||
callback(null, stream);
|
||||
});
|
||||
return callback(null, stream);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -839,9 +829,9 @@ function backup(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
appdb.exists(appId, function (error, exists) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
@@ -854,13 +844,14 @@ function backup(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(app, addonsToRestore, callback) {
|
||||
function restoreApp(app, addonsToRestore, backupId, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof addonsToRestore, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(app.lastBackupId);
|
||||
|
||||
backups.getRestoreUrl(app.lastBackupId, function (error, result) {
|
||||
backups.getRestoreUrl(backupId, function (error, result) {
|
||||
if (error && error.reason == BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -873,3 +864,31 @@ function restoreApp(app, addonsToRestore, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listBackups(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.exists(appId, function (error, exists) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
|
||||
// TODO pagination is not implemented in the backend yet
|
||||
backups.getAllPaged(0, 1000, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var appBackups = [];
|
||||
|
||||
result.forEach(function (backup) {
|
||||
appBackups = appBackups.concat(backup.dependsOn.filter(function (d) {
|
||||
return d.indexOf('appbackup_' + appId) === 0;
|
||||
}));
|
||||
});
|
||||
|
||||
// alphabetic should be sufficient
|
||||
appBackups.sort();
|
||||
|
||||
callback(null, appBackups);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+115
-24
@@ -66,17 +66,22 @@ var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', {
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.initialize(callback);
|
||||
}
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
function debugApp(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
var prefix = app ? (app.location || '(bare)') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function reserveHttpPort(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var server = net.createServer();
|
||||
server.listen(0, function () {
|
||||
var port = server.address().port;
|
||||
@@ -91,7 +96,10 @@ function reserveHttpPort(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function configureNginx(app, callback) {
|
||||
function configureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
|
||||
@@ -102,11 +110,16 @@ function configureNginx(app, callback) {
|
||||
}
|
||||
|
||||
function unconfigureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// TODO: maybe revoke the cert
|
||||
nginx.unconfigureApp(app, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(!app.containerId); // otherwise, it will trigger volumeFrom
|
||||
|
||||
debugApp(app, 'creating container');
|
||||
@@ -119,6 +132,9 @@ function createContainer(app, callback) {
|
||||
}
|
||||
|
||||
function deleteContainers(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting containers');
|
||||
|
||||
docker.deleteContainers(app.id, function (error) {
|
||||
@@ -129,10 +145,16 @@ function deleteContainers(app, callback) {
|
||||
}
|
||||
|
||||
function createVolume(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
|
||||
}
|
||||
|
||||
function deleteVolume(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
|
||||
}
|
||||
|
||||
@@ -165,6 +187,9 @@ function removeOAuthProxyCredentials(app, callback) {
|
||||
}
|
||||
|
||||
function addCollectdProfile(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
|
||||
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -173,6 +198,9 @@ function addCollectdProfile(app, callback) {
|
||||
}
|
||||
|
||||
function removeCollectdProfile(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
|
||||
shell.sudo('removeCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
|
||||
@@ -180,6 +208,9 @@ function removeCollectdProfile(app, callback) {
|
||||
}
|
||||
|
||||
function verifyManifest(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Verifying manifest');
|
||||
|
||||
var manifest = app.manifest;
|
||||
@@ -193,6 +224,9 @@ function verifyManifest(app, callback) {
|
||||
}
|
||||
|
||||
function downloadIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
|
||||
|
||||
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
|
||||
@@ -211,46 +245,64 @@ function downloadIcon(app, callback) {
|
||||
}
|
||||
|
||||
function registerSubdomain(app, callback) {
|
||||
// even though the bare domain is already registered in the appstore, we still
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain location [%s]', app.location);
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomains.add(app.location, 'A', [ sysinfo.getIp() ], function (error, changeId) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
retryCallback(null, error || changeId);
|
||||
// even though the bare domain is already registered in the appstore, we still
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain location [%s]', app.location);
|
||||
|
||||
subdomains.add(app.location, 'A', [ ip ], function (error, changeId) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error || changeId);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
updateApp(app, { dnsRecordId: result }, callback);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
updateApp(app, { dnsRecordId: result }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubdomain(app, location, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// do not unregister bare domain because we show a error/cloudron info page there
|
||||
if (location === '') {
|
||||
debugApp(app, 'Skip unregister of empty subdomain');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
subdomains.remove(location, 'A', [ sysinfo.getIp() ], function (error) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
|
||||
retryCallback(null, error);
|
||||
subdomains.remove(location, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
fs.unlink(path.join(paths.APPICONS_DIR, app.id + '.png'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
|
||||
callback(null);
|
||||
@@ -258,6 +310,9 @@ function removeIcon(app, callback) {
|
||||
}
|
||||
|
||||
function waitForDnsPropagation(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!config.CLOUDRON) {
|
||||
debugApp(app, 'Skipping dns propagation check for development');
|
||||
return callback(null);
|
||||
@@ -281,6 +336,10 @@ function waitForDnsPropagation(app, callback) {
|
||||
|
||||
// updates the app object and the database
|
||||
function updateApp(app, values, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'updating app with values: %j', values);
|
||||
|
||||
appdb.update(app.id, values, function (error) {
|
||||
@@ -305,6 +364,9 @@ function updateApp(app, values, callback) {
|
||||
// - setup the container (requires image, volumes, addons)
|
||||
// - setup collectd (requires container id)
|
||||
function install(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
verifyManifest.bind(null, app),
|
||||
|
||||
@@ -369,6 +431,9 @@ function install(app, callback) {
|
||||
}
|
||||
|
||||
function backup(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
apps.backupApp.bind(null, app, app.manifest.addons),
|
||||
@@ -389,6 +454,9 @@ function backup(app, callback) {
|
||||
|
||||
// restore is also called for upgrades and infra updates. note that in those cases it is possible there is no backup
|
||||
function restore(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// we don't have a backup, same as re-install. this allows us to install from install failures (update failures always
|
||||
// have a backupId)
|
||||
if (!app.lastBackupId) {
|
||||
@@ -396,6 +464,8 @@ function restore(app, callback) {
|
||||
return install(app, callback);
|
||||
}
|
||||
|
||||
var backupId = app.lastBackupId;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -431,7 +501,7 @@ function restore(app, callback) {
|
||||
createVolume.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }),
|
||||
apps.restoreApp.bind(null, app, app.manifest.addons),
|
||||
apps.restoreApp.bind(null, app, app.manifest.addons, backupId),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '75, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
@@ -464,6 +534,9 @@ function restore(app, callback) {
|
||||
|
||||
// note that configure is called after an infra update as well
|
||||
function configure(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -519,6 +592,9 @@ function configure(app, callback) {
|
||||
|
||||
// nginx configuration is skipped because app.httpPort is expected to be available
|
||||
function update(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
|
||||
|
||||
// app does not want these addons anymore
|
||||
@@ -583,6 +659,9 @@ function update(app, callback) {
|
||||
}
|
||||
|
||||
function uninstall(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'uninstalling');
|
||||
|
||||
async.series([
|
||||
@@ -622,6 +701,9 @@ function uninstall(app, callback) {
|
||||
}
|
||||
|
||||
function runApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.startContainer(app.containerId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -630,6 +712,9 @@ function runApp(app, callback) {
|
||||
}
|
||||
|
||||
function stopApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.stopContainers(app.id, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -638,6 +723,9 @@ function stopApp(app, callback) {
|
||||
}
|
||||
|
||||
function handleRunCommand(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.runState === appdb.RSTATE_PENDING_STOP) {
|
||||
return stopApp(app, callback);
|
||||
}
|
||||
@@ -653,6 +741,9 @@ function handleRunCommand(app, callback) {
|
||||
}
|
||||
|
||||
function startTask(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// determine what to do
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ function getAllPaged(page, perPage, callback) {
|
||||
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
|
||||
return callback(null, backups); // [ { creationTime, restoreKey } ] sorted by time (latest first
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+34
-16
@@ -57,7 +57,7 @@ function Acme(options) {
|
||||
|
||||
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||
this.accountKeyPem = null; // Buffer
|
||||
|
||||
this.email = options.email;
|
||||
this.chainPem = options.prod ? safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt') : new Buffer('');
|
||||
}
|
||||
|
||||
@@ -125,26 +125,50 @@ Acme.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.registerUser = function (email, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
Acme.prototype.updateContact = function (registrationUri, callback) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('updateContact: %s %s', registrationUri, this.email);
|
||||
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
var payload = {
|
||||
resource: 'reg',
|
||||
contact: [ 'mailto:' + this.email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('updateContact: contact of user updated to %s', that.email);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.registerUser = function (callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-reg',
|
||||
contact: [ 'mailto:' + email ],
|
||||
contact: [ 'mailto:' + this.email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
debug('registerUser: %s', email);
|
||||
debug('registerUser: %s', this.email);
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
|
||||
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', email);
|
||||
debug('registerUser: registered user %s', that.email);
|
||||
|
||||
callback();
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -350,12 +374,6 @@ Acme.prototype.acmeFlow = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we cannot use owner email because we don't have it yet (the admin cert is fetched before activation)
|
||||
// one option is to update the owner email when a second cert is requested (https://github.com/ietf-wg-acme/acme/issues/30)
|
||||
var email = 'admin@cloudron.io';
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
@@ -368,8 +386,8 @@ Acme.prototype.acmeFlow = function (domain, callback) {
|
||||
}
|
||||
|
||||
var that = this;
|
||||
that.registerUser(email, function (error) {
|
||||
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
|
||||
this.registerUser(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
that.registerDomain(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+22
-9
@@ -17,6 +17,7 @@ var acme = require('./cert/acme.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
user = require('./user.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
x509 = require('x509');
|
||||
@@ -64,7 +65,15 @@ function getApi(callback) {
|
||||
var options = { };
|
||||
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
|
||||
callback(null, api, options);
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
user.getOwner(function (error, owner) {
|
||||
options.email = error ? 'admin@cloudron.io' : owner.email; // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,16 +85,20 @@ function installAdminCertificate(callback) {
|
||||
|
||||
if (tlsConfig.provider === 'caas') return callback();
|
||||
|
||||
waitForDns(config.adminFqdn(), sysinfo.getIp(), config.fqdn(), function (error) {
|
||||
if (error) return callback(error); // this cannot happen because we retry forever
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
||||
if (error) { // currently, this can never happen
|
||||
debug('Error obtaining certificate. Proceed anyway', error);
|
||||
return callback();
|
||||
}
|
||||
waitForDns(config.adminFqdn(), ip, config.fqdn(), function (error) {
|
||||
if (error) return callback(error); // this cannot happen because we retry forever
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
||||
if (error) { // currently, this can never happen
|
||||
debug('Error obtaining certificate. Proceed anyway', error);
|
||||
return callback();
|
||||
}
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+183
-103
@@ -13,14 +13,18 @@ exports = module.exports = {
|
||||
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
|
||||
updateToLatest: updateToLatest,
|
||||
update: update,
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
backup: backup,
|
||||
retire: retire,
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
isConfiguredSync: isConfiguredSync,
|
||||
|
||||
checkDiskSpace: checkDiskSpace,
|
||||
|
||||
events: new (require('events').EventEmitter)(),
|
||||
|
||||
EVENT_ACTIVATED: 'activated',
|
||||
@@ -33,12 +37,14 @@ var apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
bytes = require('bytes'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
df = require('node-df'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
progress = require('./progress.js'),
|
||||
@@ -59,7 +65,8 @@ var apps = require('./apps.js'),
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
|
||||
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
|
||||
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
|
||||
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
@@ -111,6 +118,7 @@ CloudronError.BAD_EMAIL = 'Bad email';
|
||||
CloudronError.BAD_PASSWORD = 'Bad password';
|
||||
CloudronError.BAD_NAME = 'Bad name';
|
||||
CloudronError.BAD_STATE = 'Bad state';
|
||||
CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||
CloudronError.NOT_FOUND = 'Not found';
|
||||
|
||||
function initialize(callback) {
|
||||
@@ -194,10 +202,11 @@ function setTimeZone(ip, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, ip, callback) {
|
||||
function activate(username, password, email, displayName, ip, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -205,7 +214,7 @@ function activate(username, password, email, ip, callback) {
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
user.createOwner(username, password, email, function (error, userObject) {
|
||||
user.createOwner(username, password, email, displayName, function (error, userObject) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
|
||||
@@ -245,6 +254,7 @@ function getStatus(callback) {
|
||||
version: config.version(),
|
||||
boxVersionsUrl: config.get('boxVersionsUrl'),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
});
|
||||
@@ -256,6 +266,15 @@ function getCloudronDetails(callback) {
|
||||
|
||||
if (gCloudronDetails) return callback(null, gCloudronDetails);
|
||||
|
||||
if (!config.token()) {
|
||||
gCloudronDetails = {
|
||||
region: null,
|
||||
size: null
|
||||
};
|
||||
|
||||
return callback(null, gCloudronDetails);
|
||||
}
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||
.query({ token: config.token() })
|
||||
@@ -283,31 +302,32 @@ function getConfig(callback) {
|
||||
};
|
||||
}
|
||||
|
||||
// We rely at the moment on the size being specified in 512mb,1gb,...
|
||||
// TODO provide that number from the appstore
|
||||
var memory = bytes(result.size) || 0;
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
settings.getDeveloperMode(function (error, developerMode) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
isDev: config.isDev(),
|
||||
fqdn: config.fqdn(),
|
||||
ip: sysinfo.getIp(),
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.get(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
developerMode: developerMode,
|
||||
region: result.region,
|
||||
size: result.size,
|
||||
memory: memory,
|
||||
cloudronName: cloudronName
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
isDev: config.isDev(),
|
||||
fqdn: config.fqdn(),
|
||||
ip: ip,
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.get(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
developerMode: developerMode,
|
||||
region: result.region,
|
||||
size: result.size,
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -315,8 +335,9 @@ function getConfig(callback) {
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
if (!config.token()) return;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
@@ -385,49 +406,53 @@ function addDnsRecords() {
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
|
||||
|
||||
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ sysinfo.getIp() ] };
|
||||
var webadminRecord = { subdomain: 'my', type: 'A', values: [ sysinfo.getIp() ] };
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
// DMARC requires special setup if report email id is in different domain
|
||||
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
var records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
} else {
|
||||
records.push(nakedDomainRecord);
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
records.push(dmarcRecord);
|
||||
}
|
||||
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
|
||||
var webadminRecord = { subdomain: 'my', type: 'A', values: [ ip ] };
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
// DMARC requires special setup if report email id is in different domain
|
||||
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
|
||||
|
||||
debug('addDnsRecords: %j', records);
|
||||
var records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
} else {
|
||||
records.push(nakedDomainRecord);
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
records.push(dmarcRecord);
|
||||
}
|
||||
|
||||
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
|
||||
txtRecordsWithSpf(function (error, txtRecords) {
|
||||
if (error) return retryCallback(error);
|
||||
debug('addDnsRecords: %j', records);
|
||||
|
||||
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
|
||||
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
|
||||
txtRecordsWithSpf(function (error, txtRecords) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
|
||||
retryCallback(error);
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
gUpdatingDns = false;
|
||||
|
||||
debug('addDnsRecords: done updating records with error:', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}, function (error) {
|
||||
gUpdatingDns = false;
|
||||
|
||||
debug('addDnsRecords: done updating records with error:', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -487,8 +512,16 @@ function update(boxUpdateInfo, callback) {
|
||||
var error = locker.lock(locker.OP_BOX_UPDATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
// ensure tools can 'wait' on progress
|
||||
progress.set(progress.UPDATE, 0, 'Starting');
|
||||
|
||||
// initiate the update/upgrade but do not wait for it
|
||||
if (boxUpdateInfo.upgrade) {
|
||||
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
|
||||
doShortCircuitUpdate(boxUpdateInfo, function (error) {
|
||||
if (error) debug('Short-circuit update failed', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
});
|
||||
} else if (boxUpdateInfo.upgrade) {
|
||||
debug('Starting upgrade');
|
||||
doUpgrade(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
@@ -509,6 +542,26 @@ function update(boxUpdateInfo, callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
|
||||
function updateToLatest(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
|
||||
|
||||
update(boxUpdateInfo, callback);
|
||||
}
|
||||
|
||||
function doShortCircuitUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
|
||||
config.setVersion(boxUpdateInfo.version);
|
||||
progress.clear(progress.UPDATE);
|
||||
updateChecker.resetUpdateInfo();
|
||||
callback();
|
||||
}
|
||||
|
||||
function doUpgrade(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
@@ -532,8 +585,8 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
|
||||
callback(null);
|
||||
callback();
|
||||
retire();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -551,53 +604,44 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// fetch a signed sourceTarballUrl
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
|
||||
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return updateError(new Error('Network error fetching sourceTarballUrl: ' + error));
|
||||
if (result.statusCode !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.statusCode));
|
||||
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
|
||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||
var args = {
|
||||
sourceTarballUrl: boxUpdateInfo.sourceTarballUrl,
|
||||
|
||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||
var args = {
|
||||
sourceTarballUrl: result.body.url,
|
||||
// this data is opaque to the installer
|
||||
data: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
|
||||
// this data is opaque to the installer
|
||||
data: {
|
||||
appstore: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
appstore: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
version: boxUpdateInfo.version,
|
||||
boxVersionsUrl: config.get('boxVersionsUrl')
|
||||
}
|
||||
};
|
||||
|
||||
version: boxUpdateInfo.version,
|
||||
boxVersionsUrl: config.get('boxVersionsUrl')
|
||||
}
|
||||
};
|
||||
debug('updating box %j', args);
|
||||
|
||||
debug('updating box %j', args);
|
||||
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
||||
if (error && !error.response) return updateError(error);
|
||||
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
|
||||
|
||||
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
||||
if (error && !error.response) return updateError(error);
|
||||
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
|
||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
@@ -610,8 +654,8 @@ function backup(callback) {
|
||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
// clearing backup ensures tools can 'wait' on progress
|
||||
progress.clear(progress.BACKUP);
|
||||
// ensure tools can 'wait' on progress
|
||||
progress.set(progress.BACKUP, 0, 'Starting');
|
||||
|
||||
// start the backup operation in the background
|
||||
backupBoxAndApps(function (error) {
|
||||
@@ -624,7 +668,7 @@ function backup(callback) {
|
||||
}
|
||||
|
||||
function ensureBackup(callback) {
|
||||
callback = callback || function () { };
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
backups.getAllPaged(1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
@@ -681,7 +725,7 @@ function backupBox(callback) {
|
||||
|
||||
// this function expects you to have a lock
|
||||
function backupBoxAndApps(callback) {
|
||||
callback = callback || function () { }; // callback can be empty for timer triggered backup
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
@@ -719,3 +763,39 @@ function backupBoxAndApps(callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('Checking disk space');
|
||||
|
||||
df(function (error, entries) {
|
||||
if (error) {
|
||||
debug('df error %s', error.message);
|
||||
mailer.outOfDiskSpace(error.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
var oos = entries.some(function (entry) {
|
||||
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
|
||||
(entry.mount === '/' && entry.used <= (1.25 * 1024 * 1024)); // 1.5G
|
||||
});
|
||||
|
||||
debug('Disk space checked. ok: %s', !oos);
|
||||
|
||||
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function retire(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var data = {
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
fqdn: config.fqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
|
||||
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker/<%= containerId %>/cpuacct.stat">
|
||||
Instance "<%= appId %>-cpu"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
|
||||
@@ -17,11 +17,13 @@ exports = module.exports = {
|
||||
TEST: process.env.BOX_ENV === 'test',
|
||||
|
||||
// convenience getters
|
||||
provider: provider,
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
fqdn: fqdn,
|
||||
token: token,
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
isCustomDomain: isCustomDomain,
|
||||
database: database,
|
||||
|
||||
@@ -82,6 +84,7 @@ function initConfig() {
|
||||
data.ldapPort = 3002;
|
||||
data.oauthProxyPort = 3003;
|
||||
data.simpleAuthPort = 3004;
|
||||
data.provider = 'caas';
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
@@ -98,6 +101,7 @@ function initConfig() {
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.adminEmail = 'test@cloudron.foo';
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
}
|
||||
@@ -176,6 +180,10 @@ function version() {
|
||||
return get('version');
|
||||
}
|
||||
|
||||
function setVersion(version) {
|
||||
set('version', version);
|
||||
}
|
||||
|
||||
function isCustomDomain() {
|
||||
return get('isCustomDomain');
|
||||
}
|
||||
@@ -194,3 +202,7 @@ function database() {
|
||||
function isDev() {
|
||||
return /dev/i.test(get('boxVersionsUrl'));
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
|
||||
+10
-1
@@ -25,7 +25,8 @@ var gAutoupdaterJob = null,
|
||||
gCleanupTokensJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gSchedulerSyncJob = null,
|
||||
gCertificateRenewJob = null;
|
||||
gCertificateRenewJob = null,
|
||||
gCheckDiskSpaceJob = null;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
@@ -69,6 +70,14 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop();
|
||||
gCheckDiskSpaceJob = new CronJob({
|
||||
cronTime: '00 30 */4 * * *', // every 4 hours
|
||||
onTick: cloudron.checkDiskSpace,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
|
||||
gBoxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 */10 * * * *', // every 10 minutes
|
||||
|
||||
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:developer'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -79,6 +80,10 @@ function getNonApprovedApps(callback) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
|
||||
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401) {
|
||||
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
|
||||
return callback(null, []);
|
||||
}
|
||||
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body.apps || []);
|
||||
|
||||
+19
-23
@@ -17,8 +17,10 @@ exports = module.exports = {
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
stopContainer: stopContainer,
|
||||
stopContainerByName: stopContainer,
|
||||
stopContainers: stopContainers,
|
||||
deleteContainer: deleteContainer,
|
||||
deleteContainerByName: deleteContainer,
|
||||
deleteImage: deleteImage,
|
||||
deleteContainers: deleteContainers,
|
||||
createSubcontainer: createSubcontainer
|
||||
@@ -52,7 +54,7 @@ function targetBoxVersion(manifest) {
|
||||
|
||||
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
|
||||
|
||||
return '0.0.1';
|
||||
return '99999.99999.99999'; // compatible with the latest version
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
@@ -128,6 +130,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
isAppContainer = !cmd;
|
||||
|
||||
var manifest = app.manifest;
|
||||
var developmentMode = !!manifest.developmentMode;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
@@ -153,7 +156,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
var memoryLimit = manifest.memoryLimit || (developmentMode ? 0 : 1024 * 1024 * 200); // 200mb by default
|
||||
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
|
||||
// this means cloudron exec does not work
|
||||
var isolatedNetworkNs = true;
|
||||
@@ -168,7 +171,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: cmd,
|
||||
Cmd: (isAppContainer && developmentMode) ? [ '/bin/bash', '-c', 'echo "Development mode. Use cloudron exec to debug. Sleeping" && sleep infinity' ] : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv),
|
||||
ExposedPorts: isAppContainer ? exposedPorts : { },
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
@@ -186,7 +189,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
|
||||
ReadonlyRootfs: !developmentMode, // see also Volumes in startContainer
|
||||
RestartPolicy: {
|
||||
"Name": isAppContainer ? "always" : "no",
|
||||
"MaximumRetryCount": 0
|
||||
@@ -200,9 +203,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
};
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
|
||||
// older versions wanted a writable /var/log
|
||||
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
|
||||
|
||||
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
|
||||
|
||||
docker.createContainer(containerOptions, callback);
|
||||
@@ -329,24 +329,20 @@ function deleteImage(manifest, callback) {
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
docker.getImage(dockerImage).inspect(function (error, result) {
|
||||
var removeOptions = {
|
||||
force: false, // might be shared with another instance of this app
|
||||
noprune: false // delete untagged parents
|
||||
};
|
||||
|
||||
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
|
||||
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
|
||||
// not created anymore after https://github.com/docker/docker/pull/10571
|
||||
docker.getImage(dockerImage).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
|
||||
if (error) return callback(error);
|
||||
if (error) debug('Error removing image %s : %j', dockerImage, error);
|
||||
|
||||
var removeOptions = {
|
||||
force: true,
|
||||
noprune: false
|
||||
};
|
||||
|
||||
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
|
||||
docker.getImage(result.Id).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
|
||||
if (error) debug('Error removing image %s : %j', dockerImage, error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
+5
-3
@@ -54,7 +54,7 @@ function start(callback) {
|
||||
cn: entry.id,
|
||||
uid: entry.id,
|
||||
mail: entry.email,
|
||||
displayname: entry.username,
|
||||
displayname: entry.displayName || entry.username,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
memberof: groups
|
||||
@@ -115,9 +115,11 @@ function start(callback) {
|
||||
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
|
||||
debug('ldap user bind: %s', req.dn.toString());
|
||||
|
||||
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
// extract the common name which might have different attribute names
|
||||
var commonName = req.dn.rdns[0][Object.keys(req.dn.rdns[0])[0]];
|
||||
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
user.verify(req.dn.rdns[0].cn, req.credentials || '', function (error, result) {
|
||||
user.verify(commonName, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error));
|
||||
|
||||
@@ -4,10 +4,10 @@ Dear Admin,
|
||||
|
||||
A new version of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
Please update at your convenience at <%= webadminUrl %>.
|
||||
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
Thank you,
|
||||
Update Manager
|
||||
your Cloudron
|
||||
|
||||
<% } else { %>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Dear Admin,
|
||||
|
||||
A new version of Cloudron <%= fqdn %> is available!
|
||||
|
||||
Please update at your convenience at <%= webadminUrl %>.
|
||||
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
@@ -12,7 +12,7 @@ Changelog:
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
Update Manager
|
||||
your Cloudron
|
||||
|
||||
<% } else { %>
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Cloudron Team,
|
||||
|
||||
<%= fqdn %> is running out of disk space.
|
||||
|
||||
Please see some excerpts of the logs below.
|
||||
|
||||
Thank you,
|
||||
Your Cloudron
|
||||
|
||||
-------------------------------------
|
||||
|
||||
<%- message %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Admin,
|
||||
|
||||
User with name '<%= username %>' (<%= email %>) was added in the Cloudron at <%= fqdn %>.
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
This user was not invited immediately, he has to get invited manually later, using the "send invite" button in the admin panel.
|
||||
To perform any configuration on behalf of the user, please use this link
|
||||
<%= inviteLink %>
|
||||
It allows to setup a temporary password, which the user will be able to override, once he gets invited.
|
||||
This link will become invalid as soon as the user was invited.
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
User Manager
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -15,8 +15,12 @@ To get started, create your account by visiting the following page:
|
||||
When you visit the above page, you will be prompted to enter a new password.
|
||||
After you have submitted the form, you can login using the new password.
|
||||
|
||||
<% if (invitor && invitor.email) { %>
|
||||
Thank you,
|
||||
<%= invitor.email %>
|
||||
<% } else { %>
|
||||
Thank you
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
|
||||
+58
-6
@@ -13,14 +13,20 @@ exports = module.exports = {
|
||||
boxUpdateAvailable: boxUpdateAvailable,
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
|
||||
sendInvite: sendInvite,
|
||||
sendCrashNotification: sendCrashNotification,
|
||||
|
||||
appDied: appDied,
|
||||
|
||||
outOfDiskSpace: outOfDiskSpace,
|
||||
|
||||
FEEDBACK_TYPE_FEEDBACK: 'feedback',
|
||||
FEEDBACK_TYPE_TICKET: 'ticket',
|
||||
FEEDBACK_TYPE_APP: 'app',
|
||||
sendFeedback: sendFeedback
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
_clearMailQueue: _clearMailQueue
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -35,7 +41,7 @@ var assert = require('assert'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
userdb = require('./userdb.js'),
|
||||
users = require('./user.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -170,6 +176,9 @@ function sendMails(queue) {
|
||||
function enqueue(mailOptions) {
|
||||
assert.strictEqual(typeof mailOptions, 'object');
|
||||
|
||||
if (!mailOptions.from) console.error('from is missing');
|
||||
if (!mailOptions.to) console.error('to is missing');
|
||||
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
|
||||
@@ -184,7 +193,7 @@ function render(templateFile, params) {
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
userdb.getAllAdmins(function (error, admins) {
|
||||
users.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var adminEmails = [ ];
|
||||
@@ -214,11 +223,11 @@ function mailUserEventToAdmins(user, event) {
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(user, invitor) {
|
||||
function sendInvite(user, invitor) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert(typeof invitor === 'object');
|
||||
|
||||
debug('Sending mail for userAdded');
|
||||
debug('Sending invite mail');
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
@@ -237,8 +246,30 @@ function userAdded(user, invitor) {
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
mailUserEventToAdmins(user, 'was added');
|
||||
function userAdded(user, inviteSent) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof inviteSent, 'boolean');
|
||||
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/password/setup.html?reset_token=' + user.resetToken;
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s added in Cloudron %s', user.username, config.fqdn()),
|
||||
text: render('user_added.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, inviteLink: inviteLink, format: 'text' }),
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function userRemoved(username) {
|
||||
@@ -329,6 +360,19 @@ function appUpdateAvailable(app, updateInfo) {
|
||||
});
|
||||
}
|
||||
|
||||
function outOfDiskSpace(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: 'admin@cloudron.io',
|
||||
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
|
||||
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
}
|
||||
|
||||
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
|
||||
// crashnotifier should be able to send mail when there is no db
|
||||
function sendCrashNotification(program, context) {
|
||||
@@ -362,3 +406,11 @@ function sendFeedback(user, type, subject, description) {
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
function _getMailQueue() {
|
||||
return gMailQueue;
|
||||
}
|
||||
|
||||
function _clearMailQueue() {
|
||||
gMailQueue = [];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title> Cloudron Login </title>
|
||||
<title> <%= title %> </title>
|
||||
|
||||
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
|
||||
@@ -25,10 +25,16 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (resetForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
|
||||
@@ -25,10 +25,16 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (setupForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
// From https://www.npmjs.com/package/password-generator
|
||||
|
||||
exports = module.exports = {
|
||||
generate: generate,
|
||||
validate: validate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
generatePassword = require('password-generator');
|
||||
|
||||
// http://www.w3resource.com/javascript/form/example4-javascript-form-validation-password.html
|
||||
// WARNING!!! if this is changed, the UI parts in the setup and account view have to be adjusted!
|
||||
var gPasswordTestRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/;
|
||||
|
||||
var UPPERCASE_RE = /([A-Z])/g;
|
||||
var LOWERCASE_RE = /([a-z])/g;
|
||||
var NUMBER_RE = /([\d])/g;
|
||||
var SPECIAL_CHAR_RE = /([\?\-])/g;
|
||||
|
||||
function isStrongEnough(password) {
|
||||
var uc = password.match(UPPERCASE_RE);
|
||||
var lc = password.match(LOWERCASE_RE);
|
||||
var n = password.match(NUMBER_RE);
|
||||
var sc = password.match(SPECIAL_CHAR_RE);
|
||||
|
||||
return uc && lc && n && sc;
|
||||
}
|
||||
|
||||
function generate() {
|
||||
var password = '';
|
||||
|
||||
while (!isStrongEnough(password)) password = generatePassword(8, false, /[\w\d\?\-]/);
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
function validate(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (!password.match(gPasswordTestRegExp)) return new Error('Password must be 8-30 character with at least one uppercase, one numeric and one special character');
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -12,7 +12,6 @@ exports = module.exports = {
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
|
||||
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
|
||||
SCHEDULER_FILE: path.join(config.baseDir(), 'data/addons/scheduler.json'),
|
||||
|
||||
DNS_IN_SYNC_FILE: path.join(config.baseDir(), 'data/dns_in_sync'),
|
||||
|
||||
|
||||
+14
-3
@@ -15,6 +15,7 @@ exports = module.exports = {
|
||||
updateApp: updateApp,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
listBackups: listBackups,
|
||||
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
@@ -357,7 +358,9 @@ function exec(req, res, next) {
|
||||
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
||||
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns }, function (error, duplexStream) {
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -369,8 +372,16 @@ function exec(req, res, next) {
|
||||
|
||||
duplexStream.pipe(res.socket);
|
||||
res.socket.pipe(duplexStream);
|
||||
|
||||
res.on('close', duplexStream.close);
|
||||
});
|
||||
}
|
||||
|
||||
function listBackups(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
apps.listBackups(req.params.id, function (error, result) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
});
|
||||
}
|
||||
|
||||
+15
-10
@@ -23,8 +23,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:routes/cloudron'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
superagent = require('superagent'),
|
||||
updateChecker = require('../updatechecker.js');
|
||||
superagent = require('superagent');
|
||||
|
||||
/**
|
||||
* Creating an admin user and activate the cloudron.
|
||||
@@ -42,27 +41,32 @@ function activate(req, res, next) {
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
var username = req.body.username;
|
||||
var password = req.body.password;
|
||||
var email = req.body.email;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
debug('activate: username:%s ip:%s', username, ip);
|
||||
|
||||
cloudron.activate(username, password, email, ip, function (error, info) {
|
||||
cloudron.activate(username, password, email, displayName, ip, function (error, info) {
|
||||
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
|
||||
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
|
||||
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// only in caas case do we have to notify the api server about activation
|
||||
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken:req.query.setupToken }).end(function (error, result) {
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken }).end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
|
||||
next(new HttpSuccess(201, info));
|
||||
});
|
||||
@@ -72,13 +76,16 @@ function activate(req, res, next) {
|
||||
function setupTokenAuth(req, res, next) {
|
||||
assert.strictEqual(typeof req.query, 'object');
|
||||
|
||||
// skip setupToken auth for non caas case
|
||||
if (config.provider() !== 'caas') return next();
|
||||
|
||||
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken }).end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
|
||||
next();
|
||||
});
|
||||
@@ -112,11 +119,9 @@ function getConfig(req, res, next) {
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return next(new HttpError(422, 'No update available'));
|
||||
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
cloudron.update(boxUpdateInfo, function (error) {
|
||||
cloudron.updateToLatest(function (error) {
|
||||
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
+30
-2
@@ -3,7 +3,9 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
backup: backup
|
||||
backup: backup,
|
||||
update: update,
|
||||
retire: retire
|
||||
};
|
||||
|
||||
var cloudron = require('../cloudron.js'),
|
||||
@@ -13,7 +15,7 @@ var cloudron = require('../cloudron.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function backup(req, res, next) {
|
||||
debug('trigger backup');
|
||||
debug('triggering backup');
|
||||
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
@@ -24,3 +26,29 @@ function backup(req, res, next) {
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
debug('triggering update');
|
||||
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
cloudron.updateToLatest(function (error) {
|
||||
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function retire(req, res, next) {
|
||||
debug('triggering retire');
|
||||
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
cloudron.retire(function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
+15
-8
@@ -150,14 +150,16 @@ function sendErrorPageOrRedirect(req, res, message) {
|
||||
if (typeof req.query.returnTo !== 'string') {
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
message: message,
|
||||
title: 'Cloudron Error'
|
||||
});
|
||||
} else {
|
||||
var u = url.parse(req.query.returnTo);
|
||||
if (!u.protocol || !u.host) {
|
||||
return renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message,
|
||||
title: 'Cloudron Error'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,7 +176,8 @@ function sendError(req, res, message) {
|
||||
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
message: message,
|
||||
title: 'Cloudron Error'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,7 +194,8 @@ function loginForm(req, res) {
|
||||
csrf: req.csrfToken(),
|
||||
applicationName: applicationName,
|
||||
applicationLogo: applicationLogo,
|
||||
error: req.query.error || null
|
||||
error: req.query.error || null,
|
||||
title: applicationName + ' Login'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,7 +241,7 @@ function logout(req, res) {
|
||||
// Form to enter email address to send a password reset request mail
|
||||
// -> GET /api/v1/session/password/resetRequest.html
|
||||
function passwordResetRequestSite(req, res) {
|
||||
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
|
||||
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken(), title: 'Cloudron Password Reset' });
|
||||
}
|
||||
|
||||
// This route is used for above form submission
|
||||
@@ -261,7 +265,7 @@ function passwordResetRequest(req, res, next) {
|
||||
|
||||
// -> GET /api/v1/session/password/sent.html
|
||||
function passwordSentSite(req, res) {
|
||||
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin() });
|
||||
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin(), title: 'Cloudron Password Reset' });
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/setup.html
|
||||
@@ -275,7 +279,8 @@ function passwordSetupSite(req, res, next) {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token
|
||||
resetToken: req.query.reset_token,
|
||||
title: 'Cloudron Password Setup'
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -291,7 +296,8 @@ function passwordResetSite(req, res, next) {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token
|
||||
resetToken: req.query.reset_token,
|
||||
title: 'Cloudron Password Reset'
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -310,6 +316,7 @@ function passwordReset(req, res, next) {
|
||||
|
||||
// setPassword clears the resetToken
|
||||
user.setPassword(userObject.id, req.body.password, function (error, result) {
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(406, 'Password does not meet the requirements'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
|
||||
|
||||
@@ -42,7 +42,8 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
// Test image information
|
||||
var TEST_IMAGE_REPO = 'cloudron/test';
|
||||
var TEST_IMAGE_TAG = '10.0.0';
|
||||
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG).toString('utf8').trim();
|
||||
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
|
||||
|
||||
var APP_STORE_ID = 'test', APP_ID;
|
||||
var APP_LOCATION = 'appslocation';
|
||||
@@ -50,14 +51,14 @@ var APP_LOCATION_2 = 'appslocationtwo';
|
||||
var APP_LOCATION_NEW = 'appslocationnew';
|
||||
|
||||
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
|
||||
APP_MANIFEST.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
APP_MANIFEST.dockerImage = TEST_IMAGE;
|
||||
|
||||
var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
|
||||
APP_MANIFEST_1.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
APP_MANIFEST_1.dockerImage = TEST_IMAGE;
|
||||
APP_MANIFEST_1.singleUser = true;
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='admin@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1338', EMAIL_1 ='user@me.com';
|
||||
var token = null; // authentication token
|
||||
var token_1 = null;
|
||||
|
||||
@@ -138,7 +139,7 @@ function setup(done) {
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
@@ -176,7 +177,7 @@ describe('App API', function () {
|
||||
|
||||
before(function (done) {
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
|
||||
if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
@@ -573,7 +574,7 @@ describe('App installation', function () {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
|
||||
imageDeleted = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
@@ -941,14 +942,17 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('did start the app', function (done) {
|
||||
setTimeout(function () {
|
||||
var count = 0;
|
||||
function checkStartState() {
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
expect(!err).to.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
if (res && res.statusCode === 200) return done();
|
||||
if (++count > 50) return done(new Error('Timedout'));
|
||||
setTimeout(checkStartState, 500);
|
||||
});
|
||||
}, 2000); // give some time for docker to settle
|
||||
}
|
||||
|
||||
checkStartState();
|
||||
});
|
||||
|
||||
it('can uninstall app', function (done) {
|
||||
@@ -1040,7 +1044,7 @@ describe('App installation - port bindings', function () {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
|
||||
imageDeleted = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
|
||||
@@ -19,7 +19,7 @@ var appdb = require('../../appdb.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
|
||||
@@ -20,7 +20,7 @@ var async = require('async'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
function cleanup(done) {
|
||||
@@ -392,7 +392,7 @@ describe('Clients', function () {
|
||||
var USER_0 = {
|
||||
userId: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
password: 'Strong#$%2345',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
|
||||
@@ -11,13 +11,14 @@ var async = require('async'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
os = require('os'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
shell = require('../../shell.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
var server;
|
||||
@@ -62,12 +63,25 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to internal server error on appstore side', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(500);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty username', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -93,7 +107,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: '' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -101,12 +115,12 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty name', function (done) {
|
||||
it('fails due to wrong displayName type', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar', name: '' })
|
||||
.send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -119,7 +133,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'invalidemail' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -133,7 +147,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar', name: 'tester' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
@@ -147,7 +161,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(409);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -209,7 +223,7 @@ describe('Cloudron', function () {
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql(null);
|
||||
expect(result.body.region).to.eql(null);
|
||||
expect(result.body.memory).to.eql(0);
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
done();
|
||||
@@ -233,7 +247,7 @@ describe('Cloudron', function () {
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql('1gb');
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
expect(result.body.memory).to.eql(1073741824);
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
@@ -17,7 +17,7 @@ var async = require('async'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
var server;
|
||||
|
||||
@@ -139,13 +139,14 @@ describe('OAuth2', function () {
|
||||
var USER_0 = {
|
||||
id: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
password: '@#45Strongpassword',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var APP_0 = {
|
||||
@@ -291,7 +292,7 @@ describe('OAuth2', function () {
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy),
|
||||
function (callback) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, true, '', function (error, userObject) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, true, '', false, function (error, userObject) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// update the global objects to reflect the new user id
|
||||
@@ -1239,13 +1240,14 @@ describe('Password', function () {
|
||||
var USER_0 = {
|
||||
userId: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
password: 'passWord%1234',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
// make csrf always succeed for testing
|
||||
@@ -1405,6 +1407,15 @@ describe('Password', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to weak password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'foobar', resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(406);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.adminOrigin())
|
||||
.filteringPath(function (path) {
|
||||
@@ -1415,7 +1426,7 @@ describe('Password', function () {
|
||||
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'somepassword', resetToken: USER_0.resetToken })
|
||||
.send({ password: 'ASF23$%somepassword', resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('SimpleAuth API', function () {
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
|
||||
var APP_0 = {
|
||||
id: 'app0',
|
||||
|
||||
+115
-12
@@ -10,6 +10,7 @@ var config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../../mailer.js'),
|
||||
superagent = require('superagent'),
|
||||
nock = require('nock'),
|
||||
server = require('../../server.js'),
|
||||
@@ -17,7 +18,7 @@ var config = require('../../config.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'password', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com';
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'Foobar?1337', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com', DISPLAY_NAME_0_NEW = 'New Name';
|
||||
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'tao@zen.mac';
|
||||
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar';
|
||||
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@foo.bar';
|
||||
@@ -26,6 +27,9 @@ var server;
|
||||
function setup(done) {
|
||||
server.start(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
userdb._clear(done);
|
||||
});
|
||||
}
|
||||
@@ -34,10 +38,21 @@ function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
expect(mailer._getMailQueue().length).to.equal(number);
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
describe('User API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
@@ -188,6 +203,7 @@ describe('User API', function () {
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.displayName).to.be.a('string');
|
||||
expect(res.body.password).to.not.be.ok();
|
||||
expect(res.body.salt).to.not.be.ok();
|
||||
done();
|
||||
@@ -213,15 +229,45 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('create second user succeeds', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
checkMails(2, function () {
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reinvite unknown user fails', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(res.statusCode).to.equal(404);
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('reinvite second user succeeds', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,21 +342,36 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
it('create user missing invite fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3 })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
|
||||
checkMails(3, function () {
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -331,7 +392,7 @@ describe('User API', function () {
|
||||
it('create user with same username should fail', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL })
|
||||
.send({ username: USERNAME_2, email: EMAIL, invite: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
@@ -459,13 +520,55 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done(error);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL_0_NEW);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.displayName).to.equal('');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('change displayName succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, displayName: DISPLAY_NAME_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL_0_NEW);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -493,7 +596,7 @@ describe('User API', function () {
|
||||
it('change password fails due to wrong password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'some wrong password', newPassword: 'newpassword' })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
@@ -513,7 +616,7 @@ describe('User API', function () {
|
||||
it('change password succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'new_password' })
|
||||
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
|
||||
+36
-19
@@ -12,27 +12,18 @@ exports = module.exports = {
|
||||
changeAdmin: changeAdmin,
|
||||
remove: removeUser,
|
||||
verifyPassword: verifyPassword,
|
||||
requireAdmin: requireAdmin
|
||||
requireAdmin: requireAdmin,
|
||||
sendInvite: sendInvite
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
generatePassword = require('../password.js').generate,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
user = require('../user.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
UserError = user.UserError;
|
||||
|
||||
// http://stackoverflow.com/questions/1497481/javascript-password-generator#1497512
|
||||
function generatePassword() {
|
||||
var length = 8,
|
||||
charset = 'abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
||||
retVal = '';
|
||||
for (var i = 0, n = charset.length; i < length; ++i) {
|
||||
retVal += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function profile(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
@@ -44,6 +35,7 @@ function profile(req, res, next) {
|
||||
result.username = req.user.username;
|
||||
result.email = req.user.email;
|
||||
result.admin = req.user.admin;
|
||||
result.displayName = req.user.displayName;
|
||||
}
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
@@ -54,12 +46,16 @@ function createUser(req, res, next) {
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
var username = req.body.username;
|
||||
var password = generatePassword();
|
||||
var email = req.body.email;
|
||||
var sendInvite = req.body.invite;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
user.create(username, password, email, false /* admin */, req.user /* creator */, function (error, user) {
|
||||
user.create(username, password, email, displayName, false /* admin */, req.user /* creator */, sendInvite, function (error, user) {
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
|
||||
@@ -80,19 +76,26 @@ function createUser(req, res, next) {
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
|
||||
user.update(req.user.id, req.user.username, req.body.email, function (error) {
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
user.get(req.params.userId, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, function (error) {
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,7 +150,8 @@ function info(req, res, next) {
|
||||
id: result.id,
|
||||
username: result.username,
|
||||
email: result.email,
|
||||
admin: result.admin
|
||||
admin: result.admin,
|
||||
displayName: result.displayName
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -178,6 +182,9 @@ function verifyPassword(req, res, next) {
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
|
||||
|
||||
// Only allow admins or users, operating on themselves
|
||||
if (req.params.userId && !(req.user.id === req.params.userId || req.user.admin)) return next(new HttpError(403, 'Not allowed'));
|
||||
|
||||
user.verify(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
|
||||
@@ -198,3 +205,13 @@ function requireAdmin(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function sendInvite(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
user.sendInvite(req.params.userId, function (error) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
+22
-49
@@ -12,29 +12,12 @@ var appdb = require('./appdb.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:src/scheduler'),
|
||||
docker = require('./docker.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug('Unhandled error: ', error); };
|
||||
|
||||
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
|
||||
var gState = (function loadState() {
|
||||
var state = safe.JSON.parse(safe.fs.readFileSync(paths.SCHEDULER_FILE, 'utf8'));
|
||||
return state || { };
|
||||
})();
|
||||
|
||||
function saveState(state) {
|
||||
// do not save cronJobs
|
||||
var safeState = { };
|
||||
for (var appId in state) {
|
||||
safeState[appId] = {
|
||||
schedulerConfig: state[appId].schedulerConfig,
|
||||
containerIds: state[appId].containerIds
|
||||
};
|
||||
}
|
||||
safe.fs.writeFileSync(paths.SCHEDULER_FILE, JSON.stringify(safeState, null, 4), 'utf8');
|
||||
}
|
||||
// appId -> { schedulerConfig (manifest), cronjobs }
|
||||
var gState = { };
|
||||
|
||||
function sync(callback) {
|
||||
assert(!callback || typeof callback === 'function');
|
||||
@@ -46,17 +29,18 @@ function sync(callback) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// stop tasks of apps that went away
|
||||
var allAppIds = allApps.map(function (app) { return app.id; });
|
||||
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
|
||||
if (removedAppIds.length !== 0) debug('sync: stopping jobs of removed apps %j', removedAppIds);
|
||||
|
||||
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
|
||||
stopJobs(appId, gState[appId], true /* killContainers */, iteratorDone);
|
||||
stopJobs(appId, gState[appId], iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) debug('Error stopping jobs : %j', error);
|
||||
if (error) debug('Error stopping jobs of removed apps', error);
|
||||
|
||||
gState = _.omit(gState, removedAppIds);
|
||||
|
||||
// start tasks of new apps
|
||||
debug('sync: checking apps %j', allAppIds);
|
||||
async.eachSeries(allApps, function (app, iteratorDone) {
|
||||
var appState = gState[app.id] || null;
|
||||
var schedulerConfig = app.manifest.addons.scheduler || null;
|
||||
@@ -67,8 +51,8 @@ function sync(callback) {
|
||||
return iteratorDone(); // nothing changed
|
||||
}
|
||||
|
||||
var killContainers = appState && !appState.cronJobs ? true : false; // keep the old containers on 'startup'
|
||||
stopJobs(app.id, appState, killContainers, function (error) {
|
||||
debug('sync: app %s changed', app.id);
|
||||
stopJobs(app.id, appState, function (error) {
|
||||
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
|
||||
|
||||
if (!schedulerConfig) {
|
||||
@@ -78,12 +62,9 @@ function sync(callback) {
|
||||
|
||||
gState[app.id] = {
|
||||
schedulerConfig: schedulerConfig,
|
||||
cronJobs: createCronJobs(app.id, schedulerConfig),
|
||||
containerIds: { }
|
||||
cronJobs: createCronJobs(app.id, schedulerConfig)
|
||||
};
|
||||
|
||||
saveState(gState);
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
});
|
||||
@@ -93,23 +74,22 @@ function sync(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function killContainer(containerId, callback) {
|
||||
if (!containerId) return callback();
|
||||
function killContainer(containerName, callback) {
|
||||
if (!containerName) return callback();
|
||||
|
||||
async.series([
|
||||
docker.stopContainer.bind(null, containerId),
|
||||
docker.deleteContainer.bind(null, containerId)
|
||||
docker.stopContainerByName.bind(null, containerName),
|
||||
docker.deleteContainerByName.bind(null, containerName)
|
||||
], function (error) {
|
||||
if (error) debug('Failed to kill task with containerId %s : %s', containerId, error.message);
|
||||
if (error) debug('Failed to kill task with name %s : %s', containerName, error.message);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function stopJobs(appId, appState, killContainers, callback) {
|
||||
function stopJobs(appId, appState, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appState, 'object');
|
||||
assert.strictEqual(typeof killContainers, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('stopJobs for %s', appId);
|
||||
@@ -121,9 +101,8 @@ function stopJobs(appId, appState, killContainers, callback) {
|
||||
appState.cronJobs[taskName].stop();
|
||||
}
|
||||
|
||||
if (!killContainers) return iteratorDone();
|
||||
|
||||
killContainer(appState.containerIds[taskName], iteratorDone);
|
||||
var containerName = appId + '-' + taskName;
|
||||
killContainer(containerName, iteratorDone);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -161,8 +140,6 @@ function doTask(appId, taskName, callback) {
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var appState = gState[appId];
|
||||
|
||||
debug('Executing task %s/%s', appId, taskName);
|
||||
|
||||
apps.get(appId, function (error, app) {
|
||||
@@ -173,21 +150,17 @@ function doTask(appId, taskName, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (appState.containerIds[taskName]) debug('task %s/%s has existing container %s. killing it', appId, taskName, appState.containerIds[taskName]);
|
||||
var containerName = app.id + '-' + taskName;
|
||||
|
||||
killContainer(appState.containerIds[taskName], function (error) {
|
||||
killContainer(containerName, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
|
||||
debug('Creating subcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
|
||||
|
||||
// NOTE: if you change container name here, fix addons.js to return correct container names
|
||||
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
|
||||
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appState.containerIds[taskName] = container.id;
|
||||
|
||||
saveState(gState);
|
||||
|
||||
docker.startContainer(container.id, callback);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ for try in `seq 1 5`; do
|
||||
|
||||
if tar -cvzf - -C "${app_data_snapshot}" . \
|
||||
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
|
||||
| curl --fail -X PUT "${headers[@]}" --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
| curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
|
||||
@@ -21,7 +21,7 @@ readonly program_name=$1
|
||||
|
||||
echo "${program_name}.log"
|
||||
echo "-------------------"
|
||||
journalctl --no-pager -u ${program_name} -n 100
|
||||
journalctl --all --no-pager -u ${program_name} -n 100
|
||||
echo
|
||||
echo
|
||||
echo "dmesg"
|
||||
@@ -31,7 +31,7 @@ echo
|
||||
echo
|
||||
echo "docker"
|
||||
echo "------"
|
||||
journalctl --no-pager -u docker -n 50
|
||||
journalctl --all --no-pager -u docker -n 50
|
||||
echo
|
||||
echo
|
||||
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is called once at the end of a cloudrons lifetime
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly BOX_SRC_DIR=/home/yellowtent/box
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Retiring cloudron"
|
||||
|
||||
if [[ "${BOX_ENV}" != "cloudron" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire --data "$1" # show splash
|
||||
|
||||
echo "Stopping apps"
|
||||
systemctl stop docker # stop the apps
|
||||
|
||||
echo "Stopping installer"
|
||||
systemctl stop cloudron-installer # stop the installer
|
||||
|
||||
# do this at the end since stopping the box will kill this script as well
|
||||
echo "Stopping Cloudron Smartserver"
|
||||
"${BOX_SRC_DIR}/setup/stop.sh"
|
||||
+8
-1
@@ -34,7 +34,7 @@ function initializeExpressSync() {
|
||||
var QUERY_LIMIT = '10mb', // max size for json and urlencoded queries
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests (see also setTimeout on the httpServer)
|
||||
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
@@ -108,6 +108,7 @@ function initializeExpressSync() {
|
||||
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
|
||||
router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password
|
||||
router.post('/api/v1/users/:userId/admin', usersScope, routes.user.requireAdmin, routes.user.changeAdmin);
|
||||
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
|
||||
|
||||
// form based login routes used by oauth2 frame
|
||||
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
|
||||
@@ -143,6 +144,7 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.updateApp);
|
||||
router.post('/api/v1/apps/:id/restore', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.restoreApp);
|
||||
router.post('/api/v1/apps/:id/backup', appsScope, routes.user.requireAdmin, routes.apps.backupApp);
|
||||
router.get ('/api/v1/apps/:id/backups', appsScope, routes.user.requireAdmin, routes.apps.listBackups);
|
||||
router.post('/api/v1/apps/:id/stop', appsScope, routes.user.requireAdmin, routes.apps.stopApp);
|
||||
router.post('/api/v1/apps/:id/start', appsScope, routes.user.requireAdmin, routes.apps.startApp);
|
||||
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
|
||||
@@ -170,6 +172,9 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.create);
|
||||
|
||||
// disable server timeout. we use the timeout middleware to handle timeouts on a route level
|
||||
httpServer.setTimeout(0);
|
||||
|
||||
// upgrade handler
|
||||
httpServer.on('upgrade', function (req, socket, head) {
|
||||
if (req.headers['upgrade'] !== 'tcp') return req.end('Only TCP upgrades are possible');
|
||||
@@ -221,6 +226,8 @@ function initializeInternalExpressSync() {
|
||||
|
||||
// internal routes
|
||||
router.post('/api/v1/backup', routes.internal.backup);
|
||||
router.post('/api/v1/update', routes.internal.update);
|
||||
router.post('/api/v1/retire', routes.internal.retire);
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ exports = module.exports = {
|
||||
getTlsConfig: getTlsConfig,
|
||||
setTlsConfig: setTlsConfig,
|
||||
|
||||
getUpdateConfig: getUpdateConfig,
|
||||
setUpdateConfig: setUpdateConfig,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getAll: getAll,
|
||||
|
||||
@@ -39,6 +42,7 @@ exports = module.exports = {
|
||||
DNS_CONFIG_KEY: 'dns_config',
|
||||
BACKUP_CONFIG_KEY: 'backup_config',
|
||||
TLS_CONFIG_KEY: 'tls_config',
|
||||
UPDATE_CONFIG_KEY: 'update_config',
|
||||
|
||||
events: new (require('events').EventEmitter)()
|
||||
};
|
||||
@@ -62,6 +66,7 @@ var gDefaults = (function () {
|
||||
result[exports.DNS_CONFIG_KEY] = { };
|
||||
result[exports.BACKUP_CONFIG_KEY] = { };
|
||||
result[exports.TLS_CONFIG_KEY] = { provider: 'caas' };
|
||||
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -323,6 +328,30 @@ function setBackupConfig(backupConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getUpdateConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.UPDATE_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.UPDATE_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value)); // { prerelease }
|
||||
});
|
||||
}
|
||||
|
||||
function setUpdateConfig(updateConfig, callback) {
|
||||
assert.strictEqual(typeof updateConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.UPDATE_CONFIG_KEY, JSON.stringify(updateConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.UPDATE_CONFIG_KEY, updateConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultSync(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ function getAllPaged(backupConfig, page, perPage, callback) {
|
||||
if (result.statusCode !== 200) return callback(new Error(result.text));
|
||||
if (!result.body || !util.isArray(result.body.backups)) return callback(new Error('Unexpected response'));
|
||||
|
||||
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
|
||||
return callback(null, result.body.backups);
|
||||
});
|
||||
}
|
||||
|
||||
+27
-1
@@ -36,7 +36,33 @@ function getAllPaged(backupConfig, page, perPage, callback) {
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(new Error('Not implemented yet'));
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket,
|
||||
EncodingType: 'url',
|
||||
Prefix: backupConfig.prefix + '/backup_'
|
||||
};
|
||||
|
||||
s3.listObjects(params, function (error, data) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var results = data.Contents.map(function (backup) {
|
||||
return {
|
||||
creationTime: backup.LastModified,
|
||||
restoreKey: backup.Key.slice(backupConfig.prefix.length + 1),
|
||||
dependsOn: [] // FIXME empty dependsOn is wrong and version property is missing!!
|
||||
};
|
||||
});
|
||||
|
||||
results.sort(function (a, b) { return a.creationTime < b.creationTime; });
|
||||
|
||||
return callback(null, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getSignedUploadUrl(backupConfig, filename, callback) {
|
||||
|
||||
+54
-14
@@ -1,28 +1,68 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./sysinfo/caas.js'),
|
||||
config = require('./config.js'),
|
||||
ec2 = require('./sysinfo/ec2.js'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
SysInfoError: SysInfoError,
|
||||
|
||||
getIp: getIp
|
||||
};
|
||||
|
||||
var os = require('os');
|
||||
|
||||
var gCachedIp = null;
|
||||
|
||||
function getIp() {
|
||||
if (gCachedIp) return gCachedIp;
|
||||
function SysInfoError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
var ifaces = os.networkInterfaces();
|
||||
for (var dev in ifaces) {
|
||||
if (dev.match(/^(en|eth|wlp).*/) === null) continue;
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
for (var i = 0; i < ifaces[dev].length; i++) {
|
||||
if (ifaces[dev][i].family === 'IPv4') {
|
||||
gCachedIp = ifaces[dev][i].address;
|
||||
return gCachedIp;
|
||||
}
|
||||
}
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SysInfoError, Error);
|
||||
SysInfoError.INTERNAL_ERROR = 'Internal Error';
|
||||
|
||||
return null;
|
||||
function getApi(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
switch (config.provider()) {
|
||||
case '': return callback(null, caas); // current fallback for caas
|
||||
case 'caas': return callback(null, caas);
|
||||
case 'digitalocean': return callback(null, caas);
|
||||
case 'ec2': return callback(null, ec2);
|
||||
default: return callback(new Error('Unkown provider ' + config.provider()));
|
||||
}
|
||||
}
|
||||
|
||||
function getIp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gCachedIp) return callback(null, gCachedIp);
|
||||
|
||||
getApi(function (error, api) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
gCachedIp = ip;
|
||||
|
||||
callback(null, gCachedIp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getIp: getIp
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
os = require('os'),
|
||||
SysInfoError = require('../sysinfo.js').SysInfoError;
|
||||
|
||||
function getIp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var ifaces = os.networkInterfaces();
|
||||
for (var dev in ifaces) {
|
||||
if (dev.match(/^(en|eth|wlp).*/) === null) continue;
|
||||
|
||||
for (var i = 0; i < ifaces[dev].length; i++) {
|
||||
if (ifaces[dev][i].family === 'IPv4') {
|
||||
return callback(null, ifaces[dev][i].address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'No IP found'));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getIp: getIp
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
superagent = require('superagent'),
|
||||
SysInfoError = require('../sysinfo.js').SysInfoError,
|
||||
util = require('util');
|
||||
|
||||
function getIp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get('http://169.254.169.254/latest/meta-data/public-ipv4').end(function (error, result) {
|
||||
if (error) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
|
||||
if (result.statusCode !== 200) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.text);
|
||||
});
|
||||
}
|
||||
@@ -92,6 +92,7 @@ function startAppTask(appId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when parent process dies, apptask processes are killed because KillMode=control-group in systemd unit file
|
||||
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
|
||||
|
||||
var pid = gActiveTasks[appId].pid;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js');
|
||||
|
||||
function setup(done) {
|
||||
async.series([
|
||||
database.initialize.bind(null),
|
||||
database._clear.bind(null)
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
describe('Cloudron', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('can check for disk space', function (done) {
|
||||
cloudron.checkDiskSpace(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,12 @@ describe('config', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get and set version', function (done) {
|
||||
config.setVersion('1.2.3');
|
||||
expect(config.version()).to.be('1.2.3');
|
||||
done();
|
||||
});
|
||||
|
||||
it('did set default values', function () {
|
||||
expect(config.isCustomDomain()).to.equal(false);
|
||||
expect(config.fqdn()).to.equal('localhost');
|
||||
|
||||
@@ -40,7 +40,8 @@ describe('database', function () {
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var ADMIN_0 = {
|
||||
@@ -52,7 +53,8 @@ describe('database', function () {
|
||||
salt: 'tata',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: ''
|
||||
resetToken: '',
|
||||
displayName: 'Herbert Heidelberg'
|
||||
};
|
||||
|
||||
it('can add user', function (done) {
|
||||
@@ -154,10 +156,11 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('can update the user', function (done) {
|
||||
userdb.update(USER_0.id, { email: 'some@thing.com' }, function (error) {
|
||||
userdb.update(USER_0.id, { email: 'some@thing.com', displayName: 'Heiter' }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
userdb.get(USER_0.id, function (error, user) {
|
||||
expect(user.email).to.equal('some@thing.com');
|
||||
expect(user.displayName).to.equal('Heiter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,16 @@ var database = require('../database.js'),
|
||||
|
||||
var USER_0 = {
|
||||
username: 'foobar0',
|
||||
password: 'password0',
|
||||
email: 'foo0@bar.com'
|
||||
password: 'Foobar?1234',
|
||||
email: 'foo0@bar.com',
|
||||
displayName: 'Bob bobson'
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
username: 'foobar1',
|
||||
password: 'password1',
|
||||
email: 'foo1@bar.com'
|
||||
password: 'Foobar?12345',
|
||||
email: 'foo1@bar.com',
|
||||
displayName: 'Jesus'
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
@@ -32,8 +34,8 @@ function setup(done) {
|
||||
database.initialize.bind(null),
|
||||
database._clear.bind(null),
|
||||
ldapServer.start.bind(null),
|
||||
user.create.bind(null, USER_0.username, USER_0.password, USER_0.email, true, null),
|
||||
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, false, USER_0)
|
||||
user.create.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, true, null, false),
|
||||
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, false, USER_0, false)
|
||||
], done);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,10 +123,33 @@ describe('Settings', function () {
|
||||
});
|
||||
|
||||
it('can get backup config', function (done) {
|
||||
settings.getBackupConfig(function (error, dnsConfig) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(dnsConfig.provider).to.be('caas');
|
||||
expect(dnsConfig.token).to.be('TOKEN');
|
||||
expect(backupConfig.provider).to.be('caas');
|
||||
expect(backupConfig.token).to.be('TOKEN');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get default update config config', function (done) {
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(updateConfig.prerelease).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set backup config', function (done) {
|
||||
settings.setUpdateConfig({ prerelease: true }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get backup config', function (done) {
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(updateConfig.prerelease).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../appdb.js'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
deepExtend = require('deep-extend'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
nock = require('nock'),
|
||||
settings = require('../settings.js'),
|
||||
updatechecker = require('../updatechecker.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var RELEASE_1 = {
|
||||
"sourceTarballUrl": "https://dev-cloudron-releases.s3.amazonaws.com/box-3314658ce81f328462508e14b6d388acf36ca81c.tar.gz",
|
||||
"imageId": 100,
|
||||
"imageName": "box-dev-2c7a52b-2016-01-22-150657",
|
||||
"changelog": [ ],
|
||||
"date": "2016-01-23T23:53:01.566Z",
|
||||
"author": "Girish Ramakrishnan <girish@cloudron.io>",
|
||||
"next": "2.0.0-1"
|
||||
};
|
||||
|
||||
var RELEASE_2_PRERELEASE = {
|
||||
"sourceTarballUrl": "https://dev-cloudron-releases.s3.amazonaws.com/box-3314658ce81f328462508e14b6d388acf36ca81c.tar.gz",
|
||||
"imageId": 2001,
|
||||
"imageName": "box-dev-2c7a52b-2016-01-22-150657",
|
||||
"changelog": [ ],
|
||||
"upgrade": false,
|
||||
"date": "2016-01-23T23:53:01.566Z",
|
||||
"author": "Girish Ramakrishnan <girish@cloudron.io>",
|
||||
"next": "2.0.0"
|
||||
};
|
||||
|
||||
var RELEASE_2 = {
|
||||
"sourceTarballUrl": "https://dev-cloudron-releases.s3.amazonaws.com/box-3314658ce81f328462508e14b6d388acf36ca81c.tar.gz",
|
||||
"imageId": 200,
|
||||
"imageName": "box-dev-2c7a52b-2016-01-22-150657",
|
||||
"changelog": [ ],
|
||||
"upgrade": false,
|
||||
"date": "2016-01-23T23:53:01.566Z",
|
||||
"author": "Girish Ramakrishnan <girish@cloudron.io>",
|
||||
"next": null
|
||||
};
|
||||
|
||||
var RELEASES = {
|
||||
"1.0.0": RELEASE_1,
|
||||
"2.0.0-1": RELEASE_2_PRERELEASE,
|
||||
"2.0.0": RELEASE_2
|
||||
};
|
||||
|
||||
describe('updatechecker - checkBoxUpdates', function () {
|
||||
before(function (done) {
|
||||
config.set('version', '1.0.0');
|
||||
config.set('boxVersionsUrl', 'http://localhost:4444/release.json')
|
||||
async.series([
|
||||
database.initialize
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
it('no updates', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var releaseCopy = deepExtend({}, RELEASES);
|
||||
releaseCopy['1.0.0'].next = null;
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/release.json')
|
||||
.reply(200, releaseCopy);
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('new version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var releaseCopy = deepExtend({}, RELEASES);
|
||||
delete releaseCopy['2.0.0-1'];
|
||||
releaseCopy['1.0.0'].next = '2.0.0';
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/release.json')
|
||||
.reply(200, releaseCopy);
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('existing version missing offers latest version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var releaseCopy = deepExtend({}, RELEASES);
|
||||
delete releaseCopy['1.0.0'];
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/release.json')
|
||||
.reply(200, releaseCopy);
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not offer prerelease', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var releaseCopy = deepExtend({}, RELEASES);
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/release.json')
|
||||
.reply(200, releaseCopy);
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('offers prerelease', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
settings.setUpdateConfig({ prerelease: true }, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
var releaseCopy = deepExtend({}, RELEASES);
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/release.json')
|
||||
.reply(200, releaseCopy);
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0-1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('bad response offers nothing', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var releaseCopy = _.extend({}, RELEASES);
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/release.json')
|
||||
.reply(404, releaseCopy);
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatechecker - checkAppUpdates', function () {
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'io.cloudron.app',
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL,
|
||||
installationProgress: null,
|
||||
runState: null,
|
||||
location: 'some-location-0',
|
||||
manifest: {
|
||||
version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0',
|
||||
tcpPorts: {
|
||||
PORT: {
|
||||
description: 'this is a port that i expose',
|
||||
containerPort: '1234'
|
||||
}
|
||||
}
|
||||
},
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { PORT: 5678 },
|
||||
healthy: null,
|
||||
accessRestriction: null,
|
||||
oauthProxy: false
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
config.set('version', '1.0.0');
|
||||
config.set('apiServerOrigin', 'http://localhost:4444');
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy)
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
it('no updates', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.post('/api/v1/appupdates')
|
||||
.reply(200, { appVersions: { 'io.cloudron.app': { manifest: { version: '1.0.0' } } } });
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('bad response', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.post('/api/v1/appupdates')
|
||||
.reply(500, { appVersions: { 'io.cloudron.app': { manifest: { version: '1.0.0' } } } });
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('missing info', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.post('/api/v1/appupdates')
|
||||
.reply(200, { appVersions: { 'io.cloudron.app2': { manifest: { version: '1.0.0' } } } });
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('offers new version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.post('/api/v1/appupdates')
|
||||
.reply(200, { appVersions: { 'io.cloudron.app': { manifest: { version: '2.0.0' } } } });
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0' } } });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not offer old version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.post('/api/v1/appupdates')
|
||||
.reply(200, { appVersions: { 'io.cloudron.app': { manifest: { version: '0.1.0' } } } });
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({ });
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+192
-17
@@ -8,6 +8,7 @@
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
user = require('../user.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
UserError = user.UserError;
|
||||
@@ -16,20 +17,27 @@ var USERNAME = 'nobody';
|
||||
var USERNAME_NEW = 'nobodynew';
|
||||
var EMAIL = 'nobody@no.body';
|
||||
var EMAIL_NEW = 'nobodynew@no.body';
|
||||
var PASSWORD = 'foobar';
|
||||
var NEW_PASSWORD = 'somenewpassword';
|
||||
var PASSWORD = 'sTrOnG#$34134';
|
||||
var NEW_PASSWORD = 'oTHER@#$235';
|
||||
var IS_ADMIN = true;
|
||||
var DISPLAY_NAME = 'Nobody cares';
|
||||
var DISPLAY_NAME_NEW = 'Somone cares';
|
||||
var userObject = null;
|
||||
|
||||
function cleanupUsers(done) {
|
||||
userdb._clear(function () {
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function createUser(done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
userObject = result;
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
@@ -38,14 +46,28 @@ function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
expect(mailer._getMailQueue().length).to.equal(number);
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
describe('User', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
@@ -54,14 +76,55 @@ describe('User', function () {
|
||||
before(cleanupUsers);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
it('fails due to short password', function (done) {
|
||||
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing upper case password', function (done) {
|
||||
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing numerics in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing special chars in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds and attempts to send invite', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, true, function (error, result) {
|
||||
expect(error).not.to.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.username).to.equal(USERNAME);
|
||||
expect(result.email).to.equal(EMAIL);
|
||||
|
||||
done();
|
||||
// first user is owner, do not send mail to admins
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,12 +144,15 @@ describe('User', function () {
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, false, null, 'foobar');
|
||||
}).to.throwException();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails because user exists', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.ALREADY_EXISTS);
|
||||
@@ -96,7 +162,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because password is empty', function (done) {
|
||||
user.create(USERNAME, '', EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -106,6 +172,30 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOwner', function() {
|
||||
before(cleanupUsers);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails because there is no owner', function (done) {
|
||||
user.getOwner(function (error) {
|
||||
expect(error.reason).to.be(UserError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
createUser(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
user.getOwner(function (error, owner) {
|
||||
expect(error).to.be(null);
|
||||
expect(owner.email).to.be(EMAIL);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
@@ -211,6 +301,9 @@ describe('User', function () {
|
||||
user.get(USERNAME, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.email).to.equal(EMAIL);
|
||||
expect(result.username).to.equal(USERNAME);
|
||||
expect(result.displayName).to.equal(DISPLAY_NAME);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -222,7 +315,7 @@ describe('User', function () {
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to unknown userid', function (done) {
|
||||
user.update(USERNAME+USERNAME, USERNAME_NEW, EMAIL_NEW, function (error) {
|
||||
user.update(USERNAME+USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
@@ -231,7 +324,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid username', function (done) {
|
||||
user.update(USERNAME, '', EMAIL_NEW, function (error) {
|
||||
user.update(USERNAME, '', EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.BAD_USERNAME);
|
||||
|
||||
@@ -240,7 +333,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid email', function (done) {
|
||||
user.update(USERNAME, USERNAME_NEW, 'brokenemailaddress', function (error) {
|
||||
user.update(USERNAME, USERNAME_NEW, 'brokenemailaddress', DISPLAY_NAME_NEW, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.BAD_EMAIL);
|
||||
|
||||
@@ -249,7 +342,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, function (error) {
|
||||
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
user.get(USERNAME, function (error, result) {
|
||||
@@ -257,6 +350,23 @@ describe('User', function () {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.email).to.equal(EMAIL_NEW);
|
||||
expect(result.username).to.equal(USERNAME_NEW);
|
||||
expect(result.displayName).to.equal(DISPLAY_NAME_NEW);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with same data', function (done) {
|
||||
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
user.get(USERNAME, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.email).to.equal(EMAIL_NEW);
|
||||
expect(result.username).to.equal(USERNAME_NEW);
|
||||
expect(result.displayName).to.equal(DISPLAY_NAME_NEW);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -278,17 +388,19 @@ describe('User', function () {
|
||||
it('make second user admin succeeds', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'foobar',
|
||||
password: 'ASDFkljsf#$^%2354',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, false, { username: USERNAME, email: EMAIL } /* invitor */, function (error, result) {
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, false, { username: USERNAME, email: EMAIL } /* invitor */, false, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user.changeAdmin(user1.username, true, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
|
||||
// one mail for user creation, one mail for admin change
|
||||
checkMails(2, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -296,9 +408,51 @@ describe('User', function () {
|
||||
it('succeeds to remove admin flag of first user', function (done) {
|
||||
user.changeAdmin(USERNAME, false, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get admins', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('succeeds for one admins', function (done) {
|
||||
user.getAllAdmins(function (error, admins) {
|
||||
expect(error).to.eql(null);
|
||||
expect(admins.length).to.equal(1);
|
||||
expect(admins[0].username).to.equal(USERNAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for two admins', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'Adfasdkjf#$%43',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, false, { username: USERNAME, email: EMAIL } /* invitor */, false, function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user.changeAdmin(user1.username, true, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
user.getAllAdmins(function (error, admins) {
|
||||
expect(error).to.eql(null);
|
||||
expect(admins.length).to.equal(2);
|
||||
expect(admins[0].username).to.equal(USERNAME);
|
||||
expect(admins[1].username).to.equal(user1.username);
|
||||
|
||||
// one mail for user creation one mail for admin change
|
||||
checkMails(2, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('password change', function () {
|
||||
@@ -387,14 +541,35 @@ describe('User', function () {
|
||||
it('succeeds with email', function (done) {
|
||||
user.resetPasswordByIdentifier(EMAIL, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with username', function (done) {
|
||||
user.resetPasswordByIdentifier(USERNAME, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('send invite', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails for unknown user', function (done) {
|
||||
user.sendInvite('unknown user', function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.sendInvite(userObject.id, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+81
-34
@@ -6,7 +6,8 @@ exports = module.exports = {
|
||||
checkAppUpdates: checkAppUpdates,
|
||||
checkBoxUpdates: checkBoxUpdates,
|
||||
|
||||
getUpdateInfo: getUpdateInfo
|
||||
getUpdateInfo: getUpdateInfo,
|
||||
resetUpdateInfo: resetUpdateInfo
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
@@ -17,11 +18,14 @@ var apps = require('./apps.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
|
||||
gBoxUpdateInfo = null;
|
||||
gBoxUpdateInfo = null; // { version, changelog, upgrade, sourceTarballUrl }
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function loadState() {
|
||||
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
|
||||
@@ -39,6 +43,11 @@ function getUpdateInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
function resetUpdateInfo() {
|
||||
gAppUpdateInfo = { };
|
||||
gBoxUpdateInfo = null;
|
||||
}
|
||||
|
||||
function getAppUpdates(callback) {
|
||||
apps.getAll(function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
@@ -67,7 +76,7 @@ function getAppUpdates(callback) {
|
||||
|
||||
var newManifest = latestAppVersions[apps[i].appStoreId].manifest;
|
||||
var newVersion = newManifest.version;
|
||||
if (newVersion !== oldVersion) {
|
||||
if (semver.gt(newVersion, oldVersion)) {
|
||||
appUpdateInfo[apps[i].id] = latestAppVersions[apps[i].appStoreId];
|
||||
debug('Update available for %s (%s) from %s to %s', apps[i].location, apps[i].id, oldVersion, newVersion);
|
||||
}
|
||||
@@ -116,7 +125,8 @@ function getBoxUpdates(callback) {
|
||||
callback(null, {
|
||||
version: nextVersion,
|
||||
changelog: nextVersionInfo.changelog,
|
||||
upgrade: nextVersionInfo.upgrade
|
||||
upgrade: nextVersionInfo.upgrade,
|
||||
sourceTarballUrl: nextVersionInfo.sourceTarballUrl
|
||||
});
|
||||
} else {
|
||||
debug('no new version available.');
|
||||
@@ -125,56 +135,93 @@ function getBoxUpdates(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkAppUpdates() {
|
||||
function checkAppUpdates(callback) {
|
||||
callback = callback || NOOP_CALLBACK; // null when called from a timer task
|
||||
|
||||
debug('Checking App Updates');
|
||||
|
||||
var oldState = loadState();
|
||||
var newState = { box: oldState.box }; // creaee new state so that old app ids are removed
|
||||
gAppUpdateInfo = { };
|
||||
|
||||
getAppUpdates(function (error, result) {
|
||||
if (error) debug('Error checking app updates: ', error);
|
||||
getAppUpdates(function (error, updateInfo) {
|
||||
if (error) return callback(error);
|
||||
|
||||
gAppUpdateInfo = error ? {} : result;
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(Object.keys(gAppUpdateInfo), function iterator(id, iteratorDone) {
|
||||
newState[id] = gAppUpdateInfo[id].manifest.version;
|
||||
var oldState = loadState();
|
||||
var newState = { box: oldState.box }; // create new state so that old app ids are removed
|
||||
|
||||
if (oldState[id] === gAppUpdateInfo[id].manifest.version) {
|
||||
debug('Skipping notification of app update %s since user was already notified', id);
|
||||
return iteratorDone();
|
||||
}
|
||||
async.eachSeries(Object.keys(updateInfo), function iterator(id, iteratorDone) {
|
||||
var isPrerelease = semver.parse(updateInfo[id].manifest.version).prerelease.length !== 0;
|
||||
|
||||
apps.get(id, function (error, app) {
|
||||
if (error) {
|
||||
debug('Error getting app %s %s', id, error);
|
||||
if (isPrerelease && !updateConfig.prerelease) {
|
||||
debug('Skipping update %s of app %s as this box does not want prereleases', gBoxUpdateInfo.version, id);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
mailer.appUpdateAvailable(app, gAppUpdateInfo[id]);
|
||||
iteratorDone();
|
||||
gAppUpdateInfo[id] = updateInfo[id];
|
||||
|
||||
// decide whether to send email
|
||||
newState[id] = updateInfo[id].manifest.version;
|
||||
|
||||
if (oldState[id] === updateInfo[id].manifest.version) {
|
||||
debug('Skipping notification of app update %s since user was already notified', id);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
apps.get(id, function (error, app) {
|
||||
if (error) {
|
||||
debug('Error getting app %s %s', id, error);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
mailer.appUpdateAvailable(app, updateInfo[id]);
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
saveState(newState);
|
||||
callback();
|
||||
});
|
||||
}, function () {
|
||||
saveState(newState);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkBoxUpdates() {
|
||||
function checkBoxUpdates(callback) {
|
||||
callback = callback || NOOP_CALLBACK; // null when called from a timer task
|
||||
|
||||
debug('Checking Box Updates');
|
||||
|
||||
var state = loadState();
|
||||
gBoxUpdateInfo = null;
|
||||
|
||||
getBoxUpdates(function (error, result) {
|
||||
if (error) debug('Error checking box updates: ', error);
|
||||
getBoxUpdates(function (error, updateInfo) {
|
||||
if (error || !updateInfo) return callback(error);
|
||||
|
||||
gBoxUpdateInfo = error ? null : result;
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var isPrerelease = semver.parse(updateInfo.version).prerelease.length !== 0;
|
||||
|
||||
if (isPrerelease && !updateConfig.prerelease) {
|
||||
debug('Skipping update %s since this box does not want prereleases', updateInfo.version);
|
||||
return callback();
|
||||
}
|
||||
|
||||
gBoxUpdateInfo = updateInfo;
|
||||
|
||||
// decide whether to send email
|
||||
var state = loadState();
|
||||
|
||||
if (state.box === gBoxUpdateInfo.version) {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
return callback();
|
||||
}
|
||||
|
||||
mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
state.box = updateInfo.version;
|
||||
|
||||
if (gBoxUpdateInfo && state.box !== gBoxUpdateInfo.version) {
|
||||
mailer.boxUpdateAvailable(gBoxUpdateInfo.version, gBoxUpdateInfo.changelog);
|
||||
state.box = gBoxUpdateInfo.version;
|
||||
saveState(state);
|
||||
} else {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+69
-22
@@ -13,11 +13,14 @@ exports = module.exports = {
|
||||
get: getUser,
|
||||
getByResetToken: getByResetToken,
|
||||
changeAdmin: changeAdmin,
|
||||
getAllAdmins: getAllAdmins,
|
||||
resetPasswordByIdentifier: resetPasswordByIdentifier,
|
||||
setPassword: setPassword,
|
||||
changePassword: changePassword,
|
||||
update: updateUser,
|
||||
createOwner: createOwner
|
||||
createOwner: createOwner,
|
||||
getOwner: getOwner,
|
||||
sendInvite: sendInvite
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -28,6 +31,7 @@ var assert = require('assert'),
|
||||
userdb = require('./userdb.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
validatePassword = require('./password.js').validate,
|
||||
util = require('util'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
@@ -74,7 +78,7 @@ function listUsers(callback) {
|
||||
userdb.getAll(function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result.map(function (obj) { return _.pick(obj, 'id', 'username', 'email', 'admin'); }));
|
||||
return callback(null, result.map(function (obj) { return _.pick(obj, 'id', 'username', 'email', 'admin', 'displayName'); }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,14 +91,6 @@ function validateUsername(username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (password.length < 5) return new UserError(UserError.BAD_PASSWORD, 'Password must be atleast 5 chars');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
|
||||
@@ -111,23 +107,34 @@ function validateToken(token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, admin, invitor, callback) {
|
||||
function validateDisplayName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, displayName, admin, invitor, sendInvite, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
assert(invitor || admin);
|
||||
assert.strictEqual(typeof sendInvite, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateUsername(username);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePassword(password);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
error = validateEmail(email);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateDisplayName(displayName);
|
||||
if (error) return callback(error);
|
||||
|
||||
crypto.randomBytes(CRYPTO_SALT_SIZE, function (error, salt) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -144,7 +151,8 @@ function createUser(username, password, email, admin, invitor, callback) {
|
||||
salt: salt.toString('hex'),
|
||||
createdAt: now,
|
||||
modifiedAt: now,
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: displayName
|
||||
};
|
||||
|
||||
userdb.add(user.id, user, function (error) {
|
||||
@@ -153,9 +161,9 @@ function createUser(username, password, email, admin, invitor, callback) {
|
||||
|
||||
callback(null, user);
|
||||
|
||||
// only send welcome mail if user is not an admin. This is only the case for the first user!
|
||||
// The welcome email contains a link to create a new password
|
||||
if (!user.admin) mailer.userAdded(user, invitor);
|
||||
// WARNING do not send email for admins (this can only be the case for the owner, the first user creation during activation)
|
||||
if (!admin) mailer.userAdded(user, sendInvite);
|
||||
if (sendInvite) mailer.sendInvite(user, invitor);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -244,10 +252,11 @@ function getByResetToken(resetToken, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser(userId, username, email, callback) {
|
||||
function updateUser(userId, username, email, displayName, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateUsername(username);
|
||||
@@ -256,7 +265,7 @@ function updateUser(userId, username, email, callback) {
|
||||
error = validateEmail(email);
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.update(userId, { username: username, email: email }, function (error) {
|
||||
userdb.update(userId, { username: username, email: email, displayName: displayName }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
@@ -290,6 +299,15 @@ function changeAdmin(username, admin, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAllAdmins(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
callback(null, admins);
|
||||
});
|
||||
}
|
||||
|
||||
function resetPasswordByIdentifier(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -321,7 +339,7 @@ function setPassword(userId, newPassword, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
userdb.get(userId, function (error, user) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
@@ -364,7 +382,7 @@ function changePassword(username, oldPassword, newPassword, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
verify(username, oldPassword, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
@@ -373,12 +391,41 @@ function changePassword(username, oldPassword, newPassword, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createOwner(username, password, email, callback) {
|
||||
function createOwner(username, password, email, displayName, callback) {
|
||||
userdb.count(function (error, count) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
|
||||
createUser(username, password, email, true /* admin */, null /* invitor */, callback);
|
||||
createUser(username, password, email, displayName, true /* admin */, null /* invitor */, false /* sendInvite */, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function getOwner(callback) {
|
||||
userdb.getOwner(function (error, owner) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, owner);
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvite(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.get(userId, function (error, userObject) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
userObject.resetToken = hat(256);
|
||||
|
||||
userdb.update(userId, userObject, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
mailer.sendInvite(userObject, null);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+17
-4
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
getByEmail: getByEmail,
|
||||
getByAccessToken: getByAccessToken,
|
||||
getByResetToken: getByResetToken,
|
||||
getOwner: getOwner,
|
||||
getAll: getAll,
|
||||
getAllAdmins: getAllAdmins,
|
||||
add: add,
|
||||
@@ -22,7 +23,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:userdb'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'admin', 'resetToken' ].join(',');
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'admin', 'resetToken', 'displayName' ].join(',');
|
||||
|
||||
function get(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
@@ -56,6 +57,18 @@ function getByEmail(email, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getOwner(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// the first created user it the admin
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE admin=1 ORDER BY createdAt LIMIT 1', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(resetToken, callback) {
|
||||
assert.strictEqual(typeof resetToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -100,10 +113,11 @@ function add(userId, user, callback) {
|
||||
assert.strictEqual(typeof user.createdAt, 'string');
|
||||
assert.strictEqual(typeof user.modifiedAt, 'string');
|
||||
assert.strictEqual(typeof user.resetToken, 'string');
|
||||
assert.strictEqual(typeof user.displayName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ userId, user.username, user.password, user.email, user.admin, user.salt, user.createdAt, user.modifiedAt, user.resetToken ];
|
||||
database.query('INSERT INTO users (id, username, password, email, admin, salt, createdAt, modifiedAt, resetToken) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
var data = [ userId, user.username, user.password, user.email, user.admin, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName ];
|
||||
database.query('INSERT INTO users (id, username, password, email, admin, salt, createdAt, modifiedAt, resetToken, displayName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -186,4 +200,3 @@ function adminCount(callback) {
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
|
||||
* @author Alex_Crack
|
||||
* @version v0.0.5
|
||||
* @version v0.1.0
|
||||
* @link https://github.com/alexcrack/angular-ui-notification
|
||||
* @license MIT
|
||||
*/
|
||||
.ui-notification{position:fixed;z-index:9999;top:-100px;right:10px;width:300px;cursor:pointer;-webkit-transition:all ease .5s;-o-transition:all ease .5s;transition:all ease .5s;color:#fff;background:#337ab7;box-shadow:5px 5px 10px rgba(0,0,0,.3)}.ui-notification.killed{-webkit-transition:opacity ease 1s;-o-transition:opacity ease 1s;transition:opacity ease 1s;opacity:0}.ui-notification>h3{font-size:14px;font-weight:700;display:block;margin:10px 10px 0;padding:0 0 5px;text-align:left;border-bottom:1px solid rgba(255,255,255,.3)}.ui-notification a{color:#fff}.ui-notification a:hover{text-decoration:underline}.ui-notification>.message{margin:10px}.ui-notification.warning{color:#fff;background:#f0ad4e}.ui-notification.error{color:#fff;background:#d9534f}.ui-notification.success{color:#fff;background:#5cb85c}.ui-notification.info{color:#fff;background:#5bc0de}.ui-notification:hover{opacity:.7}
|
||||
.ui-notification{position:fixed;z-index:9999;width:300px;cursor:pointer;-webkit-transition:all ease .5s;-o-transition:all ease .5s;transition:all ease .5s;color:#fff;border-radius:0;background:#337ab7;box-shadow:5px 5px 10px rgba(0,0,0,.3)}.ui-notification.killed{-webkit-transition:opacity ease 1s;-o-transition:opacity ease 1s;transition:opacity ease 1s;opacity:0}.ui-notification>h3{font-size:14px;font-weight:700;display:block;margin:10px 10px 0;padding:0 0 5px;text-align:left;border-bottom:1px solid rgba(255,255,255,.3)}.ui-notification a{color:#fff}.ui-notification a:hover{text-decoration:underline}.ui-notification>.message{margin:10px}.ui-notification.warning{color:#fff;background:#f0ad4e}.ui-notification.error{color:#fff;background:#d9534f}.ui-notification.success{color:#fff;background:#5cb85c}.ui-notification.info{color:#fff;background:#5bc0de}.ui-notification:hover{opacity:.7}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
|
||||
* @author Alex_Crack
|
||||
* @version v0.0.5
|
||||
* @version v0.1.0
|
||||
* @link https://github.com/alexcrack/angular-ui-notification
|
||||
* @license MIT
|
||||
*/
|
||||
angular.module("ui-notification",[]),angular.module("ui-notification").value("uiNotificationTemplates","angular-ui-notification.html"),angular.module("ui-notification").factory("Notification",["$timeout","uiNotificationTemplates","$http","$compile","$templateCache","$rootScope","$injector","$sce",function(t,e,i,n,a,o,l,r){var s=10,c=10,u=10,f=10,m=5e3,p=[],d=function(l,d){"object"!=typeof l&&(l={message:l}),l.template=l.template?l.template:e,l.delay=angular.isUndefined(l.delay)?m:l.delay,l.type=d?d:"",i.get(l.template,{cache:a}).success(function(e){var i=o.$new();if(i.message=r.trustAsHtml(l.message),i.title=r.trustAsHtml(l.title),i.t=l.type.substr(0,1),i.delay=l.delay,"object"==typeof l.scope)for(var a in l.scope)i[a]=l.scope[a];var m=function(){for(var t=0,e=0,i=s,n=p.length-1;n>=0;n--){var a=p[n],o=parseInt(a[0].offsetHeight),l=parseInt(a[0].offsetWidth);r+o>window.innerHeight&&(i=s,e++,t=0);var r=i+(0===t?0:u),m=c+e*(f+l);a.css("top",r+"px"),a.css("right",m+"px"),i=r+o,t++}},d=n(e)(i);d.addClass(l.type),d.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd click",function(t){t=t.originalEvent||t,("click"===t.type||"opacity"===t.propertyName&&t.elapsedTime>=1)&&(d.remove(),p.splice(p.indexOf(d),1),m())}),angular.isNumber(l.delay)&&t(function(){d.addClass("killed")},l.delay),angular.element(document.getElementsByTagName("body")).append(d),p.push(d),t(m)}).error(function(t){throw new Error("Template ("+l.template+") could not be loaded. "+t)})};return d.config=function(t){s=t.top?t.top:s,u=t.verticalSpacing?t.verticalSpacing:u},d.primary=function(t){this(t,"primary")},d.error=function(t){this(t,"error")},d.success=function(t){this(t,"success")},d.info=function(t){this(t,"info")},d.warning=function(t){this(t,"warning")},d.clearAll=function(){var t=angular.element(document.getElementsByClassName("ui-notification"));t&&angular.forEach(t,function(t){t.remove()})},d}]),angular.module("ui-notification").run(["$templateCache",function(t){t.put("angular-ui-notification.html",'<div class="ui-notification"><h3 ng-show="title" ng-bind-html="title"></h3><div class="message" ng-bind-html="message"></div></div>')}]);
|
||||
angular.module("ui-notification",[]),angular.module("ui-notification").provider("Notification",function(){this.options={delay:5e3,startTop:10,startRight:10,verticalSpacing:10,horizontalSpacing:10,positionX:"right",positionY:"top",replaceMessage:!1,templateUrl:"angular-ui-notification.html"},this.setOptions=function(t){if(!angular.isObject(t))throw new Error("Options should be an object!");this.options=angular.extend({},this.options,t)},this.$get=["$timeout","$http","$compile","$templateCache","$rootScope","$injector","$sce","$q","$window",function(t,e,i,n,o,s,a,r,l){var p=this.options,c=p.startTop,u=p.startRight,d=p.verticalSpacing,m=p.horizontalSpacing,g=p.delay,f=[],h=!1,y=function(s,y){var v=r.defer();return"object"!=typeof s&&(s={message:s}),s.scope=s.scope?s.scope:o,s.template=s.templateUrl?s.templateUrl:p.templateUrl,s.delay=angular.isUndefined(s.delay)?g:s.delay,s.type=y||p.type||"",s.positionY=s.positionY?s.positionY:p.positionY,s.positionX=s.positionX?s.positionX:p.positionX,s.replaceMessage=s.replaceMessage?s.replaceMessage:p.replaceMessage,e.get(s.template,{cache:n}).success(function(e){var n=s.scope.$new();n.message=a.trustAsHtml(s.message),n.title=a.trustAsHtml(s.title),n.t=s.type.substr(0,1),n.delay=s.delay;var o=function(){for(var t=0,e=0,i=c,n=u,o=[],a=f.length-1;a>=0;a--){var r=f[a];if(s.replaceMessage&&a<f.length-1)r.addClass("killed");else{var l=parseInt(r[0].offsetHeight),p=parseInt(r[0].offsetWidth),g=o[r._positionY+r._positionX];h+l>window.innerHeight&&(g=c,e++,t=0);var h=i=g?0===t?g:g+d:c,y=n+e*(m+p);r.css(r._positionY,h+"px"),"center"==r._positionX?r.css("left",parseInt(window.innerWidth/2-p/2)+"px"):r.css(r._positionX,y+"px"),o[r._positionY+r._positionX]=h+l,t++}}},r=i(e)(n);r._positionY=s.positionY,r._positionX=s.positionX,r.addClass(s.type),r.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd click",function(t){t=t.originalEvent||t,("click"===t.type||"opacity"===t.propertyName&&t.elapsedTime>=1)&&(r.remove(),f.splice(f.indexOf(r),1),o())}),angular.isNumber(s.delay)&&t(function(){r.addClass("killed")},s.delay),angular.element(document.getElementsByTagName("body")).append(r);var p=-(parseInt(r[0].offsetHeight)+50);r.css(r._positionY,p+"px"),f.push(r),n._templateElement=r,n.kill=function(e){e?(f.splice(f.indexOf(n._templateElement),1),n._templateElement.remove(),t(o)):n._templateElement.addClass("killed")},t(o),h||(angular.element(l).bind("resize",function(){t(o)}),h=!0),v.resolve(n)}).error(function(t){throw new Error("Template ("+s.template+") could not be loaded. "+t)}),v.promise};return y.primary=function(t){return this(t,"primary")},y.error=function(t){return this(t,"error")},y.success=function(t){return this(t,"success")},y.info=function(t){return this(t,"info")},y.warning=function(t){return this(t,"warning")},y.clearAll=function(){angular.forEach(f,function(t){t.addClass("killed")})},y}]}),angular.module("ui-notification").run(["$templateCache",function(t){t.put("angular-ui-notification.html",'<div class="ui-notification"><h3 ng-show="title" ng-bind-html="title"></h3><div class="message" ng-bind-html="message"></div></div>')}]);
|
||||
@@ -0,0 +1,4 @@
|
||||
/*! showdown-target-blank 02-11-2015 */
|
||||
|
||||
!function(){"use strict";var a=function(){return[{type:"output",regex:"<a(.*?)>",replace:function(a,b){return'<a target="_blank"'+b+">"}}]};"undefined"!=typeof window&&window.showdown&&window.showdown.extensions&&window.showdown.extension("targetblank",a),"undefined"!=typeof module&&(module.exports=a)}();
|
||||
//# sourceMappingURL=showdown-target-blank.min.js.map
|
||||
+12
-1
@@ -33,6 +33,7 @@
|
||||
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
|
||||
$scope.webServerOriginLink = '/';
|
||||
$scope.errorMessage = '';
|
||||
$scope.statusOk = false;
|
||||
|
||||
// try to fetch at least config.json to get appstore url
|
||||
$http.get('/config.json').success(function(data, status) {
|
||||
@@ -43,6 +44,15 @@
|
||||
else console.error(status, data);
|
||||
});
|
||||
|
||||
// try to fetch the cloudron status
|
||||
$http.get('/api/v1/cloudron/status').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
|
||||
$scope.statusOk = true;
|
||||
}).error(function (data, status) {
|
||||
console.error(status, data);
|
||||
$scope.statusOk = false;
|
||||
});
|
||||
|
||||
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.errorCode = search.errorCode || 0;
|
||||
@@ -62,7 +72,8 @@
|
||||
|
||||
<div ng-show="errorCode == 0">
|
||||
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
|
||||
Please check your cloudron status <a href="{{ webServerOriginLink }}">here</a>.
|
||||
<span ng-hide="statusOk">Please check your cloudron status <a href="{{ webServerOriginLink }}">here</a>.</span>
|
||||
<span ng-show="statusOk">Please try again reloading the page <a href="/">here</a>.</span>
|
||||
</div>
|
||||
|
||||
<div ng-show="errorCode == 1">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="/3rdparty/slick.css"/>
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script src="3rdparty/js/showdown-1.1.0.min.js"></script>
|
||||
<script src="3rdparty/js/showdown-target-blank.min.js"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script src="js/index.js"></script>
|
||||
@@ -85,15 +86,13 @@
|
||||
<br/>
|
||||
<fieldset>
|
||||
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (update_form.password.$dirty && update_form.password.$invalid) || (!update_form.password.$dirty && update.error.password) }">
|
||||
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!update_form.password.$dirty && update.error.password) || (update_form.password.$dirty && update_form.password.$invalid)">
|
||||
<small ng-show="update_form.password.$error.required && !update.error.password">A password is required</small>
|
||||
<small ng-show="update_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="update_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="update.error.password">Incorrect password</small>
|
||||
<div class="control-label" ng-show="(update_form.password.$dirty && update_form.password.$invalid) || (!update_form.password.$dirty && update.error.password)">
|
||||
<small ng-show=" update_form.password.$dirty && update_form.password.$invalid">Password required</small>
|
||||
<small ng-show="!update_form.password.$dirty && update.error.password">Wrong password</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="update.password" id="inputUpdatePassword" name="password" placeholder="Password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="update.password" id="inputUpdatePassword" name="password" placeholder="Password" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="update_form.$invalid || update.busy"/>
|
||||
</form>
|
||||
@@ -120,7 +119,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img ng-src="{{ client.avatar }}" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="#/">Cloudron</a>
|
||||
</div>
|
||||
<!-- /.navbar-header -->
|
||||
|
||||
@@ -66,6 +66,11 @@ angular.module('Application').service('AppStore', ['$http', 'Client', function (
|
||||
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
|
||||
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
|
||||
|
||||
// check cache
|
||||
for (var app in this._appsCache) {
|
||||
if (this._appsCache[app].id === appId && this._appsCache[app].manifest.version === version) return callback(null, this._appsCache[app]);
|
||||
}
|
||||
|
||||
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
|
||||
if (status !== 200) return callback(new AppStoreError(status, data));
|
||||
return callback(null, data);
|
||||
|
||||
+70
-15
@@ -86,6 +86,9 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._clientId = '<%= oauth.clientId %>';
|
||||
this._clientSecret = '<%= oauth.clientSecret %>';
|
||||
this.apiOrigin = '<%= oauth.apiOrigin %>';
|
||||
this.avatar = '';
|
||||
|
||||
this.resetAvatar();
|
||||
|
||||
this.setToken(localStorage.token);
|
||||
}
|
||||
@@ -99,7 +102,33 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
message = error;
|
||||
}
|
||||
|
||||
Notification.error({ title: 'Cloudron Error', message: message, delay: 5000 });
|
||||
Notification.error({ title: 'Cloudron Error', message: message });
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Example usage with an action:
|
||||
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/certs';
|
||||
|
||||
Client.notify('title', 'message', true, actionScope);
|
||||
|
||||
*/
|
||||
Client.prototype.notify = function (title, message, persitent, type, actionScope) {
|
||||
var options = { title: title, message: message};
|
||||
|
||||
if (persitent) options.delay = 'never'; // any non Number means never timeout
|
||||
|
||||
if (actionScope) {
|
||||
if (typeof actionScope.action !== 'string') throw('an actionScope has to have an action url');
|
||||
options.scope = actionScope;
|
||||
}
|
||||
|
||||
if (type === 'error') Notification.error(options);
|
||||
else if (type === 'success') Notification.success(options);
|
||||
else if (type === 'info') Notification.info(options);
|
||||
else throw('Invalid notification type "' + type + '"');
|
||||
};
|
||||
|
||||
Client.prototype.setReady = function () {
|
||||
@@ -121,6 +150,13 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
callback(this._config);
|
||||
};
|
||||
|
||||
Client.prototype.resetAvatar = function () {
|
||||
this.avatar = this.apiOrigin + '/api/v1/cloudron/avatar?' + String(Math.random()).slice(2);
|
||||
|
||||
var favicon = $('#favicon');
|
||||
if (favicon) favicon.attr('href', this.avatar);
|
||||
};
|
||||
|
||||
Client.prototype.setUserInfo = function (userInfo) {
|
||||
// In order to keep the angular bindings alive, set each property individually
|
||||
this._userInfo.id = userInfo.id;
|
||||
@@ -307,10 +343,10 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.isServerFirstTime = function (callback) {
|
||||
Client.prototype.getStatus = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, !data.activated);
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
@@ -381,21 +417,24 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
};
|
||||
|
||||
Client.prototype.getAppLogStream = function (appId) {
|
||||
var source = new EventSource('/api/v1/apps/' + appId + '/logstream');
|
||||
var source = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream');
|
||||
return source;
|
||||
};
|
||||
|
||||
Client.prototype.getAppLogUrl = function (appId) {
|
||||
return '/api/v1/apps/' + appId + '/logs?access_token=' + this._token;
|
||||
};
|
||||
|
||||
Client.prototype.getAppIconUrls = function (app) {
|
||||
return {
|
||||
cloudron: this.apiOrigin + app.iconUrl + '?access_token=' + this._token,
|
||||
store: this._config.apiServerOrigin + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon'
|
||||
cloudron: app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + this._token) : null,
|
||||
store: app.appStoreId ? (this._config.apiServerOrigin + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon') : null
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.sendInvite = function (username, callback) {
|
||||
$http.post(client.apiOrigin + '/api/v1/users/' + username + '/invite', {}).success(function (data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setAdmin = function (username, admin, callback) {
|
||||
var payload = {
|
||||
username: username,
|
||||
@@ -408,11 +447,12 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.createAdmin = function (username, password, email, setupToken, callback) {
|
||||
Client.prototype.createAdmin = function (username, password, email, displayName, setupToken, callback) {
|
||||
var payload = {
|
||||
username: username,
|
||||
password: password,
|
||||
email: email
|
||||
email: email,
|
||||
displayName: displayName
|
||||
};
|
||||
|
||||
var that = this;
|
||||
@@ -511,10 +551,12 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.createUser = function (username, email, callback) {
|
||||
Client.prototype.createUser = function (username, email, displayName, sendInvite, callback) {
|
||||
var data = {
|
||||
username: username,
|
||||
email: email
|
||||
email: email,
|
||||
displayName: displayName,
|
||||
invite: !!sendInvite
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/users', data).success(function(data, status) {
|
||||
@@ -523,12 +565,25 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.updateUser = function (user, password, callback) {
|
||||
var data = {
|
||||
password: password,
|
||||
email: user.email,
|
||||
displayName: user.displayName
|
||||
};
|
||||
|
||||
$http.put(client.apiOrigin + '/api/v1/users/' + user.id, data).success(function(data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.removeUser = function (userId, password, callback) {
|
||||
var data = {
|
||||
password: password
|
||||
};
|
||||
|
||||
$http({ method: 'DELETE', url: '/api/v1/users/' + userId, data: data, headers: { 'Content-Type': 'application/json' }}).success(function(data, status) {
|
||||
$http({ method: 'DELETE', url: client.apiOrigin + '/api/v1/users/' + userId, data: data, headers: { 'Content-Type': 'application/json' }}).success(function(data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', []);
|
||||
|
||||
app.controller('ErrorController', ['$scope', '$http', function ($scope, $http) {
|
||||
$scope.webServerOriginLink = '/';
|
||||
$scope.errorMessage = '';
|
||||
|
||||
// try to fetch at least config.json to get appstore url
|
||||
$http.get('config.json').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
|
||||
$scope.webServerOriginLink = data.webServerOrigin + '/console.html';
|
||||
}).error(function (data, status) {
|
||||
if (status === 404) console.error('No config.json found');
|
||||
else console.error(status, data);
|
||||
});
|
||||
|
||||
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.errorCode = search.errorCode || 0;
|
||||
$scope.errorContext = search.errorContext || '';
|
||||
}]);
|
||||
@@ -1,14 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global showdown:false */
|
||||
|
||||
// deal with accessToken in the query, this is passed for example on password reset
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
if (search.accessToken) localStorage.token = search.accessToken;
|
||||
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'slick', 'ui-notification']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
delay: 10000,
|
||||
startTop: 60,
|
||||
positionX: 'left',
|
||||
templateUrl: 'templates/notification.html'
|
||||
});
|
||||
}]);
|
||||
|
||||
// setup all major application routes
|
||||
app.config(['$routeProvider', function ($routeProvider) {
|
||||
$routeProvider.when('/', {
|
||||
@@ -162,20 +173,25 @@ app.filter('prettyDate', function () {
|
||||
if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31)
|
||||
return;
|
||||
|
||||
return day_diff == 0 && (
|
||||
return day_diff === 0 && (
|
||||
diff < 60 && 'just now' ||
|
||||
diff < 120 && '1 minute ago' ||
|
||||
diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
|
||||
diff < 7200 && '1 hour ago' ||
|
||||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
||||
day_diff == 1 && 'Yesterday' ||
|
||||
day_diff === 1 && 'Yesterday' ||
|
||||
day_diff < 7 && day_diff + ' days ago' ||
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago';
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter();
|
||||
var converter = new showdown.Converter({
|
||||
extensions: ['targetblank'],
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
@@ -203,3 +219,17 @@ app.directive('laterName', function () { // (2)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
|
||||
var original = $location.path;
|
||||
$location.path = function (path, reload) {
|
||||
if (reload === false) {
|
||||
var lastRoute = $route.current;
|
||||
var un = $rootScope.$on('$locationChangeSuccess', function () {
|
||||
$route.current = lastRoute;
|
||||
un();
|
||||
});
|
||||
}
|
||||
return original.apply($location, [path]);
|
||||
};
|
||||
}]);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user