├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── SERVER.md ├── TODO.md ├── admin ├── .well-known │ └── humans.txt ├── README.md ├── backup │ └── letsencrypt.tar.xz ├── rebuild └── setup │ ├── backup-remote │ ├── backup-remote.service │ ├── backup-remote.timer │ ├── install.sh │ ├── redirect │ ├── redirect.js │ ├── redirect.service │ ├── renew-cert │ ├── renew-cert.service │ ├── renew-cert.timer │ ├── tree │ ├── tree.service │ ├── uninstall.sh │ ├── update │ ├── update.js │ ├── update.service │ └── updator.sh ├── app.js ├── jail └── Dockerfile ├── lib ├── README.md ├── api.js ├── app.js ├── base64url.js ├── conf.js ├── driver.js ├── fs.js ├── irc.js ├── json-pointer.js ├── log.js ├── lookup.js ├── plug.js ├── profiler.js ├── public-file.js ├── pwd-check.js ├── sandbox-shell.js ├── test.js └── type.js ├── package-lock.json ├── package.json └── renovate.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.dat 3 | *.out 4 | *.pid 5 | *.swp 6 | 7 | /web 8 | /plugs 9 | /metadata.json 10 | /admin/private 11 | /admin/log 12 | /admin/db 13 | 14 | /node_modules 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fruits"] 2 | path = fruits 3 | url = https://github.com/garden/fruits 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 | EUPL © the European Union 2007, 2016 3 | 4 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 | below) which is provided under the terms of this Licence. Any use of the Work, 6 | other than as authorised under this Licence is prohibited (to the extent such 7 | use is covered by a right of the copyright holder of the Work). 8 | 9 | The Work is provided under the terms of this Licence when the Licensor (as 10 | defined below) has placed the following notice immediately following the 11 | copyright notice for the Work: 12 | 13 | Licensed under the EUPL 14 | 15 | or has expressed by any other means his willingness to license under the EUPL. 16 | 17 | 1. Definitions 18 | 19 | In this Licence, the following terms have the following meaning: 20 | 21 | - ‘The Licence’: this Licence. 22 | 23 | - ‘The Original Work’: the work or software distributed or communicated by the 24 | Licensor under this Licence, available as Source Code and also as Executable 25 | Code as the case may be. 26 | 27 | - ‘Derivative Works’: the works or software that could be created by the 28 | Licensee, based upon the Original Work or modifications thereof. This Licence 29 | does not define the extent of modification or dependence on the Original Work 30 | required in order to classify a work as a Derivative Work; this extent is 31 | determined by copyright law applicable in the country mentioned in Article 15. 32 | 33 | - ‘The Work’: the Original Work or its Derivative Works. 34 | 35 | - ‘The Source Code’: the human-readable form of the Work which is the most 36 | convenient for people to study and modify. 37 | 38 | - ‘The Executable Code’: any code which has generally been compiled and which is 39 | meant to be interpreted by a computer as a program. 40 | 41 | - ‘The Licensor’: the natural or legal person that distributes or communicates 42 | the Work under the Licence. 43 | 44 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 | Licence, or otherwise contributes to the creation of a Derivative Work. 46 | 47 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 | the Work under the terms of the Licence. 49 | 50 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 | renting, distributing, communicating, transmitting, or otherwise making 52 | available, online or offline, copies of the Work or providing access to its 53 | essential functionalities at the disposal of any other natural or legal 54 | person. 55 | 56 | 2. Scope of the rights granted by the Licence 57 | 58 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 | sublicensable licence to do the following, for the duration of copyright vested 60 | in the Original Work: 61 | 62 | - use the Work in any circumstance and for all usage, 63 | - reproduce the Work, 64 | - modify the Work, and make Derivative Works based upon the Work, 65 | - communicate to the public, including the right to make available or display 66 | the Work or copies thereof to the public and perform publicly, as the case may 67 | be, the Work, 68 | - distribute the Work or copies thereof, 69 | - lend and rent the Work or copies thereof, 70 | - sublicense rights in the Work or copies thereof. 71 | 72 | Those rights can be exercised on any media, supports and formats, whether now 73 | known or later invented, as far as the applicable law permits so. 74 | 75 | In the countries where moral rights apply, the Licensor waives his right to 76 | exercise his moral right to the extent allowed by law in order to make effective 77 | the licence of the economic rights here above listed. 78 | 79 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 | any patents held by the Licensor, to the extent necessary to make use of the 81 | rights granted on the Work under this Licence. 82 | 83 | 3. Communication of the Source Code 84 | 85 | The Licensor may provide the Work either in its Source Code form, or as 86 | Executable Code. If the Work is provided as Executable Code, the Licensor 87 | provides in addition a machine-readable copy of the Source Code of the Work 88 | along with each copy of the Work that the Licensor distributes or indicates, in 89 | a notice following the copyright notice attached to the Work, a repository where 90 | the Source Code is easily and freely accessible for as long as the Licensor 91 | continues to distribute or communicate the Work. 92 | 93 | 4. Limitations on copyright 94 | 95 | Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 | any exception or limitation to the exclusive rights of the rights owners in the 97 | Work, of the exhaustion of those rights or of other applicable limitations 98 | thereto. 99 | 100 | 5. Obligations of the Licensee 101 | 102 | The grant of the rights mentioned above is subject to some restrictions and 103 | obligations imposed on the Licensee. Those obligations are the following: 104 | 105 | Attribution right: The Licensee shall keep intact all copyright, patent or 106 | trademarks notices and all notices that refer to the Licence and to the 107 | disclaimer of warranties. The Licensee must include a copy of such notices and a 108 | copy of the Licence with every copy of the Work he/she distributes or 109 | communicates. The Licensee must cause any Derivative Work to carry prominent 110 | notices stating that the Work has been modified and the date of modification. 111 | 112 | Copyleft clause: If the Licensee distributes or communicates copies of the 113 | Original Works or Derivative Works, this Distribution or Communication will be 114 | done under the terms of this Licence or of a later version of this Licence 115 | unless the Original Work is expressly distributed only under this version of the 116 | Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 | (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 | the Work or Derivative Work that alter or restrict the terms of the Licence. 119 | 120 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 | Works or copies thereof based upon both the Work and another work licensed under 122 | a Compatible Licence, this Distribution or Communication can be done under the 123 | terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 | Licence’ refers to the licences listed in the appendix attached to this Licence. 125 | Should the Licensee's obligations under the Compatible Licence conflict with 126 | his/her obligations under this Licence, the obligations of the Compatible 127 | Licence shall prevail. 128 | 129 | Provision of Source Code: When distributing or communicating copies of the Work, 130 | the Licensee will provide a machine-readable copy of the Source Code or indicate 131 | a repository where this Source will be easily and freely available for as long 132 | as the Licensee continues to distribute or communicate the Work. 133 | 134 | Legal Protection: This Licence does not grant permission to use the trade names, 135 | trademarks, service marks, or names of the Licensor, except as required for 136 | reasonable and customary use in describing the origin of the Work and 137 | reproducing the content of the copyright notice. 138 | 139 | 6. Chain of Authorship 140 | 141 | The original Licensor warrants that the copyright in the Original Work granted 142 | hereunder is owned by him/her or licensed to him/her and that he/she has the 143 | power and authority to grant the Licence. 144 | 145 | Each Contributor warrants that the copyright in the modifications he/she brings 146 | to the Work are owned by him/her or licensed to him/her and that he/she has the 147 | power and authority to grant the Licence. 148 | 149 | Each time You accept the Licence, the original Licensor and subsequent 150 | Contributors grant You a licence to their contributions to the Work, under the 151 | terms of this Licence. 152 | 153 | 7. Disclaimer of Warranty 154 | 155 | The Work is a work in progress, which is continuously improved by numerous 156 | Contributors. It is not a finished work and may therefore contain defects or 157 | ‘bugs’ inherent to this type of development. 158 | 159 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 | and without warranties of any kind concerning the Work, including without 161 | limitation merchantability, fitness for a particular purpose, absence of defects 162 | or errors, accuracy, non-infringement of intellectual property rights other than 163 | copyright as stated in Article 6 of this Licence. 164 | 165 | This disclaimer of warranty is an essential part of the Licence and a condition 166 | for the grant of any rights to the Work. 167 | 168 | 8. Disclaimer of Liability 169 | 170 | Except in the cases of wilful misconduct or damages directly caused to natural 171 | persons, the Licensor will in no event be liable for any direct or indirect, 172 | material or moral, damages of any kind, arising out of the Licence or of the use 173 | of the Work, including without limitation, damages for loss of goodwill, work 174 | stoppage, computer failure or malfunction, loss of data or any commercial 175 | damage, even if the Licensor has been advised of the possibility of such damage. 176 | However, the Licensor will be liable under statutory product liability laws as 177 | far such laws apply to the Work. 178 | 179 | 9. Additional agreements 180 | 181 | While distributing the Work, You may choose to conclude an additional agreement, 182 | defining obligations or services consistent with this Licence. However, if 183 | accepting obligations, You may act only on your own behalf and on your sole 184 | responsibility, not on behalf of the original Licensor or any other Contributor, 185 | and only if You agree to indemnify, defend, and hold each Contributor harmless 186 | for any liability incurred by, or claims asserted against such Contributor by 187 | the fact You have accepted any warranty or additional liability. 188 | 189 | 10. Acceptance of the Licence 190 | 191 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 | placed under the bottom of a window displaying the text of this Licence or by 193 | affirming consent in any other similar way, in accordance with the rules of 194 | applicable law. Clicking on that icon indicates your clear and irrevocable 195 | acceptance of this Licence and all of its terms and conditions. 196 | 197 | Similarly, you irrevocably accept this Licence and all of its terms and 198 | conditions by exercising any rights granted to You by Article 2 of this Licence, 199 | such as the use of the Work, the creation by You of a Derivative Work or the 200 | Distribution or Communication by You of the Work or copies thereof. 201 | 202 | 11. Information to the public 203 | 204 | In case of any Distribution or Communication of the Work by means of electronic 205 | communication by You (for example, by offering to download the Work from a 206 | remote location) the distribution channel or media (for example, a website) must 207 | at least provide to the public the information requested by the applicable law 208 | regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 | stored and reproduced by the Licensee. 210 | 211 | 12. Termination of the Licence 212 | 213 | The Licence and the rights granted hereunder will terminate automatically upon 214 | any breach by the Licensee of the terms of the Licence. 215 | 216 | Such a termination will not terminate the licences of any person who has 217 | received the Work from the Licensee under the Licence, provided such persons 218 | remain in full compliance with the Licence. 219 | 220 | 13. Miscellaneous 221 | 222 | Without prejudice of Article 9 above, the Licence represents the complete 223 | agreement between the Parties as to the Work. 224 | 225 | If any provision of the Licence is invalid or unenforceable under applicable 226 | law, this will not affect the validity or enforceability of the Licence as a 227 | whole. Such provision will be construed or reformed so as necessary to make it 228 | valid and enforceable. 229 | 230 | The European Commission may publish other linguistic versions or new versions of 231 | this Licence or updated versions of the Appendix, so far this is required and 232 | reasonable, without reducing the scope of the rights granted by the Licence. New 233 | versions of the Licence will be published with a unique version number. 234 | 235 | All linguistic versions of this Licence, approved by the European Commission, 236 | have identical value. Parties can take advantage of the linguistic version of 237 | their choice. 238 | 239 | 14. Jurisdiction 240 | 241 | Without prejudice to specific agreement between parties, 242 | 243 | - any litigation resulting from the interpretation of this License, arising 244 | between the European Union institutions, bodies, offices or agencies, as a 245 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 | of Justice of the European Union, as laid down in article 272 of the Treaty on 247 | the Functioning of the European Union, 248 | 249 | - any litigation arising between other parties and resulting from the 250 | interpretation of this License, will be subject to the exclusive jurisdiction 251 | of the competent court where the Licensor resides or conducts its primary 252 | business. 253 | 254 | 15. Applicable Law 255 | 256 | Without prejudice to specific agreement between parties, 257 | 258 | - this Licence shall be governed by the law of the European Union Member State 259 | where the Licensor has his seat, resides or has his registered office, 260 | 261 | - this licence shall be governed by Belgian law if the Licensor has no seat, 262 | residence or registered office inside a European Union Member State. 263 | 264 | Appendix 265 | 266 | ‘Compatible Licences’ according to Article 5 EUPL are: 267 | 268 | - GNU General Public License (GPL) v. 2, v. 3 269 | - GNU Affero General Public License (AGPL) v. 3 270 | - Open Software License (OSL) v. 2.1, v. 3.0 271 | - Eclipse Public License (EPL) v. 1.0 272 | - CeCILL v. 2.0, v. 2.1 273 | - Mozilla Public Licence (MPL) v. 2 274 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 | works other than software 277 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 | Reciprocity (LiLiQ-R+). 280 | 281 | The European Commission may update this Appendix to later versions of the above 282 | licences without producing a new version of the EUPL, as long as they provide 283 | the rights granted in Article 2 of this Licence and protect the covered Source 284 | Code from exclusive appropriation. 285 | 286 | All other changes or additions to this Appendix require the production of a new 287 | EUPL version. 288 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile: start/stop and manage your tree server. 2 | SHELL = bash 3 | 4 | # The output of console.log statements goes in this file when you `make`. 5 | LOG = admin/log/tree.log 6 | # The pid of the process (stored in a file). 7 | PID = .pid 8 | # The current date in ISO 8601 format. 9 | DATE = $(shell date "+%Y%m%dT%H%M%S%z") 10 | TLS_CIPHER_LIST = 'TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384' 11 | 12 | default: stop install start 13 | 14 | start: stop 15 | @echo "[tree] start" 16 | @port=$$(jq .http.port -r <./admin/private/env.json); \ 17 | host=$$(jq .http.host -r <./admin/private/env.json); \ 18 | if [ `id -u` -ne "0" -a "$$port" -lt 1024 ]; \ 19 | then \ 20 | sudo -p '[sudo] password for $(USER): ' echo; \ 21 | sudo -n bash -c 'make run' & \ 22 | else \ 23 | make run & \ 24 | fi; \ 25 | if [ $$! -ne "0" ]; then echo $$! > $(PID); fi; \ 26 | echo "[info] tree running on http://$$host:$$port (see $(LOG))"; \ 27 | echo "[info] use 'make stop' to kill it" 28 | 29 | run: 30 | @echo "[tree] run" 31 | node --async-stack-traces --tls-cipher-list=$(TLS_CIPHER_LIST) \ 32 | ./app.js >> $(LOG) 2>&1 33 | 34 | stop: 35 | @echo "[tree] stop" 36 | @if [ -e $(PID) ]; then \ 37 | ps -p $$(cat $(PID)) >/dev/null 2>&1; \ 38 | if [ $$? -eq 0 ]; then \ 39 | pgid="$$(ps -q "$$(cat $(PID))" -o pgid=)"; \ 40 | pkill -g "$$pgid" 2>/dev/null || sudo pkill -g "$$pgid"; \ 41 | fi; \ 42 | rm $(PID); \ 43 | fi 44 | 45 | restart: stop start 46 | 47 | save: 48 | @if [ -e web/.git ]; then mv web/.git .git-bk; fi 49 | @cp -r web/* plugs/ 50 | @rm -rf plugs/test/ 51 | @cat metadata.json | jq 'del(.files.test)' > plugs/metadata.json 52 | @if [ -e .git-bk ]; then mv .git-bk web/.git; fi 53 | @echo "[info] you may now commit what is in plugs/" 54 | 55 | load: 56 | @# WARNING: This operation overwrites files in web/. 57 | @if [ ! -e web/ ]; then mkdir web; fi 58 | @# We must not copy the metadata to web/. 59 | @mv plugs/metadata.json plug-metadata.json 60 | @cp -rf plugs/* web/ 61 | @cp plug-metadata.json new-metadata.json 62 | @if [ -e metadata.json ]; then \ 63 | jq -s '.[0] * .[1]' metadata.json plug-metadata.json >new-metadata.json; \ 64 | fi 65 | @mv new-metadata.json metadata.json 66 | @mv plug-metadata.json plugs/metadata.json 67 | @echo "[info] deployed web/ and metadata from plugs/" 68 | 69 | backup: 70 | @mkdir -p backup 71 | @tar cf backup/web-$(DATE).tar web 72 | @tar --append -f backup/web-$(DATE).tar metadata.json 73 | @xz backup/web-$(DATE).tar.xz 74 | @rm backup/web-$(DATE).tar 75 | @echo "[info] copied web/, metadata and database to backup/web-$(DATE).tar.xz" 76 | 77 | restore: 78 | @tar xf "$$(ls backup/*.tar.xz | tail -1)" 79 | @echo '[info] deployed web/, metadata and database from '"$$(ls backup/*.tar.xz | tail -1)" 80 | 81 | # When files move around in web/, some dead metadata entries stay in metadata. 82 | # They need to be garbage collected from time to time. 83 | gc: 84 | node ./admin/rebuild 85 | 86 | test: 87 | node lib/test.js 88 | 89 | install: install-bin web/ node_modules/ 90 | 91 | install-bin: 92 | @bash admin/setup/install.sh 93 | 94 | web/: plugs/ 95 | @if [ ! -e web/ ]; then \ 96 | echo "[install] extracting web"; \ 97 | make load; \ 98 | fi 99 | 100 | plugs/: 101 | @echo "[install] obtaining plugs" 102 | @git clone http://github.com/garden/plugs 103 | 104 | node_modules/: 105 | @echo "[install] npm dependencies" 106 | # libicu-dev is needed for nodemailer. 107 | @sudo apt install libicu-dev 108 | @npm install 109 | 110 | uninstall: 111 | @bash admin/setup/uninstall.sh 112 | 113 | update-camp: 114 | npm update camp 115 | 116 | update-ot: 117 | npm update ot 118 | curl 'https://raw.githubusercontent.com/Operational-Transformation/ot.js/master/dist/ot-min.js' > web/lib/js/ot-min.js 119 | curl 'https://raw.githubusercontent.com/Operational-Transformation/ot.js/master/dist/ot.js' > web/lib/js/ot.js 120 | 121 | privkey.pem: 122 | @echo "[https] generating privkey.pem KEY" 123 | @cd admin/private/https; openssl genrsa -aes256 -out privkey.pem 1024 124 | 125 | fullchain.pem: privkey.pem 126 | @echo "[https] generating fullchain.pem CSR" 127 | @cd admin/private/https; openssl req -new -key privkey.pem -out fullchain.pem 128 | 129 | cert.pem: privkey.pem fullchain.pem 130 | @echo "[https] generating cert.pem CRT" 131 | @cd admin/private/https; openssl x509 -req -days 365 -in fullchain.pem -signkey privkey.pem -out cert.pem 132 | 133 | rmhttps: 134 | @echo "[https] deleting https credentials" 135 | @rm -rf admin/private/https/* 136 | 137 | https: cert.pem 138 | @echo "[info] you can now use privkey.pem, fullchain.pem, cert.pem" 139 | 140 | jail: 141 | @echo "[jail] Constructing the program jail." 142 | cd jail && sudo docker build -t tree-jail . 143 | 144 | help: 145 | @cat Makefile | less 146 | 147 | me a: 148 | @cd . 149 | 150 | sandwich: 151 | @if [ `id -u` = "0" ] ; then echo "OKAY." ; else echo "What? Make it yourself." ; fi 152 | 153 | .PHONY: default run install install-bin uninstall start stop restart save load backup gc test update-camp update-ot rmhttps https jail help me a sandwich 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tree 2 | 3 | > A multiplayer file system. 4 | > All files *synced* realtime, offline-ready. 5 | > Use *apps* for each file types. 6 | > Make your own apps on the platform. 7 | > *Access control* per-file. 8 | 9 | [thefiletree.com](https://thefiletree.com) 10 | 11 | ## Install 12 | 13 | ```bash 14 | git clone https://github.com/garden/tree 15 | cd tree 16 | mkdir -p admin/private; cp env.json admin/private/ # See admin/README.md 17 | # For development: 18 | make start 19 | open http://[::1]:1234 20 | # For production: 21 | make install 22 | tail -f admin/log/tree.log # to see errors. 23 | sudo systemctl restart tree # when you need to restart the server. 24 | ``` 25 | 26 | ## Interface 27 | 28 | - Create 29 | - file: `PUT /` (content is body) 30 | - folder: `MKCOL /` 31 | - Read 32 | - `GET /` (show in default browser app) 33 | - `GET /?app=data` (raw, optional header `Depth:3` for folder; TODO slice) 34 | - `GET /?app=metadata Content-Type:multipart/form-data` (JSON) 35 | - Update 36 | - single file: 37 | - data: 38 | - overwrite: `PUT /` 39 | - TODO append: `POST /?op=append` 40 | - TODO partial: `PATCH /` 41 | - sync: websocket `/?op=edit&app=text` (TODO json, dir) 42 | - metadata: `PUT /?app=metadata Content-Type:application/json` (TODO: `PATCH /`) 43 | - multiple files: `POST /?op=append&content Content-Type:multipart/form-data` 44 | - folder: 45 | - TODO copy: `COPY /from Destination:/to Overwrite:T` 46 | - TODO move: `MOVE /from Destination:/to Overwrite:F` 47 | - TODO shell: `POST /?op=shell&cmd=make&keep=a.out` (stdin is body, stdout is result, unless websocket) 48 | - Delete: `DELETE /` 49 | 50 | ## Contribute 51 | 52 | - Open [issues](https://github.com/garden/tree/issues) 53 | - Send [pull requests](http://help.github.com/send-pull-requests) 54 | - Contact [Thaddee Tyl](https://twitter.com/espadrine) 55 | 56 | ## Plans 57 | 58 | - Allow user-made apps 59 | - Send `APP_AUTH` 60 | - Use a safe templating system 61 | - JSON sync 62 | - File search 63 | - Snapshots (may require to migrate the file content to CockroachDB) 64 | - Remote desktop 65 | 66 | ## Dependencies 67 | 68 | This project is covered by the GNU General Public License (version 2) and contains code from: 69 | 70 | - [ScoutCamp](https://github.com/espadrine/sc/), a powerful web server (LGPL license) 71 | - [CodeMirror](https://github.com/marijnh/CodeMirror/), a nifty in-browser code editor (MIT license) 72 | - [Canop](https://github.com/espadrine/canop/), a real time sync system (MIT license) 73 | - [CockroachDB](https://www.cockroachlabs.com), a distributed SQL database 74 | -------------------------------------------------------------------------------- /SERVER.md: -------------------------------------------------------------------------------- 1 | This is documentation for the `app.js` server script. Please edit if you find 2 | it out-of-date or incomplete. 3 | 4 | # Routing requests 5 | 6 | * Requested resources are templated into plugs if metadata or the type requires it: 7 | - If resource is a zip file, serve directly (do not template / route). 8 | - If resource is a text file, embed its content into a text editor. 9 | - If resource is a folder, embed a list of its files into a file explorer. 10 | - If resource metadata require a specific plug, use that plug as a template. 11 | 12 | # Ajax actions 13 | 14 | * `profiler` serves profiling information about the server. 15 | 16 | * `fs` filesystem primitives: 17 | - `create` a new entry (file or folder) 18 | - `read` an entry (file or folder) 19 | - `apply` an operation (insert or delete content) 20 | - `delete` an entry 21 | 22 | * `meta-save` mutate metadata of a file system entry 23 | 24 | * `upload` import bulk files as new entries 25 | 26 | * `join` an IRC channel 27 | 28 | * `say` something on an IRC channel 29 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Ideas on future perspectives. 2 | 3 | 4 | # / 5 | 6 | When the file system is mature enough, its root will be accessed directly by `/` 7 | instead of by `/root/`. In order to achieve this, some key elements of the tree 8 | will have to be self-hosted, like the Pencil and the Gateway. This means we 9 | will have figured out a way to know when to send files directly and when to open 10 | them through file tree plugins. 11 | 12 | EDIT: This was done as of 2012-04-04. 13 | 14 | 15 | # Operational Transformation 16 | 17 | The current Operational Transformation in use (apart from having software bugs) depletes much space on the server side (we have to store a copy of the file for each edit by a client, which creates a lot of copies!), and the transformation 18 | operation used (patch) is quite expensive CPU-wise. 19 | 20 | We intend to use finer Operation Transformations such as those devised in Xerox 21 | PARC [1]. We may add information as we go, since those algorithms have been well understood and optimized since then. We will document our effort in relevant documentation files (probably along `/lib/sync.js`). 22 | 23 | [1] http://delivery.acm.org/10.1145/220000/215706/p111-nichols.pdf 24 | 25 | EDIT: This was done as of 2012-04-18. Special thanks to 26 | [Tim Baumann] (https://github.com/operational-transformation/ot.js). 27 | 28 | 29 | # Sandbox 30 | 31 | We wish to have the following functionalities: 32 | 33 | - Run some programs (such as compilers) as an FFI. 34 | - The program cannot see or go above the root directory. 35 | - The program is limited in time and memory. 36 | - (Future requirement:) The program cannot modify password-protected files 37 | without input from the user. 38 | 39 | Ideas: 40 | 41 | - Use chroot 42 | - Use `bash -rs` (or `zsh`, for that matter) 43 | (Is that really useful? Can we run `bash` from there? Through vi?) 44 | - Use a custom `rc` file that resets all shell variables. 45 | - Use `ulimit` in that bash 46 | 47 | Issues: 48 | 49 | - Can this be cross-platform? The above ideas are Unix-only. 50 | 51 | 52 | # Security 53 | 54 | ## Old design. 55 | 56 | We get a salt from the server. We send a PBKDF2ed SHA256 key from the 57 | passphase the user enters. On receiving the SHA, the server makes it go 58 | through another PBKDF2, and compares it to its own sha. If they match, 59 | it decodes the AES256-ciphered data with the SHA it got (not the one it 60 | computed). The resulting file is sent in clear to the client. 61 | 62 | The reason why the client only sends the SHA256 is to prove that we do not 63 | store keys in the clear. It is a PR force with no real value, since we 64 | already are under https. 65 | 66 | We absolutely need TLS (otherwise the whole security system crumbles down). 67 | We can get it free from StartCom, whose root certificate is in Firefox (and 68 | probably everywhere else, too). 69 | 70 | ## Reasons that design cannot succeed. 71 | 72 | That design isn't aware of the Read/Write access differences that we want. 73 | Instead, it encrypts all the data even when unnecessary. This results in a lot of 74 | encrypting / decrypting that doesn't need to be there. 75 | 76 | In addition to taking a lot of CPU cycles, it also takes a lot of effort for engineers 77 | mainly because all of the system needs to be implemented at once. 78 | 79 | Besides, having the key both encrypted in a PBKDF2 and used in encrypting the 80 | data raises differential cryptanalysis concerns. 81 | 82 | Finally, the performance of PBKDF2 is slower on a CPU than on a GPU, allowing advanced 83 | code breaking techniques to brute-force the key fast: increasing the number of 84 | iterations makes normal use to have an increase in execution time that is a lot 85 | smaller for code breaking use. 86 | 87 | ## New design. 88 | 89 | Metadata access and write access requires the following: 90 | 91 | - Put OT support in the File System library. 92 | - Provide deniable access to file system primitives. 93 | 94 | Read access requires more engineering (and a little bit more design effort). 95 | 96 | ### Metadata access 97 | 98 | Users can restrict the metadata modification by using a passphrase. That 99 | passphrase's hash is stored with scrypt in the file's metadata, under the name 100 | `metakey`. Without the key, users can view all metadata but the keys. With the 101 | key, they can modify the metadata. 102 | 103 | A request for editing the metadata will follow these steps: 104 | 105 | 1. Get the key from the user. 106 | 2. Send the key in the `metakey` field of a server-side request to 107 | `/$meta-save`, protected by HTTPS, with the modifications, or through the 108 | Authorization HTTP header field. 109 | 3. Get the scrypt of that key, using the salt and iteration count indicated in 110 | `metakey`. Authorize an unlimited number of tries, with a second wait between 111 | each try. 112 | 4. If the hash we got is the same as the one that is in `metakey`, the 113 | modifications get applied. Otherwise, they are denied. 114 | 115 | ### Write access 116 | 117 | Users can restrict write access by using a passphrase. The system knows that a 118 | file has a write access restriction if the file's metadata has a non-empty 119 | `writekey` field or the first parent folder that has a `readkey` fields is 120 | non-empty. That passphrase is stored with scrypt in the file's metadata, under 121 | the name `writekey`. 122 | 123 | A request for read access always succeeds. That property makes encrypting the 124 | data useless. 125 | 126 | Any request that modifies the contents of the data, without including a 127 | `writekey` field with the correct passphrase, will fail. The operational 128 | transformation system will forbid modification. 129 | 130 | Without a correct write key, the user is granted read-only access. 131 | 132 | ### Read access 133 | 134 | Users can restrict read access by using a passphrase. The system knows that a 135 | file has a read access restriction if the file's metadata has a non-empty 136 | `readkey` field or the first parent folder that has a `readkey` field is 137 | non-empty. That key overrides the write key; the `writekey` field becomes 138 | useless. 139 | 140 | The `readkey` works similarly to how the `writekey` works, except that it won't 141 | give read-only access if the provided password doesn't match the stored scrypt 142 | hash. Accessing those files either requires a `readkey` or an `Authorization` 143 | HTTP header, the latter of which is made easier by the fact that sending an 144 | incorrect key causes serving this page through `WWW-Authenticate`. 145 | 146 | Also, the files are all encrypted using the standard scrypt system. They are 147 | decrypted and stored in the memory while editing. 148 | 149 | Note that this means that users have to wait for the file to be decrypted before 150 | they can start editing it. 151 | 152 | 153 | # User-Space File System 154 | 155 | Create a proper distributed file system, using a flexible protocol over a secure 156 | websocket connection. 157 | We should have two main frames: content and delta. 158 | Think of them as I-frames and P-frames in video encoding. 159 | 160 | We should construct a user-space filesystem through FUSE. Make metadata work 161 | fast (load it all up in memory at startup?) and more flexible (use the SET file 162 | format instead?) so that we can actually annotate the metadata with comments. 163 | Of course, we would prettify it on every disk write. 164 | 165 | 166 | # Fork/Join 167 | 168 | Allow a user to fork a file. 169 | That FS-level operation forces the system to keep track of all changes made to 170 | that file. 171 | Once done, the user can join back to the original file. 172 | The system should prevent conflicts from ever happening (which is theoretically 173 | possible). 174 | -------------------------------------------------------------------------------- /admin/.well-known/humans.txt: -------------------------------------------------------------------------------- 1 | - Thaddée Tyl 2 | - Twitter: https://twitter.com/espadrine 3 | - GitHub: https://github.com/espadrine 4 | - Blog: https://espadrine.github.io/blog/ 5 | - Website: https://espadrine.github.io/ 6 | -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # Tree Services 2 | 3 | Service scripts for managing a secure [file tree] (https://github.com/garden/tree) instance. 4 | 5 | - `make install`: provision a new computer. 6 | - `make start`: launch the HTTP server (and associated services, if needed, like the database). 7 | - `make stop`: stop the HTTP server. 8 | 9 | In production, when freshly into a new server, you need to do the following: 10 | 11 | ```bash 12 | # Change the root password to something very secure. 13 | passwd 14 | # Create the dom user. 15 | useradd --create-home --user-group --key UMASK=022 dom 16 | passwd dom 17 | usermod -aG sudo dom 18 | cd /home/dom 19 | # Clone the project. 20 | sudo apt update 21 | sudo apt install git make 22 | git clone https://github.com/garden/tree.git 23 | cd tree 24 | mkdir -p admin/private 25 | ``` 26 | 27 | Back on your local directory: 28 | 29 | ```bash 30 | scp admin/private/prod.json tree:/home/dom/tree/admin/private/env.json 31 | # If you need to restore data: 32 | scp backup/web*.tar.xz tree:/home/dom/tree/backup/ 33 | ``` 34 | 35 | Back in the remote production server in /home/dom/tree: 36 | 37 | ``` 38 | make install 39 | ``` 40 | 41 | ## Files 42 | 43 | - `rebuild`: reconstruct metadata from files seen on disk. Run without parameters for help. 44 | - `log/`: log files. 45 | - `.well-known/`: files served by the HTTP server under the `/.well-known/` path. Used for Let’s Encrypt. 46 | - `setup/`: installation and systemctl scripts. 47 | - `tree`: HTTPS server of TheFileTree. 48 | - `redirect`: HTTP server redirecting to HTTPS. 49 | - `update`: HTTP server listening for GitHub push events on port 1123 to update your `tree` repo. Deprecated. 50 | - `private/`: folder with the server’s parameters. It typically contains env.json. Also, https certificate files will be in `private/https/`, and the private database root CA keys are in `private/dbcerts`. 51 | - `db/`: database content and certificates, but not the private root CA key. 52 | 53 | A typical env.json looks like this: 54 | 55 | ```json 56 | { 57 | // Enforce production restrictions. 58 | "env": "production", 59 | "http": { 60 | "host": "thefiletree.com", 61 | "port": 443, 62 | "secure": true, 63 | "key": "/etc/letsencrypt/live/thefiletree.com/privkey.pem", 64 | "cert": "/etc/letsencrypt/live/thefiletree.com/cert.pem", 65 | "ca": ["/etc/letsencrypt/live/thefiletree.com/fullchain.pem"], 66 | "cors": { 67 | "origin": "https://thefiletree.com" 68 | } 69 | }, 70 | "mailer": { 71 | "secure": true, 72 | "requireTLS": true, 73 | "host": "in.mailjet.com", 74 | "port": 2525, 75 | "from": "hi@thefiletree.com", 76 | "auth": { 77 | // Fake; insert the real ones. 78 | "user": "d5e5b767ecc46c6f83e0d4a969bccbd3", 79 | "pass": "44de026d1b312dcfa905225ff3c37d4c" 80 | } 81 | }, 82 | "pg": { 83 | "user": "tyl", 84 | "host": "free-tier5.gcp-europe-west1.cockroachlabs.cloud", 85 | "port": "26257", 86 | "database": "thefiletree-1974.defaultdb", 87 | // Also fake, insert your own. 88 | "password": "co-zAZxDUIoiXJ1TeJTz2t", 89 | "ssl": { 90 | "rejectUnauthorized": true, 91 | "ca": "./admin/db/certs/root.crt" 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | When starting out on a new server, you will first need to use a self-signed TLS 98 | certificate to perform the Let's Encrypt DNS validation and. For that purpose, 99 | you need to change the certificate paths to the ones we generate for you: 100 | 101 | ```json 102 | { 103 | "key": "admin/private/https/privkey.pem", 104 | "cert": "admin/private/https/cert.pem", 105 | "ca": ["admin/private/https/fullchain.pem"] 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /admin/backup/letsencrypt.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garden/tree/c2999439f617a60dbd22e4f4d3edb17d585b090e/admin/backup/letsencrypt.tar.xz -------------------------------------------------------------------------------- /admin/rebuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var exec = require('child_process').execFileSync; 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var treefs = require('../lib/fs.js'); 6 | 7 | var args = process.argv; 8 | args.shift(); // Node executable 9 | args.shift(); // This script 10 | 11 | var printDiff = false; // Whether to print the diff on stderr. 12 | if (args[0] === '--diff') { 13 | printDiff = true; 14 | args.shift(); 15 | } 16 | var pathFilter = new RegExp(); 17 | if (args[0] === '--filter') { 18 | args.shift(); 19 | try { 20 | var filter = args.shift(); 21 | if (filter[0] !== '/') { 22 | console.log("Path filter must start from the root"); 23 | process.exit(0); 24 | } 25 | var filterRegex = '^(' + filter.split('/').slice(1) 26 | .map(function(elt, idx, arr) { 27 | var unescaped = '/' + arr.slice(0, idx + 1).join('/'); 28 | var escaped = unescaped.replace(/([\.\\\/\?\+])/g, '\\$1'); 29 | return escaped.replace(/\*/g, '[^/]+'); 30 | }).join('|') + ')$'; 31 | pathFilter = new RegExp(filterRegex); 32 | } catch (e) { 33 | console.log("Invalid path filter"); 34 | process.exit(0); 35 | } 36 | } 37 | var metaFile = args[0]; 38 | 39 | if (!metaFile) { 40 | console.log("Print an updated metadata file with files seen on the drive,"); 41 | console.log("and excluding files not seen on the drive."); 42 | console.log("Usage: ./tools/meta/rebuild [--diff] [--filter PATH] metadata.json"); 43 | console.log(); 44 | console.log(" --diff: only show what changes would be made"); 45 | console.log(" --filter PATH: update metadata for files matching PATH"); 46 | console.log(" * for globbing, [] for char selection"); 47 | process.exit(1); 48 | } 49 | 50 | try { 51 | var metadata = JSON.parse(fs.readFileSync(metaFile)); 52 | } catch(e) { 53 | console.error(`Could not open ${metaFile}: ${e.message}`); 54 | process.exit(0); 55 | } 56 | 57 | function checkFile(file, vpath) { 58 | var promises = []; 59 | file.meta = file.meta || {}; 60 | var realPath = treefs.realFromVirtual(vpath); 61 | try { 62 | var stat = fs.statSync(realPath); 63 | // If the path failed, ignore it. 64 | } catch(e) { return Promise.resolve(); } 65 | if (stat.isFile()) { 66 | if (file.meta.type === 'folder' || file.meta.type === undefined) { 67 | promises.push(treefs.guessType(vpath) 68 | .then(function(type) { file.meta.type = type; }) 69 | .catch(console.error)); 70 | } 71 | } else if (stat.isDirectory()) { 72 | file.meta.type = 'folder'; 73 | file.files = file.files || {}; 74 | var subfiles = fs.readdirSync(realPath); 75 | // Remove files that are not on disk. 76 | for (var metaSubfile in file.files) { 77 | var subfileName = vpath + '/' + metaSubfile; 78 | if (subfiles.indexOf(metaSubfile) === -1 && pathFilter.test(subfileName)) { 79 | if (printDiff) { console.log('- ' + subfileName); } 80 | delete file.files[metaSubfile]; 81 | } 82 | }; 83 | // Add files that are on disk. 84 | subfiles.forEach(function(subfile) { 85 | var subfileName = vpath + '/' + subfile; 86 | if (file.files[subfile] === undefined && pathFilter.test(subfileName)) { 87 | if (printDiff) { console.log('+ ' + subfileName); } 88 | file.files[subfile] = {}; 89 | } 90 | }); 91 | } 92 | 93 | // Remove me: set old metadata information. 94 | try { 95 | var oldMetaPath = './plugs/meta' + vpath; 96 | if (file.meta.type === 'folder') { 97 | oldMetaPath += '/.DS-Store'; 98 | } 99 | var oldMeta = JSON.parse('' + fs.readFileSync(oldMetaPath)); 100 | if (oldMeta.plug !== undefined) { 101 | if (oldMeta.plug === 'none') { file.meta.app = 'data'; } 102 | else { file.meta.app = oldMeta.plug; } 103 | } 104 | if (file.meta.type === 'binary' && oldMeta.type !== 'binary') { file.meta.type = oldMeta.type; } 105 | } catch(e) {} 106 | 107 | if (file.meta.modified === undefined) { 108 | file.meta.modified = (stat.mtime || new Date()).toISOString(); 109 | } 110 | if (file.meta.updated === undefined) { 111 | file.meta.updated = (stat.ctime || file.meta.modified).toISOString(); 112 | } 113 | 114 | for (var filename in file.files) { 115 | promises.push(checkFile(file.files[filename], vpath + '/' + filename)); 116 | } 117 | return Promise.all(promises); 118 | } 119 | 120 | checkFile(metadata, '') 121 | .then(function() { 122 | if (!printDiff) { 123 | console.log(JSON.stringify(metadata)); 124 | } 125 | }) 126 | .catch(console.error); 127 | -------------------------------------------------------------------------------- /admin/setup/backup-remote: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | cd /home/tyl/file/tree 4 | bkexec="$(ssh tree ' 5 | cd /home/dom/tree; 6 | rm backup/*; 7 | make backup && 8 | ls backup/*.tar.xz | tail -1 9 | ')" 10 | echo "$bkexec" 11 | bkfile="$(echo "$bkexec" | tail -1 | tr -d '\r')" 12 | scp tree:/home/dom/tree/"$bkfile" backup/ 13 | # Delete older backups. 14 | rm $(ls backup/*.tar.xz | head -n -1) 15 | kdialog --passivepopup "Backup completed." --title TheFileTree 16 | -------------------------------------------------------------------------------- /admin/setup/backup-remote.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Download backup of thefiletree.com 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/home/tyl/file/tree/admin/setup/backup-remote 7 | -------------------------------------------------------------------------------- /admin/setup/backup-remote.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Download backup of thefiletree.com 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /admin/setup/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 4 | cd "$dir"/../.. 5 | mkdir -p admin/log admin/private/https admin/private/dbcerts 6 | 7 | # This script assumes an Ubuntu installation. 8 | 9 | # install git, curl, jq, node, npm 10 | 11 | if ! which git >/dev/null; then 12 | echo "[install] git" 13 | sudo apt install git 14 | fi 15 | 16 | if ! which curl >/dev/null; then 17 | echo "[install] curl" 18 | sudo apt install curl 19 | fi 20 | 21 | if ! which jq >/dev/null; then 22 | echo "[install] jq" 23 | sudo apt install jq 24 | fi 25 | 26 | if ! which g++ >/dev/null; then 27 | echo "[install] g++" 28 | sudo apt install g++ 29 | fi 30 | 31 | if ! which node >/dev/null; then 32 | echo "[install] node" 33 | node_js_version=$(wget -q -O - "https://nodejs.org/dist/index.tab" \ 34 | | tail -n +2 | head -n 1 | cut -f1) 35 | (set -x 36 | wget -Nq "https://nodejs.org/dist/${node_js_version}/node-${node_js_version}-linux-x64.tar.xz" 37 | tar xf node-*.tar.xz 38 | rm node-*-linux-x64.tar.xz 39 | sudo mv node-*-linux-x64 /usr/local/nodejs 40 | for exe in $(ls /usr/local/nodejs/bin); do 41 | sudo ln -s /usr/local/nodejs/bin/"${exe}" /usr/local/bin/"$exe" 42 | done 43 | ) 44 | fi 45 | 46 | if ! which openssl >/dev/null; then 47 | echo "[install] openssl" 48 | sudo apt install openssl 49 | fi 50 | 51 | if [[ "$env" == production ]]; then 52 | 53 | # Database setup 54 | 55 | echo "[install] database: please create a CockroachDB Serverless cluster: https://cockroachlabs.cloud/signup" 56 | echo "[install] database: then add its information to admin/private/env.json, see admin/README.md." 57 | 58 | # HTTPS (self-signed to bootstrap Let’s encrypt) 59 | 60 | echo "[install] tls: self-signed" 61 | (set -x 62 | if [[ ! -e admin/private/https/cert.pem ]]; then 63 | pushd admin/private/https 64 | openssl genrsa -aes256 -out privkey.pem 1024 65 | openssl req -new -nodes -key privkey.pem -out fullchain.pem 66 | openssl x509 -req -days 365 -in fullchain.pem -signkey privkey.pem -out cert.pem 67 | cp privkey.pem{,.orig} 68 | openssl rsa -in privkey.pem.orig -out privkey.pem 69 | popd 70 | fi 71 | ) 72 | 73 | # Services 74 | 75 | echo "[install] systemd: services for auto restart" 76 | (set -x 77 | if [[ ! -e /etc/systemd/system/tree.service ]]; then 78 | # install service scripts 79 | sudo cp admin/setup/tree.service /etc/systemd/system/ 80 | sudo cp admin/setup/redirect.service /etc/systemd/system/ 81 | sudo cp admin/setup/update.service /etc/systemd/system/ 82 | sudo cp admin/setup/renew-cert.service /etc/systemd/system/ 83 | sudo cp admin/setup/renew-cert.timer /etc/systemd/system/ 84 | sudo systemctl daemon-reload 85 | 86 | # start all services 87 | sudo systemctl start tree.service 88 | sudo systemctl start redirect.service 89 | sudo systemctl start update.service 90 | sudo systemctl start renew-cert.timer 91 | sudo systemctl enable tree.service 92 | sudo systemctl enable redirect.service 93 | fi 94 | ) 95 | 96 | # Let’s encrypt 97 | 98 | if [[ ! -e admin/private/https/letsencrypt ]]; then 99 | echo "[install] tls: let’s encrypt setup" 100 | if ! which certbot >/dev/null; then 101 | echo "[install] tls: certbot" 102 | (set -x 103 | sudo apt-get update 104 | sudo apt-get install software-properties-common 105 | sudo add-apt-repository ppa:certbot/certbot 106 | sudo apt-get update 107 | sudo apt-get install certbot 108 | ) 109 | fi 110 | (set -x 111 | sudo certbot certonly --webroot -d "$host" -w admin && \ 112 | touch admin/private/https/letsencrypt 113 | ) 114 | fi 115 | 116 | else # Not production 117 | 118 | # install CockroachDB 119 | 120 | if ! which cockroach >/dev/null; then 121 | echo "[install] cockroach: binary" 122 | (set -x 123 | wget -Nq "https://binaries.cockroachdb.com/cockroach-latest.linux-amd64.tgz" 124 | tar xfz cockroach-*.linux-amd64.tgz 125 | sudo cp -i cockroach-*.linux-amd64/cockroach /usr/local/bin 126 | rm -r cockroach-*.linux-amd64* 127 | ) 128 | fi 129 | 130 | host=$(/dev/null 2>&1 152 | then 153 | db_database=$(jq > /home/dom/tree/admin/log/update.log 2>&1 4 | make backup >> /home/dom/tree/admin/log/update.log 2>&1 5 | git pull origin master >> /home/dom/tree/admin/log/update.log 2>&1 6 | systemctl stop tree.service >> /home/dom/tree/admin/log/update.log 2>&1 7 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Run this with node to start your file tree server. 2 | // Copyright © 2011-2014 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | 6 | // IMPORT MODULES 7 | // 8 | 9 | var Camp = require('camp'); 10 | var fs = require('fs'); 11 | var app = require('./lib/app'); 12 | var api = require('./lib/api'); 13 | var configuration = require('./lib/conf'); 14 | 15 | 16 | // SERVER SETUP 17 | // 18 | 19 | // Start the server with command line options 20 | var camp = Camp.start(configuration.http); 21 | 22 | // Custom templating filter 23 | function templateScript(text) { 24 | if (!text) { return 'undefined'; } 25 | return text.replace(/ { 54 | fs.readFile('admin/.well-known/' + app.safePath(req.data[0]), (err, data) => { 55 | if (err) { res.statusCode = 404; res.end('Page not found\n'); return; } 56 | res.end(data); 57 | }); 58 | }); 59 | // Redirect all requests to a templated app. 60 | camp.path('*', app.resolve); 61 | camp.on('upgrade', app.websocket); 62 | -------------------------------------------------------------------------------- /jail/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Thaddée Tyl 3 | 4 | RUN apt-get -y update 5 | RUN apt-get install -y build-essential wget curl libedit-dev g++ clang make \ 6 | patch binutils-gold python ruby sbcl openjdk-7-jdk mono-complete llvm clang \ 7 | golang scala texlive-full libicu-dev rsync libxml2 git 8 | RUN mkdir /home/node-js && cd /home/node-js && \ 9 | wget -Nq http://nodejs.org/dist/node-latest.tar.gz && \ 10 | tar xzf node-latest.tar.gz && cd node-v* && ./configure && make && \ 11 | make install && rm -rf /home/node-js 12 | RUN curl -sSf https://static.rust-lang.org/rustup.sh | sh 13 | 14 | ENV SWIFT_BRANCH development 15 | ENV SWIFT_VERSION DEVELOPMENT-SNAPSHOT-2016-05-03-a 16 | ENV SWIFT_PLATFORM ubuntu14.04 17 | # Install Swift keys 18 | RUN wget -q -O - https://swift.org/keys/all-keys.asc | gpg --import - && \ 19 | gpg --keyserver hkp://pool.sks-keyservers.net --refresh-keys Swift 20 | # Install Swift Ubuntu 14.04 Snapshot 21 | RUN SWIFT_ARCHIVE_NAME=swift-$SWIFT_VERSION-$SWIFT_PLATFORM && \ 22 | SWIFT_URL=https://swift.org/builds/$SWIFT_BRANCH/$(echo "$SWIFT_PLATFORM" | tr -d .)/swift-$SWIFT_VERSION/$SWIFT_ARCHIVE_NAME.tar.gz && \ 23 | wget $SWIFT_URL && \ 24 | wget $SWIFT_URL.sig && \ 25 | gpg --verify $SWIFT_ARCHIVE_NAME.tar.gz.sig && \ 26 | tar -xvzf $SWIFT_ARCHIVE_NAME.tar.gz --directory / --strip-components=1 && \ 27 | rm -rf $SWIFT_ARCHIVE_NAME* /tmp/* /var/tmp/* 28 | # Set Swift Path 29 | ENV PATH /usr/bin:$PATH 30 | 31 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 32 | RUN useradd --create-home --user-group --key UMASK=022 myself 33 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | THE FILE TREE LIBRARY 2 | ===================== 3 | 4 | 5 | These are the gears of the File Tree. 6 | 7 | fs.js 8 | ----- 9 | 10 | This is our filesystem. Files and folders are loaded from `../root/`. 11 | 12 | A word of warning: the apis are in furious development right now. This notice 13 | will be removed when that is no longer so. 14 | 15 | ### API: 16 | 17 | - `type :: Object` 18 | See the type API. 19 | 20 | - `file :: Function (path :: String, callback :: Function (err :: Error, file 21 | :: File))` 22 | Obtain a file, given the "fake" File Tree `path`. 23 | 24 | - `fileFromPath :: Object` 25 | The keys are the "fake" paths of all files we have in memory. The values are 26 | those files (of type File). 27 | Ideally, don't use this. I would rather you used file(). 28 | 29 | ### Files: 30 | 31 | File objects contain the following functions: 32 | 33 | #### Meta 34 | 35 | * `this.meta :: Object` contains all meta information, such as the type 36 | (`this.meta.type`). 37 | * `this.updateLastModified :: Function` updates the `Last-Modified` metadata. 38 | * `this.isOfType :: Function (mimeType :: String)` checks whether the file is 39 | of a certain type, or falls back to it (ie, derives from it). 40 | * `this.path :: String` is the path of the file from the root. 41 | * `this.count :: Number` is the number of users that currently read this file. 42 | * `this.driver :: {String: {}}` is a map from common file actions to their 43 | low-level implementation. See below, at "driver.js". 44 | 45 | #### Content 46 | 47 | * `this.content :: Object` lets you obtain the contents of the file. 48 | The content may be `null` (in order not to waste precious memory) unless you 49 | have opened the file. 50 | * `this.open :: Function ( cb :: Function (err) )`: populate `this.content`, to 51 | manipulate the file's content synchronously. 52 | * `this.write :: Function (cb)`: if you want to write the file to disk. 53 | * `this.writeMeta :: Function (cb)`: if you want to write metadata to disk. 54 | * `this.subfiles :: Function (cb :: Function(err, subfiles))`: gives all the 55 | leafs of a folder recursively, as an Array, including folders. 56 | * `this.files :: Function (cb :: Function(err, files))`: gives all the 57 | children of a folder as an Array of files (whilst its content gives an Array 58 | of Strings). 59 | 60 | #### Extensibility 61 | 62 | * `this.rm :: Function(name, cb(err))` lets you remove the file from the tree. 63 | * `this.create :: Function(name, type cb(err))` lets you add a file (of type 64 | `type`, which is either 'dir', 'binary' or 'text') as a 65 | child of the current file, which must itself be a directory. 66 | 67 | 68 | lookup.js 69 | --------- 70 | 71 | This module provides the functionality necessary for looking up metadata values. 72 | 73 | The exported object is a function which you can feed a file and a query object 74 | (as is given in an HTTP request). It returns a lookup function. 75 | 76 | * `makeLookup :: Function(file, query)` returns the function below. 77 | * `lookup :: Function(key :: String, callback :: Function(value))` takes a key 78 | (which is a JS property accessor-ish, such as 'foo.bar["baz"]'). It returns 79 | the first value, first in the query string, then in the metadata of the file, 80 | that matches this key. If a callback is given, it also looks for this key in 81 | the metadata of the file's parent, and so on until it reaches the root of the 82 | tree. 83 | * `parseJSONQuery :: Function(key :: String)` is a property of the `makeLookup` 84 | function, and returns a list of all successive keys that are to be looked up 85 | for a specific property accessor, given as a string. This function is used by 86 | the `lookup()` function, and is exported for testing purposes. The parser in 87 | use does not accept spaces (except in strings), nor comments. 88 | 89 | 90 | type.js 91 | ------- 92 | 93 | This rules the file type system. 94 | 95 | - `addType(mimeType :: String, parents :: Array)` adds a new mime type to the 96 | type system, with fallbacks as a list of types (integers). 97 | - `fromName :: Object` gives the type (a number) from the mime type (String). 98 | - `nameFromType :: Array` gives the mime type from the number type. 99 | - `isCompatible(type :: Number, ancestor :: Number)` is true if `type` is 100 | compatible with `ancestor`. 101 | - `driver(type :: Number)` yields the driver corresponding to the indicated 102 | type. Drivers are specified in the `driver.js` file. 103 | 104 | 105 | driver.js 106 | --------- 107 | 108 | The need to hide low-level implementation of the different types of file makes 109 | the driver system necessary. It contains all primitive functions that do basic 110 | things with each type of file. 111 | 112 | - `primitives :: {String: {}}` is a map from internal types to a list of 113 | functions like `read`, `write`, `rm`, `mkdir`, `mkfile`, that the file system 114 | can use. Each file has a `driver` element that points to the primitives 115 | corresponding to its type. 116 | - `normalize :: Function(path :: String)` takes a virtual path and returns the 117 | same path, sanitized. For instance, "../foo.html/../bar.html" becomes 118 | "/bar.html". 119 | - `absolute :: Function(path :: String)` takes a virtual path and returns the 120 | real path on the host file system. 121 | - `relative :: Function(path :: String)` takes a virtual path and returns the 122 | path from the current directory to that file. 123 | - `virtual :: Function(path :: String)` takes a real path and returns the 124 | corresponding virtual path. 125 | - `loadMeta :: Function(path :: String, cb :: Function)` takes a virtual path 126 | and returns an error and the metadata (as an Object) in the callback. 127 | - `dumpMeta :: Function(path :: String, metadata :: Object, cb :: Function)` 128 | takes a virtual path and metadata, writes that metadata to disk, and returns 129 | an error in the callback. 130 | 131 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | // /api endpoint and authentication. 2 | 3 | var log = require('./log'); 4 | var fs = require('./fs'); 5 | var pg = require('pg'); 6 | var configuration = require('./conf'); 7 | var pgConfig = configuration.pg; 8 | pgConfig.pg = pg; 9 | var EmailLogin = require('email-login'); 10 | var emailLogin = new EmailLogin({ 11 | db: new EmailLogin.PgDb(pgConfig), 12 | mailer: configuration.mailer, 13 | }); 14 | var website = configuration.website; 15 | 16 | exports.main = function (camp) { 17 | camp.post('/api/1/signup', signup); 18 | camp.post('/api/1/signin', signin); 19 | camp.path('/api/1/login', login); 20 | camp.path('/api/1/logback', logback); 21 | camp.post('/api/1/logout', logout); 22 | camp.get('/~', home); 23 | camp.handle(authenticate); 24 | }; 25 | 26 | function error(code, msg, err, res) { 27 | if (err) { log.error(err); } 28 | res.statusCode = code || 500; 29 | res.json({errors: [msg || 'Internal Server Error']}); 30 | } 31 | 32 | // The following should be considered a valid name: 0ééλ統𝌆 33 | // Names are normalized in NFC. 34 | var allowedUsernames = /^[\w\xa0-\u1fff\u2c00-\u2dff\u2e80-\ud7ff\uf900-\uffef\u{10000}-\u{2fa1f}]{1,20}$/u; 35 | var reservedNames = /^(file|block|snap|root|lib|api|test|app|about|demo|doc|\w|\w\w)$/; 36 | 37 | function allowedUsername(name) { 38 | if (!allowedUsernames.test(name) || reservedNames.test(name)) { 39 | return Promise.resolve(false); 40 | } 41 | return fs.meta('/' + name).then(function(meta) { 42 | return meta === undefined; 43 | }); 44 | } 45 | 46 | function normalizeNFC(name) { 47 | if (typeof name === 'string') { return name.normalize(); } 48 | } 49 | 50 | function signup(req, res) { 51 | var email = req.data.email; 52 | var name = normalizeNFC(req.data.name); 53 | log(`Signup requested from ${email} for ${name}.`); 54 | allowedUsername(name).then(function(allowed) { 55 | if (!allowed) { 56 | error(400, "Disallowed name", null, res); 57 | return; 58 | } 59 | log(`Sending signin email confirmation for ${email}.`); 60 | emailConfirmation(req, res, { 61 | email: email, 62 | name: name, 63 | subject: 'TheFileTree account creation: confirm your email address', 64 | confirmUrl: function(tok) { 65 | return website + "/api/1/login?token=" + encodeURIComponent(tok) + 66 | '&name=' + encodeURIComponent(name); 67 | }, 68 | }, function(err) { 69 | if (err != null) { error(500, "Failed to send email", err, res); return; } 70 | log(`Signin mail confirmation sent to ${email}.`); 71 | res.end(); 72 | }); 73 | }); 74 | } 75 | 76 | // Prepare logging back in. 77 | function signin(req, res) { 78 | var email = req.data.email; 79 | log(`Sending signin email confirmation for ${email}.`); 80 | emailConfirmation(req, res, { 81 | email: email, 82 | subject: 'TheFileTree account log in: confirm your email address', 83 | confirmUrl: function(tok) { 84 | return website + "/api/1/logback?token=" + encodeURIComponent(tok); 85 | }, 86 | }, function(err) { 87 | if (err != null) { error(500, "Failed to send email", err, res); return; } 88 | log(`Signin mail confirmation sent to ${email}.`); 89 | res.end(); 90 | }); 91 | } 92 | 93 | // options: 94 | // - email: address to send the email to. 95 | // - name: (optional) name of the user to create. 96 | // - subject: content of the email's subject line. 97 | // - confirmUrl: function(token: String) returns the confirmation link URL. 98 | // callback: run when the email is sent. 99 | function emailConfirmation(req, res, options = {}, callback) { 100 | var email = options.email; 101 | var name = normalizeNFC(options.name); 102 | if (!email) { error(400, "Empty email", null, res); } 103 | emailLogin.login(function(err, token, session) { 104 | if (err != null) { error(500, "Sign up failed", err, res); return; } 105 | req.cookies.set('token', token); 106 | emailLogin.proveEmail({ 107 | token: token, 108 | email: email, 109 | name: options.subject || 'TheFileTree', 110 | confirmUrl: options.confirmUrl, 111 | }, function(err) { 112 | if (err != null) { 113 | error(500, "Sending the email confirmation failed", err, res); 114 | return; 115 | } 116 | callback(); 117 | }); 118 | }); 119 | } 120 | 121 | function login(req, res) { 122 | var name = normalizeNFC(req.data.name); 123 | emailLogin.confirmEmail(req.cookies.get('token'), req.data.token, 124 | function(err, token, session) { 125 | if (err != null) { 126 | log.error(err); 127 | res.redirect('/app/account/email-not-confirmed.html'); 128 | return; 129 | } 130 | if (token) { 131 | var home = '/' + name; 132 | fs.create(home, { type: 'folder' }) 133 | .then(function() { return fs.meta(home); }) 134 | .then(function(meta) { 135 | meta.acl = {}; 136 | meta.acl[name] = 'x'; 137 | meta.acl['*'] = '-'; 138 | return fs.updateMeta(home, meta); 139 | }).then(function() { 140 | emailLogin.setAccountData(session.email, {name: name}, function(err) { 141 | if (err != null) { error(500, "Login failed", err, res); return; } 142 | req.cookies.set('token', token); 143 | res.redirect('/app/account/logged-in.html'); 144 | }); 145 | }).catch(function(err) { 146 | error(400, "Failed to create your home folder, " + name, err, res); 147 | }); 148 | } else { 149 | res.redirect('/app/account/email-not-confirmed.html'); 150 | } 151 | }); 152 | } 153 | 154 | function logback(req, res) { 155 | emailLogin.confirmEmail(req.cookies.get('token'), req.data.token, 156 | function(err, token, session) { 157 | if (err != null) { 158 | log.error(err); 159 | res.redirect('/app/account/email-not-confirmed.html'); 160 | return; 161 | } 162 | if (token) { 163 | req.cookies.set('token', token); 164 | res.redirect('/app/account/logged-back.html'); 165 | } else { 166 | res.redirect('/app/account/email-not-confirmed.html'); 167 | } 168 | }); 169 | } 170 | 171 | function logout(req, res) { 172 | var token = req.cookies.get('token'); 173 | req.cookies.set('token'); 174 | res.redirect('/app/account/'); 175 | // Clearing database information is not safety-critical. 176 | emailLogin.logout(token, function(err) { 177 | if (err) { log.error(err); } 178 | }); 179 | } 180 | 181 | function home(req, res) { 182 | if (req.user && (typeof req.user.name === 'string')) { 183 | res.redirect('/' + req.user.name.normalize()); 184 | } else { res.redirect('/'); } 185 | } 186 | 187 | function authenticate(req, res, next) { 188 | if (res) { 189 | if (configuration.http.cors && configuration.http.cors.origin) { 190 | res.setHeader('Access-Control-Allow-Origin', 191 | configuration.http.cors.origin); 192 | } 193 | } 194 | req.env = {timer: {}}; 195 | 196 | var timerStart = process.hrtime(); 197 | var authToken = req.cookies.get('token'); 198 | emailLogin.authenticate(authToken, 199 | function(err, authenticated, session, token) { 200 | if (token) { req.cookies.set('token', token); } 201 | if (authenticated && session.emailVerified()) { 202 | var name = normalizeNFC(session.account.data.name); 203 | fs.meta('/' + name).then(function(userMeta) { 204 | try { 205 | var decodedToken = EmailLogin.decodeToken(authToken); // {id, token} 206 | } catch(e) { log.error(e); } 207 | var timerEnd = process.hrtime(timerStart); 208 | req.env.timer.auth = timerEnd[0] / 1e3 + timerEnd[1] / 1e6; 209 | req.user = { 210 | email: session.email, 211 | name: name, 212 | meta: userMeta, 213 | secret: new Buffer(decodedToken.token, 'base64'), 214 | }; 215 | next(); 216 | }).catch(function(e) { 217 | error(500, 'Failed to read user metadata', e, res); 218 | }) 219 | } else { next(); } 220 | }); 221 | } 222 | exports.authenticate = authenticate; 223 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | // Map paths to apps. 2 | // The following code is covered by the AGPLv3 license. 3 | 4 | var fs = require('./fs'); 5 | var log = require('./log'); 6 | var authenticate = require('./api').authenticate; 7 | var base64url = require('./base64url'); 8 | var crypto = require('crypto'); 9 | var WebSocket = require('ws'); 10 | var canop = require('canop'); 11 | var urlLib = require('url'); 12 | var pathLib = require('path'); 13 | var Camp = require('camp'); 14 | var camp; 15 | exports.main = function (server) { 16 | camp = server; 17 | fs.initAutosave(); 18 | }; 19 | 20 | function error(err, res, code, msg) { 21 | log.error(err + (err.stack? '\n' + err.stack: '')); 22 | if (res) { unloggedError(err, res, code, msg); } 23 | } 24 | 25 | function unloggedError(err, res, code = 500, msg = 'Internal server error\n') { 26 | res.statusCode = code; 27 | res.end(msg); 28 | } 29 | 30 | function success(body, res, code) { 31 | res.statusCode = code || 200; 32 | res.compressed().end(body); 33 | } 34 | 35 | // Return true if we rely on the HTTP cache (and called res.end()). 36 | function httpCached(req, res, lastModified) { 37 | if (lastModified !== undefined) { 38 | var jsonLastModified = JSON.stringify(lastModified); 39 | res.setHeader('Last-Modified', new Date(lastModified).toUTCString()); 40 | var etag = req.headers['if-none-match']; 41 | if (etag !== undefined && etag === jsonLastModified) { 42 | res.statusCode = 304; // Resource not modified. 43 | res.end(); 44 | return true; 45 | } else { 46 | res.setHeader('ETag', jsonLastModified); 47 | } 48 | } 49 | return false; 50 | } 51 | 52 | function deleteIfDeadFile(err, path) { 53 | if (err.code === 'ENOENT') { 54 | fs.removeMeta(path); 55 | } 56 | } 57 | 58 | function sendApp(app, path, req, res, meta, permission) { 59 | var depthh = req.headers.depth, depth; 60 | if (depthh === 'infinity') { depth = Infinity; } 61 | else if (depthh === undefined) { depth = -1; } 62 | else { depth = +depthh; } 63 | var user = req.user? req.user.name: undefined; 64 | 65 | // We only allow apps that are in /app/ and in the user metadata allowedApps. 66 | var authorizedApp = (app[0] !== '/') || app.startsWith('/app/') || 67 | (req.user != null && req.user.meta != null && 68 | (req.user.meta.allowedApps instanceof Array) && 69 | req.user.meta.allowedApps.includes(app)); 70 | if (req.user !== undefined && !authorizedApp) { app = 'data'; } 71 | 72 | if (app === 'data') { 73 | res.setHeader('Content-Type', meta.type); 74 | // Prevent allowing a script to load private information. 75 | // (Ideally we would want to prevent sending credentials instead.) 76 | res.setHeader('Content-Security-Policy', "connect-src 'none'"); 77 | if (!httpCached(req, res, meta.modified)) { 78 | fs.getStream(path, {depth: depth, user: user}).then(function(stream) { 79 | stream.pipe(res.compressed()); }) 80 | .catch(function(err) { deleteIfDeadFile(err, path); error(err, res); }); 81 | } 82 | } else if (app === 'metadata') { 83 | if (!ownerAccess.test(permission)) { denyAccess(res); return; } 84 | if (!httpCached(req, res, meta.updated)) { res.json(meta); } 85 | } else { 86 | if (app[0] !== '/') { app = '/app/' + app; } 87 | fs.metaGet(['params'], app, meta) 88 | .then(function(paramKeys) { 89 | paramKeys = paramKeys || []; 90 | log('\tusing ' + app, paramKeys); 91 | Promise.all(paramKeys.map(function(field) { 92 | if (field === 'data') { 93 | return fs.get(path, {depth: 0, user: user}) 94 | .then(function(content) { 95 | if (meta.type === 'text' || meta.type.slice(0, 5) === 'text/') { 96 | return Promise.resolve(String(content)); 97 | } else if (meta.type === 'folder' || 98 | meta.type.slice(0, 7) === 'folder/') { 99 | return Promise.resolve(JSON.parse(String(content))); 100 | } else { 101 | return Promise.resolve(content.toString('base64')); 102 | } 103 | }).catch(function(err) { deleteIfDeadFile(err, path); error(err, res); }); 104 | } else if (field === 'metadata') { 105 | if (ownerAccess.test(permission)) { return meta; 106 | } else { return {type: meta.type, updated: meta.updated}; } 107 | } else if (field[0] === '/') { 108 | // FIXME: look up metadata in the file metadata, then user dir. 109 | return fs.metaGet(field, path, meta); 110 | } 111 | })) 112 | .then(function(paramValues) { 113 | var params = Object.create(null); 114 | var templateData = Object.create(null); 115 | params.path = templateData.path = path; 116 | params.user = templateData.user = {name: user? req.user.name: undefined}; 117 | params.permission = templateData.permission = permission; 118 | params.metadata = {type: meta.type, updated: meta.updated}; 119 | addSpecialAppsTemplateData(app, templateData, req.user); 120 | for (var i = 0; i < paramValues.length; i++) { 121 | params[paramKeys[i]] = templateData[paramKeys[i]] = paramValues[i]; 122 | } 123 | templateData.params = params; 124 | return templateData; 125 | }) 126 | .then(function(templateData) { 127 | var appPage = app + '/page.html'; 128 | // We only trust root apps with the power to execute templates. 129 | if (app.startsWith('/app/')) { 130 | var appRealPath = fs.realFromVirtual(appPage); 131 | var appTemplate = camp.template(appRealPath); 132 | res.template(templateData, appTemplate); 133 | } else { 134 | if (!httpCached(req, res, meta.modified)) { 135 | fs.getStream(appPage, {user: user}).then(function(stream) { 136 | stream.pipe(res.compressed()); }) 137 | .catch(function(err) { error(err, res); }); 138 | } 139 | } 140 | }).catch(function(err) { error(err, res); }); 141 | }).catch(function(err) { error(err, res); }); 142 | } 143 | }; 144 | 145 | var safePath = exports.safePath = function safePath(path) { 146 | path = path.replace(/(\/|^)(\.\.?(\/|$))+/g, '/'); 147 | if (path.length > 1) { path = path.replace(/\/$/, ''); } 148 | return path; 149 | }; 150 | 151 | function addSpecialAppsTemplateData(app, templateData, user) { 152 | if (app === '/app/account') { 153 | templateData.loggedIn = user !== undefined; 154 | templateData.user.email = user? user.email: undefined; 155 | } 156 | // appAuth. See https://thefiletree.com/espadrine/post/identity.md 157 | //var hmac = crypto.createHmac('sha256', user.secret); 158 | //hmac.update(app); 159 | //var hash = base64url.fromBase64(hmac.digest('base64')); 160 | //templateData.appAuthHeader = `${app} hmac-sha256 ${hash}`; 161 | } 162 | 163 | function denyAccess(res) { 164 | log.error("Access denied"); 165 | // We don't want users to be able to tell which page exists, 166 | // so that they cannot get information from other users' paths. 167 | pageNotFound(res); 168 | } 169 | 170 | function pageNotFound(res) { 171 | res.statusCode = 404; 172 | res.end("Page not found or access denied\n"); 173 | } 174 | 175 | // Validates access to the path. 176 | // Call cb(function(meta, permission)) if the authorization was valid. 177 | function authorize(path, req, res, cb) { 178 | fs.meta(path).then(function(meta) { 179 | if (meta === undefined) { if (res) { pageNotFound(res); } return; } 180 | fs.permission(path, req.user? req.user.name: undefined) 181 | .then(function(permission) { 182 | if (permission === "-") { if (res) { denyAccess(res); } 183 | } else { cb(meta, permission); } 184 | }).catch(function(err) { error(err, res); }); 185 | }).catch(function(err) { error(err, res); }); 186 | }; 187 | 188 | function logHttpRequest(req, path) { 189 | var timer = Object.keys(req.env.timer) 190 | .map(function(key) { return `${key}: ${req.env.timer[key]} ms`; }) 191 | .join('; '); 192 | log(`${req.method} ${path}${req.user? ` as ${req.user.name}`: ''} (${timer})`); 193 | } 194 | 195 | var writeAccess = /^[wx]$/; 196 | var ownerAccess = /^x$/; 197 | 198 | // Main entry point for request treatment. 199 | exports.resolve = function resolveApp(req, res) { 200 | var app = req.data.app; 201 | var op = req.data.op; 202 | var path = safePath(req.path); 203 | var timerStart = process.hrtime(); 204 | res.on('close', function() { 205 | var timerEnd = process.hrtime(timerStart); 206 | req.env.timer.req = timerEnd[0] / 1e3 + timerEnd[1] / 1e6; 207 | logHttpRequest(req, path); 208 | }); 209 | 210 | if (req.method === 'GET' || req.method === 'POST') { 211 | authorize(path, req, res, function(meta, permission) { 212 | if (op === undefined) { 213 | if (app !== undefined) { 214 | sendApp(app, path, req, res, meta, permission); 215 | } else { 216 | new Promise(function(resolve, reject) { 217 | if (meta.app !== undefined) { 218 | resolve(meta.app); 219 | } else { 220 | type = meta.type || 'binary'; 221 | var genericType = type; 222 | var is = type.indexOf('/'); // index of first slash. 223 | if (is >= 0) { genericType = type.slice(0, is); } 224 | fs.metaFind(['apps', type], path, 225 | {meta: meta, or: [['apps', genericType]]}) 226 | .then(resolve).catch(reject); 227 | } 228 | }).then(function(appPath) { 229 | if (appPath === undefined) { appPath = 'data'; } 230 | sendApp(appPath, path, req, res, meta, permission); 231 | }).catch(function(err) { error(err, res); }); 232 | } 233 | 234 | } else if (op === 'append') { 235 | if (req.form !== undefined) { 236 | if (req.form.error) { unloggedError(req.form.error, res, 400); return; } 237 | Promise.all(req.files.content.map(function(file) { 238 | return fs.moveToFolder(file.path, path + '/' + file.name); 239 | })).then(function() { success(null, res); }) 240 | .catch(function(err) { error(err, res); }); 241 | } else { 242 | // FIXME: append req.body to file. 243 | error(null, res, 501); 244 | } 245 | } 246 | }); 247 | } else if (req.method === 'PUT') { 248 | var body = ''; 249 | req.on('data', function(chunk) { body += String(chunk); }); 250 | req.on('error', function(err) { error(err, res); }); 251 | req.on('end', function() { 252 | if (app === 'metadata') { 253 | authorize(path, req, res, function(meta, permission) { 254 | if (!ownerAccess.test(permission)) { denyAccess(res); return; } 255 | try { var meta = JSON.parse(body); } catch (e) { 256 | unloggedError(e, res, 400); return; } 257 | fs.updateMeta(path, meta).then(function() { success(null, res, 204); }) 258 | .catch(function(err) { error(err, res); }); 259 | }); 260 | } else { 261 | authorize(pathLib.dirname(path), req, res, function(meta, permission) { 262 | if (!writeAccess.test(permission)) { denyAccess(res); return; } 263 | fs.create(path, {body: body}).then(function() { success(null, res, 201); }) 264 | .catch(function(err) { error(err, res); }); 265 | }); 266 | } 267 | }); 268 | } else if (req.method === 'MKCOL') { 269 | authorize(pathLib.dirname(path), req, res, function(meta, permission) { 270 | if (!writeAccess.test(permission)) { denyAccess(res); return; } 271 | fs.create(path, {type: 'folder'}).then(function() { success(null, res, 201); }) 272 | .catch(function(err) { error(err, res); }); 273 | }); 274 | } else if (req.method === 'DELETE') { 275 | authorize(path, req, res, function(meta, permission) { 276 | if (!writeAccess.test(permission)) { denyAccess(res); return; } 277 | fs.remove(path).then(function() { success(null, res, 204); }) 278 | .catch(function(err) { error(err, res); }); 279 | }); 280 | } else { unloggedError(new Error("Invalid request"), res, 400); } 281 | }; 282 | 283 | var textWebSocketServers = new Map(); 284 | var editAutosaveInterval = 5000; // milliseconds. 285 | exports.websocket = function handleWebsocket(req, socket, head) { 286 | log('WEBSOCKET', req.url); 287 | var url = urlLib.parse(req.url, true); 288 | var urlPath; 289 | try { 290 | urlPath = decodeURIComponent(url.pathname); 291 | } catch(e) { return; } 292 | var app = url.query.app; 293 | var op = url.query.op; 294 | var path = safePath(urlPath); 295 | 296 | Camp.augmentReqRes(req, {}, camp); 297 | authenticate(req, undefined, function() { 298 | authorize(path, req, undefined, function(metadata, permission) { 299 | if (op === 'edit' && app === 'text') { 300 | // Get the data at that path. 301 | fs.get(path).then(function(buf) { 302 | var data = buf.toString(); 303 | var servers = textWebSocketServers.get(path); 304 | var wsServer, canopServer; 305 | if (servers !== undefined) { 306 | wsServer = servers.wsServer; 307 | canopServer = servers.canopServer; 308 | saveFile = servers.saveFile; 309 | autosaveTimer = servers.autosaveTimer; 310 | } else { 311 | wsServer = new WebSocket.Server({noServer: true}); 312 | canopServer = new canop.Server({data, base: metadata.revision}); 313 | var dirtyFile = false; // Is the data not saved on disk? 314 | canopServer.on('change', function() { dirtyFile = true; }); 315 | var saveFile = function() { 316 | if (wsServer && dirtyFile) { 317 | fs.put(path, canopServer.data, 318 | {metadata: {revision: canopServer.base}}) 319 | .then(function() { 320 | dirtyFile = false; 321 | log("Saved " + path + " to disk."); 322 | canopServer.signal({saved: canopServer.canonMark()}); 323 | }).catch(function(err) { 324 | log.error("Unable to save actively edited " + path + ":", 325 | err); 326 | }); 327 | } 328 | }; 329 | // Autosave the edited data. 330 | var autosaveTimer = setInterval(saveFile, editAutosaveInterval); 331 | textWebSocketServers.set(path, 332 | {wsServer, canopServer, saveFile, autosaveTimer}); 333 | } 334 | 335 | wsServer.handleUpgrade(req, socket, head, function(ws) { 336 | // FIXME: add readonly support in Canop, and set as readonly if the 337 | // client doesn't have write access. 338 | var writeAccessReceive = function(receive) { 339 | ws.on('message', receive); 340 | }; 341 | var readOnlyReceive = function(receive) { 342 | ws.on('message', function(message) { 343 | try { 344 | var protocol = canopServer.readProtocol(message); 345 | } catch(e) { console.log(e); return; } 346 | if ([0, 3, 6].includes(protocol[0])) { receive(message); } 347 | else console.log(protocol[0]); 348 | }); 349 | }; 350 | var client = { 351 | send: function(msg) { ws.send(msg); }, 352 | onReceive: writeAccess.test(permission)? 353 | writeAccessReceive: readOnlyReceive, 354 | }; 355 | canopServer.addClient(client); 356 | ws.on('close', function() { 357 | canopServer.removeClient(client); 358 | if (wsServer.clients.length <= 0) { 359 | // Clean up the (now empty) server. 360 | textWebSocketServers.delete(path); 361 | clearInterval(autosaveTimer); 362 | saveFile(); 363 | wsServer.close(); 364 | } 365 | }); 366 | }); 367 | }).catch(function(err) { log.error(err); }); 368 | } else { log.error("Invalid WebSocket request"); } 369 | }); 370 | }); 371 | }; 372 | -------------------------------------------------------------------------------- /lib/base64url.js: -------------------------------------------------------------------------------- 1 | exports.fromBase64 = 2 | function base64urlFromBase64(b) { 3 | return b.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 4 | }; 5 | -------------------------------------------------------------------------------- /lib/conf.js: -------------------------------------------------------------------------------- 1 | // Read configuration data. 2 | var fs = require('fs'); 3 | 4 | try { 5 | var conf = require('../admin/private/env.json'); 6 | } catch(e) { 7 | var conf = {}; 8 | } 9 | 10 | try { 11 | conf.pg.ssl.ca = fs.readFileSync(conf.pg.ssl.ca).toString(); 12 | } catch(e) { 13 | conf.pg.ssl = false; 14 | } 15 | 16 | // website 17 | conf.website = (conf.http.secure? 'https': 'http') + '://' + 18 | conf.http.host + 19 | (/^80|443$/.test(conf.http.port)? '': (':' + conf.http.port)); 20 | 21 | module.exports = conf; 22 | -------------------------------------------------------------------------------- /lib/driver.js: -------------------------------------------------------------------------------- 1 | // Primitives to deal with the way we store data on disk. 2 | // Copyright © 2011-2016 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var fs = require('fs'); 6 | var os = require('os'); 7 | var fsos = require('fsos'); 8 | var path = require('path'); 9 | var child = require('child_process'); 10 | 11 | var cwd = process.cwd(); 12 | var rootDir = 'web'; 13 | var root = path.join(cwd, rootDir); 14 | var metaRoot = path.join(cwd, 'meta'); 15 | 16 | function setMetaRoot(root) { metaRoot = root; } 17 | 18 | 19 | // Switch between virtual and disk paths 20 | 21 | function normalize(vpath) { 22 | // The following features several Windows hacks. 23 | if (/^[^\/]/.test(vpath)) { 24 | vpath = '/' + vpath; 25 | } 26 | var npath = path.normalize(vpath 27 | .replace(/\\/g,'/').replace(/\/\//g,'/')).replace(/\\/g,'/'); 28 | //console.log('NORMALIZED PATH OF', vpath, 'IS', npath); 29 | return npath; 30 | } 31 | 32 | function absolute(vpath) { 33 | var apath = path.join(root, normalize(vpath)); 34 | //console.log('ABSOLUTE PATH OF', vpath, 'IS', apath); 35 | return apath; 36 | }; 37 | 38 | function relative(vpath) { 39 | var rpath = path.join(rootDir, normalize(vpath)); 40 | //console.log('RELATIVE PATH OF', vpath, 'IS', rpath); 41 | return rpath; 42 | }; 43 | 44 | function virtual(dpath) { 45 | var rpath = path.relative(root, dpath).replace(/^(\.\.\/?)+/, ''); 46 | var vpath = normalize(rpath); 47 | //console.log('VIRTUAL PATH OF', dpath, 'IS', vpath); 48 | return vpath; 49 | } 50 | 51 | function temporary(tmpfile) { 52 | var tpath = path.join(os.tmpdir(), path.basename(tmpfile)); 53 | return tpath; 54 | } 55 | 56 | 57 | // Driver primitives 58 | 59 | var primitives = {}; // Map from mime types to I/O primitives. 60 | 61 | primitives['dir'] = { 62 | read: function(vpath, cb) {fs.readdir(absolute(vpath), cb);}, 63 | mkfile: function(vpath, cb) { 64 | fsos.set(absolute(vpath), '').then(cb).catch(cb); 65 | }, 66 | mkdir: function(vpath, cb) {fs.mkdir(absolute(vpath), cb);}, 67 | import: function(tmpfile, vpath, cb) { 68 | var source = temporary(tmpfile), destination = absolute(vpath); 69 | //console.log('importing', source, 'as', destination); 70 | fs.rename(source, destination, function(error) { 71 | // Might fail if `source` and `destination` are on different partitions. 72 | if (error) { 73 | var stream = fs.ReadStream(source); 74 | stream.on('end', function() { fs.unlink(source); cb(); }); 75 | stream.pipe(fs.WriteStream(destination)); 76 | } else cb(); 77 | }); 78 | }, 79 | rm: function(vpath, cb) { 80 | fs.rmdir(absolute(vpath), cb); 81 | }, 82 | }; 83 | 84 | primitives['binary'] = { 85 | read: function(vpath, cb) {fs.readFile(absolute(vpath), cb);}, 86 | write: function(vpath, content, metadata, cb) { 87 | dumpMeta(vpath, metadata); 88 | fsos.set(absolute(vpath), content).then(cb).catch(cb); 89 | }, 90 | rm: function(vpath, cb) { 91 | fs.unlink(absolute(vpath), cb); 92 | }, 93 | }; 94 | 95 | primitives['text'] = { 96 | read: function(vpath, cb) {fs.readFile(absolute(vpath), 'utf8', cb);}, 97 | write: function(vpath, content, metadata, cb) { 98 | dumpMeta(vpath, metadata); 99 | fsos.set(absolute(vpath), '' + content).then(cb).catch(cb); 100 | }, 101 | rm: primitives['binary'].rm 102 | }; 103 | 104 | 105 | // Exports 106 | 107 | exports.primitives = primitives; 108 | exports.normalize = normalize; 109 | exports.absolute = absolute; 110 | exports.relative = relative; 111 | exports.virtual = virtual; 112 | exports.temporary = temporary; 113 | exports.setMetaRoot = setMetaRoot; 114 | -------------------------------------------------------------------------------- /lib/fs.js: -------------------------------------------------------------------------------- 1 | // File system primitives. 2 | // The following code is covered by the AGPLv3 license. 3 | 4 | var fs = require('fs'); 5 | var pathLib = require('path'); 6 | 7 | var log = require('./log'); 8 | var jsonPointer = require('./json-pointer'); 9 | var listFromJsonPointer = jsonPointer.listFromJsonPointer; 10 | var jsonPointerFromList = jsonPointer.jsonPointerFromList; 11 | 12 | var camp = require('camp'); 13 | var mimeTypes = camp.mime; 14 | var stream = require('stream'); 15 | var Writable = stream.Writable; 16 | 17 | var metadataFile = 'metadata.json'; 18 | var metadataSavingInterval = 5000; // milliseconds. 19 | // When modifying the metadata, reset dirtyMetadata. 20 | var metadata = JSON.parse(fs.readFileSync(metadataFile)); 21 | var dirtyMetadata = false; // Are there changes to commit to disk? 22 | 23 | function saveMetadata() { 24 | return new Promise(function(resolve, reject) { 25 | log('Saving metadata to disk.') 26 | try { 27 | var content = JSON.stringify(metadata); 28 | } catch(e) { 29 | var errmsg = "Metadata is no longer JSON-serializable!"; 30 | log.error(errmsg); 31 | reject(new Error(errmsg)); 32 | return; 33 | } 34 | fs.writeFile(metadataFile, content, function(err) { 35 | if (err != null) { reject(err); return; } 36 | resolve(); 37 | }); 38 | }); 39 | } 40 | 41 | function initAutosave() { 42 | setInterval(function saveMetadataIfNeeded() { 43 | if (dirtyMetadata) { 44 | saveMetadata(); 45 | dirtyMetadata = false; 46 | } 47 | }, metadataSavingInterval); 48 | } 49 | 50 | // Return a Promise. 51 | // options: 52 | // - depth: 0 to include subfile metadata, 1 to include their subfiles and 53 | // metadata, etc. 54 | // - user: name, to check for permission 55 | function getStream(path, options) { 56 | options = options || {}; 57 | var realPath = realFromVirtual(path); 58 | // Stat it; if it is a directory, send a JSON list of subfiles; otherwise the 59 | // content. 60 | return new Promise(function(resolve, reject) { 61 | fs.stat(realPath, function(err, stats) { 62 | if (err != null) { reject(err); return; } 63 | if (stats.isFile()) { 64 | resolve(fs.createReadStream(realPath)); 65 | } else if (stats.isDirectory()) { 66 | var subfiles = flatSubfiles(path, options.depth, options.user); 67 | resolve(streamFromData(Buffer.from(JSON.stringify(subfiles)))); 68 | } else { 69 | reject(new Error('getStream: unknown file type on disk')); 70 | } 71 | }); 72 | }); 73 | } 74 | 75 | // Return the `files` field of a directory, without the subsubfiles. 76 | // depth: if < 0, return just a list of file names. 77 | // If 0, a map from file names to {meta: {type, updated}, files}. 78 | // If 1 or more, include subfiles recursively. 79 | // user: name, to check for permission 80 | function flatSubfiles(path, depth, user) { 81 | var pointer = fileNodePointer(path); 82 | pointer.push('files'); 83 | var data = findPointer(pointer, metadata); 84 | var perms = filePerms(path, user); 85 | if (depth < 0) { 86 | if (permFromPerms(perms) === '-') { return []; } 87 | return Object.keys(data).filter(function(filename) { 88 | var childPerms = permsFromParent(data[filename].meta, user, perms); 89 | return permFromPerms(childPerms) !== '-'; 90 | }); 91 | } else { 92 | if (permFromPerms(perms) === '-') { return {}; } 93 | return listFiles(data, depth, user, perms); 94 | } 95 | } 96 | 97 | // files: map from file to {meta: {type, updated}, files} 98 | // depth: number 99 | // user: name, to check for permission 100 | // perms: [user perm, others perm] 101 | function listFiles(files, depth, user, perms) { 102 | var result = {}; 103 | for (var key in files) { 104 | var meta = files[key].meta; 105 | var childPerms = permsFromParent(meta, user, perms); 106 | if (permFromPerms(childPerms) === '-') { continue; } 107 | result[key] = {meta: {type: meta.type, updated: meta.updated}}; 108 | if (depth > 0) { 109 | result[key].files = listFiles(files[key].files, 110 | depth - 1, user, childPerms); 111 | } 112 | } 113 | return result; 114 | } 115 | 116 | function filePerms(path, user) { 117 | var parts = path.split('/'); 118 | parts[0] = '/'; 119 | return permsFromParts([], parts, user, [undefined, undefined]); 120 | } 121 | 122 | // Same as above, with promises as future-proofing for external use. 123 | function permission(path, user) { 124 | return Promise.resolve(permFromPerms(filePerms(path, user))); 125 | } 126 | 127 | // prefixParts: eg. ['/', 'foo'] for /foo/bar 128 | // parts: eg. ['bar'] for /foo/bar 129 | // perms: [user perm, others perm] 130 | function permsFromParts(prefixParts, parts, user, perms) { 131 | var part = parts.shift(); 132 | if (part === undefined) { return perms; } 133 | prefixParts.push(part); 134 | var path = pathLib.join.apply(pathLib, prefixParts); 135 | var pathPerms = permsFromParent(fileMeta(path), user, perms); 136 | return permsFromParts(prefixParts, parts, user, pathPerms); 137 | } 138 | 139 | // meta: a file's metadata object. 140 | // perms: [user perm, others perm] for the parent folder of the file. 141 | // Returns a list [permission for user, permission for others]. 142 | function permsFromParent(meta, user, perms) { 143 | if (!meta.acl) { return perms; 144 | } else if (user === undefined) { return [undefined, meta.acl['*'] || perms[1]]; 145 | } else if (meta.acl[user]) { return [meta.acl[user], perms[1]]; 146 | } else { return [perms[0], meta.acl['*'] || perms[1]]; } 147 | } 148 | 149 | // Return a permission from [user perm, others perm]. 150 | function permFromPerms(perms) { return perms[0] || perms[1] || '-'; } 151 | 152 | // Same as getStream(), but returns a Promise. 153 | // options: 154 | // - depth: 0 to include subfile metadata, 1 to include their subfiles and 155 | // metadata {type, updated}, etc. 156 | function get(path, options) { 157 | return new Promise(function(resolve, reject) { 158 | var content = Buffer.alloc(0); 159 | var writer = new stream.Writable({ 160 | write: function(chunk, encoding, callback) { 161 | content = Buffer.concat([content, chunk]); 162 | callback(); 163 | }, 164 | }); 165 | writer.on('finish', function() { resolve(content); }); 166 | writer.on('error', reject); 167 | getStream(path, options).then(function(stream) { stream.pipe(writer); }) 168 | .catch(reject); 169 | }); 170 | } 171 | 172 | // path: string, data: String or Buffer or Uint8Array. 173 | // options: 174 | // - metadata: object containing metadata fields to set. 175 | // Save the data. Return a Promise. 176 | function put(path, data, options) { 177 | options = options || {}; 178 | var metadataToSet = options.metadata || {}; 179 | var realPath = realFromVirtual(path); 180 | return new Promise(function(resolve, reject) { 181 | fs.writeFile(realPath, data, function(err) { 182 | if (err) { reject(err); return; } 183 | meta(path) 184 | .then(function(metadata) { 185 | var now = new Date().toISOString(); 186 | metadata.modified = now; 187 | for (var key in metadataToSet) { 188 | metadata[key] = metadataToSet[key]; 189 | } 190 | updateMeta(path, metadata) 191 | .then(resolve) 192 | .catch(function(err) { resolve(); }); // Ignore the issue. 193 | }) 194 | .catch(reject); 195 | }) 196 | }); 197 | } 198 | 199 | // JSON pointer to the {meta, files} object corresponding to path. 200 | function fileNodePointer(path) { 201 | var list = (path === '/') ? [] : path.slice(1).split('/'); 202 | var pointer = []; 203 | for (var i = 0; i < list.length; i++) { 204 | pointer.push('files', list[i]); 205 | } 206 | return pointer; 207 | } 208 | 209 | // Return the metadata for path. 210 | function fileMeta(path) { 211 | var pointer = fileNodePointer(path); 212 | pointer.push('meta'); 213 | return findPointer(pointer, metadata); 214 | } 215 | 216 | // Same as above, with promises as future-proofing for external use. 217 | function meta(path) { 218 | return Promise.resolve(fileMeta(path)); 219 | } 220 | 221 | // meta: JSON-serializable object. 222 | function updateMeta(path, meta) { 223 | return new Promise(function(resolve, reject) { 224 | try { 225 | // We must simply ensure that it remains JSON-serializable. 226 | JSON.stringify(meta); 227 | var info = findPointer(fileNodePointer(path), metadata); 228 | var now = new Date().toISOString(); 229 | meta.updated = now; 230 | // Atomic change, so that the metadata reads stay consistent. 231 | info.meta = meta; 232 | dirtyMetadata = true; 233 | } catch(e) { reject(e); } 234 | resolve(); 235 | }); 236 | } 237 | 238 | // Get the element pointed to by pointer in the path's metadata. 239 | // options: 240 | // - meta: if set, assume that the path's metadata is this. 241 | // - or: list of JSON pointers to use if the search yields nothing. 242 | function metaGet(pointer, path, options) { 243 | options = options || {}; 244 | var metadata = options.meta; 245 | if (typeof pointer === 'string') { pointer = listFromJsonPointer(pointer); } 246 | var pointers = [pointer]; 247 | if (options.or !== undefined) { 248 | options.or = options.or.map(function(pointer) { 249 | if (typeof pointer === 'string') { 250 | return listFromJsonPointer(pointer); 251 | } else { return pointer; } 252 | }); 253 | var pointers = pointers.concat(options.or); 254 | } 255 | return new Promise(function(resolve, reject) { 256 | if (metadata !== undefined) { 257 | resolve(findFirstPointer(pointers, metadata)); 258 | } else { 259 | meta(path) 260 | .then(function(metadata) { resolve(findFirstPointer(pointers, metadata)); }) 261 | .catch(reject); 262 | } 263 | }); 264 | } 265 | 266 | // Get the element pointed to by pointer in the path's metadata 267 | // or its ancestry's. 268 | // options: 269 | // - meta: if set, assume that the path's metadata is this. 270 | // - or: list of JSON pointers to use if the search yields nothing. 271 | function metaFind(pointer, path, options) { 272 | options = options || {}; 273 | if (typeof pointer === 'string') { pointer = listFromJsonPointer(pointer); } 274 | return new Promise(function(resolve, reject) { 275 | metaGet(pointer, path, options) 276 | .then(function(found) { 277 | if (found !== undefined) { resolve(found); } 278 | else if (path === '/') { resolve(undefined); } 279 | else { 280 | metaFind(pointer, pathLib.dirname(path), {or: options.or}) 281 | .then(resolve) 282 | .catch(reject); 283 | } 284 | }) 285 | .catch(reject); 286 | }); 287 | } 288 | 289 | // options: 290 | // - type: defaults to guessing the type. 291 | // - content: data. 292 | function create(path, options) { 293 | options = options || {}; 294 | return new Promise(function(resolve, reject) { 295 | var realPath = realFromVirtual(path); 296 | if (options.type === 'folder') { 297 | fs.mkdir(realPath, function(err) { 298 | if (err != null) { reject(err); return; } 299 | appendToFolder(path, options.type).then(resolve).catch(reject); 300 | }); 301 | } else { 302 | ((options.type === undefined) ? 303 | guessType(path) : 304 | Promise.resolve(options.type) 305 | ).then(function(type) { 306 | // Creating a raw binary file makes no sense; those are only uploaded. 307 | if (type === 'binary') { type = 'text'; } 308 | fs.writeFile(realPath, options.content || '', {flag: 'ax'}, 309 | function(err) { 310 | if (err != null) { reject(err); return; } 311 | appendToFolder(path, type).then(resolve).catch(reject); 312 | }); 313 | }).catch(reject); 314 | } 315 | }); 316 | } 317 | 318 | function appendToFolder(path, type) { 319 | return new Promise(function(resolve, reject) { 320 | var parent = pathLib.dirname(path); 321 | var parentInfo = findPointer(fileNodePointer(parent), metadata); 322 | var filename = pathLib.basename(path); 323 | var now = new Date().toISOString(); 324 | var newMetadata = { modified: now, updated: now }; 325 | ((type === undefined) ? guessType(path) : Promise.resolve(type) 326 | ).then(function(type) { 327 | newMetadata.type = type; 328 | parentInfo.files = parentInfo.files || {}; 329 | parentInfo.files[filename] = {meta: newMetadata}; 330 | if (type === 'folder') { 331 | parentInfo.files[filename].files = {}; 332 | } 333 | dirtyMetadata = true; 334 | resolve(); 335 | }).catch(reject); 336 | }); 337 | } 338 | 339 | function moveToFolder(sourceRealPath, path) { 340 | return new Promise(function(resolve, reject) { 341 | var realPath = realFromVirtual(path); 342 | fs.rename(sourceRealPath, realPath, function(err) { 343 | if (err != null) { 344 | var stream = fs.ReadStream(sourceRealPath); 345 | stream.on('end', function() { 346 | appendToFolder(path).then(resolve).catch(reject); 347 | fs.unlink(sourceRealPath, function(){}); 348 | }); 349 | stream.on('error', reject); 350 | stream.pipe(fs.WriteStream(realPath)); 351 | } else { 352 | appendToFolder(path).then(resolve).catch(reject); 353 | } 354 | }); 355 | }); 356 | } 357 | 358 | function removeMeta(path) { 359 | return new Promise(function(resolve, reject) { 360 | var filename = pathLib.basename(path); 361 | var parent = pathLib.dirname(path); 362 | var parentInfo = findPointer(fileNodePointer(parent), metadata); 363 | if (parentInfo == null) { 364 | reject(new Error(`removeMeta: file ${path} not found`)); 365 | return; 366 | } 367 | delete parentInfo.files[filename]; 368 | dirtyMetadata = true; 369 | resolve(); 370 | }); 371 | } 372 | 373 | function remove(path) { 374 | return new Promise(function(resolve, reject) { 375 | var realPath = realFromVirtual(path); 376 | fs.stat(realPath, function(err, stats) { 377 | if (err != null) { reject(err); return; } 378 | 379 | var filename = pathLib.basename(path); 380 | var parent = pathLib.dirname(path); 381 | var parentInfo = findPointer(fileNodePointer(parent), metadata); 382 | 383 | if (stats.isFile()) { 384 | fs.unlink(realPath, function(err) { 385 | if (err != null) { reject(err); return; } 386 | delete parentInfo.files[filename]; 387 | dirtyMetadata = true; 388 | resolve(); 389 | }); 390 | } else if (stats.isDirectory()) { 391 | fs.rmdir(realPath, function(err) { 392 | if (err != null) { reject(err); return; } 393 | delete parentInfo.files[filename]; 394 | dirtyMetadata = true; 395 | resolve(); 396 | }); 397 | } else { 398 | reject(new Error('delete: unknown file type on disk')); 399 | } 400 | }); 401 | }); 402 | } 403 | 404 | var cwd = process.cwd(); 405 | var rootPath = pathLib.join(cwd, 'web'); 406 | function realFromVirtual(path) { 407 | // The following features several Windows hacks. 408 | if (/^[^\/]/.test(path)) { path = '/' + path; } 409 | var npath = pathLib.normalize(path 410 | .replace(/\\/g,'/').replace(/\/\//g,'/')).replace(/\\/g,'/'); 411 | return pathLib.join(rootPath, npath); 412 | } 413 | 414 | // Return a plausible MIME type for the file, or 'binary', in a promise. 415 | function guessType(path) { 416 | var ext = pathLib.extname(path).slice(1) || pathLib.basename(path); 417 | return new Promise(function(resolve, reject) { 418 | metaFind(['mime', ext], path) 419 | .then(function(mime) { resolve(mime || mimeTypes[ext] || 'binary'); }) 420 | .catch(reject); 421 | }); 422 | }; 423 | 424 | // Return the first element found in json for a pointer. 425 | function findFirstPointer(pointers, json) { 426 | for (var i = 0, len = pointers.length; i < len; i++) { 427 | var found = findPointer(pointers[i], json); 428 | if (found !== undefined) { return found; } 429 | } 430 | } 431 | 432 | // Return the element in json that is identified by pointer. 433 | function findPointer(pointer, json) { 434 | pointer = pointer.slice(); 435 | while (pointer.length > 0) { 436 | if (json === undefined) { return; } 437 | json = json[pointer.shift()]; 438 | } 439 | return json; 440 | }; 441 | 442 | // Return {meta, files} 443 | function fileNode(path) { 444 | var pointer = fileNodePointer(path); 445 | return findPointer(pointer, metadata); 446 | } 447 | 448 | function cloneValue(v) { 449 | if (v == null || typeof v === 'boolean' || typeof v === 'number' 450 | || typeof v === 'string') { 451 | return v; 452 | } else if (Object(v) instanceof Array) { 453 | return v.slice().map(cloneValue); 454 | } else { 455 | return cloneObject(v); 456 | } 457 | } 458 | 459 | function cloneObject(obj) { 460 | var res = Object.create(null); 461 | for (var key in obj) { 462 | res[key] = cloneValue(obj[key]); 463 | } 464 | return res; 465 | } 466 | 467 | function streamFromData(data) { 468 | var newStream = new stream.Readable(); 469 | newStream._read = function() { newStream.push(data); newStream.push(null); }; 470 | return newStream; 471 | } 472 | 473 | exports.create = create; 474 | exports.remove = remove; 475 | exports.get = get; 476 | exports.getStream = getStream; 477 | exports.put = put; 478 | exports.moveToFolder = moveToFolder; 479 | exports.meta = meta; 480 | exports.updateMeta = updateMeta; 481 | exports.removeMeta = removeMeta; 482 | exports.metaGet = metaGet; 483 | exports.metaFind = metaFind; 484 | exports.fileNodePointer = fileNodePointer; 485 | exports.findPointer = findPointer; 486 | exports.permission = permission; 487 | exports.guessType = guessType; 488 | exports.realFromVirtual = realFromVirtual; 489 | exports.initAutosave = initAutosave; 490 | -------------------------------------------------------------------------------- /lib/irc.js: -------------------------------------------------------------------------------- 1 | // IRC persistent client API. 2 | // Copyright © 2011-2014 Jan Keromnes, Thaddee Tyl. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var irc = require('irc'), 6 | fs = require('./fs'), 7 | driver = require('./driver'), 8 | clients = {}; 9 | 10 | exports.join = function (data, end) { 11 | data.path = driver.normalize(data.path); 12 | 13 | if (!clients[data.path]) { 14 | var client = new irc.Client( 15 | data.serv, 16 | data.nick, { 17 | channels: [data.chan], 18 | autoConnect: false 19 | }); 20 | 21 | clients[data.path] = client; 22 | 23 | client.connect(10, function() { 24 | client.join(data.chan, function() { 25 | client.ready = true; 26 | if (end) end(); 27 | }); 28 | }); 29 | 30 | client.addListener('message', function(from, to, message) { 31 | console.log(from + ' => ' + to + ': ' + message); 32 | }); 33 | client.addListener('pm', function(from, message) { 34 | console.log(from + ' => ME: ' + message); 35 | }); 36 | client.addListener('message' + data.chan, function(from, message) { 37 | console.log(from + ' => ' + data.chan + ': ' + message); 38 | fs.file (data.path, function (err, file) { 39 | file.open(function() { 40 | file.content += '\n[' + (new Date()+'').substring(16,24) + '] '; 41 | file.content += from + ': ' + message; 42 | }); 43 | }); 44 | }); 45 | } else if (end) end(); 46 | } 47 | 48 | function say (client, chan, message, end) { 49 | if (!client.ready) { 50 | setTimeout(function() { 51 | if (client.buffer === undefined) client.buffer = []; 52 | if (message) client.buffer.push(message); 53 | say (client, chan, null, end) 54 | }, 500); 55 | } else { 56 | if (client.buffer !== undefined) { 57 | var i = 0; // FIXME use async? 58 | for (;;) { 59 | client.say(chan, client.buffer[i]); 60 | i++; 61 | if (i >= client.buffer.length) break; 62 | } 63 | client.buffer = undefined; 64 | } 65 | if (message) client.say(chan, message); 66 | if (end) end(); 67 | } 68 | } 69 | 70 | exports.say = function (data, end) { 71 | data.path = driver.normalize(data.path); 72 | 73 | exports.join(data, function() { 74 | // TODO change serv / chan / nick if changed 75 | say(clients[data.path], data.chan, data.message, end); 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /lib/json-pointer.js: -------------------------------------------------------------------------------- 1 | // Convert a JSON Pointer to a list. 2 | var listFromJsonPointer = function(pointer) { 3 | if (typeof pointer !== 'string') { 4 | throw new Error('listFromJsonPointer() only supports strings, ' + 5 | 'something else was given') 6 | } 7 | 8 | var parts = pointer.split('/').slice(1) 9 | return parts.map(function(part) { 10 | if (!/~/.test(part)) { return part } 11 | // It is important to end with the ~ replacement, 12 | // to avoid converting `~01` to a `/`. 13 | return part.replace(/~1/g, '/').replace(/~0/g, '~') 14 | }) 15 | } 16 | 17 | var jsonPointerFromList = function(path) { 18 | if (!(Object(path) instanceof Array)) { 19 | throw new Error('jsonPointerFromList() only supports arrays, ' + 20 | 'something else was given') 21 | } 22 | 23 | return '/' + path.map(function(part) { 24 | // It is important to start with the ~ replacement, 25 | // to avoid converting `/` to `~01`. 26 | return part.replace(/~/g, '~0').replace(/\//g, '~1') 27 | }).join('/') 28 | } 29 | 30 | exports.listFromJsonPointer = listFromJsonPointer; 31 | exports.jsonPointerFromList = jsonPointerFromList; 32 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | // Zero-pad a number in a string. 2 | // eg. 4 becomes 04 but 17 stays 17. 3 | function pad(string) { 4 | string = String(string); 5 | if (string.length < 2) { 6 | return "0" + string; 7 | } else { 8 | return string; 9 | } 10 | } 11 | 12 | // Compact date representation. 13 | // eg. 0611093840 for June 11, 9:38:40 UTC. 14 | function date() { 15 | var date = new Date(); 16 | return pad(date.getUTCMonth() + 1) + 17 | pad(date.getUTCDate()) + 18 | pad(date.getUTCHours()) + 19 | pad(date.getUTCMinutes()) + 20 | pad(date.getUTCSeconds()); 21 | } 22 | 23 | function log(...msg) { 24 | console.log(date(), ...msg); 25 | }; 26 | log.error = function error(...msg) { 27 | console.error(date(), ...msg); 28 | }; 29 | module.exports = log; 30 | -------------------------------------------------------------------------------- /lib/lookup.js: -------------------------------------------------------------------------------- 1 | // Look metadata up. 2 | // Copyright © 2011-2014 Thaddee Tyl. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var path = require('path'); 6 | var fs = require('./fs'); 7 | 8 | // States for the JSON query parser. 9 | var identifier = 0; 10 | var identifierEscape = 1; 11 | var string = 2; 12 | 13 | // parseJSONQuery("foo.bar['baz'].quux") 14 | // returns ["foo", "bar", "baz", "quux"]. 15 | function parseJSONQuery(jsonquery) { 16 | var keys = []; 17 | var key = ''; 18 | var state = identifier; 19 | var stringType; // Either " or '. 20 | for (var i = 0; i < jsonquery.length; i++) { 21 | if (state === identifier) { 22 | if (jsonquery[i] === '.') { 23 | keys.push(key); 24 | key = ''; 25 | } else if (jsonquery[i] === '[') { 26 | state = string; 27 | if (key.length > 0) { keys.push(key); key = ''; } 28 | i++; // Skip the string type. 29 | stringType = jsonquery[i]; 30 | } else if (jsonquery[i] === '\\') { 31 | state = identifierEscape; 32 | } else { 33 | key += jsonquery[i]; 34 | } 35 | } else if (state === identifierEscape) { 36 | if (jsonquery[i] === 'u') { 37 | key += JSON.parse('"\\u' + jsonquery.slice(i + 1, i + 5) + '"'); 38 | i += 4; // Jump u and the four hex digits. 39 | state = identifier; 40 | } else { 41 | console.error('parseJSONQuery: invalid identifier for JSON query ' + 42 | jsonquery + '.'); 43 | return keys; 44 | } 45 | } else if (state === string) { 46 | for (var j = i; 47 | jsonquery[j] !== stringType && j < jsonquery.length; 48 | j++) { 49 | if (jsonquery[j] === '\\') { 50 | j++; // This is an escape → skip one char. 51 | } 52 | } 53 | // JSON only allows "-delimited strings. 54 | key = JSON.parse('"' + jsonquery.slice(i, j) 55 | .replace(/\\'/g, "'").replace(/"/g, '\\"') + '"'); 56 | i = j + 1; // Position ourself after the closing bracket. 57 | state = identifier; 58 | } else { 59 | throw new Error('parseJSONQuery: invalid state for JSON query ' + 60 | jsonquery + '.'); 61 | } 62 | } 63 | keys.push(key); 64 | return keys; 65 | } 66 | 67 | function getMeta(obj, jsonquery) { 68 | var keys = parseJSONQuery(jsonquery); 69 | var value = obj; 70 | for (var i = 0; i < keys.length; i++) { 71 | value = value[keys[i]]; 72 | if (value === undefined) return undefined; 73 | } 74 | return value; 75 | } 76 | 77 | function recurseMeta(file, key, cb) { 78 | var got; 79 | if ((got = getMeta(file.meta, key)) !== undefined) { 80 | if (cb) { 81 | cb(got); 82 | } else return got; 83 | } else if (file.path.length === 1) { 84 | // We are at the root, cannot go further up. 85 | if (cb) { 86 | cb(null); 87 | } else return null; 88 | } else { 89 | if (cb) { 90 | fs.file(path.dirname(file.path), function (err, parent) { 91 | if (err) { 92 | console.error('lookup:recurseMeta:', err.stack); 93 | cb(null); 94 | } else recurseMeta(parent, key, cb); 95 | }); 96 | } else return null; 97 | } 98 | } 99 | 100 | function makeLookup(file, query) { 101 | // From most specific (url?key=value) to most generic (inherited metadata) 102 | return function lookup(key, cb) { 103 | if (query[key]) { 104 | if (cb) { 105 | cb(query[key]); 106 | } else { 107 | return query[key]; 108 | } 109 | } else { 110 | return recurseMeta(file, key, cb); 111 | } 112 | }; 113 | } 114 | 115 | module.exports = makeLookup; 116 | module.exports.parseJSONQuery = parseJSONQuery; 117 | -------------------------------------------------------------------------------- /lib/plug.js: -------------------------------------------------------------------------------- 1 | // Plug application system. 2 | // Copyright © 2011-2016 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var fs = require('./fs'); 6 | var stream = require('stream'); 7 | var nodepath = require('path'); 8 | var driver = require('./driver'); 9 | var lookup = require('./lookup'); 10 | var pwdCheck = require('./pwd-check'); 11 | var publicFile = require('./public-file'); 12 | 13 | 14 | // We need Camp's socket.io object here. We get it at initialization. 15 | var camp; 16 | 17 | exports.main = function (server) { camp = server; }; 18 | 19 | // The plug system. 20 | 21 | exports.resolve = function (query, path, endres, ask) { 22 | 23 | // Get the file/folder corresponding to the query. 24 | fs.open(ask.path, function openQueryFile(err, file) { 25 | // If there was an error, abort! 26 | if (err) { 27 | ask.res.statusCode = 404; 28 | end(err, '404.html', {error: err.message}); 29 | return; 30 | } 31 | 32 | var filePath = file.path; 33 | var content = file.content; 34 | function sendraw(data) { 35 | console.log('send', filePath); 36 | // Set HTTP header Content-Type. 37 | if (file.meta.type !== undefined) { 38 | ask.res.setHeader('Content-Type', file.meta.type); 39 | } 40 | if (data && data.content) { 41 | ask.res.end(data.content); 42 | } else { 43 | // Bypass the template engine. 44 | endres(null, { template: streamFromData(content) }); 45 | } 46 | } 47 | 48 | function end(err, plug, data) { 49 | if (err) { 50 | console.error(err); 51 | endres(null, { template: '/404.html' }); 52 | return; 53 | } 54 | if (!plug || plug === 'none') { 55 | sendraw(data); return; 56 | } else { 57 | console.log(plug); 58 | if (plug.slice(-5) !== '.html') { 59 | plug = nodepath.join('/app', plug, 'page.html'); 60 | } 61 | fs.file(plug, function(err, file) { 62 | if (err) { sendraw(); return; } 63 | console.log('plug', file.path, '<', filePath); 64 | endres(data || {}, { template: file.path }); 65 | }); 66 | } 67 | } 68 | 69 | // TODO If file requires a password and nothing matches, abort! 70 | pwdCheck(file, ask.password, 'readkey', function checkedPwd(err) { 71 | if (err != null) { 72 | ask.res.statusCode = 401; 73 | ask.res.setHeader('WWW-Authenticate', 'Basic') 74 | end(err, '404.html', {error: err.message}); 75 | return; 76 | } 77 | 78 | // Set HTTP header Last-Modified. 79 | if (file.meta['Last-Modified']) { 80 | ask.res.setHeader('Last-Modified', 81 | (new Date(file.meta['Last-Modified'])).toGMTString()); 82 | } 83 | 84 | // PLUG MECHANISM 85 | 86 | var data = {}; 87 | 88 | // Environment variable lookup mechanism. 89 | data.lookup = lookup(file, query); 90 | 91 | // File info. 92 | data.file = publicFile(file); 93 | data.files = []; 94 | data.title = nodepath.basename(filePath) || 'The File Tree'; 95 | data.extname = nodepath.extname(filePath).slice(1); 96 | 97 | // Navigation crumbs 98 | data.nav = []; 99 | var crumbs = filePath.split('/').filter(function(e) { 100 | return e.length > 0; 101 | }); 102 | var subpath = '/'; 103 | data.nav.push({name: '/', path: subpath}); 104 | for (var i = 0; i < crumbs.length; i++) { 105 | crumbs[i] += (i < crumbs.length - 1 || file.isOfType('dir') ? '/' : ''); 106 | subpath += crumbs[i]; 107 | data.nav.push({name: crumbs[i], path: subpath}); 108 | } 109 | 110 | findPlug(file, data, end); 111 | }); 112 | }); 113 | }; 114 | 115 | // Find the correct plug. 116 | // 117 | // 1. Look for the non-inherited `plug` metadata. 118 | // 2. Look for the inherited `plugs.` metadata. 119 | function findPlug(file, data, end) { 120 | var plug = data.lookup('plug'); 121 | 122 | if (file.isOfType('dir')) { 123 | 124 | // Get subfiles. 125 | file.files (function (err, files) { 126 | if (err) console.error(err); 127 | for (var i = 0; i < files.length; i++) { 128 | var filepath = driver.normalize( 129 | files[i].path + (files[i].isOfType('dir') ? '/' : '')); 130 | data.files.push({name: nodepath.basename(filepath), 131 | path: filepath, 132 | time: files[i].meta['Last-Modified'], 133 | type: files[i].meta.type}); 134 | } 135 | ///console.log('server:root: data sent from dir is', data); 136 | 137 | if (plug === 'none') { 138 | // We must send a newline-separated list of subfile names. 139 | var nameFromFile = function(f) { 140 | return nodepath.basename(f.path) + 141 | // Add a trailing slash for directories 142 | (f.isOfType('dir')? '/': ''); 143 | }; 144 | var subfileNames = files.map(nameFromFile).join('\n'); 145 | end(err, null, {content: subfileNames}); 146 | } else if (plug == null) { 147 | data.lookup('plugs.dir', function(plug) { 148 | end(err, plug || 'gateway.html', data); 149 | }); 150 | } else { 151 | end(err, plug, data); 152 | } 153 | }); 154 | 155 | } else if (plug === 'none') { 156 | end(); // no plug 157 | return; 158 | 159 | } else if (file.isOfType('text')) { 160 | 161 | file.ot(camp.io, function(err) { 162 | if (plug == null) { 163 | data.lookup('plugs.text', function(plug) { 164 | end(err, plug || 'pencil.html', data); 165 | }); 166 | } else { 167 | end(err, plug, data); 168 | } 169 | }); 170 | 171 | } else if (file.isOfType('binary')) { 172 | 173 | if (plug == null) { 174 | data.lookup('plugs.binary', function(plug) { 175 | end(null, plug || 'none', data); 176 | }); 177 | } else { 178 | end(null, plug, data); 179 | } 180 | } 181 | } 182 | 183 | function objCopy(o) { 184 | var newObject = Object.create(null); 185 | for (var p in o) { 186 | try { 187 | newObject[p] = JSON.parse(JSON.stringify((o[p]))); 188 | } catch(e) {} 189 | } 190 | return newObject; 191 | } 192 | 193 | function streamFromData(data) { 194 | var newStream = new stream.Readable(); 195 | newStream._read = function() { newStream.push(data); newStream.push(null); }; 196 | return newStream; 197 | } 198 | -------------------------------------------------------------------------------- /lib/profiler.js: -------------------------------------------------------------------------------- 1 | // Gather information about different elements in the system. 2 | // It is linked to the profiler.html template. 3 | // Copyright © 2011-2014 Jan Keromnes, Thaddee Tyl. All rights reserved. 4 | // The following code is covered by the AGPLv3 license. 5 | 6 | var fs = require('./fs'); 7 | 8 | // Gather information. 9 | // 10 | // Each subsystem exports a .profile function, which returns an 11 | // object which contains a list of 12 | // {doc:'what this number means', data:123}. 13 | 14 | var sources = { 15 | 'Generic': function () { 16 | var mem = process.memoryUsage(), 17 | uptime = process.uptime(), 18 | loadAvg = require('os').loadavg(); 19 | return [ 20 | {doc: "Node", data: process.version}, 21 | {doc: "Platform", data: process.platform}, 22 | {doc: "Architecture", data: process.arch}, 23 | {doc: "Uptime", data: uptime, unit: "seconds"}, 24 | {doc: "Heap used", data: mem.heapUsed, unit: "bytes"}, 25 | {doc: "Heap total", data: mem.heapTotal, unit: "bytes"}, 26 | {doc: "Resident Set Size", data: mem.rss, unit: "bytes"}, 27 | {doc: "Load Average (1, 5, 15 min.)", data: loadAvg, unit: "# of proc."}, 28 | ]; 29 | }, 30 | 'File system': fs.profile 31 | }; 32 | 33 | var profiles = {}; 34 | 35 | function runProfiles (param) { 36 | for (var s in sources) { 37 | profiles[s] = sources[s](param[s]); // `sources[s]` is a function. 38 | } 39 | return profiles; 40 | } 41 | 42 | 43 | exports.run = runProfiles; 44 | 45 | -------------------------------------------------------------------------------- /lib/public-file.js: -------------------------------------------------------------------------------- 1 | // Public-facing metadata. 2 | // Copyright © 2011-2014 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | function publicMeta(meta) { 6 | return JSON.parse(JSON.stringify(meta, function(key, val) { 7 | // Withhold the keys. 8 | return (key === 'metakey' || key === 'writekey' || key === 'readkey')? 9 | '[withheld]': val; })); 10 | } 11 | 12 | function publicFile(file) { 13 | var newFile = objCopy(file); 14 | newFile.meta = publicMeta(newFile.meta); 15 | newFile.mime = file.meta.type; 16 | return newFile; 17 | } 18 | 19 | function objCopy(o) { 20 | var newObject = Object.create(null); 21 | for (var p in o) { 22 | try { 23 | newObject[p] = JSON.parse(JSON.stringify((o[p]))); 24 | } catch(e) {} 25 | } 26 | return newObject; 27 | } 28 | 29 | module.exports = publicFile; 30 | module.exports.meta = publicMeta; 31 | -------------------------------------------------------------------------------- /lib/pwd-check.js: -------------------------------------------------------------------------------- 1 | // Check passwords for file access. 2 | // Copyright © 2011-2014 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var scrypt = require('scrypt'); 6 | var lookup = require('./lookup'); 7 | 8 | function pwdCheck(file, key, keyName, cb) { 9 | // Password check. 10 | lookup(file, {})(keyName, function lookupMade(hash) { 11 | if (hash != null) { 12 | if (typeof key !== 'string') { 13 | return cb(new Error('Give a (string) key `' + keyName + '` ' 14 | + 'in the query to modify ' + file.path + '.')); 15 | } 16 | scrypt.verifyHash(hash, key, function(err) { 17 | if (!!err) { 18 | cb(new Error('File ' + file.path 19 | + ' requires the correct ' + keyName + '. ' 20 | + 'Modify `' + keyName 21 | + '` in the query to match the stored hash.\n' + err)); 22 | } else { 23 | cb(); 24 | } 25 | }); 26 | } else { 27 | cb(); 28 | } 29 | }); 30 | } 31 | 32 | module.exports = pwdCheck; 33 | -------------------------------------------------------------------------------- /lib/sandbox-shell.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process'); 2 | var path = require('path'); 3 | var async = require('async'); 4 | var driver = require('./driver'); 5 | 6 | // Configuration. 7 | var sandboxName = 'tree-jail'; // Name of docker image. 8 | var userName = 'myself'; // Name of user in jail. 9 | var shell = '/bin/bash'; // Shell program to use. 10 | var sandboxTimeout = 60 * 1000; // Kill sandbox after 60s. 11 | 12 | // This function checks that the sandbox exists. 13 | // It returns true in a callback if the sandbox exists. 14 | function validSandbox(cb) { 15 | var check = cp.spawn('docker', ['images', '-q', sandboxName]); 16 | var hasSandbox = false; 17 | check.stdout.on('data', function(data) { hasSandbox = true; }); 18 | check.on('close', function() { cb(hasSandbox); }); 19 | } 20 | 21 | var home = '/home/' + userName; 22 | var sandboxContainerCount = 0; 23 | // In case we run into a harsher hard limit, 24 | // we don't care about the stderr output. 25 | var ulimit = 'ulimit -S -t ' + (sandboxTimeout / 1000) 26 | + ' -f ' + (1048576 * 32) // 32 GB files 27 | + ' -d ' + (1048576 * 32) // 32 GB data segment 28 | + ' -s ' + (1024 * 8) // 8 MB stack size 29 | + ' -c ' + 0 // No core file 30 | + ' -m ' + (1048576 * 32) // 32 GB resident set size 31 | + ' -u ' + 1024 // 1024 forked processes 32 | + ' -n ' + 1024 // 1024 file descriptors 33 | + ' -l ' + 64 // 64 KB locked-in-memory size 34 | + ' -v ' + (1048576 * 32) // 32 GB virtual memory 35 | + ' -i ' + 62756 // pending signals 36 | + ' -q ' + (1024 * 800) // 800 KB message queues 37 | + ' -e 0 -r 0 2>/dev/null'; 38 | 39 | // This function starts a sandbox. 40 | // It launches a shell in it (see variable `shell`). 41 | // When the shell is ready, `cb` is called. 42 | function Sandbox(cb) { 43 | var self = this; 44 | self.name = 'jail' + sandboxContainerCount; 45 | sandboxContainerCount++; 46 | var start = cp.spawn('docker', 47 | ['run', '-id', '--name', self.name, 48 | '--user', userName, '--workdir', home, 49 | sandboxName, shell]); 50 | 51 | // Wait for the container to be created. 52 | start.on('close', function(code, signal) { 53 | if (code === 0) { 54 | cb(); 55 | } else { 56 | cb(new Error('Creating the sandbox failed.')); 57 | } 58 | }); 59 | } 60 | 61 | Sandbox.prototype = { 62 | startTimer: function(timeout) { 63 | // Ready to kill it. 64 | var self = this; 65 | if (self.killTimeout !== undefined) { 66 | var killSandbox = function() { 67 | self.rm(function(){}); 68 | }; 69 | self.killTimeout = setTimeout(killSandbox, timeout); 70 | } 71 | }, 72 | 73 | // Run the command in the sandbox. 74 | // The sandbox is deleted once the command has run. 75 | // options: 76 | // - stdout, stderr: function(data: Buffer). 77 | // - stdin: a Buffer. 78 | // cb: takes the return code. 79 | run: function(command, options, cb) { 80 | var self = this; 81 | 82 | var exec = cp.spawn('docker', ['exec', '-i', '--user=' + userName, 83 | self.name, shell, '-c', ulimit + '; ' + command]); 84 | 85 | // Set up I/O. 86 | var stdoutput = new Buffer(0); 87 | exec.stdout.on('data', function(data) { 88 | stdoutput = Buffer.concat([stdoutput, data]); 89 | }); 90 | var stderrput = new Buffer(0); 91 | exec.stderr.on('data', function(data) { 92 | stderrput = Buffer.concat([stderrput, data]); 93 | }); 94 | exec.on('close', function(code, signal) { 95 | clearTimeout(self.killTimeout); 96 | if (options.stdout) { options.stdout(stdoutput); } 97 | if (options.stderr) { options.stderr(stderrput); } 98 | cb(code); 99 | }); 100 | 101 | self.startTimer(sandboxTimeout); 102 | 103 | if (options.stdin !== undefined) { 104 | exec.stdin.write(options.stdin); 105 | } 106 | }, 107 | 108 | // Delete the sandbox container. 109 | // cb: takes null, or an error. 110 | rm: function(cb) { 111 | var self = this; 112 | 113 | // Remove the sandbox container. 114 | var removeSandbox = function() { 115 | var remove = cp.spawn('docker', ['rm', self.name]); 116 | remove.on('close', function(code, signal) { 117 | if (code === 0) { 118 | cb(); 119 | } else { 120 | cb(new Error('Deleting the sandbox failed.')); 121 | } 122 | }); 123 | }; 124 | 125 | // Ensure all sandbox processes are dead. 126 | // The killing fails if the container is not running, 127 | // which we ignore. 128 | var kill = cp.spawn('docker', ['kill', self.name]); 129 | kill.on('close', removeSandbox); 130 | }, 131 | }; 132 | 133 | // Copy a directory to the sandbox. 134 | // The directory is a String rooted in the virtual tree. 135 | // Returns in a callback, as null or an error. 136 | // sandbox: object of type Sandbox. 137 | function copyToSandbox(directory, sandbox, cb) { 138 | var sandboxProcName = sandbox.name; 139 | 140 | // FIXME: chmod the files such that files not authorized for edition 141 | // cannot be modified. 142 | var chmodFiles = function chmodFiles(cb) { 143 | // The userName's group has the same name. 144 | var chmod = cp.spawn('docker', 145 | ['exec', '--user=root', sandboxProcName, 146 | 'chown', userName + ':' + userName, '-R', home]); 147 | chmod.on('close', function(code, signal) { 148 | if (code === 0) { 149 | cb(null); 150 | } else { 151 | cb(new Error('Setting files\' ownership to ' + userName + ' failed.')); 152 | } 153 | }); 154 | }; 155 | 156 | var copy = cp.spawn('docker', 157 | ['cp', path.join(driver.absolute(directory)) + '/.' 158 | , sandboxProcName + ':' + home]); 159 | copy.on('close', function(code, signal) { 160 | if (code === 0) { 161 | chmodFiles(cb); 162 | } else { 163 | cb(new Error('Copying to the sandbox failed.')); 164 | } 165 | }); 166 | } 167 | 168 | // Copy a list of files (as Strings) from the sandbox to the tree. 169 | // The directory is a String rooted in the virtual tree. 170 | // Returns in a callback, as null or an error. 171 | // sandbox: object of type Sandbox. 172 | function copyFromSandbox(directory, files, sandbox, cb) { 173 | async.each(files, function(filename, cb) { 174 | var copy = cp.spawn('docker', 175 | ['cp', sandbox.name + ':' + path.join(home, filename), 176 | path.join(driver.absolute(directory), 177 | path.dirname(filename))]); 178 | copy.on('close', function(code, signal) { 179 | if (code === 0) { 180 | cb(null); // Everything went well. 181 | } else { 182 | cb(new Error('Copying from the sandbox failed.')); 183 | } 184 | }); 185 | }, cb); 186 | // FIXME: chmod the files. 187 | } 188 | 189 | // Removes a list of `files` in the sandbox. 190 | // sandbox: object of type Sandbox. 191 | function rmFiles(files, sandbox, cb) { 192 | var locations = files.map(function(file) { 193 | return path.join(home, file); 194 | }); 195 | try { 196 | async.each(locations, function(location, cb) { 197 | var deletion = cp.spawn('docker', ['exec', sandbox.name, 198 | 'rm', '-r', location]); 199 | deletion.stderr.on('data', function(d) { console.error('err:'+d); }); 200 | deletion.on('close', function deleted(code, signal) { 201 | if (code === 0) { cb(null); 202 | } else { cb(new Error('Deleting sandbox files for setup failed.')); } 203 | }); 204 | }, cb); 205 | } catch(e) { cb(e); return; } 206 | } 207 | 208 | // Prevent having more than one at a time. 209 | var sandboxInUse = false; 210 | 211 | // Ensure that calls are sequential. 212 | // The directory is a String rooted in the virtual tree. 213 | // The command is a string of shell code (see variable `shell`). 214 | // options: 215 | // - stdout, stderr: function(data: Buffer). 216 | // - stdin: a Buffer. 217 | // - rmFiles: list of files to remove, rooted at the directory picked. 218 | // - fileOutput: list of files to keep, and put back in the main file tree. 219 | // cb: takes the return value, either null or an error. 220 | function runOnDirectory(directory, command, options, cb) { 221 | if (sandboxInUse) { cb(new Error('Sandbox currently in use')); return; } 222 | sandboxInUse = true; 223 | // Options 224 | options.rmFiles = options.rmFiles || []; 225 | 226 | // How to clean up the sandbox. 227 | var end = function(enderr) { 228 | // Whatever the outcome, we assume the sandbox wasn't corrupted. 229 | // That avoids having to build it again every time, 230 | // and it is a reasonable assumption. 231 | sandbox.rm(function(err) { 232 | // The error should not occur but isn't critical. Log it. 233 | if (err != null) { console.error(err); } 234 | sandboxInUse = false; 235 | cb(enderr); 236 | }); 237 | }; 238 | 239 | // How to copy file outputs. 240 | var saveFileOutput = function(enderr) { 241 | if (enderr != null) { return end(enderr); } 242 | if (options.fileOutput && options.fileOutput.length > 0) { 243 | copyFromSandbox(directory, options.fileOutput, sandbox, end); 244 | } else { end(); } 245 | }; 246 | 247 | // How to use the sandbox. 248 | var filesRemoved = function(err) { 249 | if (err != null) { end(err); return; } 250 | sandbox.run(command, options, function(code) { 251 | if (code === 0) { saveFileOutput(null); 252 | } else { end(new Error('Error while running command in the sandbox,' 253 | + ' process code: ' + code)); 254 | } 255 | }); 256 | }; 257 | 258 | // How to copy files into the sandbox. 259 | var sandboxCreated = function() { 260 | copyToSandbox(directory, sandbox, function(err) { 261 | if (err != null) { end(err); return; } 262 | // If the copy of data happened without issue: 263 | rmFiles(options.rmFiles, sandbox, filesRemoved); 264 | }); 265 | }; 266 | 267 | // How to start up the sandbox. 268 | var sandbox = new Sandbox(sandboxCreated); 269 | } 270 | 271 | exports.validSandbox = validSandbox; 272 | exports.runOnDirectory = runOnDirectory; 273 | -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | // Testing library elements of the File Tree server side. 2 | // Copyright © 2011-2016 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var assert = require('assert'); 6 | var async = require('async'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var child = require('child_process'); 10 | 11 | var tests = []; 12 | 13 | // fs.js 14 | // 15 | 16 | // This test adds a `$test` folder. 17 | tests.push(function testFs(end) { 18 | 19 | var tfs = require('./fs'); 20 | 21 | tfs.file("/", function (err, root) { 22 | assert(!err, "fs.js: could not load root."); 23 | root.create("$test", "dir", function(err) { 24 | assert(!err, "fs.js: could not create $test."); 25 | tfs.file("/$test", function(err, testFile) { 26 | assert(!err, "fs.js: could not get $test."); 27 | // Add file "file". 28 | testFile.create("file", "text", function(err) { 29 | assert(!err, "fs.js: " + (err? err.stack: '')); 30 | 31 | // Testing subfiles. 32 | testFile.subfiles(function(err, rootLeafs) { 33 | assert(!err, "fs.js:" + (err? err.stack: '')); 34 | assert( 35 | rootLeafs.indexOf('file') !== -1, 36 | "Root leafs are wrong:\n" + 37 | rootLeafs 38 | ); 39 | 40 | // We don't want to leave junk, so we destroy the file. 41 | testFile.rm(function (err) { 42 | // Testing removal. 43 | root.subfiles(function(err, rootLeafs) { 44 | assert( 45 | rootLeafs.indexOf('file') === -1, 46 | "Removal of files doesn't work." 47 | ); 48 | end(null); 49 | }, 3); 50 | }); 51 | }, 2); 52 | 53 | }); 54 | }); 55 | }); 56 | }); 57 | 58 | }); 59 | 60 | 61 | 62 | // driver.js 63 | // 64 | 65 | tests.push(function testDriver(end) { 66 | 67 | var driver = require('./driver'), 68 | sample = '$foo/bar/baz.js', 69 | rpath = driver.absolute(sample); // real path name. 70 | 71 | function doWithMetaFile() { 72 | // Check that the metadata was dumped. 73 | driver.loadMeta(sample, function (err, metadata) { 74 | assert(!err, (err? err.stack: '')); 75 | assert(metadata.hello === 'world', 76 | "Metadata was not serialized properly:\n" + 77 | "it serialized " + JSON.stringify(metadata) + "\n" + 78 | "while it should have serialized {\"hello\":\"world\"}."); 79 | 80 | // Remove the meta file (and the rest). 81 | child.spawn('rm', ['-r', 'meta/$foo']); 82 | child.spawn('rm', ['-r', 'web/$foo']); 83 | end(null); 84 | }); 85 | } 86 | 87 | child.spawn('mkdir', ['-p', path.dirname(rpath)]).on('exit', function (code) { 88 | var err = fs.writeFileSync(rpath, "Whatever."); 89 | if (err) { console.error("Cannot create file", rpath); } 90 | // Add a sample meta file. 91 | driver.dumpMeta(sample, {'hello':'world'}, doWithMetaFile); 92 | }); 93 | 94 | }); 95 | 96 | 97 | 98 | // lookup.js 99 | // 100 | 101 | tests.push(function testLookup(end) { 102 | 103 | var makeLookup = require('./lookup'); 104 | var tfs = require('./fs'); 105 | 106 | // Test the JSON parser. 107 | var queryOutput = makeLookup.parseJSONQuery("foo.bar['b\\'a\\'z'].qu\\u0075x"); 108 | assert.deepEqual(queryOutput, ['foo', 'bar', 'b\'a\'z', 'quux'], 109 | 'JSON query parser failed with the following output: ' + 110 | JSON.stringify(queryOutput)); 111 | queryOutput = makeLookup.parseJSONQuery("[\"Dalai Lama\"]['']"); 112 | assert.deepEqual(queryOutput, ['Dalai Lama', ''], 113 | 'JSON query parser starting with brackets and consecutive brackets, ' + 114 | 'gave the following invalid output: ' + JSON.stringify(queryOutput)); 115 | 116 | // We want to create a file `file`. 117 | tfs.file('$test', function(err, folder) { 118 | assert(!err, 'Lookup: Cannot get folder $test'); 119 | // We want the folder to have {foo: {bar: "baz"}}. 120 | folder.meta.foo = {bar: "baz"}; 121 | folder.create('file', 'text', function(err) { 122 | assert(!err, 'Lookup: Cannot create file'); 123 | tfs.file('$test/file', function(err, file) { 124 | assert(!err, 'Lookup: Cannot get file'); 125 | // We want the file to have {foo: {quux: "baz"}}. 126 | file.meta.foo = {quux: "baz"}; 127 | testLookup(file); 128 | }); 129 | }); 130 | }); 131 | 132 | function testLookup(file) { 133 | var lookup = makeLookup(file, {}); 134 | async.parallel([function(done) { 135 | lookup('foo', function(data) { 136 | assert(data != null, 'Lookup of direct metadata succeeds'); 137 | assert(data.quux === "baz", 'Lookup of direct metadata'); 138 | done(); 139 | }); 140 | }, function(done) { 141 | lookup('foo.bar', function(data) { 142 | assert(data === "baz", 'Lookup of indirect metadata'); 143 | done(); 144 | }); 145 | }], function whenDone() { 146 | file.rm(function(err) { 147 | assert(!err, 'Lookup: Cannot remove file'); 148 | end(null); 149 | }); 150 | }); 151 | } 152 | 153 | }); 154 | 155 | 156 | // Run all tests sequentially. 157 | async.series(tests, function end() { 158 | // Remove the meta file (and the rest). 159 | child.spawn('rm', ['-r', 'meta/$test']); 160 | child.spawn('rm', ['-r', 'web/$test']); 161 | }); 162 | -------------------------------------------------------------------------------- /lib/type.js: -------------------------------------------------------------------------------- 1 | // Types of files. 2 | // Copyright © 2011-2016 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // The following code is covered by the AGPLv3 license. 4 | 5 | var driver = require('./driver'); 6 | 7 | // Types are internally represented by integers. 8 | // 9 | // We go from _type_ world to _int_ world and back 10 | // with `fromName` and `nameFromType`. 11 | 12 | var fromName = {}; // Links the type to a unique integer. 13 | var nbTypes = 0; // Number of different types. 14 | var fallbacks = []; // Array of parent types (all being integers). 15 | var nameFromType = []; // Array of types (Strings). 16 | 17 | function addType(mimeType, parents) { 18 | fallbacks.push(parents || []); 19 | nameFromType.push(mimeType); 20 | fromName[mimeType] = nbTypes; 21 | nbTypes++; 22 | } 23 | 24 | 25 | // The following algorithm is not cyclic-aware. 26 | // Please do not create cycles in the type system. 27 | function isCompatible(type, ancestor) { 28 | if (type === ancestor) return true; 29 | if (!fallbacks[type]) { 30 | console.error('TYPE:isCompatible: type %s (%s) does not have fallbacks', 31 | nameFromType[type], type); 32 | return false; 33 | } 34 | for (var i = 0; i < fallbacks[type].length; i++) { 35 | if (isCompatible(fallbacks[type][i], ancestor)) return true; 36 | } 37 | return false; 38 | } 39 | 40 | // Each type corresponds directly to a driver of the same name. 41 | // FIXME: we no longer use fallback types, a lot of code here can be simplified. 42 | 43 | addType('dir'); 44 | addType('binary'); 45 | addType('text'); 46 | 47 | 48 | // Driver access. 49 | 50 | var drivers = []; // Sparse array from types to I/O primitives. 51 | 52 | for (var prims in driver.primitives) { 53 | drivers[fromName[prims]] = driver.primitives[prims]; 54 | } 55 | 56 | 57 | // This is not cycle-aware. 58 | // Do not create loops in the type system. 59 | function findDriver(type) { 60 | var driver; 61 | if (driver = drivers[type]) { 62 | return driver; // There is a driver for this type. 63 | } else if (fallbacks[type]) { 64 | for (var i = 0; i < fallbacks[type].length; i++) { 65 | if (driver = findDriver(fallbacks[type][i])) { 66 | return driver; 67 | } 68 | } 69 | } 70 | console.error("Type:findDriver: cannot find driver of type %s.", 71 | nameFromType[type]); 72 | return null; 73 | } 74 | 75 | 76 | // We export the following: 77 | // 78 | // - addType(mimeType :: String, parents :: Array) 79 | // - fromName: Map from types (String) to type number 80 | // - nameFromType: Array of all types in order 81 | // - isCompatible(type :: Number, ancestor :: Number) 82 | // - driver(type :: Number) 83 | 84 | exports.addType = addType; 85 | exports.fromName = fromName; 86 | exports.nameFromType = nameFromType; 87 | exports.isCompatible = isCompatible; 88 | exports.driver = findDriver; 89 | 90 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filetree", 3 | "version": "20.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "filetree", 9 | "version": "20.0.0", 10 | "license": "AGPL-3.0", 11 | "dependencies": { 12 | "async": "2.0.1", 13 | "camp": "^17.2.4", 14 | "canop": "0.4.1", 15 | "email-login": "1.3.2", 16 | "fsos": "1.1.3", 17 | "irc": "0.5.2", 18 | "pg": "8.5.1", 19 | "ws": "3.3.1" 20 | } 21 | }, 22 | "node_modules/accepts": { 23 | "version": "1.3.7", 24 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 25 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 26 | "dependencies": { 27 | "mime-types": "~2.1.24", 28 | "negotiator": "0.6.2" 29 | } 30 | }, 31 | "node_modules/after": { 32 | "version": "0.8.2", 33 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 34 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 35 | }, 36 | "node_modules/arraybuffer.slice": { 37 | "version": "0.0.7", 38 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 39 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" 40 | }, 41 | "node_modules/asap": { 42 | "version": "2.0.5", 43 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", 44 | "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=" 45 | }, 46 | "node_modules/async": { 47 | "version": "2.0.1", 48 | "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz", 49 | "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", 50 | "dependencies": { 51 | "lodash": "^4.8.0" 52 | } 53 | }, 54 | "node_modules/async-limiter": { 55 | "version": "1.0.0", 56 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 57 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 58 | }, 59 | "node_modules/backo2": { 60 | "version": "1.0.2", 61 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 62 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 63 | }, 64 | "node_modules/base64-arraybuffer": { 65 | "version": "0.1.5", 66 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 67 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 68 | }, 69 | "node_modules/base64id": { 70 | "version": "2.0.0", 71 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 72 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 73 | }, 74 | "node_modules/better-assert": { 75 | "version": "1.0.2", 76 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 77 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 78 | "dependencies": { 79 | "callsite": "1.0.0" 80 | } 81 | }, 82 | "node_modules/blob": { 83 | "version": "0.0.5", 84 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 85 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" 86 | }, 87 | "node_modules/buffer-writer": { 88 | "version": "2.0.0", 89 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 90 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", 91 | "engines": { 92 | "node": ">=4" 93 | } 94 | }, 95 | "node_modules/callsite": { 96 | "version": "1.0.0", 97 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 98 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 99 | }, 100 | "node_modules/camp": { 101 | "version": "17.2.4", 102 | "resolved": "https://registry.npmjs.org/camp/-/camp-17.2.4.tgz", 103 | "integrity": "sha512-rCe0NHOqAjuGfMRrCKzoOYgEu4Bpjpq6Vt2/qDMyFd98PKd8O3ySKzE1rXootMorEOs4ilM2T1gBF3MuuftpDQ==", 104 | "dependencies": { 105 | "cookies": "^0.7.3", 106 | "fleau": "~16.2.0", 107 | "formidable": "~1.2.0", 108 | "multilog": "~14.11.22", 109 | "socket.io": "^2.2.0", 110 | "spdy": "^4.0.0", 111 | "ws": "^6.2.1" 112 | } 113 | }, 114 | "node_modules/camp/node_modules/ws": { 115 | "version": "6.2.1", 116 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", 117 | "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", 118 | "dependencies": { 119 | "async-limiter": "~1.0.0" 120 | } 121 | }, 122 | "node_modules/canop": { 123 | "version": "0.4.1", 124 | "resolved": "https://registry.npmjs.org/canop/-/canop-0.4.1.tgz", 125 | "integrity": "sha512-y0aG05BbSdwG+PHZpCug7G+GhW3UmurOO5UiUnrluQFgEhO80sCVSHki0SVSNhoNWUZLwGACTtNj0rz4DKjm5Q==" 126 | }, 127 | "node_modules/component-bind": { 128 | "version": "1.0.0", 129 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 130 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 131 | }, 132 | "node_modules/component-emitter": { 133 | "version": "1.2.1", 134 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 135 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 136 | }, 137 | "node_modules/component-inherit": { 138 | "version": "0.0.3", 139 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 140 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 141 | }, 142 | "node_modules/cookie": { 143 | "version": "0.3.1", 144 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 145 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 146 | }, 147 | "node_modules/cookies": { 148 | "version": "0.7.3", 149 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", 150 | "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", 151 | "dependencies": { 152 | "depd": "~1.1.2", 153 | "keygrip": "~1.0.3" 154 | } 155 | }, 156 | "node_modules/core-util-is": { 157 | "version": "1.0.2", 158 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 159 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 160 | }, 161 | "node_modules/debug": { 162 | "version": "4.1.1", 163 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 164 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 165 | "dependencies": { 166 | "ms": "^2.1.1" 167 | } 168 | }, 169 | "node_modules/depd": { 170 | "version": "1.1.2", 171 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 172 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 173 | }, 174 | "node_modules/detect-node": { 175 | "version": "2.0.4", 176 | "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", 177 | "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" 178 | }, 179 | "node_modules/email-login": { 180 | "version": "1.3.2", 181 | "resolved": "https://registry.npmjs.org/email-login/-/email-login-1.3.2.tgz", 182 | "integrity": "sha512-iXE7C3e5hCBltEvE2QXu7kI423tupjW2BU/iL5UyZyCdcUMyIjwhmmVSRRnutGKZ9v8Yflz3FrrI8JldF4C42Q==", 183 | "dependencies": { 184 | "fsos": "~1.1.1", 185 | "nodemailer": "~6.4.17", 186 | "promise": "~7.1.1" 187 | }, 188 | "engines": { 189 | "node": ">=0.10.0" 190 | } 191 | }, 192 | "node_modules/email-login/node_modules/fsos": { 193 | "version": "1.1.6", 194 | "resolved": "https://registry.npmjs.org/fsos/-/fsos-1.1.6.tgz", 195 | "integrity": "sha512-44MKwAuDfB14pojgokzqEhavMO0s1vv4H+WhsmHYB8fmoJI6YUephlD30Vak6paE6bbY3xd3b3Wa7vAgSglk8A==", 196 | "dependencies": { 197 | "mkdirp": "~0.5.1", 198 | "promise": "~7.0.4" 199 | } 200 | }, 201 | "node_modules/email-login/node_modules/fsos/node_modules/mkdirp": { 202 | "version": "0.5.5", 203 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 204 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 205 | "dependencies": { 206 | "minimist": "^1.2.5" 207 | } 208 | }, 209 | "node_modules/email-login/node_modules/fsos/node_modules/promise": { 210 | "version": "7.0.4", 211 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.0.4.tgz", 212 | "integrity": "sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk=", 213 | "dependencies": { 214 | "asap": "~2.0.3" 215 | } 216 | }, 217 | "node_modules/email-login/node_modules/minimist": { 218 | "version": "1.2.5", 219 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 220 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 221 | }, 222 | "node_modules/email-login/node_modules/promise": { 223 | "version": "7.1.1", 224 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz", 225 | "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=", 226 | "dependencies": { 227 | "asap": "~2.0.3" 228 | } 229 | }, 230 | "node_modules/engine.io": { 231 | "version": "3.4.1", 232 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.1.tgz", 233 | "integrity": "sha512-8MfIfF1/IIfxuc2gv5K+XlFZczw/BpTvqBdl0E2fBLkYQp4miv4LuDTVtYt4yMyaIFLEr4vtaSgV4mjvll8Crw==", 234 | "dependencies": { 235 | "accepts": "~1.3.4", 236 | "base64id": "2.0.0", 237 | "cookie": "0.3.1", 238 | "debug": "~4.1.0", 239 | "engine.io-parser": "~2.2.0", 240 | "ws": "^7.1.2" 241 | } 242 | }, 243 | "node_modules/engine.io-client": { 244 | "version": "3.4.1", 245 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.1.tgz", 246 | "integrity": "sha512-RJNmA+A9Js+8Aoq815xpGAsgWH1VoSYM//2VgIiu9lNOaHFfLpTjH4tOzktBpjIs5lvOfiNY1dwf+NuU6D38Mw==", 247 | "dependencies": { 248 | "component-emitter": "1.2.1", 249 | "component-inherit": "0.0.3", 250 | "debug": "~4.1.0", 251 | "engine.io-parser": "~2.2.0", 252 | "has-cors": "1.1.0", 253 | "indexof": "0.0.1", 254 | "parseqs": "0.0.5", 255 | "parseuri": "0.0.5", 256 | "ws": "~6.1.0", 257 | "xmlhttprequest-ssl": "~1.5.4", 258 | "yeast": "0.1.2" 259 | } 260 | }, 261 | "node_modules/engine.io-client/node_modules/ws": { 262 | "version": "6.1.4", 263 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", 264 | "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", 265 | "dependencies": { 266 | "async-limiter": "~1.0.0" 267 | } 268 | }, 269 | "node_modules/engine.io-parser": { 270 | "version": "2.2.0", 271 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", 272 | "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", 273 | "dependencies": { 274 | "after": "0.8.2", 275 | "arraybuffer.slice": "~0.0.7", 276 | "base64-arraybuffer": "0.1.5", 277 | "blob": "0.0.5", 278 | "has-binary2": "~1.0.2" 279 | } 280 | }, 281 | "node_modules/engine.io/node_modules/ws": { 282 | "version": "7.2.5", 283 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz", 284 | "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==" 285 | }, 286 | "node_modules/fleau": { 287 | "version": "16.2.0", 288 | "resolved": "https://registry.npmjs.org/fleau/-/fleau-16.2.0.tgz", 289 | "integrity": "sha1-ruZ14mI37qfkNGH69MYadIe9MkM=" 290 | }, 291 | "node_modules/formidable": { 292 | "version": "1.2.2", 293 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", 294 | "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" 295 | }, 296 | "node_modules/fsos": { 297 | "version": "1.1.3", 298 | "resolved": "https://registry.npmjs.org/fsos/-/fsos-1.1.3.tgz", 299 | "integrity": "sha512-yTDF/deE+MNP6f6xhHMpuARtcGgCTOe2N2GJCZ60uBvggyX2Vdxpdt+4au7XNnuUvhDtDHUOuDwxUKrwLkocgw==", 300 | "dependencies": { 301 | "mkdirp": "~0.5.1", 302 | "promise": "~7.0.4" 303 | } 304 | }, 305 | "node_modules/handle-thing": { 306 | "version": "2.0.1", 307 | "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", 308 | "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" 309 | }, 310 | "node_modules/has-binary2": { 311 | "version": "1.0.3", 312 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 313 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", 314 | "dependencies": { 315 | "isarray": "2.0.1" 316 | } 317 | }, 318 | "node_modules/has-cors": { 319 | "version": "1.1.0", 320 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 321 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 322 | }, 323 | "node_modules/hpack.js": { 324 | "version": "2.1.6", 325 | "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", 326 | "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", 327 | "dependencies": { 328 | "inherits": "^2.0.1", 329 | "obuf": "^1.0.0", 330 | "readable-stream": "^2.0.1", 331 | "wbuf": "^1.1.0" 332 | } 333 | }, 334 | "node_modules/hpack.js/node_modules/isarray": { 335 | "version": "1.0.0", 336 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 337 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 338 | }, 339 | "node_modules/hpack.js/node_modules/readable-stream": { 340 | "version": "2.3.7", 341 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 342 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 343 | "dependencies": { 344 | "core-util-is": "~1.0.0", 345 | "inherits": "~2.0.3", 346 | "isarray": "~1.0.0", 347 | "process-nextick-args": "~2.0.0", 348 | "safe-buffer": "~5.1.1", 349 | "string_decoder": "~1.1.1", 350 | "util-deprecate": "~1.0.1" 351 | } 352 | }, 353 | "node_modules/http-deceiver": { 354 | "version": "1.2.7", 355 | "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", 356 | "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" 357 | }, 358 | "node_modules/iconv": { 359 | "version": "2.2.3", 360 | "resolved": "https://registry.npmjs.org/iconv/-/iconv-2.2.3.tgz", 361 | "integrity": "sha1-4ITWDut9c9p/CpwJbkyKvgkL+u0=", 362 | "optional": true, 363 | "dependencies": { 364 | "nan": "^2.3.5" 365 | } 366 | }, 367 | "node_modules/indexof": { 368 | "version": "0.0.1", 369 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 370 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 371 | }, 372 | "node_modules/inherits": { 373 | "version": "2.0.4", 374 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 375 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 376 | }, 377 | "node_modules/irc": { 378 | "version": "0.5.2", 379 | "resolved": "https://registry.npmjs.org/irc/-/irc-0.5.2.tgz", 380 | "integrity": "sha1-NxT0doNlqW0LL3dryRFmvrJGS7w=", 381 | "dependencies": { 382 | "iconv": "~2.2.1", 383 | "irc-colors": "^1.1.0", 384 | "node-icu-charset-detector": "~0.2.0" 385 | }, 386 | "optionalDependencies": { 387 | "iconv": "~2.2.1", 388 | "node-icu-charset-detector": "~0.2.0" 389 | } 390 | }, 391 | "node_modules/irc-colors": { 392 | "version": "1.4.2", 393 | "resolved": "https://registry.npmjs.org/irc-colors/-/irc-colors-1.4.2.tgz", 394 | "integrity": "sha512-QZ1g4d9XTGKgBAp7lrltCetefqd3zfYs3SFQ4YyRSORORCmy/9EkU/r8LJrlSnaWc3Z+54EgHXBRlOHaCvpyHA==" 395 | }, 396 | "node_modules/isarray": { 397 | "version": "2.0.1", 398 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 399 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 400 | }, 401 | "node_modules/keygrip": { 402 | "version": "1.0.3", 403 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", 404 | "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" 405 | }, 406 | "node_modules/lodash": { 407 | "version": "4.17.15", 408 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 409 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 410 | }, 411 | "node_modules/mime-db": { 412 | "version": "1.44.0", 413 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 414 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 415 | }, 416 | "node_modules/mime-types": { 417 | "version": "2.1.27", 418 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 419 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 420 | "dependencies": { 421 | "mime-db": "1.44.0" 422 | } 423 | }, 424 | "node_modules/minimalistic-assert": { 425 | "version": "1.0.1", 426 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 427 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 428 | }, 429 | "node_modules/mkdirp": { 430 | "version": "0.5.5", 431 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 432 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 433 | "dependencies": { 434 | "minimist": "^1.2.5" 435 | } 436 | }, 437 | "node_modules/mkdirp/node_modules/minimist": { 438 | "version": "1.2.5", 439 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 440 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 441 | }, 442 | "node_modules/ms": { 443 | "version": "2.1.2", 444 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 445 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 446 | }, 447 | "node_modules/multilog": { 448 | "version": "14.11.22", 449 | "resolved": "https://registry.npmjs.org/multilog/-/multilog-14.11.22.tgz", 450 | "integrity": "sha1-w/AQjQnR/bNBDq8eGT5SkI4jpDM=" 451 | }, 452 | "node_modules/nan": { 453 | "version": "2.10.0", 454 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", 455 | "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", 456 | "optional": true 457 | }, 458 | "node_modules/negotiator": { 459 | "version": "0.6.2", 460 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 461 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 462 | }, 463 | "node_modules/node-icu-charset-detector": { 464 | "version": "0.2.0", 465 | "resolved": "https://registry.npmjs.org/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz", 466 | "integrity": "sha1-wjINo3Tdy2cfxUy0oOBB4Vb/1jk=", 467 | "optional": true, 468 | "dependencies": { 469 | "nan": "^2.3.3" 470 | } 471 | }, 472 | "node_modules/nodemailer": { 473 | "version": "6.4.17", 474 | "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.17.tgz", 475 | "integrity": "sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ==", 476 | "engines": { 477 | "node": ">=6.0.0" 478 | } 479 | }, 480 | "node_modules/object-component": { 481 | "version": "0.0.3", 482 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", 483 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" 484 | }, 485 | "node_modules/obuf": { 486 | "version": "1.1.2", 487 | "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", 488 | "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" 489 | }, 490 | "node_modules/packet-reader": { 491 | "version": "1.0.0", 492 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 493 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 494 | }, 495 | "node_modules/parseqs": { 496 | "version": "0.0.5", 497 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", 498 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", 499 | "dependencies": { 500 | "better-assert": "~1.0.0" 501 | } 502 | }, 503 | "node_modules/parseuri": { 504 | "version": "0.0.5", 505 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", 506 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", 507 | "dependencies": { 508 | "better-assert": "~1.0.0" 509 | } 510 | }, 511 | "node_modules/pg": { 512 | "version": "8.5.1", 513 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", 514 | "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", 515 | "dependencies": { 516 | "buffer-writer": "2.0.0", 517 | "packet-reader": "1.0.0", 518 | "pg-connection-string": "^2.4.0", 519 | "pg-pool": "^3.2.2", 520 | "pg-protocol": "^1.4.0", 521 | "pg-types": "^2.1.0", 522 | "pgpass": "1.x" 523 | }, 524 | "engines": { 525 | "node": ">= 8.0.0" 526 | }, 527 | "peerDependencies": { 528 | "pg-native": ">=2.0.0" 529 | }, 530 | "peerDependenciesMeta": { 531 | "pg-native": { 532 | "optional": true 533 | } 534 | } 535 | }, 536 | "node_modules/pg-connection-string": { 537 | "version": "2.4.0", 538 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", 539 | "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" 540 | }, 541 | "node_modules/pg-int8": { 542 | "version": "1.0.1", 543 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 544 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", 545 | "engines": { 546 | "node": ">=4.0.0" 547 | } 548 | }, 549 | "node_modules/pg-pool": { 550 | "version": "3.2.2", 551 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", 552 | "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==", 553 | "peerDependencies": { 554 | "pg": ">=8.0" 555 | } 556 | }, 557 | "node_modules/pg-protocol": { 558 | "version": "1.4.0", 559 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", 560 | "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" 561 | }, 562 | "node_modules/pg-types": { 563 | "version": "2.2.0", 564 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 565 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 566 | "dependencies": { 567 | "pg-int8": "1.0.1", 568 | "postgres-array": "~2.0.0", 569 | "postgres-bytea": "~1.0.0", 570 | "postgres-date": "~1.0.4", 571 | "postgres-interval": "^1.1.0" 572 | }, 573 | "engines": { 574 | "node": ">=4" 575 | } 576 | }, 577 | "node_modules/pgpass": { 578 | "version": "1.0.2", 579 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 580 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 581 | "dependencies": { 582 | "split": "^1.0.0" 583 | } 584 | }, 585 | "node_modules/postgres-array": { 586 | "version": "2.0.0", 587 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 588 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", 589 | "engines": { 590 | "node": ">=4" 591 | } 592 | }, 593 | "node_modules/postgres-bytea": { 594 | "version": "1.0.0", 595 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 596 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", 597 | "engines": { 598 | "node": ">=0.10.0" 599 | } 600 | }, 601 | "node_modules/postgres-date": { 602 | "version": "1.0.7", 603 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 604 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", 605 | "engines": { 606 | "node": ">=0.10.0" 607 | } 608 | }, 609 | "node_modules/postgres-interval": { 610 | "version": "1.2.0", 611 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 612 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 613 | "dependencies": { 614 | "xtend": "^4.0.0" 615 | }, 616 | "engines": { 617 | "node": ">=0.10.0" 618 | } 619 | }, 620 | "node_modules/process-nextick-args": { 621 | "version": "2.0.1", 622 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 623 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 624 | }, 625 | "node_modules/promise": { 626 | "version": "7.0.4", 627 | "resolved": "http://registry.npmjs.org/promise/-/promise-7.0.4.tgz", 628 | "integrity": "sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk=", 629 | "dependencies": { 630 | "asap": "~2.0.3" 631 | } 632 | }, 633 | "node_modules/readable-stream": { 634 | "version": "3.6.0", 635 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 636 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 637 | "dependencies": { 638 | "inherits": "^2.0.3", 639 | "string_decoder": "^1.1.1", 640 | "util-deprecate": "^1.0.1" 641 | } 642 | }, 643 | "node_modules/safe-buffer": { 644 | "version": "5.1.2", 645 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 646 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 647 | }, 648 | "node_modules/select-hose": { 649 | "version": "2.0.0", 650 | "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", 651 | "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" 652 | }, 653 | "node_modules/socket.io": { 654 | "version": "2.3.0", 655 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", 656 | "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", 657 | "dependencies": { 658 | "debug": "~4.1.0", 659 | "engine.io": "~3.4.0", 660 | "has-binary2": "~1.0.2", 661 | "socket.io-adapter": "~1.1.0", 662 | "socket.io-client": "2.3.0", 663 | "socket.io-parser": "~3.4.0" 664 | } 665 | }, 666 | "node_modules/socket.io-adapter": { 667 | "version": "1.1.2", 668 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", 669 | "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" 670 | }, 671 | "node_modules/socket.io-client": { 672 | "version": "2.3.0", 673 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", 674 | "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", 675 | "dependencies": { 676 | "backo2": "1.0.2", 677 | "base64-arraybuffer": "0.1.5", 678 | "component-bind": "1.0.0", 679 | "component-emitter": "1.2.1", 680 | "debug": "~4.1.0", 681 | "engine.io-client": "~3.4.0", 682 | "has-binary2": "~1.0.2", 683 | "has-cors": "1.1.0", 684 | "indexof": "0.0.1", 685 | "object-component": "0.0.3", 686 | "parseqs": "0.0.5", 687 | "parseuri": "0.0.5", 688 | "socket.io-parser": "~3.3.0", 689 | "to-array": "0.1.4" 690 | } 691 | }, 692 | "node_modules/socket.io-client/node_modules/ms": { 693 | "version": "2.0.0", 694 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 695 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 696 | }, 697 | "node_modules/socket.io-client/node_modules/socket.io-parser": { 698 | "version": "3.3.0", 699 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", 700 | "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", 701 | "dependencies": { 702 | "component-emitter": "1.2.1", 703 | "debug": "~3.1.0", 704 | "isarray": "2.0.1" 705 | } 706 | }, 707 | "node_modules/socket.io-client/node_modules/socket.io-parser/node_modules/debug": { 708 | "version": "3.1.0", 709 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 710 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 711 | "dependencies": { 712 | "ms": "2.0.0" 713 | } 714 | }, 715 | "node_modules/socket.io-parser": { 716 | "version": "3.4.0", 717 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.0.tgz", 718 | "integrity": "sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==", 719 | "dependencies": { 720 | "component-emitter": "1.2.1", 721 | "debug": "~4.1.0", 722 | "isarray": "2.0.1" 723 | } 724 | }, 725 | "node_modules/spdy": { 726 | "version": "4.0.2", 727 | "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", 728 | "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", 729 | "dependencies": { 730 | "debug": "^4.1.0", 731 | "handle-thing": "^2.0.0", 732 | "http-deceiver": "^1.2.7", 733 | "select-hose": "^2.0.0", 734 | "spdy-transport": "^3.0.0" 735 | } 736 | }, 737 | "node_modules/spdy-transport": { 738 | "version": "3.0.0", 739 | "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", 740 | "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", 741 | "dependencies": { 742 | "debug": "^4.1.0", 743 | "detect-node": "^2.0.4", 744 | "hpack.js": "^2.1.6", 745 | "obuf": "^1.1.2", 746 | "readable-stream": "^3.0.6", 747 | "wbuf": "^1.7.3" 748 | } 749 | }, 750 | "node_modules/split": { 751 | "version": "1.0.1", 752 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 753 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 754 | "dependencies": { 755 | "through": "2" 756 | } 757 | }, 758 | "node_modules/string_decoder": { 759 | "version": "1.1.1", 760 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 761 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 762 | "dependencies": { 763 | "safe-buffer": "~5.1.0" 764 | } 765 | }, 766 | "node_modules/through": { 767 | "version": "2.3.8", 768 | "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", 769 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 770 | }, 771 | "node_modules/to-array": { 772 | "version": "0.1.4", 773 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 774 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 775 | }, 776 | "node_modules/util-deprecate": { 777 | "version": "1.0.2", 778 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 779 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 780 | }, 781 | "node_modules/wbuf": { 782 | "version": "1.7.3", 783 | "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", 784 | "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", 785 | "dependencies": { 786 | "minimalistic-assert": "^1.0.0" 787 | } 788 | }, 789 | "node_modules/ws": { 790 | "version": "3.3.1", 791 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.1.tgz", 792 | "integrity": "sha512-8A/uRMnQy8KCQsmep1m7Bk+z/+LIkeF7w+TDMLtX1iZm5Hq9HsUDmgFGaW1ACW5Cj0b2Qo7wCvRhYN2ErUVp/A==", 793 | "dependencies": { 794 | "async-limiter": "~1.0.0", 795 | "safe-buffer": "~5.1.0", 796 | "ultron": "~1.1.0" 797 | } 798 | }, 799 | "node_modules/ws/node_modules/safe-buffer": { 800 | "version": "5.1.2", 801 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 802 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 803 | }, 804 | "node_modules/ws/node_modules/ultron": { 805 | "version": "1.1.1", 806 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 807 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 808 | }, 809 | "node_modules/xmlhttprequest-ssl": { 810 | "version": "1.5.5", 811 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 812 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 813 | }, 814 | "node_modules/xtend": { 815 | "version": "4.0.2", 816 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 817 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 818 | "engines": { 819 | "node": ">=0.4" 820 | } 821 | }, 822 | "node_modules/yeast": { 823 | "version": "0.1.2", 824 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 825 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 826 | } 827 | }, 828 | "dependencies": { 829 | "accepts": { 830 | "version": "1.3.7", 831 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 832 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 833 | "requires": { 834 | "mime-types": "~2.1.24", 835 | "negotiator": "0.6.2" 836 | } 837 | }, 838 | "after": { 839 | "version": "0.8.2", 840 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 841 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 842 | }, 843 | "arraybuffer.slice": { 844 | "version": "0.0.7", 845 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 846 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" 847 | }, 848 | "asap": { 849 | "version": "2.0.5", 850 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", 851 | "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=" 852 | }, 853 | "async": { 854 | "version": "2.0.1", 855 | "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz", 856 | "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", 857 | "requires": { 858 | "lodash": "^4.8.0" 859 | } 860 | }, 861 | "async-limiter": { 862 | "version": "1.0.0", 863 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 864 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 865 | }, 866 | "backo2": { 867 | "version": "1.0.2", 868 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 869 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 870 | }, 871 | "base64-arraybuffer": { 872 | "version": "0.1.5", 873 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 874 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 875 | }, 876 | "base64id": { 877 | "version": "2.0.0", 878 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 879 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 880 | }, 881 | "better-assert": { 882 | "version": "1.0.2", 883 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 884 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 885 | "requires": { 886 | "callsite": "1.0.0" 887 | } 888 | }, 889 | "blob": { 890 | "version": "0.0.5", 891 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 892 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" 893 | }, 894 | "buffer-writer": { 895 | "version": "2.0.0", 896 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 897 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 898 | }, 899 | "callsite": { 900 | "version": "1.0.0", 901 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 902 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 903 | }, 904 | "camp": { 905 | "version": "17.2.4", 906 | "resolved": "https://registry.npmjs.org/camp/-/camp-17.2.4.tgz", 907 | "integrity": "sha512-rCe0NHOqAjuGfMRrCKzoOYgEu4Bpjpq6Vt2/qDMyFd98PKd8O3ySKzE1rXootMorEOs4ilM2T1gBF3MuuftpDQ==", 908 | "requires": { 909 | "cookies": "^0.7.3", 910 | "fleau": "~16.2.0", 911 | "formidable": "~1.2.0", 912 | "multilog": "~14.11.22", 913 | "socket.io": "^2.2.0", 914 | "spdy": "^4.0.0", 915 | "ws": "^6.2.1" 916 | }, 917 | "dependencies": { 918 | "ws": { 919 | "version": "6.2.1", 920 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", 921 | "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", 922 | "requires": { 923 | "async-limiter": "~1.0.0" 924 | } 925 | } 926 | } 927 | }, 928 | "canop": { 929 | "version": "0.4.1", 930 | "resolved": "https://registry.npmjs.org/canop/-/canop-0.4.1.tgz", 931 | "integrity": "sha512-y0aG05BbSdwG+PHZpCug7G+GhW3UmurOO5UiUnrluQFgEhO80sCVSHki0SVSNhoNWUZLwGACTtNj0rz4DKjm5Q==" 932 | }, 933 | "component-bind": { 934 | "version": "1.0.0", 935 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 936 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 937 | }, 938 | "component-emitter": { 939 | "version": "1.2.1", 940 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 941 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 942 | }, 943 | "component-inherit": { 944 | "version": "0.0.3", 945 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 946 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 947 | }, 948 | "cookie": { 949 | "version": "0.3.1", 950 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 951 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 952 | }, 953 | "cookies": { 954 | "version": "0.7.3", 955 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", 956 | "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", 957 | "requires": { 958 | "depd": "~1.1.2", 959 | "keygrip": "~1.0.3" 960 | } 961 | }, 962 | "core-util-is": { 963 | "version": "1.0.2", 964 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 965 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 966 | }, 967 | "debug": { 968 | "version": "4.1.1", 969 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 970 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 971 | "requires": { 972 | "ms": "^2.1.1" 973 | } 974 | }, 975 | "depd": { 976 | "version": "1.1.2", 977 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 978 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 979 | }, 980 | "detect-node": { 981 | "version": "2.0.4", 982 | "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", 983 | "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" 984 | }, 985 | "email-login": { 986 | "version": "1.3.2", 987 | "resolved": "https://registry.npmjs.org/email-login/-/email-login-1.3.2.tgz", 988 | "integrity": "sha512-iXE7C3e5hCBltEvE2QXu7kI423tupjW2BU/iL5UyZyCdcUMyIjwhmmVSRRnutGKZ9v8Yflz3FrrI8JldF4C42Q==", 989 | "requires": { 990 | "fsos": "~1.1.1", 991 | "nodemailer": "~6.4.17", 992 | "promise": "~7.1.1" 993 | }, 994 | "dependencies": { 995 | "fsos": { 996 | "version": "1.1.6", 997 | "resolved": "https://registry.npmjs.org/fsos/-/fsos-1.1.6.tgz", 998 | "integrity": "sha512-44MKwAuDfB14pojgokzqEhavMO0s1vv4H+WhsmHYB8fmoJI6YUephlD30Vak6paE6bbY3xd3b3Wa7vAgSglk8A==", 999 | "requires": { 1000 | "mkdirp": "~0.5.1", 1001 | "promise": "~7.0.4" 1002 | }, 1003 | "dependencies": { 1004 | "mkdirp": { 1005 | "version": "0.5.5", 1006 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 1007 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 1008 | "requires": { 1009 | "minimist": "^1.2.5" 1010 | } 1011 | }, 1012 | "promise": { 1013 | "version": "7.0.4", 1014 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.0.4.tgz", 1015 | "integrity": "sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk=", 1016 | "requires": { 1017 | "asap": "~2.0.3" 1018 | } 1019 | } 1020 | } 1021 | }, 1022 | "minimist": { 1023 | "version": "1.2.5", 1024 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 1025 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 1026 | }, 1027 | "promise": { 1028 | "version": "7.1.1", 1029 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz", 1030 | "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=", 1031 | "requires": { 1032 | "asap": "~2.0.3" 1033 | } 1034 | } 1035 | } 1036 | }, 1037 | "engine.io": { 1038 | "version": "3.4.1", 1039 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.1.tgz", 1040 | "integrity": "sha512-8MfIfF1/IIfxuc2gv5K+XlFZczw/BpTvqBdl0E2fBLkYQp4miv4LuDTVtYt4yMyaIFLEr4vtaSgV4mjvll8Crw==", 1041 | "requires": { 1042 | "accepts": "~1.3.4", 1043 | "base64id": "2.0.0", 1044 | "cookie": "0.3.1", 1045 | "debug": "~4.1.0", 1046 | "engine.io-parser": "~2.2.0", 1047 | "ws": "^7.1.2" 1048 | }, 1049 | "dependencies": { 1050 | "ws": { 1051 | "version": "7.2.5", 1052 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz", 1053 | "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==" 1054 | } 1055 | } 1056 | }, 1057 | "engine.io-client": { 1058 | "version": "3.4.1", 1059 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.1.tgz", 1060 | "integrity": "sha512-RJNmA+A9Js+8Aoq815xpGAsgWH1VoSYM//2VgIiu9lNOaHFfLpTjH4tOzktBpjIs5lvOfiNY1dwf+NuU6D38Mw==", 1061 | "requires": { 1062 | "component-emitter": "1.2.1", 1063 | "component-inherit": "0.0.3", 1064 | "debug": "~4.1.0", 1065 | "engine.io-parser": "~2.2.0", 1066 | "has-cors": "1.1.0", 1067 | "indexof": "0.0.1", 1068 | "parseqs": "0.0.5", 1069 | "parseuri": "0.0.5", 1070 | "ws": "~6.1.0", 1071 | "xmlhttprequest-ssl": "~1.5.4", 1072 | "yeast": "0.1.2" 1073 | }, 1074 | "dependencies": { 1075 | "ws": { 1076 | "version": "6.1.4", 1077 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", 1078 | "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", 1079 | "requires": { 1080 | "async-limiter": "~1.0.0" 1081 | } 1082 | } 1083 | } 1084 | }, 1085 | "engine.io-parser": { 1086 | "version": "2.2.0", 1087 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", 1088 | "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", 1089 | "requires": { 1090 | "after": "0.8.2", 1091 | "arraybuffer.slice": "~0.0.7", 1092 | "base64-arraybuffer": "0.1.5", 1093 | "blob": "0.0.5", 1094 | "has-binary2": "~1.0.2" 1095 | } 1096 | }, 1097 | "fleau": { 1098 | "version": "16.2.0", 1099 | "resolved": "https://registry.npmjs.org/fleau/-/fleau-16.2.0.tgz", 1100 | "integrity": "sha1-ruZ14mI37qfkNGH69MYadIe9MkM=" 1101 | }, 1102 | "formidable": { 1103 | "version": "1.2.2", 1104 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", 1105 | "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" 1106 | }, 1107 | "fsos": { 1108 | "version": "1.1.3", 1109 | "resolved": "https://registry.npmjs.org/fsos/-/fsos-1.1.3.tgz", 1110 | "integrity": "sha512-yTDF/deE+MNP6f6xhHMpuARtcGgCTOe2N2GJCZ60uBvggyX2Vdxpdt+4au7XNnuUvhDtDHUOuDwxUKrwLkocgw==", 1111 | "requires": { 1112 | "mkdirp": "~0.5.1", 1113 | "promise": "~7.0.4" 1114 | } 1115 | }, 1116 | "handle-thing": { 1117 | "version": "2.0.1", 1118 | "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", 1119 | "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" 1120 | }, 1121 | "has-binary2": { 1122 | "version": "1.0.3", 1123 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 1124 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", 1125 | "requires": { 1126 | "isarray": "2.0.1" 1127 | } 1128 | }, 1129 | "has-cors": { 1130 | "version": "1.1.0", 1131 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 1132 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 1133 | }, 1134 | "hpack.js": { 1135 | "version": "2.1.6", 1136 | "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", 1137 | "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", 1138 | "requires": { 1139 | "inherits": "^2.0.1", 1140 | "obuf": "^1.0.0", 1141 | "readable-stream": "^2.0.1", 1142 | "wbuf": "^1.1.0" 1143 | }, 1144 | "dependencies": { 1145 | "isarray": { 1146 | "version": "1.0.0", 1147 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1148 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 1149 | }, 1150 | "readable-stream": { 1151 | "version": "2.3.7", 1152 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 1153 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 1154 | "requires": { 1155 | "core-util-is": "~1.0.0", 1156 | "inherits": "~2.0.3", 1157 | "isarray": "~1.0.0", 1158 | "process-nextick-args": "~2.0.0", 1159 | "safe-buffer": "~5.1.1", 1160 | "string_decoder": "~1.1.1", 1161 | "util-deprecate": "~1.0.1" 1162 | } 1163 | } 1164 | } 1165 | }, 1166 | "http-deceiver": { 1167 | "version": "1.2.7", 1168 | "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", 1169 | "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" 1170 | }, 1171 | "iconv": { 1172 | "version": "2.2.3", 1173 | "resolved": "https://registry.npmjs.org/iconv/-/iconv-2.2.3.tgz", 1174 | "integrity": "sha1-4ITWDut9c9p/CpwJbkyKvgkL+u0=", 1175 | "optional": true, 1176 | "requires": { 1177 | "nan": "^2.3.5" 1178 | } 1179 | }, 1180 | "indexof": { 1181 | "version": "0.0.1", 1182 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 1183 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 1184 | }, 1185 | "inherits": { 1186 | "version": "2.0.4", 1187 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1188 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1189 | }, 1190 | "irc": { 1191 | "version": "0.5.2", 1192 | "resolved": "https://registry.npmjs.org/irc/-/irc-0.5.2.tgz", 1193 | "integrity": "sha1-NxT0doNlqW0LL3dryRFmvrJGS7w=", 1194 | "requires": { 1195 | "iconv": "~2.2.1", 1196 | "irc-colors": "^1.1.0", 1197 | "node-icu-charset-detector": "~0.2.0" 1198 | } 1199 | }, 1200 | "irc-colors": { 1201 | "version": "1.4.2", 1202 | "resolved": "https://registry.npmjs.org/irc-colors/-/irc-colors-1.4.2.tgz", 1203 | "integrity": "sha512-QZ1g4d9XTGKgBAp7lrltCetefqd3zfYs3SFQ4YyRSORORCmy/9EkU/r8LJrlSnaWc3Z+54EgHXBRlOHaCvpyHA==" 1204 | }, 1205 | "isarray": { 1206 | "version": "2.0.1", 1207 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 1208 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 1209 | }, 1210 | "keygrip": { 1211 | "version": "1.0.3", 1212 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", 1213 | "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" 1214 | }, 1215 | "lodash": { 1216 | "version": "4.17.15", 1217 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 1218 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 1219 | }, 1220 | "mime-db": { 1221 | "version": "1.44.0", 1222 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 1223 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 1224 | }, 1225 | "mime-types": { 1226 | "version": "2.1.27", 1227 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 1228 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 1229 | "requires": { 1230 | "mime-db": "1.44.0" 1231 | } 1232 | }, 1233 | "minimalistic-assert": { 1234 | "version": "1.0.1", 1235 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 1236 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 1237 | }, 1238 | "mkdirp": { 1239 | "version": "0.5.5", 1240 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 1241 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 1242 | "requires": { 1243 | "minimist": "^1.2.5" 1244 | }, 1245 | "dependencies": { 1246 | "minimist": { 1247 | "version": "1.2.5", 1248 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 1249 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 1250 | } 1251 | } 1252 | }, 1253 | "ms": { 1254 | "version": "2.1.2", 1255 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1256 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1257 | }, 1258 | "multilog": { 1259 | "version": "14.11.22", 1260 | "resolved": "https://registry.npmjs.org/multilog/-/multilog-14.11.22.tgz", 1261 | "integrity": "sha1-w/AQjQnR/bNBDq8eGT5SkI4jpDM=" 1262 | }, 1263 | "nan": { 1264 | "version": "2.10.0", 1265 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", 1266 | "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", 1267 | "optional": true 1268 | }, 1269 | "negotiator": { 1270 | "version": "0.6.2", 1271 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 1272 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 1273 | }, 1274 | "node-icu-charset-detector": { 1275 | "version": "0.2.0", 1276 | "resolved": "https://registry.npmjs.org/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz", 1277 | "integrity": "sha1-wjINo3Tdy2cfxUy0oOBB4Vb/1jk=", 1278 | "optional": true, 1279 | "requires": { 1280 | "nan": "^2.3.3" 1281 | } 1282 | }, 1283 | "nodemailer": { 1284 | "version": "6.4.17", 1285 | "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.17.tgz", 1286 | "integrity": "sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ==" 1287 | }, 1288 | "object-component": { 1289 | "version": "0.0.3", 1290 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", 1291 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" 1292 | }, 1293 | "obuf": { 1294 | "version": "1.1.2", 1295 | "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", 1296 | "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" 1297 | }, 1298 | "packet-reader": { 1299 | "version": "1.0.0", 1300 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 1301 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 1302 | }, 1303 | "parseqs": { 1304 | "version": "0.0.5", 1305 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", 1306 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", 1307 | "requires": { 1308 | "better-assert": "~1.0.0" 1309 | } 1310 | }, 1311 | "parseuri": { 1312 | "version": "0.0.5", 1313 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", 1314 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", 1315 | "requires": { 1316 | "better-assert": "~1.0.0" 1317 | } 1318 | }, 1319 | "pg": { 1320 | "version": "8.5.1", 1321 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", 1322 | "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", 1323 | "requires": { 1324 | "buffer-writer": "2.0.0", 1325 | "packet-reader": "1.0.0", 1326 | "pg-connection-string": "^2.4.0", 1327 | "pg-pool": "^3.2.2", 1328 | "pg-protocol": "^1.4.0", 1329 | "pg-types": "^2.1.0", 1330 | "pgpass": "1.x" 1331 | } 1332 | }, 1333 | "pg-connection-string": { 1334 | "version": "2.4.0", 1335 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", 1336 | "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" 1337 | }, 1338 | "pg-int8": { 1339 | "version": "1.0.1", 1340 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 1341 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 1342 | }, 1343 | "pg-pool": { 1344 | "version": "3.2.2", 1345 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", 1346 | "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==", 1347 | "requires": {} 1348 | }, 1349 | "pg-protocol": { 1350 | "version": "1.4.0", 1351 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", 1352 | "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" 1353 | }, 1354 | "pg-types": { 1355 | "version": "2.2.0", 1356 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 1357 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 1358 | "requires": { 1359 | "pg-int8": "1.0.1", 1360 | "postgres-array": "~2.0.0", 1361 | "postgres-bytea": "~1.0.0", 1362 | "postgres-date": "~1.0.4", 1363 | "postgres-interval": "^1.1.0" 1364 | } 1365 | }, 1366 | "pgpass": { 1367 | "version": "1.0.2", 1368 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 1369 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 1370 | "requires": { 1371 | "split": "^1.0.0" 1372 | } 1373 | }, 1374 | "postgres-array": { 1375 | "version": "2.0.0", 1376 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 1377 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 1378 | }, 1379 | "postgres-bytea": { 1380 | "version": "1.0.0", 1381 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 1382 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 1383 | }, 1384 | "postgres-date": { 1385 | "version": "1.0.7", 1386 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 1387 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" 1388 | }, 1389 | "postgres-interval": { 1390 | "version": "1.2.0", 1391 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 1392 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 1393 | "requires": { 1394 | "xtend": "^4.0.0" 1395 | } 1396 | }, 1397 | "process-nextick-args": { 1398 | "version": "2.0.1", 1399 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1400 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 1401 | }, 1402 | "promise": { 1403 | "version": "7.0.4", 1404 | "resolved": "http://registry.npmjs.org/promise/-/promise-7.0.4.tgz", 1405 | "integrity": "sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk=", 1406 | "requires": { 1407 | "asap": "~2.0.3" 1408 | } 1409 | }, 1410 | "readable-stream": { 1411 | "version": "3.6.0", 1412 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 1413 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 1414 | "requires": { 1415 | "inherits": "^2.0.3", 1416 | "string_decoder": "^1.1.1", 1417 | "util-deprecate": "^1.0.1" 1418 | } 1419 | }, 1420 | "safe-buffer": { 1421 | "version": "5.1.2", 1422 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1423 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1424 | }, 1425 | "select-hose": { 1426 | "version": "2.0.0", 1427 | "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", 1428 | "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" 1429 | }, 1430 | "socket.io": { 1431 | "version": "2.3.0", 1432 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", 1433 | "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", 1434 | "requires": { 1435 | "debug": "~4.1.0", 1436 | "engine.io": "~3.4.0", 1437 | "has-binary2": "~1.0.2", 1438 | "socket.io-adapter": "~1.1.0", 1439 | "socket.io-client": "2.3.0", 1440 | "socket.io-parser": "~3.4.0" 1441 | } 1442 | }, 1443 | "socket.io-adapter": { 1444 | "version": "1.1.2", 1445 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", 1446 | "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" 1447 | }, 1448 | "socket.io-client": { 1449 | "version": "2.3.0", 1450 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", 1451 | "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", 1452 | "requires": { 1453 | "backo2": "1.0.2", 1454 | "base64-arraybuffer": "0.1.5", 1455 | "component-bind": "1.0.0", 1456 | "component-emitter": "1.2.1", 1457 | "debug": "~4.1.0", 1458 | "engine.io-client": "~3.4.0", 1459 | "has-binary2": "~1.0.2", 1460 | "has-cors": "1.1.0", 1461 | "indexof": "0.0.1", 1462 | "object-component": "0.0.3", 1463 | "parseqs": "0.0.5", 1464 | "parseuri": "0.0.5", 1465 | "socket.io-parser": "~3.3.0", 1466 | "to-array": "0.1.4" 1467 | }, 1468 | "dependencies": { 1469 | "ms": { 1470 | "version": "2.0.0", 1471 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1472 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1473 | }, 1474 | "socket.io-parser": { 1475 | "version": "3.3.0", 1476 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", 1477 | "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", 1478 | "requires": { 1479 | "component-emitter": "1.2.1", 1480 | "debug": "~3.1.0", 1481 | "isarray": "2.0.1" 1482 | }, 1483 | "dependencies": { 1484 | "debug": { 1485 | "version": "3.1.0", 1486 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 1487 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 1488 | "requires": { 1489 | "ms": "2.0.0" 1490 | } 1491 | } 1492 | } 1493 | } 1494 | } 1495 | }, 1496 | "socket.io-parser": { 1497 | "version": "3.4.0", 1498 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.0.tgz", 1499 | "integrity": "sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==", 1500 | "requires": { 1501 | "component-emitter": "1.2.1", 1502 | "debug": "~4.1.0", 1503 | "isarray": "2.0.1" 1504 | } 1505 | }, 1506 | "spdy": { 1507 | "version": "4.0.2", 1508 | "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", 1509 | "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", 1510 | "requires": { 1511 | "debug": "^4.1.0", 1512 | "handle-thing": "^2.0.0", 1513 | "http-deceiver": "^1.2.7", 1514 | "select-hose": "^2.0.0", 1515 | "spdy-transport": "^3.0.0" 1516 | } 1517 | }, 1518 | "spdy-transport": { 1519 | "version": "3.0.0", 1520 | "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", 1521 | "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", 1522 | "requires": { 1523 | "debug": "^4.1.0", 1524 | "detect-node": "^2.0.4", 1525 | "hpack.js": "^2.1.6", 1526 | "obuf": "^1.1.2", 1527 | "readable-stream": "^3.0.6", 1528 | "wbuf": "^1.7.3" 1529 | } 1530 | }, 1531 | "split": { 1532 | "version": "1.0.1", 1533 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 1534 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 1535 | "requires": { 1536 | "through": "2" 1537 | } 1538 | }, 1539 | "string_decoder": { 1540 | "version": "1.1.1", 1541 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1542 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1543 | "requires": { 1544 | "safe-buffer": "~5.1.0" 1545 | } 1546 | }, 1547 | "through": { 1548 | "version": "2.3.8", 1549 | "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", 1550 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 1551 | }, 1552 | "to-array": { 1553 | "version": "0.1.4", 1554 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 1555 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 1556 | }, 1557 | "util-deprecate": { 1558 | "version": "1.0.2", 1559 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1560 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1561 | }, 1562 | "wbuf": { 1563 | "version": "1.7.3", 1564 | "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", 1565 | "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", 1566 | "requires": { 1567 | "minimalistic-assert": "^1.0.0" 1568 | } 1569 | }, 1570 | "ws": { 1571 | "version": "3.3.1", 1572 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.1.tgz", 1573 | "integrity": "sha512-8A/uRMnQy8KCQsmep1m7Bk+z/+LIkeF7w+TDMLtX1iZm5Hq9HsUDmgFGaW1ACW5Cj0b2Qo7wCvRhYN2ErUVp/A==", 1574 | "requires": { 1575 | "async-limiter": "~1.0.0", 1576 | "safe-buffer": "~5.1.0", 1577 | "ultron": "~1.1.0" 1578 | }, 1579 | "dependencies": { 1580 | "safe-buffer": { 1581 | "version": "5.1.2", 1582 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1583 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1584 | }, 1585 | "ultron": { 1586 | "version": "1.1.1", 1587 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 1588 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 1589 | } 1590 | } 1591 | }, 1592 | "xmlhttprequest-ssl": { 1593 | "version": "1.5.5", 1594 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 1595 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 1596 | }, 1597 | "xtend": { 1598 | "version": "4.0.2", 1599 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1600 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1601 | }, 1602 | "yeast": { 1603 | "version": "0.1.2", 1604 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 1605 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 1606 | } 1607 | } 1608 | } 1609 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "TheFileTree (https://thefiletree.com/)", 3 | "name": "filetree", 4 | "description": "Web-based collaborative file system; a multiplayer notepad!", 5 | "version": "20.0.0", 6 | "homepage": "https://thefiletree.com", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/garden/tree" 10 | }, 11 | "license": "EUPL-1.2", 12 | "scripts": { 13 | "test": "make test" 14 | }, 15 | "dependencies": { 16 | "async": "2.0.1", 17 | "camp": "^17.2.4", 18 | "canop": "0.4.1", 19 | "email-login": "1.3.2", 20 | "fsos": "1.1.3", 21 | "irc": "0.5.2", 22 | "pg": "8.5.1", 23 | "ws": "3.3.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | --------------------------------------------------------------------------------