├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── codeclimate.yml │ ├── codeql-analysis.yml │ ├── commit.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ ├── core.js ├── dist │ └── metadata.json ├── homeDirFolder │ ├── otherfile.xml │ └── test.txt ├── packages.json └── packages │ └── JestTest │ ├── metadata.json │ └── server.js ├── __tests__ ├── adapters │ ├── auth │ │ └── null.js │ ├── settings │ │ ├── fs.js │ │ └── null.js │ └── vfs │ │ └── system.js ├── auth.js ├── core.js ├── filesystem.js ├── package.js ├── packages.js ├── settings.js └── utils │ ├── core.js │ └── vfs.js ├── index.js ├── package-lock.json ├── package.json └── src ├── adapters ├── auth │ └── null.js ├── settings │ ├── fs.js │ └── null.js └── vfs │ └── system.js ├── auth.js ├── config.js ├── core.js ├── filesystem.js ├── package.js ├── packages.js ├── providers ├── auth.js ├── core.js ├── packages.js ├── settings.js └── vfs.js ├── settings.js ├── utils ├── core.js └── vfs.js └── vfs.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | 11 | [test.txt] 12 | insert_final_newline = false 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: andersevenrud 4 | patreon: andersevenrud 5 | open_collective: osjs 6 | liberapay: os-js 7 | custom: https://paypal.me/andersevenrud 8 | -------------------------------------------------------------------------------- /.github/workflows/codeclimate.yml: -------------------------------------------------------------------------------- 1 | name: CodeClimate Coverage 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | coverage: 9 | runs-on: ubuntu-latest 10 | name: CodeClimate Coverage 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '14' 16 | - run: npm ci 17 | - uses: paambaati/codeclimate-action@v2.4.0 18 | env: 19 | CC_TEST_REPORTER_ID: 125467a1ee6a975ccd559c38662bdc1dd9540cd2803f98db94e1cf3596738773 20 | with: 21 | coverageCommand: npm run coverage 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 3 * * 5' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | #- name: Autobuild 57 | # uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | commitlint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | - uses: wagoid/commitlint-github-action@v5 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | build: 9 | name: Lint tests (node latest) 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '14.x' 16 | - run: npm ci 17 | - run: npm run eslint 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | name: Unit tests (node ${{ matrix.node }}) 11 | strategy: 12 | matrix: 13 | node: [ '14', '12' ] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: npm ci 20 | - run: npm run jest 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /session-store.db 2 | /coverage 3 | .swp 4 | .tern-* 5 | node_modules 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | npx --no -- commitlint --edit "$1" 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run eslint 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /doc 2 | .swp 3 | .git 4 | .tern-* 5 | node_modules 6 | package-lock.json 7 | .eslintrc 8 | .gitignore 9 | .esdoc.json 10 | .flowconfig 11 | .travis.yml 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for osjs-server 2 | 3 | ## 3.4.3 - 2024-01-09 4 | 5 | * fix(vfs): exception falls through in mountpoint check 6 | 7 | ## 3.4.2 - 2024-01-04 8 | 9 | * fix(vfs): late stream error 10 | 11 | ## 3.4.1 - 2024-01-03 12 | 13 | * fix(vfs): defer stream creation in vfs requests (#80) 14 | 15 | ## 3.4.0 - 2023-12-29 16 | 17 | * fix(vfs): race condition on temp file cleanup 18 | * docs(vfs): update search function annotation (#70) 19 | * ci: add husky and commit checks 20 | * ci: add commitlint workflow 21 | * ci: update actions 22 | * Add VFS capabilities method (#65) 23 | * Remove defunct jest CI installation 24 | * Update 'npm install' to 'npm ci' in CI 25 | * Remove global npm install from test CI 26 | * Update dependencies 27 | 28 | ## 3.3.3 - 2022-07-31 29 | 30 | * Remove now defunct esdoc deploy 31 | * Update source code license comments 32 | * Update LICENSE 33 | * Support unserialization of url query parameters 34 | 35 | ## 3.3.2 - 2022-07-19 36 | 37 | * Fix shortcuts file overwrite 38 | 39 | ## 3.3.1 - 2022-07-13 40 | 41 | * Home directory template support (#58) 42 | 43 | ## 3.3.0 - 2022-07-13 44 | 45 | * Added .editorconfig (#61) 46 | * Update codeclimate github workflow 47 | * Fix typo in github test workflow 48 | * Update node version requirements 49 | * Use spesific jest version in github actions 50 | * Add extra error check in Core#listen listen callback 51 | * Add promise to Core#listen 52 | 53 | ## 3.2.4 - 2021-08-03 54 | 55 | * Add configuration for http bind host (#55) 56 | 57 | ## 3.2.3 - 2021-07-23 58 | 59 | * Allow default exports from ESM in packages 60 | 61 | ## 3.2.2 - 2021-07-17 62 | 63 | * Added possibility to configure raw body parser 64 | 65 | ## 3.2.1 - 2021-07-15 66 | 67 | * Added express router service bindings 68 | 69 | ## 3.1.20 - 2021-07-13 70 | 71 | * Added 'session' option to Filesystem#call 72 | 73 | ## 3.1.19 - 2021-06-15 74 | 75 | * Added configurable body parser size limit (#52) 76 | 77 | ## 3.1.18 - 2021-02-21 78 | 79 | * Support UTF characters on VFS file downloads (#50) 80 | 81 | ## 3.1.17 - 2021-01-03 82 | 83 | * Send entire session object in login/out signal (#47) 84 | 85 | ## 3.1.16 - 2021-01-01 86 | 87 | * Don't allow client to use internal signals 88 | 89 | ## 3.1.15 - 2021-01-01 90 | 91 | * Added signals for user login/logout (#45) (#46) 92 | 93 | ## 3.1.14 - 2020-11-26 94 | 95 | No changes. Forgot to pull with rebase before publish. 96 | 97 | ## 3.1.13 - 2020-11-26 98 | 99 | * Updated dependencies 100 | 101 | ## 3.1.12 - 2020-08-22 102 | 103 | * Updated esdoc setup 104 | 105 | ## 3.1.11 - 2020-08-20 106 | 107 | * Updated documentation 108 | * Updated esdoc configs 109 | * Updated dependencies 110 | * Added 'websocket' to express service contract 111 | 112 | ## 3.1.10 - 2020-07-28 113 | 114 | * Try to create home directory on login (#37) (#38) 115 | 116 | ## 3.1.9 - 2020-07-23 117 | 118 | * Add appropriate error message on missing vfs adapter methods 119 | * Fixed search in readonly mountpoints (fixes #36) 120 | 121 | ## 3.1.8 - 2020-07-22 122 | 123 | * Minor cleanups 124 | 125 | ## 3.1.7 - 2020-07-22 126 | 127 | * Abstracted away req/res from VFS calls in favor of options (#34) 128 | * Support async adapter functions (#34) 129 | 130 | ## 3.1.6 - 2020-07-17 131 | 132 | * Send content-type mime on readfile if available (#35) 133 | 134 | ## 3.1.5 - 2020-06-27 135 | 136 | * Moved ranged VFS responses down to API (from adapter) 137 | 138 | ## 3.1.4 - 2020-06-24 139 | 140 | * VFS readfile downloads no longer relies on physical paths (fixes #33) 141 | 142 | ## 3.1.3 - 2020-06-11 143 | 144 | * Added some error logging to VFS 145 | * Updated Core#destroy async expressions 146 | 147 | ## 3.1.2 - 2020-04-12 148 | 149 | * Require node 10 or later 150 | * Made Core destroy procedure async 151 | 152 | ## 3.1.1 - 2020-04-11 153 | 154 | * Added websocket client ping (#30) 155 | 156 | ## 3.1.0 - 2020-04-10 157 | 158 | * Added support for https (#26) (#27) 159 | * Added timestamps to CHANGELOG.md 160 | 161 | ## 3.0.55 - 2020-02-16 162 | 163 | * Updated dependencies 164 | 165 | ## 3.0.54 - 2020-02-14 166 | 167 | * Removed process.exit from Core 168 | 169 | ## 3.0.53 - 2020-01-21 170 | 171 | * Updated exports 172 | 173 | ## 3.0.52 - 2020-01-19 174 | 175 | * Updated dependencies 176 | 177 | ## 3.0.51 - 2020-01-19 178 | 179 | * Updated dependencies 180 | * Updated dotfile usage 181 | * Updated copyright notices in preambles 182 | 183 | ## 3.0.50 - 2020-01-15 184 | 185 | * Eslint pass 186 | * Updated dotfiles 187 | * Updated dependencies 188 | 189 | ## 3.0.49 - 2019-11-21 190 | 191 | * Added strict check argument to routeAuthenticated 192 | 193 | ## 3.0.48 - 2019-11-21 194 | 195 | * Add a default group set in realpath (#21) 196 | 197 | ## 3.0.47 - 2019-11-21 198 | 199 | * Added abitlity to make VFS group checking non-strict (#22) (#23) 200 | 201 | ## 3.0.46 - 2019-10-18 202 | 203 | * Fix issue with path resolution in VFS on cross requests (fixes #19) 204 | 205 | ## 3.0.45 - 2019-10-18 206 | 207 | * Correctly detect VFS options on GET (fixes #18) 208 | 209 | ## 3.0.44 - 2019-06-11 210 | 211 | * Support more characters in vfs mountpoint names 212 | 213 | ## 3.0.43 - 2019-06-02 214 | 215 | * Added ranged HTTP response support in system adapter (fixes #15) (#16) 216 | 217 | ## 3.0.42 - 2019-05-24 218 | 219 | * Supress warnings from invalid websocket messages 220 | 221 | ## 3.0.41 - 2019-04-13 222 | 223 | * Updated dependencies 224 | 225 | ## 3.0.40 - 2019-04-13 226 | 227 | * Added Filesystem#call for abstracted calls 228 | 229 | ## 3.0.39 - 2019-04-12 230 | 231 | * Added Auth#register and adapter support 232 | 233 | ## 3.0.38 - 2019-04-09 234 | 235 | * Updated Filesystem#realpath signature 236 | 237 | ## 3.0.37 - 2019-04-08 238 | 239 | * Updated dependencies 240 | 241 | ## 3.0.36 - 2019-03-27 242 | 243 | * Add rolling session updates (fixes #6) 244 | 245 | ## 3.0.35 - 2019-03-26 246 | 247 | * Added 'routeAuthenticated' group behavior option (closes #13) 248 | 249 | ## 3.0.34 - 2019-03-26 250 | 251 | * Added denyUsers and requiredGroups to authenticator 252 | 253 | ## 3.0.33 - 2019-03-26 254 | 255 | * Emit warning when files missing in dist (closes #11) 256 | * Updated consola logging pause in tests 257 | * Added some abstraction to system VFS adapter 258 | * Updated auth.js comment header 259 | * Updated esdoc 260 | 261 | ## 3.0.32 - 2019-03-24 262 | 263 | * Send VFS watch trigger type in broadcast call 264 | * Updated unit tests 265 | * Updated chokidar dependency 266 | * Updated system vfs adapter watcher 267 | * Added 'osjs/fs' service 268 | * Updated watch handling in Filesystem class 269 | * Added missing return in VFS watch for system adapter 270 | * Updated providers 271 | * Updated logging 272 | * Changed from 'signale' to 'consola' logger 273 | * Minor cleanup in Core 274 | * Refactored package loading procedure 275 | 276 | ## 3.0.30 - 2019-03-23 277 | 278 | * Added files section to package.json 279 | 280 | ## 3.0.29 - 2019-03-23 281 | 282 | * Added back killswitch to Core 283 | 284 | ## 3.0.28 - 2019-03-23 285 | 286 | * Updated README 287 | * Split out and cleaned up some core from CoreServiceProvider 288 | * Some cleanups in src/utils/vfs.js 289 | * Some cleanups for Package class integrations 290 | * Minor cleanup in src/providers/core.js 291 | * Minor cleanup in src/vfs.fs 292 | * Fixed typo in package.json 293 | * Added 'test' script to package.json 294 | * Added unit tests 295 | * Updated package.json scripts 296 | * Split up some functions used in Core 297 | * Updated gitignore 298 | * Updated eslintrc 299 | * Updated some checks and returns in Core#boot process 300 | * Added JSON parse check in argv override for Core options 301 | * Fixed spelling error in Core#destroy check 302 | * Added wss property to Core 303 | * Moved some developer stuff from Core to provider 304 | * Minor fixes in Filesystem class 305 | * Run 'httpServer.close()' on Core#destroy 306 | * Updated Settings init 307 | * Make Settings#init return result from adapter 308 | * Minor cleanup in Auth class 309 | * Updated default adapters 310 | * Properly close watches on Core#destroy 311 | * Don't use process.exit in Core 312 | 313 | ## 3.0.27 314 | 315 | * Hotfix for some VFS methods 316 | 317 | ## 3.0.26 - 2019-03-19 318 | 319 | * Added 'osjs/core:ping' event 320 | * Refactored VFS implementation 321 | * Cleaned up some methods in Filesystem class 322 | * Simplified some VFS method abstraction 323 | * Refactored VFS methods interface signatures 324 | * Split up adapters from Settings class 325 | * Split up package loading from Packages class 326 | * Moved some VFS files 327 | * Cleaned up core provider init 328 | * Split out Auth from AuthProvider 329 | 330 | ## 3.0.25 - 2019-02-25 331 | 332 | * Fixed 'fs' Settings adapter (fixes #14) 333 | 334 | ## 3.0.24 - 2019-02-19 335 | 336 | * Added Core.getInstance 337 | 338 | ## 3.0.23 - 2019-02-05 339 | 340 | * Added 'realpath' method to VFS (for internal usage) 341 | 342 | ## 3.0.22 - 2019-02-02 343 | 344 | * Updated routeAuthenticated group gating (#13) 345 | 346 | ## 3.0.21 - 2019-01-26 347 | 348 | * Update websocket message handling 349 | 350 | ## 3.0.20 - 2019-01-26 351 | 352 | * Addded 'call' method to expres service 353 | * Added support for injecting middleware for routes 354 | 355 | ## 3.0.19 - 2019-01-19 356 | 357 | * Updated dependencies 358 | * Update config.js (#10) 359 | * Updated README 360 | 361 | ## 3.0.18 - 2019-01-04 362 | 363 | * Updated internal socket message handling 364 | 365 | ## 3.0.17 - 2019-01-04 366 | 367 | * Fixed issue with non-client socket messaging 368 | 369 | ## 3.0.16 - 2019-01-01 370 | 371 | * Added direct support for core websocket in applications 372 | * Emit even on destruction 373 | 374 | ## 3.0.15 - 2018-12-29 375 | 376 | * Additional ws broadcast methods (#4) 377 | * Force session touch on ping (#6) 378 | 379 | ## 3.0.14 - 2018-12-23 380 | 381 | * Added configurable default auth groups 382 | 383 | ## 3.0.13 - 2018-12-22 384 | 385 | * Handle HEAD requests properly in VFS calls 386 | * Make sure route helpers cast method to lowercase 387 | 388 | ## 3.0.12 - 2018-12-16 389 | 390 | * Updated MIME definitions 391 | 392 | ## 3.0.11 - 2018-12-09 393 | 394 | * Added configurable VFS root directory 395 | 396 | ## 3.0.9 - 2018-12-04 397 | 398 | * Updated filehound dependency (fixes #3) 399 | 400 | ## 3.0.8 - 2018-12-01 401 | 402 | * Make sure 'attributes' is set in a mountpoint 403 | 404 | ## 3.0.7 - 2018-12-01 405 | 406 | * Added a workaround from filehound blowing up 407 | * Added 'searchable' vfs mountpoint attribute 408 | 409 | ## 3.0.6 - 2018-11-25 410 | 411 | * Added 'engines' to package.json 412 | 413 | ## 3.0.5 - 2018-11-25 414 | 415 | * Updated dependencies 416 | * Updated mime support 417 | 418 | ## 3.0.4 - 2018-11-24 419 | 420 | * Added configuration of form/file post size limits 421 | 422 | ## 3.0.3 - 2018-11-19 423 | 424 | * Added configurable 'manifest' file 425 | * Added configurable 'discovery' file usage 426 | * Removed unused Packages#constructor argument 427 | 428 | ## 3.0.2 - 2018-11-10 429 | 430 | * Added support for custom mime resolution in VFS 431 | 432 | ## 3.0.1 - 2018-10-28 433 | 434 | * Updated @osjs/common 435 | 436 | ## 3.0.0-alpha.42 - 2018-10-26 437 | 438 | * Better VFS service exposure 439 | 440 | ## 3.0.0-alpha.41 - 2018-09-29 441 | 442 | * Updated @osjs/common 443 | 444 | ## 3.0.0-alpha.40 - 2018-09-27 445 | 446 | * Updated dependencies 447 | 448 | ## 3.0.0-alpha.39 - 2018-08-14 449 | 450 | * Added 'nocache' package in development mode 451 | * Added 'ensure' to mkdir VFS method options 452 | * Updated some VFS method HTTP methods 453 | * Ensure JSON vfs posts are not going through formidable 454 | * Added 'touch' VFS endpoint 455 | 456 | ## 3.0.0-alpha.38 - 2018-08-11 457 | 458 | * Added updated 'fs' settings adapter 459 | * Add proper VFS exposure in provider 460 | * Add some extra adapter error handling and fallback 461 | 462 | ## 3.0.0-alpha.37 - 2018-08-04 463 | 464 | * Broadcast dist file changes in dev mode 465 | 466 | ## 3.0.0-alpha.36 - 2018-07-25 467 | 468 | * Fixed some syntax errors 469 | * Fixed eslint comment warnings 470 | 471 | ## 3.0.0-alpha.35 - 2018-07-24 472 | 473 | * Split up Settings provider 474 | * Split up Package Provider 475 | * Split up VFS Provider / Filesystem 476 | * Detach some VFS mountpoint properties 477 | * Misc cleanups after VFS changes 478 | * Support for operations between different adapters 479 | * Cleaned up VFS request binding etc. 480 | * Match VFS parameters from client in adapter methods 481 | 482 | ## 3.0.0-alpha.34 - 2018-07-21 483 | 484 | * Fixed package reload (dev mode) 485 | 486 | ## 3.0.0-alpha.33 - 2018-07-21 487 | 488 | * Add extra filtering in package script loading 489 | 490 | ## 3.0.0-alpha.32 - 2018-07-20 491 | 492 | * Fixed removal of directories in system VFS adapter 493 | * VFS search improvements 494 | * Updated eslintrc 495 | 496 | ## 3.0.0-alpha.31 - 2018-07-19 497 | 498 | * Updated @osjs/common dependency 499 | 500 | ## 3.0.0-alpha.30 - 2018-07-18 501 | 502 | * Added VFS search() method 503 | * Updated travis-ci 504 | * Added travis-ci badge to README 505 | * Added initial travis-ci config 506 | * Better package loading on boot 507 | 508 | ## 3.0.0-alpha.29 - 2018-07-16 509 | 510 | * Added 'download' for 'readfile' in system vfs 511 | 512 | ## 3.0.0-alpha.28 - 2018-07-14 513 | 514 | * Allow override certain configurations via argv 515 | 516 | ## 3.0.0-alpha.27 - 2018-07-14 517 | 518 | * Updated @osjs/common dependency 519 | * Updated default configuration 520 | * Use 'connect-loki' instead of 'session-file-store' (#2) 521 | 522 | ## 3.0.0-alpha.26 - 2018-07-10 523 | 524 | * Updated dependencies 525 | * Remove 'extended' usage in body-parser 526 | * Added 'vfs.watch' config option 527 | * Updated logging 528 | * Added vfs change/watch events broadcasting over WS 529 | * Added read-only support for mountpoints 530 | 531 | ## 3.0.0-alpha.25 - 2018-07-06 532 | 533 | * Added 'ping' endpoint + cookie maxAge 534 | * Added missing .eslintrc, cleanup 535 | 536 | ## 3.0.0-alpha.24 - 2018-06-21 537 | 538 | * Added group-based permissions to VFS 539 | * Force-save session on login 540 | 541 | ## 3.0.0-alpha.23 - 2018-06-17 542 | 543 | * Provide 'fs' settings adapter 544 | 545 | ## 3.0.0-alpha.22 - 2018-06-09 546 | 547 | * Added group checking to authenticated routes 548 | * Add 'httpServer' reference in core 549 | 550 | ## 3.0.0-alpha.21 - 2018-05-23 551 | 552 | * Emit starting events (#1) 553 | * Added urlencoded body-parser middleware (#1) 554 | 555 | ## 3.0.0-alpha.20 - 2018-05-22 556 | 557 | * Added proxy support via configuration 558 | 559 | ## 3.0.0-alpha.19 - 2018-05-10 560 | 561 | * Solved an issue with readdir on Windows 562 | 563 | ## 3.0.0-alpha.18 - 2018-05-10 564 | 565 | * Remove 'registerDefault' from Core options 566 | 567 | This requires the distribution to manually register base providers. 568 | See 'index.js' in the base repository. 569 | 570 | ## 3.0.0-alpha.17 - 2018-05-06 571 | 572 | * Added npmignore 573 | * Added CHANGELOG 574 | 575 | ## 3.0.0-alpha.16 - 2018-05-05 576 | 577 | * Broadcast package/meta updates in dev mode 578 | * Solved an issue with session saving 579 | 580 | ## 3.0.0-alpha.15 - 2018-04-29 581 | 582 | * Added session customization, file as default 583 | * Added broadcasting (ws) support 584 | * Cleaned up HTTP VFS API, better error handling 585 | * Updated some vfs handling 586 | * Handle moutpoints properly, cleanups 587 | 588 | ## 3.0.0-alpha.14 - 2018-04-29 589 | 590 | * Updated application initialization 591 | * Provide more user information on login 592 | * Updated http session handling, require user id from login 593 | 594 | ## 3.0.0-alpha.13 - 2018-04-29 595 | 596 | * A more functional approach for Auth + Settings 597 | 598 | ## 3.0.0-alpha.11 - 2018-04-27 599 | 600 | * Updated provider loading 601 | * Renamed server.js -> core.js 602 | * Minor cleanup in VFS provider 603 | * Create 'osjs/vfs' service 604 | * Added basic Settings service provider, cleanups 605 | * Pass on 'config' in Auth constructor 606 | * Correct passing on args to Auth class 607 | * Split default config + CoreBase update 608 | * Now using '@osjs/common' module 609 | * Added symbol to provider logging 610 | * Updated default auth routes 611 | * VFS now uses authenticated middleware 612 | * Added 'osjs/express' provider 613 | * Copy service provider instanciating from client 614 | * Added provider options in registration 615 | * Keep same Auth interface as in client 616 | * Updated auth handler 617 | * Removed a configuration option 618 | * Added 'null' auth handler 619 | * Added options argument in service provider 620 | * Added support for passing on default provider options 621 | 622 | ## 3.0.0-alpha.10 - 2018-04-22 623 | 624 | * Added default 'home' mountpoint 625 | * Added session support to segment parsing in vfs 626 | * Sanitize paths given to VFS 627 | * Update VFS configuration layout 628 | * Added mounting of system directories 629 | * Added config() to core 630 | * Optimize readdir() in vfs 631 | * Temporarily strip prefixes from inbound VFS call paths 632 | 633 | ## 3.0.0-alpha.9 - 2018-04-15 634 | 635 | * Added copy() VFS method 636 | * Use 'fs-extra' instead of builtin 'fs' 637 | * Clean up temporaries on upload in vfs 638 | * Added multipart/upload to VFS req parsing, writefile() method 639 | * Updated VFS methods and integration 640 | 641 | ## 3.0.0-alpha.8 - 2018-04-07 642 | 643 | * Changed app public path to '/apps/' 644 | 645 | ## 3.0.0-alpha.7 - 2018-03-31 646 | 647 | * Added engines dependendy to package.json 648 | * Added esdoc config, updated docs 649 | * Removed DefaultServiceProvider 650 | * Pass on a 'proc' object instead of metadata in applications 651 | * Added helpers to application init call 652 | 653 | ## 3.0.0-alpha.6 - 2018-03-25 654 | 655 | * Provide error code in scandir fail 656 | * Corrected URLs in package.json 657 | 658 | ## 3.0.0-alpha.5 - 2018-03-19 659 | 660 | Initial public release 661 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | OS.js - JavaScript Cloud/Web Desktop Platform 4 | 5 | Copyright (c) Anders Evenrud 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | OS.js Logo 3 |

4 | 5 | [OS.js](https://www.os-js.org/) is an [open-source](https://raw.githubusercontent.com/os-js/OS.js/master/LICENSE) web desktop platform with a window manager, application APIs, GUI toolkit, filesystem abstractions and much more. 6 | 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/b2e4c52db03e57b4ad76/maintainability)](https://codeclimate.com/github/os-js/osjs-server/maintainability) 8 | [![Test Coverage](https://api.codeclimate.com/v1/badges/b2e4c52db03e57b4ad76/test_coverage)](https://codeclimate.com/github/os-js/osjs-server/test_coverage) 9 | 10 | [![Support](https://img.shields.io/badge/patreon-support-orange.svg)](https://www.patreon.com/user?u=2978551&ty=h&u=2978551) 11 | [![Support](https://img.shields.io/badge/opencollective-donate-red.svg)](https://opencollective.com/osjs) 12 | [![Donate](https://img.shields.io/badge/liberapay-donate-yellowgreen.svg)](https://liberapay.com/os-js/) 13 | [![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/andersevenrud) 14 | [![Community](https://img.shields.io/badge/join-community-green.svg)](https://community.os-js.org/) 15 | 16 | # OS.js v3 Server 17 | 18 | This is the main node server module for OS.js. 19 | 20 | Contains all services and interfaces required to run a node server for OS.js 21 | 22 | ## Contribution 23 | 24 | * **Sponsor on [Github](https://github.com/sponsors/andersevenrud)** 25 | * **Become a [Patreon](https://www.patreon.com/user?u=2978551&ty=h&u=2978551)** 26 | * **Support on [Open Collective](https://opencollective.com/osjs)** 27 | * [Contribution Guide](https://github.com/os-js/OS.js/blob/master/CONTRIBUTING.md) 28 | 29 | ## Documentation 30 | 31 | See the [Official Manuals](https://manual.os-js.org/) for articles, tutorials and guides. 32 | 33 | ## Links 34 | 35 | * [Official Chat](https://gitter.im/os-js/OS.js) 36 | * [Community Forums and Announcements](https://community.os-js.org/) 37 | * [Homepage](https://os-js.org/) 38 | * [Twitter](https://twitter.com/osjsorg) ([author](https://twitter.com/andersevenrud)) 39 | * [Facebook](https://www.facebook.com/os.js.org) 40 | * [Docker Hub](https://hub.docker.com/u/osjs/) 41 | -------------------------------------------------------------------------------- /__mocks__/core.js: -------------------------------------------------------------------------------- 1 | const consola = require('consola'); 2 | consola.pauseLogs(); 3 | 4 | const temp = require('temp').track(); 5 | const path = require('path'); 6 | const config = require('../src/config.js'); 7 | 8 | const { 9 | Core, 10 | CoreServiceProvider, 11 | PackageServiceProvider, 12 | VFSServiceProvider, 13 | AuthServiceProvider, 14 | SettingsServiceProvider 15 | } = require('../index.js'); 16 | 17 | module.exports = (options = {}) => { 18 | const tempPath = temp.mkdirSync('osjs-vfs'); 19 | 20 | const osjs = new Core(Object.assign({ 21 | tempPath, 22 | development: false, 23 | port: 0, 24 | root: __dirname, 25 | public: path.resolve(__dirname, 'dist'), 26 | vfs: { 27 | root: tempPath, 28 | watch: true 29 | }, 30 | mime: { 31 | filenames: { 32 | 'defined file': 'test/jest' 33 | } 34 | } 35 | }, config), { 36 | kill: false 37 | }); 38 | 39 | osjs.configuration.vfs.mountpoints[1].attributes.chokidar = { 40 | persistent: false 41 | }; 42 | osjs.configuration.vfs.mountpoints[1].attributes.watch = true; 43 | 44 | osjs.register(CoreServiceProvider, {before: true}); 45 | osjs.register(PackageServiceProvider); 46 | osjs.register(VFSServiceProvider); 47 | osjs.register(AuthServiceProvider); 48 | osjs.register(SettingsServiceProvider); 49 | 50 | return osjs.boot() 51 | .then(() => osjs); 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /__mocks__/dist/metadata.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "type": "application", 3 | "name": "JestTest", 4 | "category": null, 5 | "server": "server.js", 6 | "title": { 7 | "en_EN": "Jest test" 8 | }, 9 | "description": { 10 | "en_EN": "Jest test" 11 | }, 12 | "files": [ 13 | "main.js", 14 | "main.css" 15 | ] 16 | }] 17 | -------------------------------------------------------------------------------- /__mocks__/homeDirFolder/otherfile.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/osjs-server/f77358e6a3bef87c49483ba9260649e2f49ca77a/__mocks__/homeDirFolder/otherfile.xml -------------------------------------------------------------------------------- /__mocks__/homeDirFolder/test.txt: -------------------------------------------------------------------------------- 1 | this is proof that copying a folder works :) -------------------------------------------------------------------------------- /__mocks__/packages.json: -------------------------------------------------------------------------------- 1 | ["__mocks__/packages/JestTest"] 2 | -------------------------------------------------------------------------------- /__mocks__/packages/JestTest/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "name": "JestTest", 4 | "category": null, 5 | "server": "server.js", 6 | "title": { 7 | "en_EN": "Jest test" 8 | }, 9 | "description": { 10 | "en_EN": "Jest test" 11 | }, 12 | "files": [ 13 | "main.js", 14 | "main.css" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /__mocks__/packages/JestTest/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | module.exports = (core, proc) => ({ 32 | init: async () => {}, 33 | start: () => {}, 34 | destroy: () => {}, 35 | onmessage: (ws, respond, args) => respond('Pong'), 36 | test: () => { 37 | throw new Error('Simulated failure'); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/adapters/auth/null.js: -------------------------------------------------------------------------------- 1 | const nullAdapter = require('../../../src/adapters/auth/null.js'); 2 | const adapter = nullAdapter(); 3 | 4 | describe('Auth null adapter', () => { 5 | test('#init', () => { 6 | return expect(adapter.init()) 7 | .resolves 8 | .toBe(true); 9 | }); 10 | 11 | test('#destroy', () => { 12 | return expect(adapter.destroy()) 13 | .resolves 14 | .toBe(true); 15 | }); 16 | 17 | test('#login', () => { 18 | return expect(adapter.login({ 19 | body: { 20 | username: 'jest' 21 | } 22 | })) 23 | .resolves 24 | .toEqual({ 25 | id: 0, 26 | username: 'jest' 27 | }); 28 | }); 29 | 30 | test('#logout', () => { 31 | return expect(adapter.logout()) 32 | .resolves 33 | .toBe(true); 34 | }); 35 | 36 | test('#register', () => { 37 | return expect(adapter.register({ 38 | body: {username: 'jest', password: 'jest'} 39 | })) 40 | .resolves 41 | .toEqual({username: 'jest'}); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/adapters/settings/fs.js: -------------------------------------------------------------------------------- 1 | const osjs = require('osjs') 2 | const fsAdapter = require('../../../src/adapters/settings/fs.js'); 3 | 4 | describe('settings fs adapter', () => { 5 | let core; 6 | let adapter; 7 | 8 | beforeAll(() => osjs().then(c => (core = c))); 9 | afterAll(() => core.destroy()); 10 | 11 | test('should create new instance', () => { 12 | adapter = fsAdapter(core); 13 | }); 14 | 15 | test('#save', () => { 16 | return expect(adapter.save({ 17 | body: {}, 18 | session: { 19 | user: { 20 | username: 'jest' 21 | } 22 | } 23 | })) 24 | .resolves 25 | .toBe(true); 26 | }); 27 | 28 | test('#load', () => { 29 | return expect(adapter.load({ 30 | session: { 31 | user: { 32 | username: 'jest' 33 | } 34 | } 35 | })) 36 | .resolves 37 | .toEqual({}); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/adapters/settings/null.js: -------------------------------------------------------------------------------- 1 | const nullAdapter = require('../../../src/adapters/settings/null.js'); 2 | const adapter = nullAdapter(); 3 | 4 | describe('Settings null adapter', () => { 5 | test('#init', () => { 6 | return expect(adapter.init()) 7 | .resolves 8 | .toBe(true); 9 | }); 10 | 11 | test('#destroy', () => { 12 | return expect(adapter.destroy()) 13 | .resolves 14 | .toBe(true); 15 | }); 16 | 17 | test('#load', () => { 18 | return expect(adapter.load()) 19 | .resolves 20 | .toEqual({}); 21 | }); 22 | 23 | test('#save', () => { 24 | return expect(adapter.save()) 25 | .resolves 26 | .toBe(true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/adapters/vfs/system.js: -------------------------------------------------------------------------------- 1 | const osjs = require('osjs'); 2 | const path = require('path'); 3 | const stream = require('stream'); 4 | const systemAdapter = require('../../../src/adapters/vfs/system.js'); 5 | 6 | describe('VFS System adapter', () => { 7 | let core; 8 | let adapter; 9 | 10 | beforeAll(() => osjs().then(c => { 11 | core = c; 12 | adapter = systemAdapter(core); 13 | })); 14 | 15 | afterAll(() => core.destroy()); 16 | 17 | const vfs = { 18 | mount: { 19 | name: 'home', 20 | root: 'home:/', 21 | attributes: { 22 | root: '{vfs}/{username}' 23 | } 24 | } 25 | }; 26 | 27 | const createOptions = (options = {}) => ({ 28 | ...options, 29 | session: { 30 | user: { 31 | username: 'jest' 32 | } 33 | } 34 | }); 35 | 36 | const request = (name, ...args) => adapter[name](vfs, vfs)(...args); 37 | 38 | test('#capabilities', () => { 39 | return expect(request('capabilities', '', createOptions())) 40 | .resolves 41 | .toMatchObject({ 42 | pagination: false, 43 | sort: false, 44 | }); 45 | }); 46 | 47 | test('#touch', () => { 48 | return expect(request('touch', 'home:/test', createOptions())) 49 | .resolves 50 | .toBe(true); 51 | }); 52 | 53 | test('#stat', () => { 54 | const realPath = path.join(core.configuration.tempPath, 'jest/test'); 55 | 56 | return expect(request('stat', 'home:/test', createOptions())) 57 | .resolves 58 | .toMatchObject({ 59 | filename: 'test', 60 | path: realPath, 61 | size: 0, 62 | isFile: true, 63 | isDirectory: false, 64 | mime: 'application/octet-stream' 65 | }); 66 | }); 67 | 68 | test('#copy', () => { 69 | return expect(request('copy', 'home:/test', 'home:/test-copy', createOptions())) 70 | .resolves 71 | .toBe(true); 72 | }); 73 | 74 | test('#rename', () => { 75 | return expect(request('rename', 'home:/test-copy', 'home:/test-rename', createOptions())) 76 | .resolves 77 | .toBe(true); 78 | }); 79 | 80 | test('#mkdir', () => { 81 | return expect(request('mkdir', 'home:/test-directory', createOptions())) 82 | .resolves 83 | .toBe(true); 84 | }); 85 | 86 | test('#mkdir - existing directory', () => { 87 | return expect(request('mkdir', 'home:/test-directory', createOptions())) 88 | .rejects 89 | .toThrowError(); 90 | }); 91 | 92 | test('#mkdir - ensure', () => { 93 | return expect(request('mkdir', 'home:/test-directory', createOptions({ensure: true}))) 94 | .resolves 95 | .toBe(true); 96 | }); 97 | 98 | test('#readfile', () => { 99 | return expect(request('readfile', 'home:/test', createOptions())) 100 | .resolves 101 | .toBeInstanceOf(stream.Readable); 102 | }); 103 | 104 | test('#writefile', () => { 105 | const s = new stream.Readable(); 106 | s._read = () => {}; 107 | s.push('jest'); 108 | s.push(null); 109 | 110 | return expect(request('writefile', 'home:/test', s, createOptions())) 111 | .resolves 112 | .toBe(true); 113 | }); 114 | 115 | test('#exists - existing file', () => { 116 | return expect(request('exists', 'home:/test-rename', createOptions())) 117 | .resolves 118 | .toBe(true); 119 | }); 120 | 121 | test('#exists - existing directory', () => { 122 | return expect(request('exists', 'home:/test-directory', createOptions())) 123 | .resolves 124 | .toBe(true); 125 | }); 126 | 127 | test('#exists - non existing file', () => { 128 | return expect(request('exists', 'home:/test-copy', createOptions())) 129 | .resolves 130 | .toBe(false); 131 | }); 132 | 133 | test('#search', () => { 134 | return expect(request('search', 'home:/', '*', createOptions())) 135 | .resolves 136 | .toEqual( 137 | expect.arrayContaining([ 138 | expect.objectContaining({ 139 | filename: 'test', 140 | isFile: true 141 | }), 142 | expect.objectContaining({ 143 | filename: 'test-rename', 144 | isFile: true 145 | }) 146 | ]) 147 | ); 148 | }); 149 | 150 | test('#readdir', () => { 151 | return expect(request('readdir', 'home:/', createOptions())) 152 | .resolves 153 | .toEqual( 154 | expect.arrayContaining([ 155 | expect.objectContaining({ 156 | filename: 'test-directory', 157 | isDirectory: true 158 | }), 159 | expect.objectContaining({ 160 | filename: 'test', 161 | isFile: true 162 | }), 163 | expect.objectContaining({ 164 | filename: 'test-rename', 165 | isFile: true 166 | }) 167 | ]) 168 | ); 169 | }); 170 | 171 | test('#unlink', () => { 172 | const files = ['home:/test', 'home:/test-directory', 'home:/test-rename']; 173 | 174 | return Promise.all(files.map(f => { 175 | return expect(request('unlink', f, createOptions())) 176 | .resolves 177 | .toBe(true); 178 | })); 179 | }); 180 | 181 | test('#unlink', () => { 182 | return expect(request('unlink', 'home:/test-directory', createOptions())) 183 | .resolves 184 | .toBe(true); 185 | }); 186 | 187 | test('#realpath', () => { 188 | const realPath = path.join(core.configuration.tempPath, 'jest/test'); 189 | 190 | return expect(request('realpath', 'home:/test', createOptions())) 191 | .resolves 192 | .toBe(realPath); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /__tests__/auth.js: -------------------------------------------------------------------------------- 1 | const osjs = require('osjs'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const Auth = require('../src/auth.js'); 5 | const Filesystem = require('../src/filesystem.js'); 6 | const {Response} = require('jest-express/lib/response'); 7 | const {Request} = require('jest-express/lib/request'); 8 | 9 | describe('Authentication', () => { 10 | let core; 11 | let auth; 12 | let request; 13 | let response; 14 | 15 | const profile = { 16 | id: 0, 17 | username: 'jest', 18 | name: 'jest', 19 | groups: [] 20 | }; 21 | 22 | beforeEach(() => { 23 | request = new Request(); 24 | request.session = { 25 | save: jest.fn(cb => cb()), 26 | destroy: jest.fn(cb => cb()) 27 | }; 28 | 29 | response = new Response(); 30 | }); 31 | 32 | afterEach(() => { 33 | request.resetMocked(); 34 | response.resetMocked(); 35 | }); 36 | 37 | beforeAll(() => 38 | osjs().then(async c => { 39 | core = c; 40 | c.make('osjs/fs'); 41 | filesystem = new Filesystem(core); 42 | await filesystem.init(); 43 | 44 | await filesystem.mount({ 45 | name: 'jest', 46 | attributes: { 47 | root: '/tmp' 48 | } 49 | }); 50 | }) 51 | ); 52 | 53 | afterAll(() => core.destroy()); 54 | 55 | test('#constructor', () => { 56 | auth = new Auth(core); 57 | }); 58 | 59 | test('#constructor - should fall back to null adapter', () => { 60 | auth = new Auth(core, { 61 | adapter: () => { 62 | throw new Error('Simulated failure'); 63 | }, 64 | denyUsers: ['jestdeny'] 65 | }); 66 | 67 | expect(auth.adapter) 68 | .not 69 | .toBe(null); 70 | }); 71 | 72 | test('#init', () => { 73 | return expect(auth.init()) 74 | .resolves 75 | .toBe(true); 76 | }); 77 | 78 | test('#login - fail on error', async () => { 79 | await auth.login(request, response); 80 | 81 | expect(response.status).toBeCalledWith(403); 82 | expect(response.json).toBeCalledWith({ 83 | error: 'Invalid login or permission denied' 84 | }); 85 | }); 86 | 87 | test('#login - success', async () => { 88 | request.setBody({username: 'jest', password: 'jest'}); 89 | 90 | await auth.login(request, response); 91 | 92 | expect(response.status).toBeCalledWith(200); 93 | expect(request.session.user).toEqual(profile); 94 | expect(request.session.save).toBeCalled(); 95 | expect(response.json).toBeCalledWith(profile); 96 | }); 97 | 98 | test('#login - createHomeDirectory string', async () => { 99 | request.setBody({username: 'jest', password: 'jest'}); 100 | 101 | await auth.login(request, response); 102 | request.fields = { 103 | path: 'home:/.desktop/.shortcuts.json' 104 | }; 105 | 106 | const result = await filesystem.request('exists', request); 107 | expect(result).toBe(true); 108 | }); 109 | 110 | test('#login - createHomeDirectory array', async () => { 111 | request.setBody({username: 'jest', password: 'jest'}); 112 | 113 | const dirpath = path.resolve( 114 | core.configuration.root, 115 | 'homeDirFolder' 116 | ); 117 | core.configuration.vfs.home.template = dirpath; 118 | 119 | await auth.login(request, response); 120 | 121 | request.fields = { 122 | path: 'home:/otherfile.xml' 123 | }; 124 | const fileExists = await filesystem.request('exists', request); 125 | expect(fileExists).toBe(true); 126 | 127 | request.fields = { 128 | path: 'home:/test.txt' 129 | }; 130 | 131 | let chunks = []; 132 | const fileStream = await filesystem.request('readfile', request, response); 133 | for await (let chunk of fileStream) { 134 | chunks.push(chunk); 135 | } 136 | 137 | const fileContents = Buffer.concat(chunks).toString(); 138 | expect(fileContents).toBe('this is proof that copying a folder works :)'); 139 | }); 140 | 141 | test('#login - fail on denied user', async () => { 142 | request.setBody({username: 'jestdeny', password: 'jest'}); 143 | 144 | await auth.login(request, response); 145 | 146 | expect(response.status).toBeCalledWith(403); 147 | expect(response.json).toBeCalledWith({ 148 | error: 'Invalid login or permission denied' 149 | }); 150 | }); 151 | 152 | test('#login - fail on missing groups', async () => { 153 | auth.options.requiredGroups = ['required']; 154 | 155 | request.setBody({username: 'jest', password: 'jest'}); 156 | 157 | await auth.login(request, response); 158 | 159 | expect(response.status).toBeCalledWith(403); 160 | expect(response.json).toBeCalledWith({ 161 | error: 'Invalid login or permission denied' 162 | }); 163 | }); 164 | 165 | test('#logout', async () => { 166 | await auth.logout(request, response); 167 | 168 | expect(request.session.destroy).toBeCalled(); 169 | expect(response.json).toBeCalledWith({}); 170 | }); 171 | 172 | test('#register', async () => { 173 | request.setBody({username: 'jest', password: 'jest'}); 174 | 175 | await auth.register(request, response); 176 | 177 | expect(response.json).toBeCalledWith({username: 'jest'}); 178 | }); 179 | 180 | test('#destroy', async () => { 181 | await auth.destroy(); 182 | auth = undefined; 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /__tests__/core.js: -------------------------------------------------------------------------------- 1 | const consola = require('consola'); 2 | consola.pauseLogs(); 3 | 4 | const Core = require('../src/core.js'); 5 | 6 | describe('Core', () => { 7 | let core; 8 | const testEvent = JSON.stringify({ 9 | params: [1, 2, 3], 10 | name: 'test/jest' 11 | }); 12 | 13 | beforeEach(() => { 14 | if (!core) { 15 | return; 16 | } 17 | 18 | const clients = [{ 19 | terminate: () => {}, 20 | send: jest.fn(), 21 | _osjs_client: { 22 | username: 'jest' 23 | } 24 | }, { 25 | terminate: () => {}, 26 | send: jest.fn(), 27 | _osjs_client: { 28 | username: 'julenissen' 29 | } 30 | }, { 31 | terminate: () => {}, 32 | send: jest.fn() 33 | }]; 34 | 35 | core.wss.clients = clients; 36 | }); 37 | 38 | test('#constructor', () => { 39 | core = new Core({ 40 | public: '/tmp', 41 | development: false, 42 | port: 0 43 | }, { 44 | kill: false, 45 | argv: ['node', 'jest', '--secret', 'kittens'] 46 | }); 47 | }); 48 | 49 | test('#constructor - requires options', () => { 50 | expect(() => new Core()) 51 | .toThrowError('The public option is required'); 52 | }); 53 | 54 | test('.getInstance', () => { 55 | expect(Core.getInstance()) 56 | .toBeInstanceOf(Core); 57 | }); 58 | 59 | test('#boot', async () => { 60 | const cb = jest.fn(); 61 | core.on('init', cb); 62 | 63 | await core.boot(); 64 | await core.boot(); 65 | 66 | expect(cb).toHaveBeenCalledTimes(1); 67 | }); 68 | 69 | test('#broadcast', () => { 70 | core.broadcast('test/jest', [1, 2, 3]); 71 | 72 | expect(core.wss.clients[0].send).toBeCalledWith(testEvent); 73 | expect(core.wss.clients[1].send).toBeCalledWith(testEvent); 74 | expect(core.wss.clients[2].send).not.toBeCalled(); 75 | }); 76 | 77 | test('#broadcastAll', () => { 78 | core.broadcastAll('test/jest', 1, 2, 3); 79 | 80 | expect(core.wss.clients[0].send).toBeCalledWith(testEvent); 81 | expect(core.wss.clients[1].send).toBeCalledWith(testEvent); 82 | expect(core.wss.clients[2].send).not.toBeCalled(); 83 | }); 84 | 85 | test('#broadcastUser', () => { 86 | core.broadcastUser('jest', 'test/jest', 1, 2, 3); 87 | 88 | expect(core.wss.clients[0].send).toBeCalledWith(testEvent); 89 | expect(core.wss.clients[1].send).not.toBeCalled(); 90 | expect(core.wss.clients[2].send).not.toBeCalled(); 91 | }); 92 | 93 | test('#destroy', async () => { 94 | const cb = jest.fn(); 95 | core.on('osjs/core:destroy', cb); 96 | await core.destroy(); 97 | await core.destroy(); 98 | expect(cb).toHaveBeenCalledTimes(1); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /__tests__/filesystem.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const osjs = require('osjs'); 3 | const path = require('path'); 4 | const Filesystem = require('../src/filesystem.js'); 5 | const {Response} = require('jest-express/lib/response'); 6 | const {Request} = require('jest-express/lib/request'); 7 | 8 | describe('Filesystem', () => { 9 | let core; 10 | let filesystem; 11 | let mountpoint; 12 | 13 | beforeAll(() => osjs().then(c => { 14 | core = c; 15 | filesystem = c.make('osjs/fs'); 16 | })); 17 | 18 | afterAll(() => core.destroy()); 19 | 20 | /* Already inited from provider 21 | test('#constructor', () => { 22 | filesystem = new Filesystem(core); 23 | }); 24 | 25 | test('#init', () => { 26 | return expect(filesystem.init()) 27 | .resolves 28 | .toBe(true); 29 | }); 30 | */ 31 | 32 | test('#mime', () => { 33 | expect(filesystem.mime('text file.txt')) 34 | .toBe('text/plain'); 35 | 36 | expect(filesystem.mime('hypertext file.html')) 37 | .toBe('text/html'); 38 | 39 | expect(filesystem.mime('image file.png')) 40 | .toBe('image/png'); 41 | 42 | expect(filesystem.mime('unknown file.666')) 43 | .toBe('application/octet-stream'); 44 | 45 | expect(filesystem.mime('defined file')) 46 | .toBe('test/jest'); 47 | }); 48 | 49 | test('#mount', async () => { 50 | mountpoint = await filesystem.mount({ 51 | name: 'jest', 52 | attributes: { 53 | root: '/tmp' 54 | } 55 | }); 56 | 57 | expect(mountpoint).toMatchObject({ 58 | root: 'jest:/', 59 | attributes: { 60 | root: '/tmp' 61 | } 62 | }); 63 | }); 64 | 65 | test('#realpath', () => { 66 | const realPath = path.join(core.configuration.tempPath, 'jest/test'); 67 | 68 | return expect(filesystem.realpath('home:/test', { 69 | username: 'jest' 70 | })) 71 | .resolves 72 | .toBe(realPath); 73 | }); 74 | 75 | test('#call', async () => { 76 | const result = await filesystem.call({ 77 | method: 'exists', 78 | user: {username: 'jest'} 79 | }, 'home:/test'); 80 | 81 | expect(result).toBe(false); 82 | }); 83 | 84 | test('#request', async () => { 85 | const request = new Request(); 86 | 87 | request.session = { 88 | user: { 89 | username: 'jest' 90 | } 91 | }; 92 | 93 | request.fields = { 94 | path: 'home:/test' 95 | }; 96 | 97 | const result = await filesystem.request('exists', request); 98 | 99 | expect(result).toBe(false); 100 | }); 101 | 102 | test('#unmount', () => { 103 | return expect(filesystem.unmount(mountpoint)) 104 | .resolves 105 | .toBe(true); 106 | }); 107 | 108 | test('#unmount - test fail', () => { 109 | return expect(filesystem.unmount({})) 110 | .resolves 111 | .toBe(false); 112 | }); 113 | 114 | test('#watch - test emitter', async () => { 115 | if (!core.config('vfs.watch')) { 116 | return; 117 | } 118 | 119 | const filename = path.join(core.config('tempPath'), 'jest/watch.txt'); 120 | const cb = jest.fn(); 121 | 122 | core.on('osjs/vfs:watch:change', cb); 123 | fs.ensureDirSync(path.dirname(filename)); 124 | fs.writeFileSync(filename, 'testing'); 125 | 126 | await new Promise(resolve => { 127 | setTimeout(resolve, 100); 128 | }); 129 | 130 | expect(cb).toBeCalledWith(expect.objectContaining({ 131 | type: 'add', 132 | target: 'home:/watch.txt', 133 | mountpoint: filesystem.mountpoints.find(m => m.name === 'home') 134 | })); 135 | }); 136 | 137 | test('#destroy', async () => { 138 | await filesystem.destroy(); 139 | filesystem = undefined; 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /__tests__/package.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const osjs = require('osjs'); 32 | const path = require('path'); 33 | const Package = require('../src/package.js'); 34 | 35 | describe('Package', () => { 36 | let core; 37 | let pkg; 38 | 39 | beforeAll(() => osjs().then(c => (core = c))); 40 | afterAll(() => core.destroy()); 41 | 42 | test('#constructor', () => { 43 | const filename = path.resolve(core.configuration.root, 'packages/JestTest/metadata.json'); 44 | const metadata = require(filename); 45 | 46 | pkg = new Package(core, { 47 | filename, 48 | metadata 49 | }); 50 | }); 51 | 52 | test('#init', () => { 53 | return expect(pkg.init()) 54 | .resolves 55 | .toBe(undefined); 56 | }); 57 | 58 | test('#validate', () => { 59 | const manifest = require( 60 | path.resolve(core.configuration.public, 'metadata.json') 61 | ); 62 | 63 | expect(pkg.validate(manifest)) 64 | .toBe(true); 65 | 66 | expect(pkg.validate([])) 67 | .toBe(false); 68 | }); 69 | 70 | test('#start', () => { 71 | expect(pkg.start()) 72 | .toBe(true); 73 | }); 74 | 75 | test('#action', () => { 76 | expect(pkg.action('init')) 77 | .toBe(true); 78 | 79 | expect(pkg.action('invalid')) 80 | .toBe(false); 81 | 82 | expect(pkg.action('test')) 83 | .toBe(false); 84 | }); 85 | 86 | test('#resource', () => { 87 | expect(pkg.resource('test')) 88 | .toBe('/apps/JestTest/test'); 89 | 90 | expect(pkg.resource('/test')) 91 | .toBe('/apps/JestTest/test'); 92 | }); 93 | 94 | test('#watch', () => { 95 | expect(pkg.watch(jest.fn())) 96 | .toBe(path.resolve(core.configuration.public, 'apps/JestTest')); 97 | }); 98 | 99 | test('#destroy', async () => { 100 | await pkg.destroy(); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /__tests__/packages.js: -------------------------------------------------------------------------------- 1 | const osjs = require('osjs'); 2 | const path = require('path'); 3 | const Packages = require('../src/packages.js'); 4 | 5 | describe('Packages', () => { 6 | let core; 7 | let packages; 8 | 9 | beforeAll(() => osjs().then(c => (core = c))); 10 | afterAll(() => core.destroy()); 11 | 12 | test('#constructor', () => { 13 | const discoveredFile = path.resolve(core.configuration.root, 'packages.json'); 14 | const manifestFile = path.resolve(core.configuration.public, 'metadata.json'); 15 | 16 | packages = new Packages(core, { 17 | discoveredFile, 18 | manifestFile 19 | }); 20 | }); 21 | 22 | test('#init', () => { 23 | return expect(packages.init()) 24 | .resolves 25 | .toBe(true); 26 | }); 27 | 28 | test('#handleMessage', () => { 29 | const params = [{ 30 | pid: 1, 31 | name: 'JestTest', 32 | args: {} 33 | }]; 34 | 35 | const ws = { 36 | send: jest.fn() 37 | }; 38 | 39 | packages.handleMessage(ws, params); 40 | 41 | expect(ws.send).toBeCalledWith(JSON.stringify({ 42 | name: 'osjs/application:socket:message', 43 | params: [{ 44 | pid: 1, 45 | args: ['Pong'] 46 | }] 47 | })); 48 | }); 49 | 50 | test('#destroy', async () => { 51 | await packages.destroy(); 52 | packages = undefined; 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/settings.js: -------------------------------------------------------------------------------- 1 | const osjs = require('osjs'); 2 | const Settings = require('../src/settings.js'); 3 | const {Response} = require('jest-express/lib/response'); 4 | const {Request} = require('jest-express/lib/request'); 5 | 6 | describe('Settings', () => { 7 | let core; 8 | let settings; 9 | let request; 10 | let response; 11 | 12 | beforeEach(() => { 13 | request = new Request(); 14 | response = new Response(); 15 | }); 16 | 17 | afterEach(() => { 18 | request.resetMocked(); 19 | response.resetMocked(); 20 | }); 21 | 22 | beforeAll(() => osjs().then(c => (core = c))); 23 | afterAll(() => core.destroy()); 24 | 25 | test('#constructor', () => { 26 | settings = new Settings(core); 27 | }); 28 | 29 | test('#constructor - should fall back to null adapter', () => { 30 | settings = new Settings(core, { 31 | adapter: () => { 32 | throw new Error('Simulated failure'); 33 | } 34 | }); 35 | 36 | expect(settings.adapter) 37 | .not 38 | .toBe(null); 39 | }); 40 | 41 | test('#init', () => { 42 | return expect(settings.init()) 43 | .resolves 44 | .toBe(true); 45 | }); 46 | 47 | test('#save', async () => { 48 | await settings.save(request, response); 49 | 50 | expect(response.json).toBeCalledWith(true); 51 | }); 52 | 53 | test('#load', async () => { 54 | await settings.load(request, response); 55 | 56 | expect(response.json).toBeCalledWith({}); 57 | }); 58 | 59 | test('#destroy', async () => { 60 | await settings.destroy(); 61 | settings = undefined; 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__tests__/utils/core.js: -------------------------------------------------------------------------------- 1 | const utils = require('../../src/utils/core.js'); 2 | const {Response} = require('jest-express/lib/response'); 3 | const {Request} = require('jest-express/lib/request'); 4 | 5 | describe('Core Utils', () => { 6 | let request; 7 | let response; 8 | let next; 9 | 10 | beforeEach(() => { 11 | request = new Request(); 12 | response = new Response(); 13 | next = jest.fn(); 14 | }); 15 | 16 | afterEach(() => { 17 | request.resetMocked(); 18 | response.resetMocked(); 19 | }); 20 | 21 | test('isAuthenticated - success on no groups', () => { 22 | request.session = {user: {groups: []}}; 23 | utils.isAuthenticated([], false)(request, response, next); 24 | expect(next).toBeCalled(); 25 | }); 26 | 27 | test('isAuthenticated - fail on some required group', () => { 28 | request.session = {user: {groups: ['other']}}; 29 | utils.isAuthenticated(['required'], false)(request, response, next); 30 | expect(response.status).toBeCalledWith(403); 31 | }); 32 | 33 | test('isAuthenticated - success on some required group', () => { 34 | request.session = {user: {groups: ['required', 'other']}}; 35 | utils.isAuthenticated(['required'], false)(request, response, next); 36 | expect(next).toBeCalled(); 37 | }); 38 | 39 | test('isAuthenticated - fail on all required group', () => { 40 | request.session = {user: {groups: ['required1']}}; 41 | utils.isAuthenticated(['required1', 'required2'], true)(request, response, next); 42 | expect(response.status).toBeCalledWith(403); 43 | }); 44 | 45 | test('isAuthenticated - success on all required group', () => { 46 | request.session = {user: {groups: ['required1', 'required2']}}; 47 | utils.isAuthenticated(['required1', 'required2'], true)(request, response, next); 48 | expect(next).toBeCalled(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/utils/vfs.js: -------------------------------------------------------------------------------- 1 | const {Readable, Stream} = require('stream'); 2 | const temp = require('temp'); 3 | const utils = require('../../src/utils/vfs.js'); 4 | 5 | const checkMountpointGroupPermission = ( 6 | userGroups = [], 7 | mountpointGroups = [], 8 | strict 9 | ) => { 10 | const check = utils.checkMountpointPermission({ 11 | session: { 12 | user: { 13 | groups: userGroups 14 | } 15 | } 16 | }, {}, 'readdir', false, strict); 17 | 18 | const mount = { 19 | name: 'osjs', 20 | root: 'osjs:/', 21 | attributes: { 22 | readOnly: true, 23 | groups: mountpointGroups 24 | } 25 | }; 26 | 27 | return check({mount}); 28 | }; 29 | 30 | describe('VFS Utils', () => { 31 | afterAll(() => { 32 | temp.cleanupSync(); 33 | }); 34 | 35 | test('getPrefix', () => { 36 | expect(utils.getPrefix('home:/')) 37 | .toBe('home'); 38 | 39 | expect(utils.getPrefix('home-dir:/')) 40 | .toBe('home-dir'); 41 | 42 | expect(utils.getPrefix('home-dir::/')) 43 | .toBe('home-dir'); 44 | }); 45 | 46 | test('sanitize', () => { 47 | expect(utils.sanitize('home:/(/)¤HF)¤"NF)(FN)(Fn98....)"')) 48 | .toBe('home:/(/)¤HF)¤NF)(FN)(Fn98....)'); 49 | 50 | expect(utils.sanitize('home-dir:/fooo')) 51 | .toBe('home-dir:/fooo'); 52 | }); 53 | 54 | test('streamFromRequest', () => { 55 | const stream = new Stream(); 56 | expect(utils.streamFromRequest({ 57 | files: { 58 | upload: stream 59 | } 60 | })).toBe(stream); 61 | 62 | expect(utils.streamFromRequest({ 63 | files: { 64 | upload: temp.openSync('osjs-jest-file-upload') 65 | } 66 | })).toBeInstanceOf(Readable); 67 | }); 68 | 69 | test('validateGroups - flat groups', () => { 70 | expect(utils.validateGroups([ 71 | 'successful', 72 | 'failure' 73 | ], '', { 74 | attributes: { 75 | groups: [ 76 | 'successful' 77 | ] 78 | } 79 | })).toBe(true); 80 | 81 | expect(utils.validateGroups([ 82 | 'failure' 83 | ], '', { 84 | attributes: { 85 | groups: [ 86 | 'successful' 87 | ] 88 | } 89 | })).toBe(false); 90 | 91 | expect(utils.validateGroups([ 92 | 'successful' 93 | ], '', { 94 | attributes: { 95 | groups: [ 96 | 'successful', 97 | 'successful2' 98 | ] 99 | } 100 | }, false)).toBe(true); 101 | }); 102 | 103 | test('validateGroups - method maps', () => { 104 | const mount = { 105 | attributes: { 106 | groups: [{ 107 | readdir: ['successful'] 108 | }] 109 | } 110 | }; 111 | 112 | expect(utils.validateGroups([ 113 | 'successful' 114 | ], 'readdir', mount)).toBe(true); 115 | 116 | expect(utils.validateGroups([ 117 | 'failure' 118 | ], 'readdir', mount)).toBe(false); 119 | }); 120 | 121 | test('checkMountpointPermission - readOnly', () => { 122 | const check = utils.checkMountpointPermission({ 123 | session: { 124 | user: { 125 | groups: [] 126 | } 127 | } 128 | }, {}, 'writefile', true); 129 | 130 | const mount = { 131 | name: 'osjs', 132 | root: 'osjs:/', 133 | attributes: { 134 | readOnly: true 135 | } 136 | }; 137 | 138 | return expect(check({mount})) 139 | .rejects 140 | .toThrowError('Mountpoint \'osjs\' is read-only'); 141 | }); 142 | 143 | test('checkMountpointPermission - groups', async () => { 144 | await expect(checkMountpointGroupPermission( 145 | [], 146 | ['required'] 147 | )) 148 | .rejects 149 | .toThrowError('Permission was denied for \'readdir\' in \'osjs\''); 150 | 151 | await expect(checkMountpointGroupPermission( 152 | ['missing'], 153 | ['required'] 154 | )) 155 | .rejects 156 | .toThrowError('Permission was denied for \'readdir\' in \'osjs\''); 157 | 158 | await expect(checkMountpointGroupPermission( 159 | ['required'], 160 | ['required', 'some-other'] 161 | )) 162 | .rejects 163 | .toThrowError('Permission was denied for \'readdir\' in \'osjs\''); 164 | 165 | await expect(checkMountpointGroupPermission( 166 | ['required'], 167 | ['required'] 168 | )).resolves.toBe(true); 169 | 170 | await expect(checkMountpointGroupPermission( 171 | ['required', 'some-other'], 172 | ['required'] 173 | )).resolves.toBe(true); 174 | 175 | await expect(checkMountpointGroupPermission( 176 | ['required'], 177 | ['required', 'some-other'], 178 | false 179 | )).resolves.toBe(true); 180 | }); 181 | 182 | test('checkMountpointPermission', () => { 183 | const check = utils.checkMountpointPermission({ 184 | session: { 185 | user: { 186 | groups: [] 187 | } 188 | } 189 | }, {}, 'writefile', false); 190 | 191 | const mount = { 192 | name: 'osjs', 193 | root: 'osjs:/', 194 | attributes: {} 195 | }; 196 | 197 | return expect(check({mount})) 198 | .resolves 199 | .toBe(true); 200 | }); 201 | 202 | test('parseFields - GET', () => { 203 | const parser = utils.parseFields(); 204 | 205 | return expect(parser({ 206 | url: '/foo/?bar=baz&jazz=bass', 207 | method: 'get' 208 | })) 209 | .resolves 210 | .toEqual({ 211 | files: {}, 212 | fields: { 213 | bar: 'baz', 214 | jazz: 'bass' 215 | } 216 | }); 217 | }); 218 | 219 | test('parseFields - POST w/JSON', () => { 220 | const parser = utils.parseFields(); 221 | 222 | return expect(parser({ 223 | url: '/foo/?bar=baz&jazz=bass', 224 | method: 'post', 225 | body: { 226 | bar: 'baz', 227 | jazz: 'bass' 228 | }, 229 | headers: { 230 | 'content-type': 'application/json' 231 | } 232 | })) 233 | .resolves 234 | .toEqual({ 235 | files: {}, 236 | fields: { 237 | bar: 'baz', 238 | jazz: 'bass' 239 | } 240 | }); 241 | }); 242 | 243 | test('parseFields - POST w/Form', () => { 244 | // TODO 245 | }); 246 | 247 | test('assembleQueryData', () => { 248 | const result1 = utils.assembleQueryData({ 249 | 'a': 'b', 250 | 'b': '{"a":"foo"}', 251 | 'c': '{"a":false,"c":null,"d":1,"e":{"a":"foo"}}' 252 | }); 253 | 254 | expect(result1).toEqual({ 255 | a: 'b', 256 | b: { 257 | a: 'foo' 258 | }, 259 | c: { 260 | a: false, 261 | c: null, 262 | d: 1, 263 | e: { 264 | a: 'foo' 265 | } 266 | } 267 | }); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const Core = require('./src/core.js'); 32 | const Auth = require('./src/auth.js'); 33 | const Filesystem = require('./src/filesystem.js'); 34 | const Settings = require('./src/settings.js'); 35 | const Packages = require('./src/packages.js'); 36 | const CoreServiceProvider = require('./src/providers/core'); 37 | const PackageServiceProvider = require('./src/providers/packages'); 38 | const VFSServiceProvider = require('./src/providers/vfs'); 39 | const AuthServiceProvider = require('./src/providers/auth'); 40 | const SettingsServiceProvider = require('./src/providers/settings'); 41 | 42 | module.exports = { 43 | Core, 44 | Auth, 45 | Filesystem, 46 | Settings, 47 | Packages, 48 | CoreServiceProvider, 49 | PackageServiceProvider, 50 | VFSServiceProvider, 51 | AuthServiceProvider, 52 | SettingsServiceProvider 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@osjs/server", 3 | "version": "3.4.3", 4 | "description": "OS.js Server", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run eslint && npm run jest", 8 | "jest": "jest", 9 | "coverage": "jest --coverage", 10 | "eslint": "eslint index.js src", 11 | "prepublishOnly": "npm run test", 12 | "prepare": "husky install" 13 | }, 14 | "files": [ 15 | "src/", 16 | "index.js", 17 | "README.md", 18 | "CHANGELOG.md", 19 | "LICENSE" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/os-js/osjs-server.git" 24 | }, 25 | "keywords": [ 26 | "osjs" 27 | ], 28 | "author": "Anders Evenrud ", 29 | "license": "BSD-2-Clause", 30 | "bugs": { 31 | "url": "https://github.com/os-js/osjs-server/issues" 32 | }, 33 | "engines": { 34 | "node": ">=12.0.0" 35 | }, 36 | "homepage": "https://github.com/os-js/osjs-server#readme", 37 | "dependencies": { 38 | "@osjs/common": "^3.0.12", 39 | "body-parser": "^1.19.0", 40 | "chokidar": "^3.4.3", 41 | "connect-loki": "^1.1.0", 42 | "consola": "^2.15.0", 43 | "deepmerge": "^4.2.2", 44 | "express": "^4.17.1", 45 | "express-http-proxy": "^1.6.2", 46 | "express-session": "^1.17.1", 47 | "express-ws": "^4.0.0", 48 | "fast-glob": "^2.2.7", 49 | "filehound": "^1.17.4", 50 | "formidable": "^1.2.2", 51 | "fs-extra": "^9.0.1", 52 | "mime": "^2.4.6", 53 | "minimist": "^1.2.5", 54 | "morgan": "^1.10.0", 55 | "nocache": "^2.1.0", 56 | "sanitize-filename": "^1.6.3", 57 | "uuid": "^8.3.1" 58 | }, 59 | "devDependencies": { 60 | "@commitlint/cli": "^17.1.2", 61 | "@commitlint/config-conventional": "^17.1.0", 62 | "@osjs/dev-meta": "^2.2.0", 63 | "jest-express": "^1.12.0", 64 | "temp": "^0.9.4", 65 | "husky": "^8.0.0" 66 | }, 67 | "eslintConfig": { 68 | "env": { 69 | "browser": true, 70 | "node": true 71 | }, 72 | "parserOptions": { 73 | "sourceType": "module" 74 | }, 75 | "extends": "@osjs/eslint-config" 76 | }, 77 | "esdoc": { 78 | "source": "./src", 79 | "destination": "./doc", 80 | "plugins": [ 81 | { 82 | "name": "esdoc-standard-plugin", 83 | "option": { 84 | "brand": { 85 | "title": "OS.js Server API", 86 | "description": "OS.js Server API Documentation", 87 | "repository": "https://github.com/os-js/osjs-core", 88 | "author": "Anders Evenrud " 89 | }, 90 | "lint": { 91 | "enable": false 92 | }, 93 | "coverage": { 94 | "enable": false 95 | }, 96 | "undocumentIdentifier": { 97 | "enable": false 98 | } 99 | } 100 | }, 101 | { 102 | "name": "esdoc-publish-html-plugin" 103 | }, 104 | { 105 | "name": "esdoc-ecmascript-proposal-plugin", 106 | "option": { 107 | "all": true 108 | } 109 | }, 110 | { 111 | "name": "esdoc-node" 112 | } 113 | ] 114 | }, 115 | "jest": { 116 | "collectCoverage": true, 117 | "coverageReporters": [ 118 | "lcov" 119 | ], 120 | "moduleNameMapper": { 121 | "^osjs$": "/__mocks__/core.js" 122 | }, 123 | "coveragePathIgnorePatterns": [ 124 | "src/esdoc.js", 125 | "src/config.js", 126 | "src/providers", 127 | "/node_modules/" 128 | ] 129 | }, 130 | "commitlint": { 131 | "extends": [ 132 | "@commitlint/config-conventional" 133 | ] 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/adapters/auth/null.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | /** 32 | * Null Auth adapter 33 | * @param {Core} core Core reference 34 | * @param {object} [options] Adapter options 35 | */ 36 | module.exports = (core, options) => ({ 37 | init: async () => true, 38 | destroy: async () => true, 39 | register: async (req, res) => ({username: req.body.username}), 40 | login: async (req, res) => ({id: 0, username: req.body.username}), 41 | logout: async (req, res) => true 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /src/adapters/settings/fs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | const fs = require('fs-extra'); 31 | const path = require('path'); 32 | 33 | /** 34 | * FS Settings adapter 35 | * @param {Core} core Core reference 36 | * @param {object} [options] Adapter options 37 | */ 38 | module.exports = (core, options) => { 39 | const fsOptions = { 40 | system: false, 41 | path: 'home:/.osjs/settings.json', 42 | ...options || {} 43 | }; 44 | 45 | const getRealFilename = (req) => fsOptions.system 46 | ? Promise.resolve(fsOptions.path) 47 | : core.make('osjs/vfs') 48 | .realpath(fsOptions.path, req.session.user); 49 | 50 | const before = req => getRealFilename(req) 51 | .then(filename => fs.ensureDir(path.dirname(filename)) 52 | .then(() => filename)); 53 | 54 | const save = req => before(req) 55 | .then(filename => fs.writeJson(filename, req.body)) 56 | .then(() => true); 57 | 58 | const load = req => before(req) 59 | .then(filename => fs.readJson(filename)) 60 | .catch(error => { 61 | core.logger.warn(error); 62 | return {}; 63 | }); 64 | 65 | return {save, load}; 66 | }; 67 | 68 | -------------------------------------------------------------------------------- /src/adapters/settings/null.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | /** 32 | * Null Settings adapter 33 | * @param {Core} core Core reference 34 | * @param {object} [options] Adapter options 35 | */ 36 | module.exports = (core, options) => ({ 37 | init: async () => true, 38 | destroy: async () => true, 39 | save: async () => true, 40 | load: async () => ({}) 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /src/adapters/vfs/system.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const path = require('path'); 33 | const fh = require('filehound'); 34 | const chokidar = require('chokidar'); 35 | 36 | /* 37 | * Creates an object readable by client 38 | */ 39 | const createFileIter = (core, realRoot, file) => { 40 | const filename = path.basename(file); 41 | const realPath = path.join(realRoot, filename); 42 | const {mime} = core.make('osjs/vfs'); 43 | 44 | const createStat = stat => ({ 45 | isDirectory: stat.isDirectory(), 46 | isFile: stat.isFile(), 47 | mime: stat.isFile() ? mime(realPath) : null, 48 | size: stat.size, 49 | path: file, 50 | filename, 51 | stat 52 | }); 53 | 54 | return fs.stat(realPath) 55 | .then(createStat) 56 | .catch(error => { 57 | core.logger.warn(error); 58 | 59 | return createStat({ 60 | isDirectory: () => false, 61 | isFile: () => true, 62 | size: 0 63 | }); 64 | }); 65 | }; 66 | 67 | /* 68 | * Segment value map 69 | */ 70 | const segments = { 71 | root: { 72 | dynamic: false, 73 | fn: () => process.cwd() 74 | }, 75 | 76 | vfs: { 77 | dynamic: false, 78 | fn: core => core.config('vfs.root', process.cwd()) 79 | }, 80 | 81 | username: { 82 | dynamic: true, 83 | fn: (core, session) => session.user.username 84 | } 85 | }; 86 | 87 | /* 88 | * Gets a segment value 89 | */ 90 | const getSegment = (core, session, seg) => segments[seg] ? segments[seg].fn(core, session) : ''; 91 | 92 | /* 93 | * Matches a string for segments 94 | */ 95 | const matchSegments = str => (str.match(/(\{\w+\})/g) || []); 96 | 97 | /* 98 | * Resolves a string with segments 99 | */ 100 | const resolveSegments = (core, session, str) => matchSegments(str) 101 | .reduce((result, current) => result.replace(current, getSegment(core, session, current.replace(/(\{|\})/g, ''))), str); 102 | 103 | /* 104 | * Resolves a given file path based on a request 105 | * Will take out segments from the resulting string 106 | * and replace them with a list of defined variables 107 | */ 108 | const getRealPath = (core, session, mount, file) => { 109 | const root = resolveSegments(core, session, mount.attributes.root); 110 | const str = file.substr(mount.root.length - 1); 111 | return path.join(root, str); 112 | }; 113 | 114 | /** 115 | * System VFS adapter 116 | * @param {Core} core Core reference 117 | * @param {object} [options] Adapter options 118 | */ 119 | module.exports = (core) => { 120 | const wrapper = (method, cb, ...args) => vfs => (file, options = {}) => { 121 | const promise = Promise.resolve(getRealPath(core, options.session, vfs.mount, file)) 122 | .then(realPath => fs[method](realPath, ...args)); 123 | 124 | return typeof cb === 'function' 125 | ? cb(promise, options) 126 | : promise.then(() => true); 127 | }; 128 | 129 | const crossWrapper = method => (srcVfs, destVfs) => (src, dest, options = {}) => Promise.resolve({ 130 | realSource: getRealPath(core, options.session, srcVfs.mount, src), 131 | realDest: getRealPath(core, options.session, destVfs.mount, dest) 132 | }) 133 | .then(({realSource, realDest}) => fs[method](realSource, realDest)) 134 | .then(() => true); 135 | 136 | return { 137 | watch: (mount, callback) => { 138 | const dest = resolveSegments(core, { 139 | user: { 140 | username: '**' 141 | } 142 | }, mount.attributes.root); 143 | 144 | const watch = chokidar.watch(dest, mount.attributes.chokidar || {}); 145 | const restr = dest.replace(/\*\*/g, '([^/]*)'); 146 | const re = new RegExp(restr + '/(.*)'); 147 | const seg = matchSegments(mount.attributes.root) 148 | .map(s => s.replace(/\{|\}/g, '')) 149 | .filter(s => segments[s].dynamic); 150 | 151 | const handle = name => file => { 152 | const test = re.exec(file); 153 | 154 | if (test && test.length > 0) { 155 | const args = seg.reduce((res, k, i) => ({[k]: test[i + 1]}), {}); 156 | callback(args, test[test.length - 1], name); 157 | } 158 | }; 159 | 160 | const events = ['add', 'addDir', 'unlinkDir', 'unlink']; 161 | events.forEach(name => watch.on(name, handle(name))); 162 | 163 | return watch; 164 | }, 165 | 166 | /** 167 | * Get filesystem capabilities 168 | * @param {String} file The file path from client 169 | * @param {Object} [options={}] Options 170 | * @return {Object[]} 171 | */ 172 | capabilities: vfs => (file, options = {}) => 173 | Promise.resolve({ 174 | sort: false, 175 | pagination: false 176 | }), 177 | 178 | /** 179 | * Checks if file exists 180 | * @param {String} file The file path from client 181 | * @param {Object} [options={}] Options 182 | * @return {Promise} 183 | */ 184 | exists: wrapper('access', promise => { 185 | return promise.then(() => true) 186 | .catch(() => false); 187 | }, fs.F_OK), 188 | 189 | /** 190 | * Get file statistics 191 | * @param {String} file The file path from client 192 | * @param {Object} [options={}] Options 193 | * @return {Object} 194 | */ 195 | stat: vfs => (file, options = {}) => 196 | Promise.resolve(getRealPath(core, options.session, vfs.mount, file)) 197 | .then(realPath => { 198 | return fs.access(realPath, fs.F_OK) 199 | .then(() => createFileIter(core, path.dirname(realPath), realPath)); 200 | }), 201 | 202 | /** 203 | * Reads directory 204 | * @param {String} root The file path from client 205 | * @param {Object} [options={}] Options 206 | * @return {Object[]} 207 | */ 208 | readdir: vfs => (root, options) => 209 | Promise.resolve(getRealPath(core, options.session, vfs.mount, root)) 210 | .then(realPath => fs.readdir(realPath).then(files => ({realPath, files}))) 211 | .then(({realPath, files}) => { 212 | const promises = files.map(f => createFileIter(core, realPath, root.replace(/\/?$/, '/') + f)); 213 | return Promise.all(promises); 214 | }), 215 | 216 | /** 217 | * Reads file stream 218 | * @param {String} file The file path from client 219 | * @param {Object} [options={}] Options 220 | * @return {stream.Readable} 221 | */ 222 | readfile: vfs => (file, options = {}) => 223 | Promise.resolve(getRealPath(core, options.session, vfs.mount, file)) 224 | .then(realPath => fs.stat(realPath).then(stat => ({realPath, stat}))) 225 | .then(({realPath, stat}) => { 226 | if (!stat.isFile()) { 227 | return false; 228 | } 229 | 230 | const range = options.range || []; 231 | return fs.createReadStream(realPath, { 232 | flags: 'r', 233 | start: range[0], 234 | end: range[1] 235 | }); 236 | }), 237 | 238 | /** 239 | * Creates directory 240 | * @param {String} file The file path from client 241 | * @param {Object} [options={}] Options 242 | * @return {boolean} 243 | */ 244 | mkdir: wrapper('mkdir', (promise, options = {}) => { 245 | return promise 246 | .then(() => true) 247 | .catch(e => { 248 | if (options.ensure && e.code === 'EEXIST') { 249 | return true; 250 | } 251 | 252 | return Promise.reject(e); 253 | }); 254 | }), 255 | 256 | /** 257 | * Writes file stream 258 | * @param {String} file The file path from client 259 | * @param {stream.Readable} data The stream 260 | * @param {Object} [options={}] Options 261 | * @return {Promise} 262 | */ 263 | writefile: vfs => (file, data, options = {}) => new Promise((resolve, reject) => { 264 | // FIXME: Currently this actually copies the file because 265 | // formidable will put this in a temporary directory. 266 | // It would probably be better to do a "rename()" on local filesystems 267 | const realPath = getRealPath(core, options.session, vfs.mount, file); 268 | 269 | const write = () => { 270 | const stream = fs.createWriteStream(realPath); 271 | data.on('error', err => reject(err)); 272 | data.on('end', () => resolve(true)); 273 | data.pipe(stream); 274 | }; 275 | 276 | fs.stat(realPath).then(stat => { 277 | if (stat.isDirectory()) { 278 | resolve(false); 279 | } else { 280 | write(); 281 | } 282 | }).catch((err) => err.code === 'ENOENT' ? write() : reject(err)); 283 | }), 284 | 285 | /** 286 | * Renames given file or directory 287 | * @param {String} src The source file path from client 288 | * @param {String} dest The destination file path from client 289 | * @param {Object} [options={}] Options 290 | * @return {boolean} 291 | */ 292 | rename: crossWrapper('rename'), 293 | 294 | /** 295 | * Copies given file or directory 296 | * @param {String} src The source file path from client 297 | * @param {String} dest The destination file path from client 298 | * @param {Object} [options={}] Options 299 | * @return {boolean} 300 | */ 301 | copy: crossWrapper('copy'), 302 | 303 | /** 304 | * Removes given file or directory 305 | * @param {String} file The file path from client 306 | * @param {Object} [options={}] Options 307 | * @return {boolean} 308 | */ 309 | unlink: wrapper('remove'), 310 | 311 | /** 312 | * Searches for files and folders 313 | * @param {String} file The file path from client 314 | * @param {Object} [options={}] Options 315 | * @return {Promise} 316 | */ 317 | search: vfs => (root, pattern, options = {}) => 318 | Promise.resolve(getRealPath(core, options.session, vfs.mount, root)) 319 | .then(realPath => { 320 | return fh.create() 321 | .paths(realPath) 322 | .match(pattern) 323 | .find() 324 | .then(files => ({realPath, files})) 325 | .catch(err => { 326 | core.logger.warn(err); 327 | 328 | return {realPath, files: []}; 329 | }); 330 | }) 331 | .then(({realPath, files}) => { 332 | const promises = files.map(f => { 333 | const rf = f.substr(realPath.length); 334 | return createFileIter( 335 | core, 336 | path.dirname(realPath.replace(/\/?$/, '/') + rf), 337 | root.replace(/\/?$/, '/') + rf 338 | ); 339 | }); 340 | return Promise.all(promises); 341 | }), 342 | 343 | /** 344 | * Touches a file 345 | * @param {String} file The file path from client 346 | * @param {Object} [options={}] Options 347 | * @return {boolean} 348 | */ 349 | touch: wrapper('ensureFile'), 350 | 351 | /** 352 | * Gets the real filesystem path (internal only) 353 | * @param {String} file The file path from client 354 | * @param {Object} [options={}] Options 355 | * @return {string} 356 | */ 357 | realpath: vfs => (file, options = {}) => 358 | Promise.resolve(getRealPath(core, options.session, vfs.mount, file)) 359 | }; 360 | }; 361 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const pathLib = require('path'); 33 | const consola = require('consola'); 34 | const logger = consola.withTag('Auth'); 35 | const nullAdapter = require('./adapters/auth/null.js'); 36 | 37 | /** 38 | * TODO: typedef 39 | * @typedef {Object} AuthAdapter 40 | */ 41 | 42 | /** 43 | * Authentication User Profile 44 | * @typedef {Object} AuthUserProfile 45 | * @property {number} id 46 | * @property {string} username 47 | * @property {string} name 48 | * @property {string[]} groups 49 | */ 50 | 51 | /** 52 | * Authentication Service Options 53 | * @typedef {Object} AuthOptions 54 | * @property {AuthAdapter} [adapter] 55 | * @property {string[]} [requiredGroups] 56 | * @property {string[]} [denyUsers] 57 | */ 58 | 59 | /** 60 | * Authentication Handler 61 | */ 62 | class Auth { 63 | 64 | /** 65 | * Creates a new instance 66 | * @param {Core} core Core instance reference 67 | * @param {AuthOptions} [options={}] Service Provider arguments 68 | */ 69 | constructor(core, options = {}) { 70 | const {requiredGroups, denyUsers} = core.configuration.auth; 71 | 72 | /** 73 | * @type {Core} 74 | */ 75 | this.core = core; 76 | 77 | /** 78 | * @type {AuthOptions} 79 | */ 80 | this.options = { 81 | adapter: nullAdapter, 82 | requiredGroups, 83 | denyUsers, 84 | ...options 85 | }; 86 | 87 | /** 88 | * @type {AuthAdapter} 89 | */ 90 | this.adapter = nullAdapter(core, this.options.config); 91 | 92 | try { 93 | this.adapter = this.options.adapter(core, this.options.config); 94 | } catch (e) { 95 | this.core.logger.warn(e); 96 | } 97 | } 98 | 99 | /** 100 | * Destroys instance 101 | */ 102 | destroy() { 103 | if (this.adapter.destroy) { 104 | this.adapter.destroy(); 105 | } 106 | } 107 | 108 | /** 109 | * Initializes adapter 110 | * @return {Promise} 111 | */ 112 | async init() { 113 | if (this.adapter.init) { 114 | return this.adapter.init(); 115 | } 116 | 117 | return true; 118 | } 119 | 120 | /** 121 | * Performs a login request 122 | * @param {Request} req HTTP request 123 | * @param {Response} res HTTP response 124 | * @return {Promise} 125 | */ 126 | async login(req, res) { 127 | const result = await this.adapter.login(req, res); 128 | 129 | if (result) { 130 | const profile = this.createUserProfile(req.body, result); 131 | 132 | if (profile && this.checkLoginPermissions(profile)) { 133 | await this.createHomeDirectory(profile, req, res); 134 | req.session.user = profile; 135 | req.session.save(() => { 136 | this.core.emit('osjs/core:logged-in', Object.freeze({ 137 | ...req.session 138 | })); 139 | 140 | res.status(200).json(profile); 141 | }); 142 | return; 143 | } 144 | } 145 | 146 | res.status(403) 147 | .json({error: 'Invalid login or permission denied'}); 148 | } 149 | 150 | /** 151 | * Performs a logout request 152 | * @param {Request} req HTTP request 153 | * @param {Response} res HTTP response 154 | * @return {Promise} 155 | */ 156 | async logout(req, res) { 157 | this.core.emit('osjs/core:logging-out', Object.freeze({ 158 | ...req.session 159 | })); 160 | 161 | await this.adapter.logout(req, res); 162 | 163 | try { 164 | req.session.destroy(); 165 | } catch (e) { 166 | logger.warn(e); 167 | } 168 | 169 | res.json({}); 170 | } 171 | 172 | /** 173 | * Performs a register request 174 | * @param {Request} req HTTP request 175 | * @param {Response} res HTTP response 176 | * @return {Promise} 177 | */ 178 | async register(req, res) { 179 | if (this.adapter.register) { 180 | const result = await this.adapter.register(req, res); 181 | 182 | return res.json(result); 183 | } 184 | 185 | return res.status(403) 186 | .json({error: 'Registration unavailable'}); 187 | } 188 | 189 | /** 190 | * Checks if login is allowed for this user 191 | * @param {AuthUserProfile} profile User profile 192 | * @return {boolean} 193 | */ 194 | checkLoginPermissions(profile) { 195 | const {requiredGroups, denyUsers} = this.options; 196 | 197 | if (denyUsers.indexOf(profile.username) !== -1) { 198 | return false; 199 | } 200 | 201 | if (requiredGroups.length > 0) { 202 | const passes = requiredGroups.every(name => { 203 | return profile.groups.indexOf(name) !== -1; 204 | }); 205 | 206 | return passes; 207 | } 208 | 209 | return true; 210 | } 211 | 212 | /** 213 | * Creates user profile object 214 | * @param {object} fields Input fields 215 | * @param {object} result Login result 216 | * @return {AuthUserProfile|boolean} 217 | */ 218 | createUserProfile(fields, result) { 219 | const ignores = ['password']; 220 | const required = ['username', 'id']; 221 | const template = { 222 | id: 0, 223 | username: fields.username, 224 | name: fields.username, 225 | groups: this.core.config('auth.defaultGroups', []) 226 | }; 227 | 228 | const missing = required 229 | .filter(k => typeof result[k] === 'undefined'); 230 | 231 | if (missing.length) { 232 | logger.warn('Missing user attributes', missing); 233 | } else { 234 | const values = Object.keys(result) 235 | .filter(k => ignores.indexOf(k) === -1) 236 | .reduce((o, k) => ({...o, [k]: result[k]}), {}); 237 | 238 | return {...template, ...values}; 239 | } 240 | 241 | return false; 242 | } 243 | 244 | /** 245 | * Tries to create home directory for a user 246 | * @param {AuthUserProfile} profile User profile 247 | * @return {Promise} 248 | */ 249 | async createHomeDirectory(profile) { 250 | const vfs = this.core.make('osjs/vfs'); 251 | const template = this.core.config('vfs.home.template', []); 252 | 253 | if (typeof template === 'string') { 254 | // If the template is a string, it is a path to a directory 255 | // that should be copied to the user's home directory 256 | const root = await vfs.realpath('home:/', profile); 257 | 258 | await fs.copy(template, root, {overwrite: false}); 259 | } else if (Array.isArray(template)) { 260 | await this.createHomeDirectoryFromArray(template, vfs, profile); 261 | } 262 | } 263 | 264 | /** 265 | * If the template is an array, it is a list of files that should be copied 266 | * to the user's home directory 267 | * @param {Object[]} template Array of objects with a specified path, 268 | * optionally with specified content but defaulting to an empty string 269 | * @param {VFSServiceProvider} vfs An instance of the virtual file system 270 | * @param {AuthUserProfile} profile User profile 271 | */ 272 | async createHomeDirectoryFromArray(template, vfs, profile) { 273 | for (const file of template) { 274 | try { 275 | const {path, contents = ''} = file; 276 | const shortcutsFile = await vfs.realpath(`home:/${path}`, profile); 277 | const dir = pathLib.dirname(shortcutsFile); 278 | 279 | if (!await fs.pathExists(shortcutsFile)) { 280 | await fs.ensureDir(dir); 281 | await fs.writeFile(shortcutsFile, contents); 282 | } 283 | } catch (e) { 284 | console.warn(`There was a problem writing '${file.path}' to the home directory template`); 285 | console.error('ERROR:', e); 286 | } 287 | } 288 | } 289 | } 290 | 291 | module.exports = Auth; 292 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const path = require('path'); 32 | const maxAge = 60 * 60 * 12; 33 | const mb = m => m * 1024 * 1024; 34 | 35 | const defaultConfiguration = { 36 | development: !(process.env.NODE_ENV || '').match(/^prod/i), 37 | logging: true, 38 | index: 'index.html', 39 | bind: '0.0.0.0', 40 | port: 8000, 41 | public: null, 42 | morgan: 'tiny', 43 | express: { 44 | maxFieldsSize: mb(20), 45 | maxFileSize: mb(200), 46 | maxBodySize: '100kb' 47 | }, 48 | https: { 49 | enabled: false, 50 | options: { 51 | key: null, 52 | cert: null 53 | } 54 | }, 55 | ws: { 56 | port: null, 57 | ping: 30 * 1000 58 | }, 59 | proxy: [ 60 | /* 61 | { 62 | source: 'pattern', 63 | destination: 'pattern', 64 | options: {} 65 | } 66 | */ 67 | ], 68 | auth: { 69 | vfsGroups: [], 70 | defaultGroups: [], 71 | requiredGroups: [], 72 | requireAllGroups: false, 73 | denyUsers: [] 74 | }, 75 | mime: { 76 | filenames: { 77 | // 'filename': 'mime/type' 78 | 'Makefile': 'text/x-makefile', 79 | '.gitignore': 'text/plain' 80 | }, 81 | define: { 82 | // 'mime/type': ['ext'] 83 | 'text/x-lilypond': ['ly', 'ily'], 84 | 'text/x-python': ['py'], 85 | 'application/tar+gzip': ['tgz'] 86 | } 87 | }, 88 | session: { 89 | store: { 90 | module: require.resolve('connect-loki'), 91 | options: { 92 | autosave: true 93 | // ttl: maxAge 94 | } 95 | }, 96 | options: { 97 | name: 'osjs.sid', 98 | secret: 'osjs', 99 | rolling: true, 100 | resave: false, 101 | saveUninitialized: false, 102 | cookie: { 103 | secure: 'auto', 104 | maxAge: 1000 * maxAge 105 | } 106 | } 107 | }, 108 | packages: { 109 | // Resolves to root by default 110 | discovery: 'packages.json', 111 | 112 | // Resolves to dist/ by default 113 | metadata: 'metadata.json' 114 | }, 115 | 116 | vfs: { 117 | watch: false, 118 | root: path.join(process.cwd(), 'vfs'), 119 | 120 | mountpoints: [{ 121 | name: 'osjs', 122 | attributes: { 123 | root: '{root}/dist', 124 | readOnly: true 125 | } 126 | }, { 127 | name: 'home', 128 | attributes: { 129 | root: '{vfs}/{username}' 130 | } 131 | }], 132 | 133 | home: { 134 | template: [{ 135 | path: '.desktop/.shortcuts.json', 136 | contents: JSON.stringify([]) 137 | }] 138 | } 139 | } 140 | }; 141 | 142 | module.exports = { 143 | defaultConfiguration 144 | }; 145 | 146 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const http = require('http'); 33 | const https = require('https'); 34 | const path = require('path'); 35 | const morgan = require('morgan'); 36 | const express = require('express'); 37 | const minimist = require('minimist'); 38 | const deepmerge = require('deepmerge'); 39 | const consola = require('consola'); 40 | const {CoreBase} = require('@osjs/common'); 41 | const {argvToConfig, createSession, createWebsocket, parseJson} = require('./utils/core.js'); 42 | const {defaultConfiguration} = require('./config.js'); 43 | const logger = consola.withTag('Core'); 44 | 45 | let _instance; 46 | 47 | /** 48 | * OS.js Server Core 49 | */ 50 | class Core extends CoreBase { 51 | 52 | /** 53 | * Creates a new instance 54 | * @param {Object} cfg Configuration tree 55 | * @param {Object} [options] Options 56 | */ 57 | constructor(cfg, options = {}) { 58 | options = { 59 | argv: process.argv.splice(2), 60 | root: process.cwd(), 61 | ...options 62 | }; 63 | 64 | const argv = minimist(options.argv); 65 | const val = k => argvToConfig[k](parseJson(argv[k])); 66 | const keys = Object.keys(argvToConfig).filter(k => Object.prototype.hasOwnProperty.call(argv, k)); 67 | const argvConfig = keys.reduce((o, k) => { 68 | logger.info(`CLI argument '--${k}' overrides`, val(k)); 69 | return {...o, ...deepmerge(o, val(k))}; 70 | }, {}); 71 | 72 | super(defaultConfiguration, deepmerge(cfg, argvConfig), options); 73 | 74 | this.logger = consola.withTag('Internal'); 75 | 76 | /** 77 | * @type {Express} 78 | */ 79 | this.app = express(); 80 | 81 | if (!this.configuration.public) { 82 | throw new Error('The public option is required'); 83 | } 84 | 85 | /** 86 | * @type {http.Server|https.Server} 87 | */ 88 | this.httpServer = this.config('https.enabled') 89 | ? https.createServer(this.config('https.options'), this.app) 90 | : http.createServer(this.app); 91 | 92 | /** 93 | * @type {object} 94 | */ 95 | this.session = createSession(this.app, this.configuration); 96 | 97 | /** 98 | * @type {object} 99 | */ 100 | this.ws = createWebsocket(this.app, this.configuration, this.session, this.httpServer); 101 | 102 | /** 103 | * @type {object} 104 | */ 105 | this.wss = this.ws.getWss(); 106 | 107 | _instance = this; 108 | } 109 | 110 | /** 111 | * Destroys the instance 112 | * @param {Function} [done] Callback when done 113 | * @return {Promise} 114 | */ 115 | async destroy(done = () => {}) { 116 | if (this.destroyed) { 117 | return; 118 | } 119 | 120 | this.emit('osjs/core:destroy'); 121 | 122 | logger.info('Shutting down...'); 123 | 124 | if (this.wss) { 125 | this.wss.close(); 126 | } 127 | 128 | const finish = (error) => { 129 | if (error) { 130 | logger.error(error); 131 | } 132 | 133 | if (this.httpServer) { 134 | this.httpServer.close(done); 135 | } else { 136 | done(); 137 | } 138 | }; 139 | 140 | try { 141 | await super.destroy(); 142 | finish(); 143 | } catch (e) { 144 | finish(e); 145 | } 146 | } 147 | 148 | /** 149 | * Starts the server 150 | * @return {Promise} 151 | */ 152 | async start() { 153 | if (!this.started) { 154 | logger.info('Starting services...'); 155 | 156 | await super.start(); 157 | 158 | logger.success('Initialized!'); 159 | 160 | await this.listen(); 161 | } 162 | 163 | return true; 164 | } 165 | 166 | /** 167 | * Initializes the server 168 | * @return {Promise} 169 | */ 170 | async boot() { 171 | if (this.booted) { 172 | return true; 173 | } 174 | 175 | this.emit('osjs/core:start'); 176 | 177 | if (this.configuration.logging) { 178 | this.wss.on('connection', (c) => { 179 | logger.log('WebSocket connection opened'); 180 | c.on('close', () => logger.log('WebSocket connection closed')); 181 | }); 182 | 183 | if (this.configuration.morgan) { 184 | this.app.use(morgan(this.configuration.morgan)); 185 | } 186 | } 187 | 188 | 189 | logger.info('Initializing services...'); 190 | 191 | await super.boot(); 192 | this.emit('init'); 193 | await this.start(); 194 | this.emit('osjs/core:started'); 195 | 196 | return true; 197 | } 198 | 199 | /** 200 | * Opens HTTP server 201 | */ 202 | async listen() { 203 | const httpPort = this.config('port'); 204 | const httpHost = this.config('bind'); 205 | const wsPort = this.config('ws.port') || httpPort; 206 | const pub = this.config('public'); 207 | const session = path.basename(path.dirname(this.config('session.store.module'))); 208 | const dist = pub.replace(process.cwd(), ''); 209 | const secure = this.config('https.enabled', false); 210 | const proto = prefix => `${prefix}${secure ? 's' : ''}://`; 211 | const host = port => `${httpHost}:${port}`; 212 | 213 | logger.info('Opening server connection'); 214 | 215 | const checkFile = path.join(pub, this.configuration.index); 216 | if (!fs.existsSync(checkFile)) { 217 | logger.warn('Missing files in "dist/" directory. Did you forget to run "npm run build" ?'); 218 | } 219 | 220 | return new Promise((resolve, reject) => { 221 | try { 222 | this.httpServer.listen(httpPort, httpHost, (e) => { 223 | if (e) { 224 | reject(e); 225 | } else { 226 | logger.success(`Using '${session}' sessions`); 227 | logger.success(`Serving '${dist}'`); 228 | logger.success(`WebSocket listening on ${proto('ws')}${host(wsPort)}`); 229 | logger.success(`Server listening on ${proto('http')}${host(httpPort)}`); 230 | resolve(); 231 | } 232 | }); 233 | } catch (e) { 234 | reject(e); 235 | } 236 | }); 237 | } 238 | 239 | /** 240 | * Broadcast given event to client 241 | * @param {string} name Event name 242 | * @param {Array} params A list of parameters to send to client 243 | * @param {Function} [filter] A function to filter clients 244 | */ 245 | broadcast(name, params, filter) { 246 | filter = filter || (() => true); 247 | 248 | if (this.ws) { 249 | this.wss.clients // This is a Set 250 | .forEach(client => { 251 | if (!client._osjs_client) { 252 | return; 253 | } 254 | 255 | if (filter(client)) { 256 | client.send(JSON.stringify({ 257 | params, 258 | name 259 | })); 260 | } 261 | }); 262 | } 263 | } 264 | 265 | /** 266 | * Broadcast given event to all clients 267 | * @param {string} name Event name 268 | * @param {Array} ...params A list of parameters to send to client 269 | */ 270 | broadcastAll(name, ...params) { 271 | return this.broadcast(name, params); 272 | } 273 | 274 | /** 275 | * Broadcast given event to client filtered by username 276 | * @param {String} username Username to send to 277 | * @param {string} name Event name 278 | * @param {Array} ...params A list of parameters to send to client 279 | */ 280 | broadcastUser(username, name, ...params) { 281 | return this.broadcast(name, params, client => { 282 | return client._osjs_client.username === username; 283 | }); 284 | } 285 | 286 | /** 287 | * Gets the server instance 288 | * @return {Core} 289 | */ 290 | static getInstance() { 291 | return _instance; 292 | } 293 | } 294 | 295 | module.exports = Core; 296 | -------------------------------------------------------------------------------- /src/filesystem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const {methodArguments} = require('./utils/vfs'); 32 | const systemAdapter = require('./adapters/vfs/system'); 33 | const {v1: uuid} = require('uuid'); 34 | const mime = require('mime'); 35 | const path = require('path'); 36 | const vfs = require('./vfs'); 37 | const {closeWatches} = require('./utils/core.js'); 38 | const consola = require('consola'); 39 | const logger = consola.withTag('Filesystem'); 40 | 41 | /** 42 | * @typedef {Object} Mountpoint 43 | * @property {string} [uuid] 44 | * @property {string} [root] 45 | * @property {object} [attributes] 46 | */ 47 | 48 | /** 49 | * TODO: typedef 50 | * @typedef {Object} FilesystemAdapter 51 | */ 52 | 53 | /** 54 | * Filesystem Service Adapter Option Map 55 | * @typedef {{name: FilesystemAdapter}} FilesystemAdapterMap 56 | */ 57 | 58 | /** 59 | * Filesystem Service Options 60 | * @typedef {Object} FilesystemOptions 61 | * @property {FilesystemAdapterMap} [adapters] 62 | */ 63 | 64 | /** 65 | * Filesystem Internal Call Options 66 | * @typedef {Object} FilesystemCallOptions 67 | * @property {string} method VFS Method name 68 | * @property {object} [user] User session data 69 | * @property {object} [session] Session data 70 | */ 71 | 72 | /** 73 | * OS.js Virtual Filesystem 74 | */ 75 | class Filesystem { 76 | 77 | /** 78 | * Create new instance 79 | * @param {Core} core Core reference 80 | * @param {FilesystemOptions} [options] Instance options 81 | */ 82 | constructor(core, options = {}) { 83 | /** 84 | * @type {Core} 85 | */ 86 | this.core = core; 87 | 88 | /** 89 | * @type {Mountpoint[]} 90 | */ 91 | this.mountpoints = []; 92 | 93 | /** 94 | * @type {FilesystemAdapterMap} 95 | */ 96 | this.adapters = {}; 97 | 98 | this.watches = []; 99 | 100 | this.router = null; 101 | 102 | this.methods = {}; 103 | 104 | /** 105 | * @type {FilesystemOptions} 106 | */ 107 | this.options = { 108 | adapters: {}, 109 | ...options 110 | }; 111 | } 112 | 113 | /** 114 | * Destroys instance 115 | * @return {Promise} 116 | */ 117 | async destroy() { 118 | const watches = this.watches.filter(({watch}) => { 119 | return watch && typeof watch.close === 'function'; 120 | }).map(({watch}) => watch); 121 | 122 | await closeWatches(watches); 123 | 124 | this.watches = []; 125 | } 126 | 127 | /** 128 | * Initializes Filesystem 129 | * @return {Promise} 130 | */ 131 | async init() { 132 | const adapters = { 133 | system: systemAdapter, 134 | ...this.options.adapters 135 | }; 136 | 137 | this.adapters = Object.keys(adapters).reduce((result, iter) => { 138 | return { 139 | [iter]: adapters[iter](this.core), 140 | ...result 141 | }; 142 | }, {}); 143 | 144 | // Routes 145 | const {router, methods} = vfs(this.core); 146 | this.router = router; 147 | this.methods = methods; 148 | 149 | // Mimes 150 | const {define} = this.core.config('mime', {define: {}, filenames: {}}); 151 | mime.define(define, {force: true}); 152 | 153 | // Mountpoints 154 | await Promise.all(this.core.config('vfs.mountpoints') 155 | .map(mount => this.mount(mount))); 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Gets MIME 162 | * @param {string} filename Input filename or path 163 | * @return {string} 164 | */ 165 | mime(filename) { 166 | const {filenames} = this.core.config('mime', { 167 | define: {}, 168 | filenames: {} 169 | }); 170 | 171 | return filenames[path.basename(filename)] 172 | ? filenames[path.basename(filename)] 173 | : mime.getType(filename) || 'application/octet-stream'; 174 | } 175 | 176 | /** 177 | * Crates a VFS request 178 | * @param {Request|object} req HTTP Request object 179 | * @param {Response|object} [res] HTTP Response object 180 | * @return {Promise<*>} 181 | */ 182 | request(name, req, res = {}) { 183 | return this.methods[name](req, res); 184 | } 185 | 186 | /** 187 | * Performs a VFS request with simulated HTTP request 188 | * @param {FilesystemCallOptions} options Request options 189 | * @param {*} ...args Arguments to pass to VFS method 190 | * @return {Promise<*>} 191 | */ 192 | call(options, ...args) { 193 | const {method, user, session} = { 194 | user: {}, 195 | session: null, 196 | ...options 197 | }; 198 | 199 | const req = methodArguments[method] 200 | .reduce(({fields, files}, key, index) => { 201 | const arg = args[index]; 202 | if (typeof key === 'function') { 203 | files = Object.assign(key(arg), files); 204 | } else { 205 | fields = { 206 | [key]: arg, 207 | ...fields 208 | }; 209 | } 210 | 211 | return {fields, files}; 212 | }, {fields: {}, files: {}}); 213 | 214 | req.session = session ? session : {user}; 215 | 216 | return this.request(method, req); 217 | } 218 | 219 | /** 220 | * Creates realpath VFS request 221 | * @param {string} filename The path 222 | * @param {AuthUserProfile} [user] User session object 223 | * @return {Promise} 224 | */ 225 | realpath(filename, user = {}) { 226 | return this.methods.realpath({ 227 | session: { 228 | user: { 229 | groups: [], 230 | ...user 231 | } 232 | }, 233 | fields: { 234 | path: filename 235 | } 236 | }); 237 | } 238 | 239 | /** 240 | * Mounts given mountpoint 241 | * @param {Mountpoint} mount Mountpoint 242 | * @return {Mountpoint} the mountpoint 243 | */ 244 | async mount(mount) { 245 | const mountpoint = { 246 | id: uuid(), 247 | root: `${mount.name}:/`, 248 | attributes: {}, 249 | ...mount 250 | }; 251 | 252 | this.mountpoints.push(mountpoint); 253 | 254 | logger.success('Mounted', mountpoint.name); 255 | 256 | await this.watch(mountpoint); 257 | 258 | return mountpoint; 259 | } 260 | 261 | /** 262 | * Unmounts given mountpoint 263 | * @param {Mountpoint} mount Mountpoint 264 | * @return {Promise} 265 | */ 266 | async unmount(mountpoint) { 267 | const found = this.watches.find(w => w.id === mountpoint.id); 268 | 269 | if (found && found.watch) { 270 | await found.watch.close(); 271 | } 272 | 273 | const index = this.mountpoints.indexOf(mountpoint); 274 | 275 | if (index !== -1) { 276 | this.mountpoints.splice(index, 1); 277 | 278 | return true; 279 | } 280 | 281 | return false; 282 | } 283 | 284 | /** 285 | * Set up a watch for given mountpoint 286 | * @param {Mountpoint} mountpoint The mountpoint 287 | * @return {Promise} 288 | */ 289 | async watch(mountpoint) { 290 | if ( 291 | !mountpoint.attributes.watch || 292 | this.core.config('vfs.watch') === false || 293 | !mountpoint.attributes.root 294 | ) { 295 | return; 296 | } 297 | 298 | const adapter = await (mountpoint.adapter 299 | ? this.adapters[mountpoint.adapter] 300 | : this.adapters.system); 301 | 302 | if (typeof adapter.watch === 'function') { 303 | await this._watch(mountpoint, adapter); 304 | } 305 | } 306 | 307 | /** 308 | * Internal method for setting up watch for given mountpoint adapter 309 | * @param {Mountpoint} mountpoint The mountpoint 310 | * @param {FilesystemAdapter} adapter The adapter 311 | * @return {Promise} 312 | */ 313 | async _watch(mountpoint, adapter) { 314 | const watch = await adapter.watch(mountpoint, (args, dir, type) => { 315 | const target = mountpoint.name + ':/' + dir; 316 | const keys = Object.keys(args); 317 | const filter = keys.length === 0 318 | ? () => true 319 | : ws => keys.every(k => ws._osjs_client[k] === args[k]); 320 | 321 | this.core.emit('osjs/vfs:watch:change', { 322 | mountpoint, 323 | target, 324 | type 325 | }); 326 | 327 | this.core.broadcast('osjs/vfs:watch:change', [{ 328 | path: target, 329 | type 330 | }, args], filter); 331 | }); 332 | 333 | watch.on('error', error => logger.warn('Mountpoint watch error', error)); 334 | 335 | this.watches.push({ 336 | id: mountpoint.id, 337 | watch 338 | }); 339 | 340 | logger.info('Watching mountpoint', mountpoint.name); 341 | } 342 | } 343 | 344 | module.exports = Filesystem; 345 | -------------------------------------------------------------------------------- /src/package.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const path = require('path'); 32 | const chokidar = require('chokidar'); 33 | 34 | /** 35 | * TODO: typedef 36 | * @typedef {Object} PackageMetadata 37 | */ 38 | 39 | /** 40 | * Package Options 41 | * @typedef {Object} PackageOptions 42 | * @property {string} filename 43 | * @property {PackageMetadata} metadata 44 | */ 45 | 46 | /** 47 | * OS.js Package Abstraction 48 | */ 49 | class Package { 50 | 51 | /** 52 | * Create new instance 53 | * @param {Core} core Core reference 54 | * @param {PackageOptions} [options] Instance options 55 | */ 56 | constructor(core, options = {}) { 57 | /** 58 | * @type {Core} 59 | */ 60 | this.core = core; 61 | 62 | this.script = options.metadata.server 63 | ? path.resolve(path.dirname(options.filename), options.metadata.server) 64 | : null; 65 | 66 | /** 67 | * @type {string} 68 | */ 69 | this.filename = options.filename; 70 | 71 | /** 72 | * @type {PackageMetadata} 73 | */ 74 | this.metadata = options.metadata; 75 | 76 | this.handler = null; 77 | 78 | this.watcher = null; 79 | } 80 | 81 | /** 82 | * Destroys instance 83 | */ 84 | async destroy() { 85 | this.action('destroy'); 86 | 87 | if (this.watcher) { 88 | await this.watcher.close(); 89 | this.watcher = null; 90 | } 91 | } 92 | 93 | /** 94 | * Run method on package script 95 | * @param {string} method Method name 96 | * @param {*} [...args] Pass arguments 97 | * @return {boolean} 98 | */ 99 | action(method, ...args) { 100 | try { 101 | if (this.handler && typeof this.handler[method] === 'function') { 102 | this.handler[method](...args); 103 | 104 | return true; 105 | } 106 | } catch (e) { 107 | this.core.logger.warn(e); 108 | } 109 | 110 | return false; 111 | } 112 | 113 | /** 114 | * Validates this package 115 | * @param {PackageMetadata[]} manifest Global manifest 116 | * @return {boolean} 117 | */ 118 | validate(manifest) { 119 | return this.script && 120 | this.metadata && 121 | !!manifest.find(iter => iter.name === this.metadata.name); 122 | } 123 | 124 | /** 125 | * Initializes this package 126 | * @return {Promise} 127 | */ 128 | init() { 129 | const mod = require(this.script); 130 | const handler = typeof mod.default === 'function' ? mod.default : mod; 131 | 132 | this.handler = handler(this.core, this); 133 | 134 | if (typeof this.handler.init === 'function') { 135 | return this.handler.init(); 136 | } 137 | 138 | return Promise.resolve(); 139 | } 140 | 141 | /** 142 | * Starts server scripts 143 | * @return {Promise} 144 | */ 145 | start() { 146 | return this.action('start'); 147 | } 148 | 149 | /** 150 | * Creates a watch in package dist 151 | * @param {Function} cb Callback function on watch changes 152 | * @return {string} Watched path 153 | */ 154 | watch(cb) { 155 | const pub = this.core.config('public'); 156 | const dist = path.join(pub, 'apps', this.metadata.name); 157 | 158 | this.watcher = chokidar.watch(dist); 159 | this.watcher.on('change', () => cb(this.metadata)); 160 | 161 | return dist; 162 | } 163 | 164 | /** 165 | * Resolve an URL for resource 166 | * @param {string} path Input path 167 | * @return {string} 168 | */ 169 | resource(path) { 170 | if (path.substr(0, 1) !== '/') { 171 | path = '/' + path; 172 | } 173 | 174 | return `/apps/${this.metadata.name}${path}`; 175 | } 176 | } 177 | 178 | module.exports = Package; 179 | -------------------------------------------------------------------------------- /src/packages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const fg = require('fast-glob'); 33 | const path = require('path'); 34 | const Package = require('./package.js'); 35 | const consola = require('consola'); 36 | const logger = consola.withTag('Packages'); 37 | 38 | const relative = filename => filename.replace(process.cwd(), ''); 39 | 40 | const readOrDefault = filename => fs.existsSync(filename) 41 | ? fs.readJsonSync(filename) 42 | : []; 43 | 44 | /** 45 | * Package Service Options 46 | * @typedef {Object} PackagesOptions 47 | * @property {string} [manifestFile] Manifest filename 48 | * @property {string} [discoveredFile] Discovery output file 49 | */ 50 | 51 | /** 52 | * OS.js Package Management 53 | */ 54 | class Packages { 55 | 56 | /** 57 | * Create new instance 58 | * @param {Core} core Core reference 59 | * @param {PackagesOptions} [options] Instance options 60 | */ 61 | constructor(core, options = {}) { 62 | /** 63 | * @type {Core} 64 | */ 65 | this.core = core; 66 | 67 | /** 68 | * @type {Package[]} 69 | */ 70 | this.packages = []; 71 | 72 | this.hotReloading = {}; 73 | 74 | /** 75 | * @type {PackagesOptions} 76 | */ 77 | this.options = { 78 | manifestFile: null, 79 | discoveredFile: null, 80 | ...options 81 | }; 82 | } 83 | 84 | /** 85 | * Initializes packages 86 | */ 87 | init() { 88 | this.core.on('osjs/application:socket:message', (ws, ...params) => { 89 | this.handleMessage(ws, params); 90 | }); 91 | 92 | return this.load(); 93 | } 94 | 95 | /** 96 | * Loads package manager 97 | * @return {Promise} 98 | */ 99 | load() { 100 | return this.createLoader() 101 | .then(packages => { 102 | this.packages = this.packages.concat(packages); 103 | 104 | return true; 105 | }); 106 | } 107 | 108 | /** 109 | * Loads all packages 110 | * @return {Promise} 111 | */ 112 | createLoader() { 113 | let result = []; 114 | const {discoveredFile, manifestFile} = this.options; 115 | const discovered = readOrDefault(discoveredFile); 116 | const manifest = readOrDefault(manifestFile); 117 | const sources = discovered.map(d => path.join(d, 'metadata.json')); 118 | 119 | logger.info('Using package discovery file', relative(discoveredFile)); 120 | logger.info('Using package manifest file', relative(manifestFile)); 121 | 122 | const stream = fg.stream(sources, { 123 | extension: false, 124 | brace: false, 125 | deep: 1, 126 | case: false 127 | }); 128 | 129 | stream.on('error', error => logger.error(error)); 130 | stream.on('data', filename => { 131 | result.push(this.loadPackage(filename, manifest)); 132 | }); 133 | 134 | return new Promise((resolve, reject) => { 135 | stream.once('end', () => { 136 | Promise.all(result) 137 | .then(result => result.filter(iter => !!iter.handler)) 138 | .then(resolve) 139 | .catch(reject); 140 | }); 141 | }); 142 | } 143 | 144 | /** 145 | * When a package dist has changed 146 | * @param {Package} pkg Package instance 147 | */ 148 | onPackageChanged(pkg) { 149 | clearTimeout(this.hotReloading[pkg.metadata.name]); 150 | 151 | this.hotReloading[pkg.metadata.name] = setTimeout(() => { 152 | logger.debug('Sending reload signal for', pkg.metadata.name); 153 | this.core.broadcast('osjs/packages:package:changed', [pkg.metadata.name]); 154 | }, 500); 155 | } 156 | 157 | /** 158 | * Loads package data 159 | * @param {string} filename Filename 160 | * @param {PackageMetadata} manifest Manifest 161 | * @return {Promise} 162 | */ 163 | loadPackage(filename, manifest) { 164 | const done = (pkg, error) => { 165 | if (error) { 166 | logger.warn(error); 167 | } 168 | 169 | return Promise.resolve(pkg); 170 | }; 171 | 172 | return fs.readJson(filename) 173 | .then(metadata => { 174 | const pkg = new Package(this.core, { 175 | filename, 176 | metadata 177 | }); 178 | 179 | return this.initializePackage(pkg, manifest, done); 180 | }); 181 | } 182 | 183 | /** 184 | * Initializes a package 185 | * @return {Promise} 186 | */ 187 | initializePackage(pkg, manifest, done) { 188 | if (pkg.validate(manifest)) { 189 | logger.info(`Loading ${relative(pkg.script)}`); 190 | 191 | try { 192 | if (this.core.configuration.development) { 193 | pkg.watch(() => { 194 | this.onPackageChanged(pkg); 195 | }); 196 | } 197 | 198 | return pkg.init() 199 | .then(() => done(pkg)) 200 | .catch(e => done(pkg, e)); 201 | } catch (e) { 202 | return done(pkg, e); 203 | } 204 | } 205 | 206 | return done(pkg); 207 | } 208 | 209 | /** 210 | * Starts packages 211 | */ 212 | start() { 213 | this.packages.forEach(pkg => pkg.start()); 214 | } 215 | 216 | /** 217 | * Destroys packages 218 | * @return {Promise} 219 | */ 220 | async destroy() { 221 | await Promise.all(this.packages.map(pkg => pkg.destroy())); 222 | 223 | this.packages = []; 224 | } 225 | 226 | /** 227 | * Handles an incoming message and signals an application 228 | * 229 | * This will call the 'onmessage' event in your application server script 230 | * 231 | * @param {WebSocket} ws Websocket Connection client 232 | * @param {Array} params A list of incoming parameters 233 | */ 234 | handleMessage(ws, params) { 235 | const {pid, name, args} = params[0]; 236 | const found = this.packages.findIndex(({metadata}) => metadata.name === name); 237 | 238 | if (found !== -1) { 239 | const {handler} = this.packages[found]; 240 | if (handler && typeof handler.onmessage === 'function') { 241 | const respond = (...respondParams) => ws.send(JSON.stringify({ 242 | name: 'osjs/application:socket:message', 243 | params: [{ 244 | pid, 245 | args: respondParams 246 | }] 247 | })); 248 | 249 | handler.onmessage(ws, respond, args); 250 | } 251 | } 252 | } 253 | } 254 | 255 | module.exports = Packages; 256 | -------------------------------------------------------------------------------- /src/providers/auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const {ServiceProvider} = require('@osjs/common'); 32 | const Auth = require('../auth.js'); 33 | 34 | /** 35 | * OS.js Auth Service Provider 36 | */ 37 | class AuthServiceProvider extends ServiceProvider { 38 | 39 | constructor(core, options) { 40 | super(core, options); 41 | 42 | this.auth = new Auth(core, options); 43 | } 44 | 45 | destroy() { 46 | this.auth.destroy(); 47 | 48 | super.destroy(); 49 | } 50 | 51 | async init() { 52 | const {route, routeAuthenticated} = this.core.make('osjs/express'); 53 | 54 | route('post', '/register', (req, res) => this.auth.register(req, res)); 55 | route('post', '/login', (req, res) => this.auth.login(req, res)); 56 | routeAuthenticated('post', '/logout', (req, res) => this.auth.logout(req, res)); 57 | 58 | await this.auth.init(); 59 | } 60 | } 61 | 62 | module.exports = AuthServiceProvider; 63 | -------------------------------------------------------------------------------- /src/providers/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const path = require('path'); 32 | const express = require('express'); 33 | const chokidar = require('chokidar'); 34 | const bodyParser = require('body-parser'); 35 | const proxy = require('express-http-proxy'); 36 | const nocache = require('nocache'); 37 | const {ServiceProvider} = require('@osjs/common'); 38 | const {isAuthenticated, closeWatches} = require('../utils/core.js'); 39 | 40 | /** 41 | * OS.js Core Service Provider 42 | */ 43 | class CoreServiceProvider extends ServiceProvider { 44 | 45 | constructor(core, options) { 46 | super(core, options); 47 | 48 | this.watches = []; 49 | } 50 | 51 | async destroy() { 52 | await closeWatches(this.watches); 53 | super.destroy(); 54 | } 55 | 56 | async init() { 57 | this.initService(); 58 | this.initExtensions(); 59 | this.initResourceRoutes(); 60 | this.initSocketRoutes(); 61 | this.initProxies(); 62 | } 63 | 64 | start() { 65 | if (this.core.configuration.development) { 66 | this.initDeveloperTools(); 67 | } 68 | } 69 | 70 | provides() { 71 | return [ 72 | 'osjs/express' 73 | ]; 74 | } 75 | 76 | /** 77 | * Initializes the service APIs 78 | */ 79 | initService() { 80 | const {app} = this.core; 81 | const {requireAllGroups} = this.core.configuration.auth; 82 | 83 | const middleware = { 84 | route: [], 85 | routeAuthenticated: [] 86 | }; 87 | 88 | this.core.singleton('osjs/express', () => ({ 89 | isAuthenticated, 90 | 91 | call: (method, ...args) => app[method](...args), 92 | use: (arg) => app.use(arg), 93 | 94 | websocket: (p, cb) => app.ws(p, cb), 95 | 96 | middleware: (authentication, cb) => { 97 | middleware[authentication ? 'routeAuthenticated' : 'route'].push(cb); 98 | }, 99 | 100 | route: (method, uri, cb) => app[method.toLowerCase()](uri, [ 101 | ...middleware.route 102 | ], cb), 103 | 104 | routeAuthenticated: (method, uri, cb, groups = [], strict = requireAllGroups) => 105 | app[method.toLowerCase()](uri, [ 106 | ...middleware.routeAuthenticated, 107 | isAuthenticated(groups, strict) 108 | ], cb), 109 | 110 | router: () => { 111 | const router = express.Router(); 112 | router.use(...middleware.route); 113 | return router; 114 | }, 115 | 116 | routerAuthenticated: (groups = [], strict = requireAllGroups) => { 117 | const router = express.Router(); 118 | router.use(...middleware.routeAuthenticated); 119 | router.use(isAuthenticated(groups, strict)); 120 | return router; 121 | } 122 | })); 123 | } 124 | 125 | /** 126 | * Initializes Express extensions 127 | */ 128 | initExtensions() { 129 | const {app, session, configuration} = this.core; 130 | const limit = configuration.express.maxBodySize; 131 | 132 | if (configuration.development) { 133 | app.use(nocache()); 134 | } else { 135 | app.disable('x-powered-by'); 136 | } 137 | 138 | // Handle sessions 139 | app.use(session); 140 | 141 | // Handle bodies 142 | app.use(bodyParser.urlencoded({ 143 | extended: false, 144 | limit 145 | })); 146 | 147 | app.use(bodyParser.json({ 148 | limit 149 | })); 150 | 151 | app.use(bodyParser.raw({ 152 | limit 153 | })); 154 | } 155 | 156 | /** 157 | * Initializes Express base routes, etc 158 | */ 159 | initResourceRoutes() { 160 | const {app, configuration} = this.core; 161 | const indexFile = path.join(configuration.public, configuration.index); 162 | 163 | app.get('/', (req, res) => res.sendFile(indexFile)); 164 | app.use('/', express.static(configuration.public)); 165 | 166 | // Internal ping 167 | app.get('/ping', (req, res) => { 168 | this.core.emit('osjs/core:ping', req); 169 | 170 | try { 171 | req.session.touch(); 172 | } catch (e) { 173 | this.core.logger.warn(e); 174 | } 175 | 176 | res.status(200).send('ok'); 177 | }); 178 | } 179 | 180 | /** 181 | * Initializes Socket routes 182 | */ 183 | initSocketRoutes() { 184 | const {app} = this.core; 185 | 186 | app.ws('/', (ws, req) => { 187 | ws.upgradeReq = ws.upgradeReq || req; 188 | ws._osjs_client = {...req.session.user}; 189 | 190 | const interval = this.core.config('ws.ping', 0); 191 | 192 | const pingInterval = interval ? setInterval(() => { 193 | ws.send(JSON.stringify({ 194 | name: 'osjs/core:ping' 195 | })); 196 | }, interval) : undefined; 197 | 198 | ws.on('close', () => { 199 | clearInterval(pingInterval); 200 | }); 201 | 202 | ws.on('message', msg => { 203 | try { 204 | const {name, params} = JSON.parse(msg); 205 | 206 | if (typeof name === 'string' && params instanceof Array) { 207 | // We don't wanna allow any internal signals from the outside! 208 | if (name.match(/^osjs/) && name !== 'osjs/application:socket:message') { 209 | return; 210 | } 211 | 212 | this.core.emit(name, ws, ...params); 213 | } 214 | } catch (e) { 215 | this.core.logger.warn(e); 216 | } 217 | }); 218 | 219 | ws.send(JSON.stringify({ 220 | name: 'osjs/core:connected', 221 | params: [{ 222 | cookie: { 223 | maxAge: this.core.config('session.options.cookie.maxAge') 224 | } 225 | }] 226 | })); 227 | }); 228 | } 229 | 230 | /** 231 | * Initializes Express proxies 232 | */ 233 | initProxies() { 234 | const {app, configuration} = this.core; 235 | const proxies = (configuration.proxy || []).map(item => ({ 236 | source: null, 237 | destination: null, 238 | options: {}, 239 | ...item 240 | })).filter(item => item.source && item.destination); 241 | 242 | proxies.forEach(item => { 243 | this.core.logger.info(`Proxying ${item.source} -> ${item.destination}`); 244 | app.use(item.source, proxy(item.destination, item.options)); 245 | }); 246 | } 247 | 248 | /** 249 | * Initializes some developer features 250 | */ 251 | initDeveloperTools() { 252 | try { 253 | const watchdir = path.resolve(this.core.configuration.public); 254 | const watcher = chokidar.watch(watchdir); 255 | 256 | watcher.on('change', filename => { 257 | // NOTE: 'ignored' does not work as expected with callback 258 | // ignored: str => str.match(/\.(js|css)$/) === null 259 | // for unknown reasons 260 | if (!filename.match(/\.(js|css)$/)) { 261 | return; 262 | } 263 | 264 | const relative = filename.replace(watchdir, ''); 265 | this.core.broadcast('osjs/dist:changed', [relative]); 266 | }); 267 | 268 | this.watches.push(watcher); 269 | } catch (e) { 270 | this.core.logger.warn(e); 271 | } 272 | } 273 | } 274 | 275 | module.exports = CoreServiceProvider; 276 | -------------------------------------------------------------------------------- /src/providers/packages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const path = require('path'); 33 | const chokidar = require('chokidar'); 34 | const {ServiceProvider} = require('@osjs/common'); 35 | const Packages = require('../packages'); 36 | const {closeWatches} = require('../utils/core'); 37 | 38 | /** 39 | * OS.js Package Service Provider 40 | */ 41 | class PackageServiceProvider extends ServiceProvider { 42 | constructor(core) { 43 | super(core); 44 | 45 | const {configuration} = this.core; 46 | const manifestFile = path.join(configuration.public, configuration.packages.metadata); 47 | const discoveredFile = path.resolve(configuration.root, configuration.packages.discovery); 48 | 49 | this.watches = []; 50 | this.packages = new Packages(core, { 51 | manifestFile, 52 | discoveredFile 53 | }); 54 | } 55 | 56 | provides() { 57 | return [ 58 | 'osjs/packages' 59 | ]; 60 | } 61 | 62 | init() { 63 | this.core.singleton('osjs/packages', () => this.packages); 64 | 65 | return this.packages.init(); 66 | } 67 | 68 | start() { 69 | this.packages.start(); 70 | 71 | if (this.core.configuration.development) { 72 | this.initDeveloperTools(); 73 | } 74 | } 75 | 76 | async destroy() { 77 | await closeWatches(this.watches); 78 | await this.packages.destroy(); 79 | super.destroy(); 80 | } 81 | 82 | /** 83 | * Initializes some developer features 84 | */ 85 | initDeveloperTools() { 86 | const {manifestFile} = this.packages.options; 87 | 88 | if (fs.existsSync(manifestFile)) { 89 | const watcher = chokidar.watch(manifestFile); 90 | watcher.on('change', () => { 91 | this.core.broadcast('osjs/packages:metadata:changed'); 92 | }); 93 | this.watches.push(watcher); 94 | } 95 | } 96 | } 97 | 98 | module.exports = PackageServiceProvider; 99 | -------------------------------------------------------------------------------- /src/providers/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const Settings = require('../settings'); 32 | const {ServiceProvider} = require('@osjs/common'); 33 | 34 | /** 35 | * OS.js Settings Service Provider 36 | */ 37 | class SettingsServiceProvider extends ServiceProvider { 38 | 39 | constructor(core, options) { 40 | super(core, options); 41 | 42 | this.settings = new Settings(core, options); 43 | } 44 | 45 | destroy() { 46 | super.destroy(); 47 | this.settings.destroy(); 48 | } 49 | 50 | async init() { 51 | this.core.make('osjs/express') 52 | .routeAuthenticated('post', '/settings', (req, res) => this.settings.save(req, res)); 53 | 54 | this.core.make('osjs/express') 55 | .routeAuthenticated('get', '/settings', (req, res) => this.settings.load(req, res)); 56 | 57 | return this.settings.init(); 58 | } 59 | } 60 | 61 | module.exports = SettingsServiceProvider; 62 | -------------------------------------------------------------------------------- /src/providers/vfs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const {ServiceProvider} = require('@osjs/common'); 32 | const Filesystem = require('../filesystem'); 33 | 34 | /** 35 | * OS.js Virtual Filesystem Service Provider 36 | */ 37 | class VFSServiceProvider extends ServiceProvider { 38 | 39 | constructor(core, options = {}) { 40 | super(core, options); 41 | 42 | this.filesystem = new Filesystem(core, options); 43 | } 44 | 45 | async destroy() { 46 | await this.filesystem.destroy(); 47 | super.destroy(); 48 | } 49 | 50 | depends() { 51 | return [ 52 | 'osjs/express' 53 | ]; 54 | } 55 | 56 | provides() { 57 | return [ 58 | 'osjs/fs', 59 | 'osjs/vfs' 60 | ]; 61 | } 62 | 63 | async init() { 64 | const filesystem = this.filesystem; 65 | 66 | await filesystem.init(); 67 | 68 | this.core.singleton('osjs/fs', () => this.filesystem); 69 | 70 | this.core.singleton('osjs/vfs', () => ({ 71 | realpath: (...args) => this.filesystem.realpath(...args), 72 | request: (...args) => this.filesystem.request(...args), 73 | call: (...args) => this.filesystem.call(...args), 74 | mime: (...args) => this.filesystem.mime(...args), 75 | 76 | get adapters() { 77 | return filesystem.adapters; 78 | }, 79 | 80 | get mountpoints() { 81 | return filesystem.mountpoints; 82 | } 83 | })); 84 | 85 | this.core.app.use('/vfs', filesystem.router); 86 | } 87 | } 88 | 89 | module.exports = VFSServiceProvider; 90 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const nullAdapter = require('./adapters/settings/null'); 32 | const fsAdapter = require('./adapters/settings/fs'); 33 | 34 | /** 35 | * TODO: typedef 36 | * @typedef {Object} SettingsAdapter 37 | */ 38 | 39 | /** 40 | * Settings Service Options 41 | * @typedef {Object} SettingsOptions 42 | * @property {SettingsAdapter|string} [adapter] 43 | */ 44 | 45 | /** 46 | * OS.js Settings Manager 47 | */ 48 | class Settings { 49 | 50 | /** 51 | * Create new instance 52 | * @param {Core} core Core reference 53 | * @param {SettingsOptions} [options] Instance options 54 | */ 55 | constructor(core, options = {}) { 56 | /** 57 | * @type {Core} 58 | */ 59 | this.core = core; 60 | 61 | this.options = { 62 | adapter: nullAdapter, 63 | ...options 64 | }; 65 | 66 | if (this.options.adapter === 'fs') { 67 | this.options.adapter = fsAdapter; 68 | } 69 | 70 | this.adapter = nullAdapter(core, this.options.config); 71 | 72 | try { 73 | this.adapter = this.options.adapter(core, this.options.config); 74 | } catch (e) { 75 | this.core.logger.warn(e); 76 | } 77 | } 78 | 79 | /** 80 | * Destroy instance 81 | */ 82 | destroy() { 83 | if (this.adapter.destroy) { 84 | this.adapter.destroy(); 85 | } 86 | } 87 | 88 | /** 89 | * Initializes adapter 90 | * @return {Promise} 91 | */ 92 | async init() { 93 | if (this.adapter.init) { 94 | return this.adapter.init(); 95 | } 96 | 97 | return true; 98 | } 99 | 100 | /** 101 | * Sends save request to adapter 102 | * @param {Request} req Express request 103 | * @param {Response} res Express response 104 | * @return {Promise} 105 | */ 106 | async save(req, res) { 107 | const result = await this.adapter.save(req, res); 108 | res.json(result); 109 | } 110 | 111 | /** 112 | * Sends load request to adapter 113 | * @param {Request} req Express request 114 | * @param {Response} res Express response 115 | * @return {Promise} 116 | */ 117 | async load(req, res) { 118 | const result = await this.adapter.load(req, res); 119 | res.json(result); 120 | } 121 | } 122 | 123 | 124 | module.exports = Settings; 125 | -------------------------------------------------------------------------------- /src/utils/core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const express_session = require('express-session'); 32 | const express_ws = require('express-ws'); 33 | 34 | /* 35 | * Converts an input argument to configuration entry 36 | * Overrides the user-created configuration file 37 | */ 38 | module.exports.argvToConfig = { 39 | 'logging': logging => ({logging}), 40 | 'development': development => ({development}), 41 | 'port': port => ({port}), 42 | 'ws-port': port => ({ws: {port}}), 43 | 'secret': secret => ({session: {options: {secret}}}), 44 | 'morgan': morgan => ({morgan}), 45 | 'discovery': discovery => ({packages: {discovery}}), 46 | 'manifest': manifest => ({packages: {manifest}}) 47 | }; 48 | 49 | /* 50 | * Create session parser 51 | */ 52 | module.exports.createSession = (app, configuration) => { 53 | const Store = require(configuration.session.store.module)(express_session); 54 | const store = new Store(configuration.session.store.options); 55 | 56 | return express_session({ 57 | store, 58 | ...configuration.session.options 59 | }); 60 | }; 61 | 62 | /* 63 | * Create WebSocket server 64 | */ 65 | module.exports.createWebsocket = (app, configuration, session, httpServer) => express_ws(app, httpServer, { 66 | wsOptions: { 67 | ...configuration.ws, 68 | verifyClient: (info, done) => { 69 | session(info.req, {}, () => { 70 | done(true); 71 | }); 72 | } 73 | } 74 | }); 75 | 76 | /* 77 | * Wrapper for parsing json 78 | */ 79 | module.exports.parseJson = str => { 80 | try { 81 | return JSON.parse(str); 82 | } catch (e) { 83 | return str; 84 | } 85 | }; 86 | 87 | /* 88 | * Checks groups for a request 89 | */ 90 | const validateGroups = (req, groups, all) => { 91 | if (groups instanceof Array && groups.length) { 92 | const userGroups = req.session.user.groups; 93 | 94 | const method = all ? 'every' : 'some'; 95 | 96 | return groups[method](g => userGroups.indexOf(g) !== -1); 97 | } 98 | 99 | return true; 100 | }; 101 | 102 | /* 103 | * Authentication middleware wrapper 104 | */ 105 | module.exports.isAuthenticated = (groups = [], all = false) => (req, res, next) => { 106 | if (req.session.user && validateGroups(req, groups, all)) { 107 | return next(); 108 | } 109 | 110 | return res 111 | .status(403) 112 | .send('Access denied'); 113 | }; 114 | 115 | /** 116 | * Closes an array of watches 117 | */ 118 | module.exports.closeWatches = (watches) => Promise.all( 119 | watches.map((w) => { 120 | return w.close() 121 | .catch(error => console.warn(error)); 122 | }) 123 | ); 124 | -------------------------------------------------------------------------------- /src/utils/vfs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const url = require('url'); 33 | const sanitizeFilename = require('sanitize-filename'); 34 | const formidable = require('formidable'); 35 | const {Stream} = require('stream'); 36 | 37 | /** 38 | * A map of error codes 39 | */ 40 | const errorCodes = { 41 | ENOENT: 404, 42 | EACCES: 401 43 | }; 44 | 45 | /** 46 | * Gets prefix of a VFS path 47 | */ 48 | const getPrefix = path => String(path).split(':')[0]; 49 | 50 | /** 51 | * Sanitizes a path 52 | */ 53 | const sanitize = filename => { 54 | const [name, str] = (filename.replace(/\/+/g, '/') 55 | .match(/^([\w-_]+):+(.*)/) || []) 56 | .slice(1); 57 | 58 | const sane = str.split('/') 59 | .map(s => sanitizeFilename(s)) 60 | .join('/') 61 | .replace(/\/+/g, '/'); 62 | 63 | return name + ':' + sane; 64 | }; 65 | 66 | /** 67 | * Gets the stream from a HTTP request 68 | */ 69 | const streamFromRequest = req => { 70 | const isStream = req.files.upload instanceof Stream; 71 | return isStream 72 | ? req.files.upload 73 | : fs.createReadStream(req.files.upload.path); 74 | }; 75 | 76 | const validateAll = (arr, compare, strict = true) => arr[strict ? 'every' : 'some'](g => compare.indexOf(g) !== -1); 77 | 78 | /** 79 | * Validates array groups 80 | */ 81 | const validateNamedGroups = (groups, userGroups, strict) => { 82 | const namedGroups = groups 83 | .filter(g => typeof g === 'string'); 84 | 85 | return namedGroups.length 86 | ? validateAll(namedGroups, userGroups, strict) 87 | : true; 88 | }; 89 | 90 | /** 91 | * Validates matp of groups based on method:[group,...] 92 | */ 93 | const validateMethodGroups = (groups, userGroups, method, strict) => { 94 | const methodGroups = groups 95 | .find(g => typeof g === 'string' ? false : (method in g)); 96 | 97 | return methodGroups 98 | ? validateAll(methodGroups[method], userGroups, strict) 99 | : true; 100 | }; 101 | 102 | /** 103 | * Validates groups 104 | */ 105 | const validateGroups = (userGroups, method, mountpoint, strict) => { 106 | const groups = mountpoint.attributes.groups || []; 107 | if (groups.length) { 108 | const namedValid = validateNamedGroups(groups, userGroups, strict); 109 | const methodValid = validateMethodGroups(groups, userGroups, method, strict); 110 | 111 | return namedValid && methodValid; 112 | } 113 | 114 | return true; 115 | }; 116 | 117 | /** 118 | * Checks permissions for given mountpoint 119 | */ 120 | const checkMountpointPermission = (req, res, method, readOnly, strict) => { 121 | const userGroups = req.session.user.groups; 122 | 123 | return ({mount}) => { 124 | if (readOnly) { 125 | const {attributes, name} = mount; 126 | 127 | if (attributes.readOnly) { 128 | const failed = typeof readOnly === 'function' 129 | ? getPrefix(readOnly(req, res)) === name 130 | : readOnly; 131 | 132 | if (failed) { 133 | return Promise.reject(createError(403, `Mountpoint '${name}' is read-only`)); 134 | } 135 | } 136 | } 137 | 138 | if (validateGroups(userGroups, method, mount, strict)) { 139 | return Promise.resolve(true); 140 | } 141 | 142 | return Promise.reject(createError(403, `Permission was denied for '${method}' in '${mount.name}'`)); 143 | }; 144 | }; 145 | 146 | /** 147 | * Creates a new custom Error 148 | */ 149 | const createError = (code, message) => { 150 | const e = new Error(message); 151 | e.code = code; 152 | return e; 153 | }; 154 | 155 | /** 156 | * Resolves a mountpoint 157 | */ 158 | const mountpointResolver = core => async (path) => { 159 | const {adapters, mountpoints} = core.make('osjs/vfs'); 160 | const prefix = getPrefix(path); 161 | const mount = mountpoints.find(m => m.name === prefix); 162 | 163 | if (!mount) { 164 | throw createError(403, `Mountpoint not found for '${prefix}'`); 165 | } 166 | 167 | const adapter = await (mount.adapter 168 | ? adapters[mount.adapter] 169 | : adapters.system); 170 | 171 | return Object.freeze({mount, adapter}); 172 | }; 173 | 174 | /* 175 | * Assembles a given object query 176 | */ 177 | const assembleQueryData = (data) => { 178 | const entries = Object 179 | .entries(data) 180 | .map(([k, v]) => { 181 | try { 182 | return [k, JSON.parse(v)]; 183 | } catch (e) { 184 | return [k, v]; 185 | } 186 | }); 187 | 188 | return Object.fromEntries(entries); 189 | }; 190 | 191 | /* 192 | * Parses URL Body 193 | */ 194 | const parseGet = req => { 195 | const {query} = url.parse(req.url, true); 196 | const assembledQuery = assembleQueryData(query); 197 | return Promise.resolve({fields: assembledQuery, files: {}}); 198 | }; 199 | 200 | /* 201 | * Parses Json Body 202 | */ 203 | const parseJson = req => { 204 | const isJson = req.headers['content-type'] && 205 | req.headers['content-type'].indexOf('application/json') !== -1; 206 | 207 | if (isJson) { 208 | return {fields: req.body, files: {}}; 209 | } 210 | 211 | return false; 212 | }; 213 | 214 | /* 215 | * Parses Form Body 216 | */ 217 | const parseFormData = (req, {maxFieldsSize, maxFileSize}) => { 218 | const form = new formidable.IncomingForm(); 219 | form.maxFieldsSize = maxFieldsSize; 220 | form.maxFileSize = maxFileSize; 221 | 222 | return new Promise((resolve, reject) => { 223 | form.parse(req, (err, fields, files) => { 224 | return err ? reject(err) : resolve({fields, files}); 225 | }); 226 | }); 227 | }; 228 | 229 | /** 230 | * Middleware for handling HTTP requests 231 | */ 232 | const parseFields = config => (req, res) => { 233 | if (['get', 'head'].indexOf(req.method.toLowerCase()) !== -1) { 234 | return Promise.resolve(parseGet(req)); 235 | } 236 | 237 | const json = parseJson(req); 238 | if (json) { 239 | return Promise.resolve(json); 240 | } 241 | 242 | return parseFormData(req, config); 243 | }; 244 | 245 | /** 246 | * A map of methods and their arguments. 247 | * Used for direct access via API 248 | */ 249 | const methodArguments = { 250 | realpath: ['path'], 251 | exists: ['path'], 252 | stat: ['path'], 253 | readdir: ['path'], 254 | readfile: ['path'], 255 | writefile: ['path', upload => ({upload})], 256 | mkdir: ['path'], 257 | unlink: ['path'], 258 | touch: ['path'], 259 | search: ['root', 'pattern'], 260 | copy: ['from', 'to'], 261 | rename: ['from', 'to'] 262 | }; 263 | 264 | module.exports = { 265 | mountpointResolver, 266 | createError, 267 | checkMountpointPermission, 268 | validateGroups, 269 | streamFromRequest, 270 | sanitize, 271 | getPrefix, 272 | parseFields, 273 | errorCodes, 274 | methodArguments, 275 | assembleQueryData 276 | }; 277 | -------------------------------------------------------------------------------- /src/vfs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | const fs = require('fs-extra'); 32 | const path = require('path'); 33 | const express = require('express'); 34 | const {Stream} = require('stream'); 35 | const { 36 | mountpointResolver, 37 | checkMountpointPermission, 38 | streamFromRequest, 39 | sanitize, 40 | parseFields, 41 | errorCodes 42 | } = require('./utils/vfs'); 43 | 44 | const respondNumber = result => typeof result === 'number' ? result : -1; 45 | const respondBoolean = result => typeof result === 'boolean' ? result : !!result; 46 | const requestPath = req => [sanitize(req.fields.path)]; 47 | const requestSearch = req => [sanitize(req.fields.root), req.fields.pattern]; 48 | const requestFile = req => [sanitize(req.fields.path), streamFromRequest(req)]; 49 | const requestCross = req => [sanitize(req.fields.from), sanitize(req.fields.to)]; 50 | 51 | /* 52 | * Parses the range request headers 53 | */ 54 | const parseRangeHeader = (range, size) => { 55 | const [pstart, pend] = range.replace(/bytes=/, '').split('-'); 56 | const start = parseInt(pstart, 10); 57 | const end = pend ? parseInt(pend, 10) : undefined; 58 | return [start, end]; 59 | }; 60 | 61 | /** 62 | * A "finally" for our chain 63 | */ 64 | const onDone = (req, res) => { 65 | if (req.files) { 66 | for (let fieldname in req.files) { 67 | try { 68 | const n = req.files[fieldname].path; 69 | if (fs.existsSync(n)) { 70 | fs.removeSync(n); 71 | } 72 | } catch (e) { 73 | console.warn('Failed to unlink temporary file', e); 74 | } 75 | } 76 | } 77 | }; 78 | 79 | /** 80 | * Wraps a vfs adapter request 81 | */ 82 | const wrapper = fn => (req, res, next) => fn(req, res) 83 | .then(result => new Promise((resolve, reject) => { 84 | if (result instanceof Stream) { 85 | result.once('error', reject); 86 | result.once('end', resolve); 87 | result.pipe(res); 88 | } else { 89 | res.json(result); 90 | resolve(); 91 | } 92 | })) 93 | .catch(error => next(error)) 94 | .finally(() => onDone(req, res)); 95 | 96 | /** 97 | * Creates the middleware 98 | */ 99 | const createMiddleware = core => { 100 | const parse = parseFields(core.config('express')); 101 | 102 | return (req, res, next) => parse(req, res) 103 | .then(({fields, files}) => { 104 | req.fields = fields; 105 | req.files = files; 106 | 107 | next(); 108 | }) 109 | .catch(error => { 110 | core.logger.warn(error); 111 | req.fields = {}; 112 | req.files = {}; 113 | 114 | next(error); 115 | }); 116 | }; 117 | 118 | const createOptions = req => { 119 | const options = req.fields.options; 120 | const range = req.headers && req.headers.range; 121 | const session = {...req.session || {}}; 122 | let result = options || {}; 123 | 124 | if (typeof options === 'string') { 125 | try { 126 | result = JSON.parse(req.fields.options) || {}; 127 | } catch (e) { 128 | // Allow to fall through 129 | } 130 | } 131 | 132 | if (range) { 133 | result.range = parseRangeHeader(req.headers.range); 134 | } 135 | 136 | return { 137 | ...result, 138 | session 139 | }; 140 | }; 141 | 142 | // Standard request with only a target 143 | const createRequestFactory = findMountpoint => (getter, method, readOnly, respond) => async (req, res) => { 144 | const call = async (target, rest, options) => { 145 | const found = await findMountpoint(target); 146 | const attributes = found.mount.attributes || {}; 147 | const strict = attributes.strictGroups !== false; 148 | 149 | if (method === 'search') { 150 | if (attributes.searchable === false) { 151 | return []; 152 | } 153 | } 154 | 155 | await checkMountpointPermission(req, res, method, readOnly, strict)(found); 156 | 157 | const vfsMethodWrapper = m => { 158 | return found.adapter[m] 159 | ? found.adapter[m](found)(target, ...rest, options) 160 | : Promise.reject(new Error(`Adapter does not support ${m}`)); 161 | }; 162 | 163 | const result = await vfsMethodWrapper(method); 164 | if (method === 'readfile') { 165 | const ranges = (!attributes.adapter || attributes.adapter === 'system') || attributes.ranges === true; 166 | const stat = await vfsMethodWrapper('stat').catch(() => ({})); 167 | 168 | if (ranges && options.range) { 169 | try { 170 | if (stat.size) { 171 | const size = stat.size; 172 | const [start, end] = options.range; 173 | const realEnd = end ? end : size - 1; 174 | const chunksize = (realEnd - start) + 1; 175 | 176 | res.writeHead(206, { 177 | 'Content-Range': `bytes ${start}-${realEnd}/${size}`, 178 | 'Accept-Ranges': 'bytes', 179 | 'Content-Length': chunksize, 180 | 'Content-Type': stat.mime 181 | }); 182 | } 183 | } catch (e) { 184 | console.warn('Failed to send a ranged response', e); 185 | } 186 | } else if (stat.mime) { 187 | res.append('Content-Type', stat.mime); 188 | } 189 | 190 | if (options.download) { 191 | const filename = encodeURIComponent(path.basename(target)); 192 | res.append('Content-Disposition', `attachment; filename*=utf-8''${filename}`); 193 | } 194 | } 195 | 196 | return respond ? respond(result) : result; 197 | }; 198 | 199 | return new Promise((resolve, reject) => { 200 | const options = createOptions(req); 201 | const [target, ...rest] = getter(req, res); 202 | const [resource] = rest; 203 | 204 | if (resource instanceof Stream) { 205 | resource.once('error', reject); 206 | } 207 | 208 | call(target, rest, options).then(resolve).catch(reject); 209 | }); 210 | }; 211 | 212 | // Request that has a source and target 213 | const createCrossRequestFactory = findMountpoint => (getter, method, respond) => async (req, res) => { 214 | const [from, to, options] = [...getter(req, res), createOptions(req)]; 215 | 216 | const srcMount = await findMountpoint(from); 217 | const destMount = await findMountpoint(to); 218 | const sameAdapter = srcMount.adapter === destMount.adapter; 219 | 220 | const srcStrict = srcMount.mount.attributes.strictGroups !== false; 221 | const destStrict = destMount.mount.attributes.strictGroups !== false; 222 | await checkMountpointPermission(req, res, 'readfile', false, srcStrict)(srcMount); 223 | await checkMountpointPermission(req, res, 'writefile', true, destStrict)(destMount); 224 | 225 | if (sameAdapter) { 226 | const result = await srcMount 227 | .adapter[method](srcMount, destMount)(from, to, options); 228 | 229 | return !!result; 230 | } 231 | 232 | // Simulates a copy/move 233 | const stream = await srcMount.adapter 234 | .readfile(srcMount)(from, options); 235 | 236 | const result = await destMount.adapter 237 | .writefile(destMount)(to, stream, options); 238 | 239 | if (method === 'rename') { 240 | await srcMount.adapter 241 | .unlink(srcMount)(from, options); 242 | } 243 | 244 | return !!result; 245 | }; 246 | 247 | /* 248 | * VFS Methods 249 | */ 250 | const vfs = core => { 251 | const findMountpoint = mountpointResolver(core); 252 | const createRequest = createRequestFactory(findMountpoint); 253 | const createCrossRequest = createCrossRequestFactory(findMountpoint); 254 | 255 | // Wire up all available VFS events 256 | return { 257 | capabilities: createRequest(requestPath, 'capabilities', false), 258 | realpath: createRequest(requestPath, 'realpath', false), 259 | exists: createRequest(requestPath, 'exists', false, respondBoolean), 260 | stat: createRequest(requestPath, 'stat', false), 261 | readdir: createRequest(requestPath, 'readdir', false), 262 | readfile: createRequest(requestPath, 'readfile', false), 263 | writefile: createRequest(requestFile, 'writefile', true, respondNumber), 264 | mkdir: createRequest(requestPath, 'mkdir', true, respondBoolean), 265 | unlink: createRequest(requestPath, 'unlink', true, respondBoolean), 266 | touch: createRequest(requestPath, 'touch', true, respondBoolean), 267 | search: createRequest(requestSearch, 'search', false), 268 | copy: createCrossRequest(requestCross, 'copy'), 269 | rename: createCrossRequest(requestCross, 'rename') 270 | }; 271 | }; 272 | 273 | /* 274 | * Creates a new VFS Express router 275 | */ 276 | module.exports = core => { 277 | const router = express.Router(); 278 | const methods = vfs(core); 279 | const middleware = createMiddleware(core); 280 | const {isAuthenticated} = core.make('osjs/express'); 281 | const vfsGroups = core.config('auth.vfsGroups', []); 282 | const logEnabled = core.config('development'); 283 | 284 | // Middleware first 285 | router.use(isAuthenticated(vfsGroups)); 286 | router.use(middleware); 287 | 288 | // Then all VFS routes (needs implementation above) 289 | router.get('/capabilities', wrapper(methods.capabilities)); 290 | router.get('/exists', wrapper(methods.exists)); 291 | router.get('/stat', wrapper(methods.stat)); 292 | router.get('/readdir', wrapper(methods.readdir)); 293 | router.get('/readfile', wrapper(methods.readfile)); 294 | router.post('/writefile', wrapper(methods.writefile)); 295 | router.post('/rename', wrapper(methods.rename)); 296 | router.post('/copy', wrapper(methods.copy)); 297 | router.post('/mkdir', wrapper(methods.mkdir)); 298 | router.post('/unlink', wrapper(methods.unlink)); 299 | router.post('/touch', wrapper(methods.touch)); 300 | router.post('/search', wrapper(methods.search)); 301 | 302 | // Finally catch promise exceptions 303 | router.use((error, req, res, next) => { 304 | // TODO: Better error messages 305 | const code = typeof error.code === 'number' 306 | ? error.code 307 | : (errorCodes[error.code] || 400); 308 | 309 | if (logEnabled) { 310 | console.error(error); 311 | } 312 | 313 | res.status(code) 314 | .json({ 315 | error: error.toString(), 316 | stack: logEnabled ? error.stack : undefined 317 | }); 318 | }); 319 | 320 | return {router, methods}; 321 | }; 322 | --------------------------------------------------------------------------------