Compare commits

...

110 Commits
v3.0.3 ... main

Author SHA1 Message Date
T. R. Bernstein
1cfb94829e fix: Add server property of FtpServer to type definition 2025-06-08 01:10:09 +02:00
T. R. Bernstein
7a51ed9d07 feat: convert commonjs to esm modules 2025-05-29 15:56:59 +02:00
T. R. Bernstein
cf7c678136 fix: Use dynamic imports in d.ts file 2025-05-29 14:39:17 +02:00
T. R. Bernstein
c716470b5b chore: Reformat all files 2025-04-17 12:33:43 +02:00
T. R. Bernstein
f443df52a7 fix: Add setting to type definitions 2025-04-17 12:31:10 +02:00
T. R. Bernstein
08ab571f4c feat: Allow for random passive port 2025-04-16 15:27:54 +02:00
T. R. Bernstein
9035aefa4d chore(package): Rename package 2025-04-16 15:27:27 +02:00
Paul
89bd13dcb1 Fix #338, Parse error message to super constructor (#339) 2024-05-13 12:43:52 -07:00
Paul
e8efe32126 fix: add errors to typescript declarations (#337)
fixes #336
2023-10-03 09:33:45 -06:00
botovance
18277e93ea chore(templates): update pull request template 2022-06-28 09:26:33 -06:00
Amr Ibrahim
a8cdcb0eb3 fix: replace .finally with .then (#322) 2022-06-24 10:55:10 -06:00
simultsop
4ba583420a refactor: reject with Error object (#319)
* refactor: reject with Error object

* fix: use error constructor

Co-authored-by: Matt Forster <hey@mattforster.ca>
2022-06-24 10:47:18 -06:00
bartbutenaers
51049f631a New event emitters (connect, server-error, closing, closed) (#314) 2022-06-24 09:03:51 -04:00
Matt Forster
50e8c455d6 docs(readme): update prime login example (#326) 2022-06-23 16:09:38 -04:00
Matt Forster
1af62a7c4f fix: update engine (#298)
This will prevent users of node 10  and below from installing and using >= v4.6
2022-05-06 10:18:08 -06:00
botovance
0b65a22296 chore(codeowners): update CODEOWNERS 2022-04-27 19:25:08 -06:00
Botovance
f59857e34a chore(codeowners): update CODEOWNERS 2022-04-19 14:07:30 -06:00
Amr Ibrahim
1ad45fc757 fix pasv_url resolverFunction docs (#297) 2022-04-18 15:50:49 -06:00
Matt Forster
bc8abb14da feat: npm updates, security zero, test corrections (#296) 2022-04-18 15:50:14 -06:00
dependabot[bot]
0b55b3f79d chore(deps): bump ssri from 6.0.1 to 6.0.2 (#293)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:51 -06:00
dependabot[bot]
4f4a6c25a5 chore(deps): bump moment from 2.22.2 to 2.29.2 (#288)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:41 -06:00
dependabot[bot]
45a4bf15bf chore(deps): bump trim-off-newlines from 1.0.1 to 1.0.3 (#289)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:32 -06:00
dependabot[bot]
a1be4416a7 chore(deps): bump node-fetch from 2.6.1 to 2.6.7 (#290)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:08 -06:00
Bas Broerse
32a0750e2c feat: allow mkd command to create directories recursively (#294)
Co-authored-by: Bas Broerse <b.broerse@sector-orange.com>
2022-04-14 14:43:33 -06:00
Matt Forster
4eb17015f1 chore: update CI context (#295) 2022-04-14 14:40:36 -06:00
dependabot[bot]
d2566e7745 chore(deps): bump npm-user-validate from 1.0.0 to 1.0.1 (#292)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 19:50:41 +00:00
dependabot[bot]
f8cd1e8f64 chore(deps): bump tar from 4.4.13 to 4.4.19 (#291)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 13:48:56 -06:00
dependabot[bot]
e0e676e7e9 chore(deps): bump pathval from 1.1.0 to 1.1.1 (#285)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 13:46:14 -06:00
dependabot[bot]
a7775a46ae chore(deps): bump minimist from 1.2.5 to 1.2.6 (#287)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 13:44:16 -06:00
Chen WeiJian
f9c81b162a Provide more friendly example code. (#282)
* Update README.md

* Update README.md
2021-12-06 09:32:59 -07:00
dependabot[bot]
e1f1aa09cd chore(deps): bump path-parse from 1.0.6 to 1.0.7 (#274)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-04 14:50:26 -06:00
Victor
b0174bb24e chore: make fileName available in getUniqueName (#273) 2021-11-04 14:49:50 -06:00
Matt Forster
5852851ded Update README.md 2021-08-09 13:51:31 -06:00
Matt Forster
1c5db00a5e chore: update readme 2021-08-09 13:50:38 -06:00
Victor
02227d653e feat: allow dynamic pasv_url depending on remote address (#269)
Co-authored-by: Matt Forster <hey@mattforster.ca>
2021-08-09 13:49:34 -06:00
dependabot[bot]
bf44cbba58 chore(deps): bump glob-parent from 5.1.1 to 5.1.2 (#262)
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.1...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:07:50 -06:00
dependabot[bot]
80ff71655e chore(deps): bump trim-newlines from 3.0.0 to 3.0.1 (#263)
Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:07:39 -06:00
dependabot[bot]
eb8e3d837f chore(deps): bump normalize-url from 5.3.0 to 5.3.1 (#265)
Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/sindresorhus/normalize-url/releases)
- [Commits](https://github.com/sindresorhus/normalize-url/commits)

---
updated-dependencies:
- dependency-name: normalize-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:07:25 -06:00
dependabot[bot]
dc37c9c435 chore(deps): bump hosted-git-info from 2.7.1 to 2.8.9 (#256)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.7.1 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.7.1...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 09:06:58 -06:00
dependabot[bot]
6a94a20f64 chore(deps): bump lodash from 4.17.19 to 4.17.21 (#255)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:47:57 -06:00
dependabot[bot]
02355eda28 chore(deps): bump handlebars from 4.7.6 to 4.7.7 (#254)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:43:03 -06:00
Andres Watson
8b32be7acc Update README.md (#253)
* Update README.md

Based on https://github.com/autovance/ftp-srv/discussions/250 I propose to explain that is not a HOST, must be an IP Address.

* Update README.md

* Update README.md

Co-authored-by: Matt Forster <hey@mattforster.ca>

Co-authored-by: Matt Forster <hey@mattforster.ca>
2021-05-05 09:44:58 -06:00
dependabot[bot]
5daaa9883c chore(deps): bump y18n from 4.0.0 to 4.0.1 (#247)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-29 10:28:31 -06:00
Connor Skees
02798e094f chore: small readme typo (#242) 2021-01-18 08:51:57 -07:00
Tyler Stewart
720c93d088 fix(feat): lazy require registry (#238)
* chore: give mocha explicit test paths

Mocha doesn't seem to find nested paths with **

* fix(feat): lazy require registry

If requiring the registry from the module scope the result is an empty object. I assume this is caused by the circular nature of this dep
2021-01-04 16:51:02 -07:00
Ricardas Jonaitis
bffcc35299 fix: dns lookup fail with IP with zeros padding (#237) 2021-01-04 08:34:40 -07:00
Matt Forster
78de22f518 chore: fix semantic release dry run 2020-12-16 15:36:11 -07:00
Matt Forster
2b140ecb0d chore: condition-circle using 'branch' 2020-12-16 15:27:40 -07:00
Matt Forster
57ddfb5e08 chore: set master as release branch 2020-12-16 15:22:56 -07:00
Matt Forster
beef19af30 chore: set release branch 2020-12-16 15:15:55 -07:00
Matt Forster
e7c5f83311 chore: setup release branch 2020-12-16 15:07:06 -07:00
Matt Forster
1ab793c04e chore: add install to release steps
requires some dev deps
2020-12-16 15:00:53 -07:00
Matt Forster
24f7126acf chore: improve ci flows (#227)
current release flow doesn't work
improve supported version testing
2020-12-16 14:58:20 -07:00
Matt Forster
457b859450 fix(fs): check resolved path against root (#224)
* fix(fs): check resolved path against root

This should prevent paths from being resolved above the root.

Should affect all commands that utilize the FS functions.

Fixes #167

* test: use __dirname for relative certs

* fix: improve path resolution

* chore: remove unused package

* fix: normalize resolve path if absolute

Otherwise join will normalize

Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-12-16 10:19:28 -07:00
Matt Forster
722da60a82 chore: set deploy context 2020-12-15 12:04:35 -07:00
dependabot[bot]
9f95d60916 chore(deps): bump ini from 1.3.5 to 1.3.7 (#223)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-10 10:46:24 -07:00
techmunk
4cd88b129c fix: ensure commandSocket is set before retrieving ip address of connection (#222) 2020-12-08 11:05:50 -07:00
dependabot[bot]
db49063b0d chore(deps-dev): bump semantic-release from 15.14.0 to 17.2.3 (#220)
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 15.14.0 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v15.14.0...v17.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-11-25 09:03:08 -07:00
dependabot[bot]
b55557292e chore(deps): bump node-fetch from 2.6.0 to 2.6.1 (#218)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-11 08:28:39 -06:00
Devonte
c87ce2fef6 fix: log errors, improved messages (#156)
Spelling errors, improved error logs.
2020-09-08 08:38:14 -06:00
Yaser
05a68cfb08 feat: pipe stream on fs write (#206)
* pipe allloewd on write stream

* code readable

* fix: 'if' statment missed curly braces

* Update src/commands/registration/stor.js

Replacing write data with pipe on STOR command

Co-authored-by: Tyler Stewart <hello@tylerstewart.ca>

* fix: socket on data replaced with socket on pipe

* fix: extra space

Co-authored-by: Tyler Stewart <hello@tylerstewart.ca>
Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-08-25 15:44:37 -06:00
Matt Forster
31290fc964 chore: security.md (#215)
* chore: security.md

* Update SECURITY.md
2020-08-18 11:44:53 -06:00
Matt Forster
a598fab03c fix: packages updates - npm advisories (#214)
Correct NPM advisories by updating package-lock.json and package
dependencies.

Major upgrade for yargs, but did not affect our code directly.
2020-08-17 15:44:23 -06:00
Matt Forster
87e8ac6ca8 chore: fix maintenance version regex 2020-08-17 13:43:27 -06:00
Matt Forster
75e34988f4 chore: add release support for backport branches (#213) 2020-08-17 13:37:49 -06:00
Tyler Stewart
e449e75219 fix: disallow PORT connections to alternate hosts
Ensure the data socket that the server connects to from the PORT command is the same IP as the current command socket.

* fix: add error handling to additional connection commands
2020-08-17 13:15:06 -06:00
Tyler Stewart
296573ae01 fix: cli arg consistency 2020-08-13 08:38:01 -06:00
Tyler Stewart
be579f65d0 docs: clarify passive options and behaviour 2020-08-13 08:38:01 -06:00
dependabot[bot]
c440050945 chore(deps): bump npm from 6.13.4 to 6.14.6 (#203)
Bumps [npm](https://github.com/npm/cli) from 6.13.4 to 6.14.6.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v6.13.4...v6.14.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-07-21 10:05:34 -06:00
Tyler Stewart
ca576acf2e fix: correctly destroy socket on close (#208)
* chore: update owner references

* fix: correctly destroy socket on close

This fixes an issue where the client would never close, hanging the server and preventing shutdown

* fix: only set timeout if greater than 0

* fix: move dependency to top

* fix: notify of command that caused error

* fix: emit disconnect event on client close
2020-07-20 16:30:04 -06:00
dependabot[bot]
649d582f30 chore(deps): bump lodash from 4.17.15 to 4.17.19 (#205)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-17 13:59:28 -06:00
dependabot[bot]
e26caad783 chore(deps): bump https-proxy-agent from 2.2.2 to 2.2.4 (#198) 2020-05-01 09:04:06 -06:00
dependabot[bot]
2470db6482 chore(deps): bump handlebars from 4.1.2 to 4.5.3 (#186) 2020-05-01 09:01:50 -06:00
dependabot[bot]
d78fae0ce6 chore(deps): bump acorn from 6.1.0 to 6.4.1 (#195) 2020-05-01 08:59:54 -06:00
Tyler Stewart
c59e191a39 fix: correct OPTS error code (#190)
If an unknown option is given, the response should be 501
2020-01-07 09:39:22 -07:00
Tyler Stewart
b2b1b2a0d3 fix(commands): 502 error on unsupported command (#185)
* fix(commands): 502 error on unsupported command

Fixes: https://github.com/trs/ftp-srv/issues/184

* test: update test assertion
2020-01-06 15:09:34 -07:00
dependabot[bot]
81fa7fcb89 chore(deps): bump npm from 6.11.2 to 6.13.4 (#183)
Bumps [npm](https://github.com/npm/cli) from 6.11.2 to 6.13.4.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v6.11.2...v6.13.4)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-14 13:00:44 -05:00
Leo Bernard
a18841d770 feat: 'disconnect' event (#175)
Fixes #174
2019-11-08 09:45:27 -07:00
Karoly Gossler
0dbb7f9070 docs: fixes the documentation of pasv_url behaviour (#172)
fixes #171
2019-09-26 12:16:19 -06:00
Tyler Stewart
0b9167e1e4 fix: explicitly promisify fs methods
`promisifyAll` will throw if a method with the suffix `Async` already
exists.

Fixes: https://github.com/trs/ftp-srv/pull/159
2019-08-22 14:57:40 -06:00
Tyler Stewart
484409d2eb feat: allow connecting from local 2019-08-22 14:57:40 -06:00
Tyler Stewart
5ffcef3312 fix: dont format message is empty 2019-08-22 14:57:40 -06:00
Tibor Papp
290769a042 Possibility to send back empty message 2019-08-22 14:57:40 -06:00
Tibor Papp
a1c7f2ffda Return response when folder is empty.
If we do not send back a response, some FTP clients can fail, when they use encryption.
2019-08-22 14:57:40 -06:00
Tyler Stewart
7153ffab4d chore: npm audit 2019-08-22 14:57:40 -06:00
Tyler Stewart
c0e132b70e fix: ensure valid fs path resolution 2019-08-22 14:57:40 -06:00
Tyler Stewart
e661bd10e2 fix: remove socket encoding
By setting the encoding, there becomes issues with binary transfers
(such as photos).
2019-08-22 14:57:40 -06:00
Tyler Stewart
bece42a0c9 feat: close passive server when client disconnects
Since a passive server is created for an individual client, when it
disconnects from the server we can assume the server is done and should
close it.
2019-08-22 14:57:40 -06:00
Tyler Stewart
b1fe56826c feat: disconnect passive server after timeout
If no client connects within 30 seconds of requesting, close the server.
This prevents multiple servers from being created and never closing.
2019-08-22 14:57:40 -06:00
Tyler Stewart
16dbc7895c chore: audit fix 2019-06-21 14:37:56 -06:00
Tyler Stewart
94f0b893e4 fix: enable better concurrent port search
Creates a new instance of a server each time the port search is called.
This ensures that concurrent calls to this function wont hang or produce
`ERR_SERVER_ALREADY_LISTEN`
2019-06-21 14:37:56 -06:00
Tyler Stewart
79d7bd9062 chore: ensure correct test path 2019-06-21 14:37:56 -06:00
Tyler Stewart
44999c714d fix(stor): ensure rejection after destroy 2019-06-21 14:37:56 -06:00
Tyler Stewart
0d9131c370 chore: simplify circle config 2019-03-08 11:14:42 -07:00
Tyler Stewart
eafaf6f642 chore: drop testing of node v6 2019-03-08 11:00:56 -07:00
Tyler Stewart
ea99e6ebbd chore: fix circle ci node_modules persistance 2019-03-08 10:47:55 -07:00
Tyler Stewart
342911eb36 fix: dont specify host when port searching (#158)
fix: handle errors on pasv/port commands
2019-03-08 17:40:48 +00:00
ls
0094614ee3 feat: socket timeout (#154)
* feat: managed new param socketTimeout expressed in ms

* feat: changed README.md

* feat: changed README.md

* feat: minor change

* feat: pull request changes

* feat: timeout value is set on 'options' object
2019-03-04 16:45:27 +00:00
Jean Gregory
0773967ba5 fix: find available port using hostname (#155)
The passive url is passed to getNextPortFactory so in passive mode, the
opening of a listening port fail when the passive url is not the ip of
an interface on the ftp server host machine.
2019-03-02 22:56:09 +00:00
Tyler Stewart
e5820688c4 feat: remove pasv url resolution (#153)
* fix: correctly try additional ports on EADDRINUSE

* feat: remove passive url host resoltion

See: https://github.com/trs/ftp-srv/issues/139

* feat: require pasv url to be specified

* fix(pasv): check for set pasv url

* chore: update scripts

* fix(cli): remove undefined values

* chore: add cli to eslint verify

* chore: run eslint

* chore: update scripts

* feat: add maximum number of retries to port selection

* chore: simplify eslint config

* chore: simplify dev dependencies

* chore: generate contributors

* chore: update readme contributors

* chore: add verify step to release

BREAKING CHANGE: remove ipify.org automatic resolution of ip
2019-02-21 18:50:16 +00:00
Tyler Stewart
a3fa1a2f71 Merge pull request #152 from trs/revert-143-remove-pasv-url-resolution
fix: revert "feat: remove host resolution"
2019-02-21 18:43:28 +00:00
Tyler Stewart
e540822d5b Revert "feat: remove host resolution (#143)"
This reverts commit 36f331d15d.
2019-02-21 11:42:32 -07:00
Tyler Stewart
36f331d15d feat: remove host resolution (#143)
* fix: correctly try additional ports on EADDRINUSE

* feat: remove passive url host resoltion

See: https://github.com/trs/ftp-srv/issues/139

* feat: require pasv url to be specified

* fix(pasv): check for set pasv url

* chore: update scripts

* fix(cli): remove undefined values

* chore: add cli to eslint verify

* chore: run eslint

* chore: update scripts

* feat: add maximum number of retries to port selection

* chore: simplify eslint config

* chore: simplify dev dependencies

* chore: generate contributors

* chore: update readme contributors
2019-02-21 18:39:07 +00:00
Devonte
03ff982959 fix: correct writable checks (#147)
fix: correct writable checks
2019-02-21 18:30:58 +00:00
Sergey Kuvakin
8c8f3922a3 feat: expose a RNTO event (#151)
* feat(commands): expose a RNTO event, updated a readme file

* feat(commands): fixed test for the RNTO

* chore: fixed prepush errors

* fix: turnback spaces into a README.md
2019-02-21 18:04:01 +00:00
zGwit
eca26ee86a fix: close connection on timeout (#145)
* Close connection when timeout

* Add promise processing
2019-01-17 02:44:48 +00:00
David Van Gompel
811db7b1a7 fix: add correct typing for tls option (#141) 2019-01-16 23:55:20 +00:00
Tyler Stewart
ab085a1bca chore: remove npm-run-all
Late to the party, but removes dev dependency `npm-run-all` to avoid malicious package
2018-12-21 15:49:38 -07:00
Robert Kieffer
a5f26480e5 fix(cli): correct --root flag logic (#135)
Fixes #134
2018-12-21 22:44:13 +00:00
Mike Estes
e41b04be46 fix: add pasv_url to typescript definitions (#131) 2018-11-19 09:49:43 -07:00
126 changed files with 19874 additions and 11372 deletions

View File

@@ -1,103 +1,112 @@
version: 2
version: 2.1
create-cache-file: &create-cache-file
run:
name: Setup cache
command: echo "$NODE_VERSION" > _cache_node_version
orbs:
node: circleci/node@5.0.2
package-json-cache: &package-json-cache
key: npm-install-{{ checksum "_cache_node_version" }}-{{ checksum "package-lock.json" }}
commands:
setup_git_bot:
description: set up the bot git user to make changes
steps:
- run:
name: "Git: Botovance"
command: |
git config --global user.name "Bot Vance"
git config --global user.email bot@autovance.com
base-build: &base-build
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- run:
name: Install
command: npm install
- save_cache:
<<: *package-json-cache
paths:
- node_modules
- run:
name: Lint
command: npm run verify:js
- run:
name: Test
command: npm run test:once
executors:
node-lts:
parameters:
node-version:
type: string
default: lts
docker:
- image: cimg/node:<< parameters.node-version >>
jobs:
test_node_10:
docker:
- image: circleci/node:10
environment:
- NODE_VERSION: 10
<<: *base-build
test_node_8:
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
<<: *base-build
test_node_6:
docker:
- image: circleci/node:6
environment:
- NODE_VERSION: 6
<<: *base-build
release:
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
lint:
executor: node-lts
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- node/install-packages
- run:
name: Lint
command: npm run verify
release_dry_run:
executor: node-lts
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Semantic Release
name: Dry Release
command: |
npm run semantic-release
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release --dry-run
release:
executor: node-lts
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Release
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release
workflows:
version: 2
test_and_tag:
jobs:
- test_node_10:
filters:
branches:
only: master
- test_node_8:
filters:
branches:
only: master
- test_node_6:
filters:
branches:
only: master
- release:
requires:
- test_node_6
- test_node_8
- test_node_10
build_and_test:
release_scheduled:
triggers:
# 6:03 UTC (mornings) 1 monday
- schedule:
cron: "3 6 * * 1"
filters:
branches:
only:
- main
jobs:
- test_node_10:
- lint
- node/test:
matrix:
parameters:
version:
- '12.22'
- '14.19'
- '16.14'
- 'current'
- release:
context: npm-deploy-av
requires:
- node/test
- lint
test:
jobs:
- lint
- node/test:
matrix:
parameters:
version:
- '12.22'
- '14.19'
- '16.14'
- 'current'
- release_dry_run:
filters:
branches:
ignore: master
- test_node_8:
filters:
branches:
ignore: master
- test_node_6:
filters:
branches:
ignore: master
only: main
requires:
- node/test
- lint
- hold_release:
type: approval
requires:
- release_dry_run
- release:
context: npm-deploy-av
requires:
- hold_release

View File

@@ -1,9 +1,2 @@
# START_CONFIT_GENERATED_CONTENT
# Common folders to ignore
node_modules/*
bower_components/*
# Config folder (optional - you might want to lint this...)
config/*
# END_CONFIT_GENERATED_CONTENT

19
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,19 @@
---
### Acceptance Checklist
- [ ] **Story**: Code is focused on the linked stories and solves a problem
- One of:
- [ ] **For Bugs**: A unit test is added or an existing one modified
- [ ] **For Features**: New unit tests are added covering the new functions or modifications
- [ ] Code Documentation changes are included for public interfaces and important / complex additions
- [ ] External Documentation is included for API changes, or other external facing interfaces
### Review Checklist
- [ ] The code does not duplicate existing functionality that exists elsewhere
- [ ] The code has been linted and follows team practices and style guidelines
- [ ] The changes in the PR are relevant to the title
- changes not related should be moved to a different PR
- [ ] All errors or error handling is actionable, and informs the viewer on how to correct it

5
.gitignore vendored
View File

@@ -1,7 +1,6 @@
test_tmp/
node_modules/
dist/
reports/
npm-debug.log
.nyc_output/
test_tmp/

5
CODEOWNERS Normal file
View File

@@ -0,0 +1,5 @@
# Lines starting with '#' are comments.
# Each line is a file pattern followed by one or more owners.
# Order is important. The last matching pattern has the most precedence.
* @quorumdms/team-gbt

View File

@@ -16,5 +16,10 @@
- Any new fixes are features should include new or updated [tests](/test).
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
- Submit your pull requests to the `master` branch, these will normally be merged into a seperate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
## Setup
- Clone the repository `git clone`
- Install dependencies `npm install`

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Tyler Stewart
Copyright (c) 2019 Tyler Stewart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

188
README.md
View File

@@ -1,5 +1,5 @@
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<a href="https://github.com/autovance/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
@@ -14,25 +14,13 @@
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
<a href="https://circleci.com/gh/autovance/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
---
- [Overview](#overview)
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [CLI](#cli)
- [Events](#events)
- [Supported Commands](#supported-commands)
- [File System](#file-system)
- [Contributing](#contributing)
- [License](#license)
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
@@ -48,16 +36,25 @@
## Usage
```js
// Quick start
// Quick start, create an active ftp server.
const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv({ options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
const port=21;
const ftpServer = new FtpSrv({
url: "ftp://0.0.0.0:" + port,
anonymous: true
});
ftpServer.listen()
.then(() => { ... });
ftpServer.on('login', ({ connection, username, password }, resolve, reject) => {
if(username === 'anonymous' && password === 'anonymous'){
return resolve({ root:"/" });
}
return reject(new errors.GeneralError('Invalid username or password', 401));
});
ftpServer.listen().then(() => {
console.log('Ftp server is starting...')
});
```
## API
@@ -73,13 +70,49 @@ _Note:_ The hostname must be the external IP address to accept external connecti
__Default:__ `"ftp://127.0.0.1:21"`
#### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
`FTP-srv` provides an IP address to the client when a `PASV` command is received in the handshake for a passive connection. Reference [PASV verb](https://cr.yp.to/ftp/retr.html#pasv). This can be one of two options:
- A function which takes one parameter containing the remote IP address of the FTP client. This can be useful when the user wants to return a different IP address depending if the user is connecting from Internet or from an LAN address.
Example:
```js
const { networkInterfaces } = require('os');
const { Netmask } = require('netmask');
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__Default:__ `"127.0.0.1"`
const nets = networkInterfaces();
function getNetworks() {
let networks = {};
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
if (net.family === 'IPv4' && !net.internal) {
networks[net.address + "/24"] = net.address
}
}
}
return networks;
}
const resolverFunction = (address) => {
// const networks = {
// '$GATEWAY_IP/32': `${public_ip}`,
// '10.0.0.0/8' : `${lan_ip}`
// }
const networks = getNetworks();
for (const network in networks) {
if (new Netmask(network).contains(address)) {
return networks[network];
}
}
return "127.0.0.1";
}
new FtpSrv({pasv_url: resolverFunction});
```
- A static IP address (ie. an external WAN **IP address** that the FTP server is bound to). In this case, only connections from localhost are handled differently returning `127.0.0.1` to the client.
If not provided, clients can only connect using an `Active` connection.
#### `pasv_min`
Tne starting port to accept passive connections.
The starting port to accept passive connections.
__Default:__ `1024`
#### `pasv_max`
@@ -124,6 +157,10 @@ __Allowable values:__
#### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
#### `timeout`
Sets the timeout (in ms) after that an idle connection is closed by the server
__Default:__ `0`
## CLI
`ftp-srv` also comes with a builtin CLI.
@@ -137,19 +174,29 @@ $ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
```
#### `url`
Set the listening URL.
Defaults to `ftp://127.0.0.1:21`
#### `--root` / `-r`
#### `--pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`).
If not provided, clients can only connect using an `Active` connection.
#### `--pasv_min`
The starting port to accept passive connections.
__Default:__ `1024`
#### `--pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `--root` / `-r`
Set the default root directory for users.
Defaults to the current directory.
#### `--credentials` / `-c`
Set the path to a json credentials file.
Format:
@@ -166,17 +213,54 @@ Format:
```
#### `--username`
Set the username for the only user. Do not provide an argument to allow anonymous login.
#### `--password`
Set the password for the given `username`.
#### `--read-only`
Disable write actions such as upload, delete, etc.
## Events
The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
### `client-error`
```js
ftpServer.on('client-error', ({connection, context, error}) => { ... });
```
Occurs when an error arises in the client connection.
`connection` [client class object](src/connection.js)
`context` string of where the error occurred
`error` error object
### `disconnect`
```js
ftpServer.on('disconnect', ({connection, id, newConnectionCount}) => { ... });
```
Occurs when a client has disconnected.
`connection` [client class object](src/connection.js)
`id` string of the disconnected connection id
`id` number of the new connection count (exclusive the disconnected client connection)
### `closed`
```js
ftpServer.on('closed', ({}) => { ... });
```
Occurs when the FTP server has been closed.
### `closing`
```js
ftpServer.on('closing', ({}) => { ... });
```
Occurs when the FTP server has started closing.
### `login`
```js
ftpServer.on('login', ({connection, username, password}, resolve, reject) => { ... });
@@ -204,15 +288,13 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
`reject` takes an error object
### `client-error`
### `server-error`
```js
ftpServer.on('client-error', ({connection, context, error}) => { ... });
ftpServer.on('server-error', ({error}) => { ... });
```
Occurs when an error arises in the client connection.
`connection` [client class object](src/connection.js)
`context` string of where the error occurred
Occurs when an error arises in the FTP server.
`error` error object
### `RETR`
@@ -235,6 +317,16 @@ Occurs when a file is uploaded.
`error` if successful, will be `null`
`fileName` name of the file that was uploaded
### `RNTO`
```js
connection.on('RNTO', (error, fileName) => { ... });
```
Occurs when a file is renamed.
`error` if successful, will be `null`
`fileName` name of the file that was renamed
## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
@@ -307,36 +399,18 @@ __Used in:__ `RNFR`, `RNTO`
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L131)
Returns a unique file name to write to
#### [`getUniqueName(fileName)`](src/fs.js#L131)
Returns a unique file name to write to. Client requested filename available if you want to base your function on it.
__Used in:__ `STOU`
<!--[RM_CONTRIBUTING]-->
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
<!--[]-->
## Contributors
- [OzairP](https://github.com/OzairP)
- [qchar](https://github.com/qchar)
- [jorinvo](https://github.com/jorinvo)
- [voxsoftware](https://github.com/voxsoftware)
- [pkeuter](https://github.com/pkeuter)
- [TimLuq](https://github.com/TimLuq)
- [edin-mg](https://github.com/edin-m)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [Johnnyrook777](https://github.com/Johnnyrook777)
<!--[RM_LICENSE]-->
## License
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
<!--[]-->
## References
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)

16
SECURITY.md Normal file
View File

@@ -0,0 +1,16 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 4.x | :white_check_mark: |
| 3.x | :white_check_mark: |
| < 3.0 | :x: |
__Critical vulnerabilities will be ported as far back as possible.__
## Reporting a Vulnerability
Report a security vulnerability directly to the maintainers by sending an email to security@autovance.com
or by reporting a vulnerability to the [NPM and Github security teams](https://docs.npmjs.com/reporting-a-vulnerability-in-an-npm-package).

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const yargs = require('yargs')
const path = require('path')
const FtpSrv = require('../src');
const errors = require('../src/errors');
const FtpSrv = require('../src')
const errors = require('../src/errors')
const args = setupYargs();
const state = setupState(args);
startFtpServer(state);
const args = setupYargs()
const state = setupState(args)
startFtpServer(state)
function setupYargs() {
return yargs
@@ -37,90 +37,97 @@ function setupYargs() {
boolean: true,
default: false
})
.option('pasv_url', {
.option('pasv-url', {
describe: 'URL to provide for passive connections',
type: 'string'
type: 'string',
alias: 'pasv_url'
})
.option('pasv_min', {
.option('pasv-min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024
default: 1024,
alias: 'pasv_min'
})
.option('pasv_max', {
.option('pasv-max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535
default: 65535,
alias: 'pasv_max'
})
.parse();
.parse()
}
function setupState(_args) {
const _state = {};
const _state = {}
function setupOptions() {
if (_args._ && _args._.length > 0) {
_state.url = _args._[0];
_state.url = _args._[0]
}
_state.pasv_url = _args.pasv_url;
_state.pasv_min = _args.pasv_min;
_state.pasv_max = _args.pasv_max;
_state.anonymous = _args.username === '';
_state.pasv_url = _args.pasv_url
_state.pasv_min = _args.pasv_min
_state.pasv_max = _args.pasv_max
_state.anonymous = _args.username === ''
}
function setupRoot() {
const dirPath = _args.root;
const dirPath = _args.root
if (dirPath) {
_state.root = process.cwd();
_state.root = dirPath
} else {
_state.root = dirPath;
_state.root = process.cwd()
}
}
function setupCredentials() {
_state.credentials = {};
_state.credentials = {}
const setCredentials = (username, password, root = null) => {
_state.credentials[username] = {
password,
root
};
};
}
}
if (_args.credentials) {
const credentialsFile = path.resolve(_args.credentials);
const credentials = require(credentialsFile);
const credentialsFile = path.resolve(_args.credentials)
const credentials = require(credentialsFile)
for (const cred of credentials) {
setCredentials(cred.username, cred.password, cred.root);
setCredentials(cred.username, cred.password, cred.root)
}
} else if (_args.username) {
setCredentials(_args.username, _args.password);
setCredentials(_args.username, _args.password)
}
}
function setupCommandBlacklist() {
if (_args.readOnly) {
_state.blacklist = ['ALLO', 'APPE', 'DELE', 'MKD', 'RMD', 'RNRF', 'RNTO', 'STOR', 'STRU'];
_state.blacklist = ['ALLO', 'APPE', 'DELE', 'MKD', 'RMD', 'RNRF', 'RNTO', 'STOR', 'STRU']
}
}
setupOptions();
setupRoot();
setupCredentials();
setupCommandBlacklist();
setupOptions()
setupRoot()
setupCredentials()
setupCommandBlacklist()
return _state;
return _state
}
function startFtpServer(_state) {
// Remove null/undefined options so they get set to defaults, below
for (const key in _state) {
if (_state[key] === undefined) delete _state[key]
}
function checkLogin(data, resolve, reject) {
const user = _state.credentials[data.username]
if (_state.anonymous || (user && user.password === data.password)) {
return resolve({root: (user && user.root) || _state.root});
return resolve({ root: (user && user.root) || _state.root })
}
return reject(new errors.GeneralError('Invalid username or password', 401));
return reject(new errors.GeneralError('Invalid username or password', 401))
}
const ftpServer = new FtpSrv({
@@ -130,8 +137,8 @@ function startFtpServer(_state) {
pasv_max: _state.pasv_max,
anonymous: _state.anonymous,
blacklist: _state.blacklist
});
})
ftpServer.on('login', checkLogin);
ftpServer.listen();
ftpServer.on('login', checkLogin)
ftpServer.listen()
}

View File

@@ -1,38 +0,0 @@
'use strict';
module.exports = {
types: [
{value: 'feat', name: 'feat: A new feature'},
{value: 'fix', name: 'fix: A bug fix'},
{value: 'docs', name: 'docs: Documentation only changes'},
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
{value: 'perf', name: 'perf: A code change that improves performance'},
{value: 'test', name: 'test: Adding missing tests'},
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
{value: 'revert', name: 'revert: Revert to a commit'},
{value: 'WIP', name: 'WIP: Work in progress'}
],
scopes: [],
// it needs to match the value for field type. Eg.: 'fix'
/*
scopeOverrides: {
fix: [
{name: 'merge'},
{name: 'style'},
{name: 'e2eTest'},
{name: 'unitTest'}
]
},
*/
allowCustomScopes: true,
allowBreakingChanges: ['feat', 'fix'],
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: false
};

View File

@@ -1,5 +0,0 @@
test/**/*.spec.js
--reporter mocha-multi-reporters
--reporter-options configFile=config/testUnit/reporters.json
--ui bdd
--bail

View File

@@ -1,6 +0,0 @@
{
"reporterEnabled": "spec",
"mochaJunitReporterReporterOptions": {
"mochaFile": "reports/junit.xml"
}
}

View File

@@ -1,162 +0,0 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"mocha",
"node"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"no-warning-comments": [
1,
{
"terms": ["todo", "fixme", "xxx"],
"location": "start"
},
],
"object-curly-spacing": [
2,
"never"
],
"array-bracket-spacing": [
2,
"never"
],
"brace-style": [
2,
"1tbs"
],
"consistent-return": 0,
"indent": [
"error",
2,
{
"SwitchCase": 1,
"MemberExpression": "off"
}
],
"no-multiple-empty-lines": [
2,
{
"max": 2
}
],
"no-use-before-define": [
2,
"nofunc"
],
"one-var": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single"
],
"keyword-spacing": 2,
"space-before-function-paren": [
2,
{
"anonymous": "always",
"named": "never"
}
],
"space-in-parens": [
2,
"never"
],
"strict": [
2,
"global"
],
"curly": [
2,
"multi-line"
],
"eol-last": 2,
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"no-eval": 2,
"no-with": 2,
"space-infix-ops": 2,
"dot-notation": [
2,
{
"allowKeywords": true
}
],
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"yoda": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-undef-init": 2,
"no-console": 1,
"camelcase": [
0,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "always"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-fallthrough": 0,
"semi": 2,
"semi-spacing": [
2,
{
"before": false,
"after": true
}
]
},
"parserOptions": {
"emcaVersion": 6,
"sourceType": "module",
"impliedStrict": true
}
}

228
ftp-srv.d.ts vendored
View File

@@ -1,125 +1,179 @@
import * as tls from 'tls'
import { Stats } from 'fs'
import { EventEmitter } from 'events';
import type { Server } from 'node:net'
const EventEmitter = import('events').EventEmitter
export class FileSystem {
readonly connection: FtpConnection
readonly root: string
readonly cwd: string
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(
connection: FtpConnection,
{
root,
cwd
}?: {
root: any
cwd: any
}
)
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string
currentDirectory(): string;
get(fileName: string): Promise<any>
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>
chdir(path?: string): Promise<string>;
write(
fileName: string,
{
append,
start
}?: {
append?: boolean
start?: any
}
): any
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(
fileName: string,
{
start
}?: {
start?: any
}
): Promise<any>
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>
chmod(path: string, mode: string): Promise<any>;
getUniqueName(fileName: string): string
}
getUniqueName(): string;
export class GeneralError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number)
}
export class SocketError extends Error {
/**
* @param message The error message.
* @param code Default value is `500`.
*/
constructor(message: string, code?: number)
}
export class FileSystemError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number)
}
export class ConnectorError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number)
}
export class FtpConnection extends EventEmitter {
server: FtpServer;
id: string;
log: any;
transferType: string;
encoding: string;
bufferSize: boolean;
readonly ip: string;
restByteCount: number | undefined;
secure: boolean
close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
reply (options: number | Object, ...letters: Array<any>): Promise<any>
server: FtpServer
id: string
log: any
transferType: string
encoding: string
bufferSize: boolean
readonly ip: string
restByteCount: number | undefined
secure: boolean
close(code: number, message: number): Promise<any>
login(username: string, password: string): Promise<any>
reply(options: number | Object, ...letters: Array<any>): Promise<any>
}
export interface FtpServerOptions {
url?: string,
pasv_min?: number,
pasv_max?: number,
greeting?: string | string[],
tls?: tls.SecureContext | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any,
url?: string
pasv_min?: number
pasv_max?: number
pasv_url?: string
random_pasv_port?: boolean
greeting?: string | string[]
tls?: import('tls').SecureContextOptions | false
anonymous?: boolean
blacklist?: Array<string>
whitelist?: Array<string>
file_format?: ((stat: import('fs').Stats) => string) | 'ls' | 'ep'
log?: any
timeout?: number
}
export class FtpServer extends EventEmitter {
constructor(options?: FtpServerOptions);
server: Server
constructor(options?: FtpServerOptions)
readonly isTLS: boolean;
readonly isTLS: boolean
listen(): any;
listen(): any
emitPromise(action: any, ...data: any[]): Promise<any>;
emitPromise(action: any, ...data: any[]): Promise<any>
// emit is exported from super class
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string
};
setupTLS(_tls: boolean):
| boolean
| {
cert: string
key: string
ca: string
}
setupGreeting(greet: string): string[];
setupGreeting(greet: string): string[]
setupFeaturesMessage(): string;
setupFeaturesMessage(): string
disconnectClient(id: string): Promise<any>;
disconnectClient(id: string): Promise<any>
close(): any;
close(): any
on(event: "login", listener: (
data: {
connection: FtpConnection,
username: string,
password: string
},
resolve: (config: {
fs?: FileSystem,
root?: string,
cwd?: string,
blacklist?: Array<string>,
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void): this;
on(
event: 'login',
listener: (
data: {
connection: FtpConnection
username: string
password: string
},
resolve: (config: {
fs?: FileSystem
root?: string
cwd?: string
blacklist?: Array<string>
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void
): this
on(event: "client-error", listener: (
data: {
connection: FtpConnection,
context: string,
error: Error,
}
) => void): this;
on(event: 'disconnect', listener: (data: { connection: FtpConnection; id: string }) => void): this
on(
event: 'client-error',
listener: (data: { connection: FtpConnection; context: string; error: Error }) => void
): this
}
export {FtpServer as FtpSrv};
export default FtpServer;
export { FtpServer as FtpSrv }
export default FtpServer

View File

@@ -1,8 +1,6 @@
const FtpSrv = require('./src');
const FileSystem = require('./src/fs');
const errors = require('./src/errors');
import FtpSrv from './src/index.js'
import FileSystem from './src/fs.js'
import ftpErrors from './src/errors.js'
module.exports = FtpSrv;
module.exports.FtpSrv = FtpSrv;
module.exports.FileSystem = FileSystem;
module.exports.ftpErrors = errors;
export default FtpSrv
export { FtpSrv, FileSystem, ftpErrors }

View File

@@ -1,23 +0,0 @@
const puppeteer = require('puppeteer');
const logoPath = `file://${process.cwd()}/logo/logo.html`;
puppeteer.launch()
.then(browser => {
return browser.newPage()
.then(page => {
return page.goto(logoPath)
.then(() => page);
})
.then(page => {
return page.setViewport({
width: 600,
height: 250,
deviceScaleFactor: 2
})
.then(() => page.screenshot({
path: 'logo.png',
omitBackground: true
}));
})
.then(() => browser.close());
});

View File

@@ -0,0 +1,30 @@
const { get } = require('https')
get(
'https://api.github.com/repos/trs/ftp-srv/contributors',
{
headers: {
'User-Agent': 'Chrome'
}
},
(res) => {
let response = ''
res.on('data', (data) => {
response += data
})
res.on('end', () => {
const contributors = JSON.parse(response).filter((contributor) => contributor.type === 'User')
for (const contributor of contributors) {
const url = contributor.html_url
const username = contributor.login
const markdown = `- [${username}](${url})\n`
process.stdout.write(markdown)
}
})
}
).on('error', (err) => {
process.stderr.write(err)
})

25
meta/logo/generate.js Normal file
View File

@@ -0,0 +1,25 @@
const puppeteer = require('puppeteer')
const logoPath = `file://${process.cwd()}/logo/logo.html`
puppeteer.launch().then((browser) => {
return browser
.newPage()
.then((page) => {
return page.goto(logoPath).then(() => page)
})
.then((page) => {
return page
.setViewport({
width: 600,
height: 250,
deviceScaleFactor: 2
})
.then(() =>
page.screenshot({
path: 'logo.png',
omitBackground: true
})
)
})
.then(() => browser.close())
})

22943
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ftp-srv",
"version": "0.0.0-development",
"name": "@tabshift/ftp-srv",
"version": "0.0.0",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
@@ -17,71 +17,78 @@
"bin",
"ftp-srv.d.ts"
],
"type": "module",
"main": "ftp-srv.js",
"bin": "./bin/index.js",
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/trs/ftp-srv"
"url": "https://github.com/autovance/ftp-srv"
},
"scripts": {
"pre-release": "npm run verify",
"commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:once --silent",
"semantic-release": "semantic-release",
"start": "npm run dev",
"test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"verify": "npm run verify:js --silent",
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success"
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
},
"release": {
"verifyConditions": "condition-circle"
"verifyConditions": "condition-circle",
"branch": "main",
"branches": [
"main"
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"cz-customizable": {
"config": "config/release/commitMessageConfig.js"
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
},
"dependencies": {
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.10",
"lodash": "^4.17.15",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^11.0.0"
"yargs": "^15.4.1"
},
"devDependencies": {
"@commitlint/cli": "^10.0.0",
"@commitlint/config-conventional": "^16.2.1",
"@icetee/ftp": "^1.0.2",
"chai": "^4.0.2",
"condition-circle": "^1.6.0",
"cross-env": "3.1.4",
"cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "4.5.0",
"eslint-config-google": "0.8.0",
"eslint-friendly-formatter": "3.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "5.1.1",
"husky": "0.13.3",
"istanbul": "0.4.5",
"mocha": "^5.2.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"npm-run-all": "^4.1.3",
"rimraf": "2.6.1",
"semantic-release": "^15.10.6",
"chai": "^4.2.0",
"condition-circle": "^2.0.2",
"eslint": "^5.14.1",
"husky": "^1.3.1",
"lint-staged": "^12.3.7",
"mocha": "^9.2.2",
"rimraf": "^2.6.1",
"semantic-release": "^19.0.2",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x",
"npm": ">=5.x"
"node": ">=12"
}
}

View File

@@ -1,76 +1,76 @@
const _ = require('lodash');
const Promise = require('bluebird');
import _ from 'lodash'
import Promise from 'bluebird'
import REGISTRY from './registry.js'
const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/)
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
class FtpCommands {
export default class FtpCommands {
constructor(connection) {
this.connection = connection;
this.previousCommand = {};
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
this.connection = connection
this.previousCommand = {}
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd))
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd))
}
parse(message) {
const strippedMessage = message.replace(/"/g, '');
let [directive, ...args] = strippedMessage.split(' ');
directive = _.chain(directive).trim().toUpper().value();
const strippedMessage = message.replace(/"/g, '')
let [directive, ...args] = strippedMessage.split(' ')
directive = _.chain(directive).trim().toUpper().value()
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const params = args.reduce(({arg, flags}, param) => {
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive)
const params = args.reduce(
({ arg, flags }, param) => {
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param)
else arg.push(param)
return { arg, flags }
},
{ arg: [], flags: [] }
)
const command = {
directive,
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
};
return command;
}
return command
}
handle(command) {
if (typeof command === 'string') command = this.parse(command);
if (typeof command === 'string') command = this.parse(command)
// Obfuscate password from logs
const logCommand = _.clone(command);
if (logCommand.directive === 'PASS') logCommand.arg = '********';
const logCommand = _.clone(command)
if (logCommand.directive === 'PASS') logCommand.arg = '********'
const log = this.connection.log.child({directive: command.directive});
log.trace({command: logCommand}, 'Handle command');
const log = this.connection.log.child({ directive: command.directive })
log.trace({ command: logCommand }, 'Handle command')
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(402, 'Command not allowed');
return this.connection.reply(502, `Command not allowed: ${command.directive}`)
}
if (_.includes(this.blacklist, command.directive)) {
return this.connection.reply(502, 'Command blacklisted');
return this.connection.reply(502, `Command blacklisted: ${command.directive}`)
}
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
return this.connection.reply(502, 'Command not whitelisted');
return this.connection.reply(502, `Command not whitelisted: ${command.directive}`)
}
const commandRegister = REGISTRY[command.directive];
const commandFlags = _.get(commandRegister, 'flags', {});
const commandRegister = REGISTRY[command.directive]
const commandFlags = _.get(commandRegister, 'flags', {})
if (!commandFlags.no_auth && !this.connection.authenticated) {
return this.connection.reply(530, 'Command requires authentication');
return this.connection.reply(530, `Command requires authentication: ${command.directive}`)
}
if (!commandRegister.handler) {
return this.connection.reply(502, 'Handler not set on command');
return this.connection.reply(502, `Handler not set on command: ${command.directive}`)
}
const handler = commandRegister.handler.bind(this.connection);
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
.finally(() => {
this.previousCommand = _.clone(command);
});
const handler = commandRegister.handler.bind(this.connection)
return Promise.resolve(handler({ log, command, previous_command: this.previousCommand })).then(() => {
this.previousCommand = _.clone(command)
})
}
}
module.exports = FtpCommands;

View File

@@ -1,14 +1,14 @@
module.exports = {
export default {
directive: 'ABOR',
handler: function () {
return this.connector.waitForConnection()
.then((socket) => {
return this.reply(426, {socket})
.then(() => this.reply(226));
})
.catch(() => this.reply(225))
.finally(() => this.connector.end());
return this.connector
.waitForConnection()
.then((socket) => {
return this.reply(426, { socket }).then(() => this.reply(226))
})
.catch(() => this.reply(225))
.then(() => this.connector.end())
},
syntax: '{{cmd}}',
description: 'Abort an active file transfer'
};
}

View File

@@ -1,11 +1,11 @@
module.exports = {
export default {
directive: 'ALLO',
handler: function () {
return this.reply(202);
return this.reply(202)
},
syntax: '{{cmd}}',
description: 'Allocate sufficient disk space to receive a file',
flags: {
obsolete: true
}
};
}

View File

@@ -1,10 +1,10 @@
const stor = require('./stor').handler;
import stor from './stor.js'
module.exports = {
export default {
directive: 'APPE',
handler: function (args) {
return stor.call(this, args);
return stor.handler.call(this, args)
},
syntax: '{{cmd}} <path>',
description: 'Append to a file'
};
}

View File

@@ -1,14 +1,16 @@
const _ = require('lodash');
const tls = require('tls');
import _ from 'lodash'
import tls from 'node:tls'
module.exports = {
export default {
directive: 'AUTH',
handler: function ({command} = {}) {
const method = _.upperCase(command.arg);
handler: function ({ command } = {}) {
const method = _.upperCase(command.arg)
switch (method) {
case 'TLS': return handleTLS.call(this);
default: return this.reply(504);
case 'TLS':
return handleTLS.call(this)
default:
return this.reply(504)
}
},
syntax: '{{cmd}} <type>',
@@ -17,26 +19,25 @@ module.exports = {
no_auth: true,
feat: 'AUTH TLS'
}
};
}
function handleTLS() {
if (!this.server.options.tls) return this.reply(502);
if (this.secure) return this.reply(202);
if (!this.server.options.tls) return this.reply(502)
if (this.secure) return this.reply(202)
return this.reply(234)
.then(() => {
const secureContext = tls.createSecureContext(this.server.options.tls);
return this.reply(234).then(() => {
const secureContext = tls.createSecureContext(this.server.options.tls)
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
})
;['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
function forwardEvent() {
this.emit.apply(this, arguments);
this.emit.apply(this, arguments)
}
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
});
this.commandSocket = secureSocket;
this.secure = true;
});
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event))
})
this.commandSocket = secureSocket
this.secure = true
})
}

View File

@@ -1,11 +1,11 @@
const cwd = require('./cwd').handler;
import cwd from './cwd.js'
module.exports = {
export default {
directive: ['CDUP', 'XCUP'],
handler: function (args) {
args.command.arg = '..';
return cwd.call(this, args);
args.command.arg = '..'
return cwd.handler.call(this, args)
},
syntax: '{{cmd}}',
description: 'Change to Parent Directory'
};
}

View File

@@ -1,22 +1,22 @@
const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path');
import Promise from 'bluebird'
import escapePath from '../../helpers/escape-path.js'
module.exports = {
export default {
directive: ['CWD', 'XCWD'],
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.chdir(command.arg))
.then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
.then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined
return this.reply(250, path)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <path>',
description: 'Change working directory'
};
}

View File

@@ -1,20 +1,20 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = {
export default {
directive: 'DELE',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.delete) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.delete(command.arg))
.then(() => {
return this.reply(250);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
.then(() => {
return this.reply(250)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <path>',
description: 'Delete file'
};
}

View File

@@ -1,22 +1,27 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
import _ from 'lodash'
import ActiveConnector from '../../connector/active.js'
const FAMILY = {
1: 4,
2: 6
};
}
module.exports = {
export default {
directive: 'EPRT',
handler: function ({command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol');
handler: function ({ log, command } = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value()
const family = FAMILY[protocol]
if (!family) return this.reply(504, 'Unknown network protocol')
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
this.connector = new ActiveConnector(this)
return this.connector
.setupConnection(ip, port, family)
.then(() => this.reply(200))
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'
};
}

View File

@@ -1,16 +1,21 @@
const PassiveConnector = require('../../connector/passive');
import PassiveConnector from '../../connector/passive.js'
module.exports = {
export default {
directive: 'EPSV',
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
const {port} = server.address();
handler: function ({ log }) {
this.connector = new PassiveConnector(this)
return this.connector
.setupServer()
.then((server) => {
const { port } = server.address()
return this.reply(229, `EPSV OK (|||${port}|)`);
});
return this.reply(229, `EPSV OK (|||${port}|)`)
})
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
},
syntax: '{{cmd}} [<protocol>]',
description: 'Initiate passive mode'
};
}

View File

@@ -1,27 +1,30 @@
const _ = require('lodash');
import _ from 'lodash'
module.exports = {
export default {
directive: 'FEAT',
handler: function () {
const registry = require('../registry');
const registry = import('../registry')
const features = Object.keys(registry)
.reduce((feats, cmd) => {
const feat = _.get(registry[cmd], 'flags.feat', null);
if (feat) return _.concat(feats, feat);
return feats;
}, ['UTF8'])
.reduce(
(feats, cmd) => {
const feat = _.get(registry[cmd], 'flags.feat', null)
if (feat) return _.concat(feats, feat)
return feats
},
['UTF8']
)
.sort()
.map((feat) => ({
message: ` ${feat}`,
raw: true
}));
}))
return features.length
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features');
: this.reply(211, 'No features')
},
syntax: '{{cmd}}',
description: 'Get the feature list implemented by the server',
flags: {
no_auth: true
}
};
}

View File

@@ -1,24 +1,30 @@
const _ = require('lodash');
import _ from 'lodash'
module.exports = {
export default {
directive: 'HELP',
handler: function ({command} = {}) {
const registry = require('../registry');
const directive = _.upperCase(command.arg);
handler: function ({ command } = {}) {
const registry = import('../registry')
const directive = _.upperCase(command.arg)
if (directive) {
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`)
const {syntax, description} = registry[directive];
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
return this.reply(214, ...reply);
const { syntax, description } = registry[directive]
const reply = _.concat([syntax.replace('{{cmd}}', directive), description])
return this.reply(214, ...reply)
} else {
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'));
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'))
return this.reply(
211,
'Supported commands:',
...supportedCommands,
'Use "HELP [command]" for syntax help.'
)
}
},
syntax: '{{cmd}} [<command>]',
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
description:
'Returns usage documentation on a command if specified, else a general help document is returned',
flags: {
no_auth: true
}
};
}

View File

@@ -1,56 +1,61 @@
const _ = require('lodash');
const Promise = require('bluebird');
const getFileStat = require('../../helpers/file-stat');
import _ from 'lodash'
import Promise from 'bluebird'
import getFileStat from '../../helpers/file-stat.js'
// http://cr.yp.to/ftp/list.html
// http://cr.yp.to/ftp/list/eplf.html
module.exports = {
export default {
directive: 'LIST',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
if (!this.fs.list) return this.reply(402, 'Not supported by file system')
const simple = command.directive === 'NLST';
const simple = command.directive === 'NLST'
const path = command.arg || '.';
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.get(path)))
.then((stat) => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
.then((files) => {
const getFileMessage = (file) => {
if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
const path = command.arg || '.'
return this.connector
.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.get(path)))
.then((stat) => (stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat]))
.then((files) => {
const getFileMessage = (file) => {
if (simple) return file.name
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'))
}
return Promise.try(() => files.map((file) => {
const message = getFileMessage(file);
return {
raw: true,
message,
socket: this.connector.socket
};
}));
})
.tap(() => this.reply(150))
.then((fileList) => {
if (fileList.length) return this.reply({}, ...fileList);
})
.tap(() => this.reply(226))
.catch(Promise.TimeoutError, (err) => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch((err) => {
log.error(err);
return this.reply(451, err.message || 'No directory');
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
return Promise.try(() =>
files.map((file) => {
const message = getFileMessage(file)
return {
raw: true,
message,
socket: this.connector.socket
}
})
)
})
.tap(() => this.reply(150))
.then((fileList) => {
if (fileList.length) return this.reply({}, ...fileList)
return this.reply({ socket: this.connector.socket, useEmptyMessage: true })
})
.tap(() => this.reply(226))
.catch(Promise.TimeoutError, (err) => {
log.error(err)
return this.reply(425, 'No connection established')
})
.catch((err) => {
log.error(err)
return this.reply(451, err.message || 'No directory')
})
.then(() => {
this.connector.end()
this.commandSocket.resume()
})
},
syntax: '{{cmd}} [<path>]',
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
};
description:
'Returns information of a file or directory if specified, else information of the current working directory is returned'
}

View File

@@ -1,25 +1,25 @@
const Promise = require('bluebird');
const moment = require('moment');
import Promise from 'bluebird'
import moment from 'moment'
module.exports = {
export default {
directive: 'MDTM',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
.then((fileStat) => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS')
return this.reply(213, modificationTime)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <path>',
description: 'Return the last-modified time of a specified file',
flags: {
feat: 'MDTM'
}
};
}

View File

@@ -1,22 +1,22 @@
const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path');
import Promise from 'bluebird'
import escapePath from '../../helpers/escape-path.js'
module.exports = {
export default {
directive: ['MKD', 'XMKD'],
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.mkdir(command.arg))
.then((dir) => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
return Promise.try(() => this.fs.mkdir(command.arg, { recursive: true }))
.then((dir) => {
const path = dir ? `"${escapePath(dir)}"` : undefined
return this.reply(257, path)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <path>',
description: 'Make directory'
};
}

View File

@@ -1,11 +1,11 @@
module.exports = {
export default {
directive: 'MODE',
handler: function ({command} = {}) {
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
handler: function ({ command } = {}) {
return this.reply(/^S$/i.test(command.arg) ? 200 : 504)
},
syntax: '{{cmd}} <mode>',
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
flags: {
obsolete: true
}
};
}

View File

@@ -1,10 +1,10 @@
const list = require('./list').handler;
import list from './list.js'
module.exports = {
export default {
directive: 'NLST',
handler: function (args) {
return list.call(this, args);
return list.handler.call(this, args)
},
syntax: '{{cmd}} [<path>]',
description: 'Returns a list of file names in a specified directory'
};
}

View File

@@ -1,11 +1,11 @@
module.exports = {
export default {
directive: 'NOOP',
handler: function () {
return this.reply(200);
return this.reply(200)
},
syntax: '{{cmd}}',
description: 'No operation',
flags: {
no_auth: true
}
};
}

View File

@@ -1,39 +1,41 @@
const _ = require('lodash');
import _ from 'lodash'
const OPTIONS = {
UTF8: utf8,
'UTF-8': utf8
};
}
module.exports = {
export default {
directive: 'OPTS',
handler: function ({command} = {}) {
if (!_.has(command, 'arg')) return this.reply(501);
handler: function ({ command } = {}) {
if (!_.has(command, 'arg')) return this.reply(501)
const [_option, ...args] = command.arg.split(' ');
const option = _.toUpper(_option);
const [_option, ...args] = command.arg.split(' ')
const option = _.toUpper(_option)
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
return OPTIONS[option].call(this, args);
if (!OPTIONS.hasOwnProperty(option)) return this.reply(501, 'Unknown option command')
return OPTIONS[option].call(this, args)
},
syntax: '{{cmd}}',
description: 'Select options for a feature'
};
}
function utf8([setting] = []) {
const getEncoding = () => {
switch (_.toUpper(setting)) {
case 'ON': return 'utf8';
case 'OFF': return 'ascii';
default: return null;
case 'ON':
return 'utf8'
case 'OFF':
return 'ascii'
default:
return null
}
};
}
const encoding = getEncoding();
if (!encoding) return this.reply(501, 'Unknown setting for option');
const encoding = getEncoding()
if (!encoding) return this.reply(501, 'Unknown setting for option')
this.encoding = encoding;
if (this.transferType !== 'binary') this.transferType = this.encoding;
this.encoding = encoding
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`)
}

View File

@@ -1,25 +1,25 @@
module.exports = {
export default {
directive: 'PASS',
handler: function ({log, command} = {}) {
if (!this.username) return this.reply(503);
if (this.authenticated) return this.reply(202);
handler: function ({ log, command } = {}) {
if (!this.username) return this.reply(503)
if (this.authenticated) return this.reply(202)
// 332 : require account name (ACCT)
const password = command.arg;
if (!password) return this.reply(501, 'Must provide password');
const password = command.arg
if (!password) return this.reply(501, 'Must provide password')
return this.login(this.username, password)
.then(() => {
return this.reply(230);
})
.catch((err) => {
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
.then(() => {
return this.reply(230)
})
.catch((err) => {
log.error(err)
return this.reply(530, err.message || 'Authentication failed')
})
},
syntax: '{{cmd}} <password>',
description: 'Authentication password',
flags: {
no_auth: true
}
};
}

View File

@@ -1,20 +1,39 @@
const PassiveConnector = require('../../connector/passive');
import Promise from 'bluebird'
import PassiveConnector from '../../connector/passive.js'
import { isLocalIP } from '../../helpers/is-local.js'
module.exports = {
export default {
directive: 'PASV',
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
const address = this.server.options.pasv_url;
const {port} = server.address();
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;
const portByte2 = port % 256;
handler: function ({ log } = {}) {
if (!this.server.options.pasv_url) {
return this.reply(502)
}
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
});
this.connector = new PassiveConnector(this)
return this.connector
.setupServer()
.then((server) => {
const { port } = server.address()
let pasvAddress = this.server.options.pasv_url
if (typeof pasvAddress === 'function') {
return Promise.try(() => pasvAddress(this.ip)).then((address) => ({ address, port }))
}
// Allow connecting from local
if (isLocalIP(this.ip)) pasvAddress = this.ip
return { address: pasvAddress, port }
})
.then(({ address, port }) => {
const host = address.replace(/\./g, ',')
const portByte1 = (port / 256) | 0
const portByte2 = port % 256
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`)
})
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
},
syntax: '{{cmd}}',
description: 'Initiate passive mode'
};
}

View File

@@ -1,9 +1,9 @@
module.exports = {
export default {
directive: 'PBSZ',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
this.bufferSize = parseInt(command.arg, 10);
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
handler: function ({ command } = {}) {
if (!this.secure) return this.reply(202, 'Not supported')
this.bufferSize = parseInt(command.arg, 10)
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0')
},
syntax: '{{cmd}}',
description: 'Protection Buffer Size',
@@ -11,4 +11,4 @@ module.exports = {
no_auth: true,
feat: 'PBSZ'
}
};
}

View File

@@ -1,21 +1,29 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
import _ from 'lodash'
import ActiveConnector from '../../connector/active.js'
module.exports = {
export default {
directive: 'PORT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
handler: function ({ log, command } = {}) {
this.connector = new ActiveConnector(this)
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);
const rawConnection = _.get(command, 'arg', '').split(',')
if (rawConnection.length !== 6) return this.reply(425)
const ip = rawConnection.slice(0, 4).join('.');
const portBytes = rawConnection.slice(4).map((p) => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
const ip = rawConnection
.slice(0, 4)
.map((b) => parseInt(b))
.join('.')
const portBytes = rawConnection.slice(4).map((p) => parseInt(p))
const port = portBytes[0] * 256 + portBytes[1]
return this.connector.setupConnection(ip, port)
.then(() => this.reply(200));
return this.connector
.setupConnection(ip, port)
.then(() => this.reply(200))
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
description: 'Specifies an address and port to which the server should connect'
};
}

View File

@@ -1,17 +1,20 @@
const _ = require('lodash');
import _ from 'lodash'
module.exports = {
export default {
directive: 'PROT',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
handler: function ({ command } = {}) {
if (!this.secure) return this.reply(202, 'Not supported')
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503)
switch (_.toUpper(command.arg)) {
case 'P': return this.reply(200, 'OK');
case 'P':
return this.reply(200, 'OK')
case 'C':
case 'S':
case 'E': return this.reply(536, 'Not supported');
default: return this.reply(504);
case 'E':
return this.reply(536, 'Not supported')
default:
return this.reply(504)
}
},
syntax: '{{cmd}}',
@@ -20,4 +23,4 @@ module.exports = {
no_auth: true,
feat: 'PROT'
}
};
}

View File

@@ -1,22 +1,22 @@
const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path');
import Promise from 'bluebird'
import escapePath from '../../helpers/escape-path.js'
module.exports = {
export default {
directive: ['PWD', 'XPWD'],
handler: function ({log} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
handler: function ({ log } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.currentDirectory())
.then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(257, path);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
.then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined
return this.reply(257, path)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}}',
description: 'Print current working directory'
};
}

View File

@@ -1,11 +1,11 @@
module.exports = {
export default {
directive: 'QUIT',
handler: function () {
return this.close(221, 'Client called QUIT');
return this.close(221, 'Client called QUIT')
},
syntax: '{{cmd}}',
description: 'Disconnect',
flags: {
no_auth: true
}
};
}

View File

@@ -1,16 +1,16 @@
const _ = require('lodash');
import _ from 'lodash'
module.exports = {
export default {
directive: 'REST',
handler: function ({command} = {}) {
const arg = _.get(command, 'arg');
const byteCount = parseInt(arg, 10);
handler: function ({ command } = {}) {
const arg = _.get(command, 'arg')
const byteCount = parseInt(arg, 10)
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater')
this.restByteCount = byteCount;
return this.reply(350, `Restarting next transfer at ${byteCount}`);
this.restByteCount = byteCount
return this.reply(350, `Restarting next transfer at ${byteCount}`)
},
syntax: '{{cmd}} <byte-count>',
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
};
}

View File

@@ -1,64 +1,66 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = {
export default {
directive: 'RETR',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.read) return this.reply(402, 'Not supported by file system')
const filePath = command.arg;
const filePath = command.arg
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = filePath;
}
const serverPath = stream.path || filePath;
return this.connector
.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.read(filePath, { start: this.restByteCount })))
.then((fsResponse) => {
let { stream, clientPath } = fsResponse
if (!stream && !clientPath) {
stream = fsResponse
clientPath = filePath
}
const serverPath = stream.path || filePath
const destroyConnection = (connection, reject) => (err) => {
if (connection) connection.destroy(err);
reject(err);
};
const destroyConnection = (connection, reject) => (err) => {
if (connection) connection.destroy(err)
reject(err)
}
const eventsPromise = new Promise((resolve, reject) => {
stream.on('data', (data) => {
if (stream) stream.pause();
if (this.connector.socket) {
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
}
});
stream.once('end', () => resolve());
stream.once('error', destroyConnection(this.connector.socket, reject));
const eventsPromise = new Promise((resolve, reject) => {
stream.on('data', (data) => {
if (stream) stream.pause()
if (this.connector.socket) {
this.connector.socket.write(data, () => stream && stream.resume())
}
})
stream.once('end', () => resolve())
stream.once('error', destroyConnection(this.connector.socket, reject))
this.connector.socket.once('error', destroyConnection(stream, reject));
});
this.connector.socket.once('error', destroyConnection(stream, reject))
})
this.restByteCount = 0;
this.restByteCount = 0
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise)
.tap(() => this.emit('RETR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
})
.catch(Promise.TimeoutError, (err) => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch((err) => {
log.error(err);
this.emit('RETR', err);
return this.reply(551, err.message);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
return this.reply(150)
.then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise)
.tap(() => this.emit('RETR', null, serverPath))
.then(() => this.reply(226, clientPath))
.then(() => stream.destroy && stream.destroy())
})
.catch(Promise.TimeoutError, (err) => {
log.error(err)
return this.reply(425, 'No connection established')
})
.catch((err) => {
log.error(err)
this.emit('RETR', err)
return this.reply(551, err.message)
})
.then(() => {
this.connector.end()
this.commandSocket.resume()
})
},
syntax: '{{cmd}} <path>',
description: 'Retrieve a copy of the file'
};
}

View File

@@ -1,10 +1,10 @@
const {handler: dele} = require('./dele');
import dele from './dele.js'
module.exports = {
export default {
directive: ['RMD', 'XRMD'],
handler: function (args) {
return dele.call(this, args);
return dele.handler.call(this, args)
},
syntax: '{{cmd}} <path>',
description: 'Remove a directory'
};
}

View File

@@ -1,22 +1,22 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = {
export default {
directive: 'RNFR',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
const fileName = command.arg;
const fileName = command.arg
return Promise.try(() => this.fs.get(fileName))
.then(() => {
this.renameFrom = fileName;
return this.reply(350);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
.then(() => {
this.renameFrom = fileName
return this.reply(350)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <name>',
description: 'Rename from'
};
}

View File

@@ -1,28 +1,30 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = {
export default {
directive: 'RNTO',
handler: function ({log, command} = {}) {
if (!this.renameFrom) return this.reply(503);
handler: function ({ log, command } = {}) {
if (!this.renameFrom) return this.reply(503)
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.rename) return this.reply(402, 'Not supported by file system')
const from = this.renameFrom;
const to = command.arg;
const from = this.renameFrom
const to = command.arg
return Promise.try(() => this.fs.rename(from, to))
.then(() => {
return this.reply(250);
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
})
.finally(() => {
delete this.renameFrom;
});
.then(() => {
return this.reply(250)
})
.tap(() => this.emit('RNTO', null, to))
.catch((err) => {
log.error(err)
this.emit('RNTO', err)
return this.reply(550, err.message)
})
.then(() => {
delete this.renameFrom
})
},
syntax: '{{cmd}} <name>',
description: 'Rename to'
};
}

View File

@@ -1,17 +1,17 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
export default function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system')
const [mode, ...fileNameParts] = command.arg.split(' ');
const fileName = fileNameParts.join(' ');
const [mode, ...fileNameParts] = command.arg.split(' ')
const fileName = fileNameParts.join(' ')
return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
.then(() => {
return this.reply(200);
})
.catch((err) => {
log.error(err);
return this.reply(500);
});
};
.then(() => {
return this.reply(200)
})
.catch((err) => {
log.error(err)
return this.reply(500)
})
}

View File

@@ -1,20 +1,20 @@
const Promise = require('bluebird');
const _ = require('lodash');
import Promise from 'bluebird'
import _ from 'lodash'
const registry = require('./registry');
import registry from './registry.js'
module.exports = {
export default {
directive: 'SITE',
handler: function ({log, command} = {}) {
const rawSubCommand = _.get(command, 'arg', '');
const subCommand = this.commands.parse(rawSubCommand);
const subLog = log.child({subverb: subCommand.directive});
handler: function ({ log, command } = {}) {
const rawSubCommand = _.get(command, 'arg', '')
const subCommand = this.commands.parse(rawSubCommand)
const subLog = log.child({ subverb: subCommand.directive })
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502)
const handler = registry[subCommand.directive].handler.bind(this);
return Promise.resolve(handler({log: subLog, command: subCommand}));
const handler = registry[subCommand.directive].handler.bind(this)
return Promise.resolve(handler({ log: subLog, command: subCommand }))
},
syntax: '{{cmd}} <subVerb> [...<subParams>]',
description: 'Sends site specific commands to remote server'
};
}

View File

@@ -1,5 +1,6 @@
module.exports = {
import chmod from './chmod.js'
export default {
CHMOD: {
handler: require('./chmod')
handler: chmod
}
};
}

View File

@@ -1,23 +1,23 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = {
export default {
directive: 'SIZE',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => {
return this.reply(213, {message: fileStat.size});
})
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
.then((fileStat) => {
return this.reply(213, { message: fileStat.size })
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <path>',
description: 'Return the size of a file',
flags: {
feat: 'SIZE'
}
};
}

View File

@@ -1,45 +1,43 @@
const _ = require('lodash');
const Promise = require('bluebird');
const getFileStat = require('../../helpers/file-stat');
import _ from 'lodash'
import Promise from 'bluebird'
import getFileStat from '../../helpers/file-stat.js'
module.exports = {
export default {
directive: 'STAT',
handler: function (args = {}) {
const {log, command} = args;
const path = _.get(command, 'arg');
const { log, command } = args
const path = _.get(command, 'arg')
if (path) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.get(path))
.then((stat) => {
if (stat.isDirectory()) {
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
.then((stat) => {
if (stat.isDirectory()) {
if (!this.fs.list) return this.reply(402, 'Not supported by file system')
return Promise.try(() => this.fs.list(path))
.then((stats) => [213, stats]);
}
return [212, [stat]];
})
.then(([code, fileStats]) => {
return Promise.map(fileStats, (file) => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
return {
raw: true,
message
};
return Promise.try(() => this.fs.list(path)).then((stats) => [213, stats])
}
return [212, [stat]]
})
.then(([code, fileStats]) => {
return Promise.map(fileStats, (file) => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'))
return {
raw: true,
message
}
}).then((messages) => [code, messages])
})
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
.catch((err) => {
log.error(err)
return this.reply(450, err.message)
})
.then((messages) => [code, messages]);
})
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
.catch((err) => {
log.error(err);
return this.reply(450, err.message);
});
} else {
return this.reply(211, 'Status OK');
return this.reply(211, 'Status OK')
}
},
syntax: '{{cmd}} [<path>]',
description: 'Returns the current status'
};
}

View File

@@ -1,75 +1,75 @@
const Promise = require('bluebird');
import Promise from 'bluebird'
module.exports = {
export default {
directive: 'STOR',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
handler: function ({ log, command } = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.write) return this.reply(402, 'Not supported by file system')
const append = command.directive === 'APPE';
const fileName = command.arg;
const append = command.directive === 'APPE'
const fileName = command.arg
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = fileName;
}
const serverPath = stream.path || fileName;
const destroyConnection = (connection, reject) => (err) => {
if (connection) {
if (connection.writeable) connection.end();
connection.destroy(err);
return this.connector
.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.write(fileName, { append, start: this.restByteCount })))
.then((fsResponse) => {
let { stream, clientPath } = fsResponse
if (!stream && !clientPath) {
stream = fsResponse
clientPath = fileName
}
reject(err);
};
const serverPath = stream.path || fileName
const streamPromise = new Promise((resolve, reject) => {
stream.once('error', destroyConnection(this.connector.socket, reject));
stream.once('finish', () => resolve());
});
const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.on('data', (data) => {
if (this.connector.socket) this.connector.socket.pause();
if (stream) {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
const destroyConnection = (connection, reject) => (err) => {
try {
if (connection) {
if (connection.writable) connection.end()
connection.destroy(err)
}
} finally {
reject(err)
}
});
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
resolve();
});
this.connector.socket.once('error', destroyConnection(stream, reject));
});
}
this.restByteCount = 0;
const streamPromise = new Promise((resolve, reject) => {
stream.once('error', destroyConnection(this.connector.socket, reject))
stream.once('finish', () => resolve())
})
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
})
.catch(Promise.TimeoutError, (err) => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch((err) => {
log.error(err);
this.emit('STOR', err);
return this.reply(550, err.message);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.pipe(stream, { end: false })
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close')
else stream.end()
resolve()
})
this.connector.socket.once('error', destroyConnection(stream, reject))
})
this.restByteCount = 0
return this.reply(150)
.then(() => this.connector.socket && this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
.then(() => stream.destroy && stream.destroy())
})
.catch(Promise.TimeoutError, (err) => {
log.error(err)
return this.reply(425, 'No connection established')
})
.catch((err) => {
log.error(err)
this.emit('STOR', err)
return this.reply(550, err.message)
})
.then(() => {
this.connector.end()
this.commandSocket.resume()
})
},
syntax: '{{cmd}} <path>',
description: 'Store data as a file at the server site'
};
}

View File

@@ -1,21 +1,21 @@
const Promise = require('bluebird');
const {handler: stor} = require('./stor');
import Promise from 'bluebird'
import stor from './stor.js'
module.exports = {
export default {
directive: 'STOU',
handler: function (args) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
if (!this.fs) return this.reply(550, 'File system not instantiated')
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system')
const fileName = args.command.arg;
const fileName = args.command.arg
return Promise.try(() => this.fs.get(fileName))
.then(() => Promise.try(() => this.fs.getUniqueName()))
.catch(() => fileName)
.then((name) => {
args.command.arg = name;
return stor.call(this, args);
});
.then(() => Promise.try(() => this.fs.getUniqueName(fileName)))
.catch(() => fileName)
.then((name) => {
args.command.arg = name
return stor.handler.call(this, args)
})
},
syntax: '{{cmd}}',
description: 'Store file uniquely'
};
}

View File

@@ -1,11 +1,11 @@
module.exports = {
export default {
directive: 'STRU',
handler: function ({command} = {}) {
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
handler: function ({ command } = {}) {
return this.reply(/^F$/i.test(command.arg) ? 200 : 504)
},
syntax: '{{cmd}} <structure>',
description: 'Set file transfer structure',
flags: {
obsolete: true
}
};
}

View File

@@ -1,11 +1,11 @@
module.exports = {
export default {
directive: 'SYST',
handler: function () {
return this.reply(215);
return this.reply(215)
},
syntax: '{{cmd}}',
description: 'Return system type',
flags: {
no_auth: true
}
};
}

View File

@@ -1,18 +1,18 @@
module.exports = {
export default {
directive: 'TYPE',
handler: function ({command} = {}) {
handler: function ({ command } = {}) {
if (/^A[0-9]?$/i.test(command.arg)) {
this.transferType = 'ascii';
this.transferType = 'ascii'
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
this.transferType = 'binary';
this.transferType = 'binary'
} else {
return this.reply(501);
return this.reply(501)
}
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`)
},
syntax: '{{cmd}} <mode>',
description: 'Set the transfer mode, binary (I) or ascii (A)',
flags: {
feat: 'TYPE A,I,L'
}
};
}

View File

@@ -1,28 +1,30 @@
module.exports = {
export default {
directive: 'USER',
handler: function ({log, command} = {}) {
if (this.username) return this.reply(530, 'Username already set');
if (this.authenticated) return this.reply(230);
handler: function ({ log, command } = {}) {
if (this.username) return this.reply(530, 'Username already set')
if (this.authenticated) return this.reply(230)
this.username = command.arg;
if (!this.username) return this.reply(501, 'Must provide username');
this.username = command.arg
if (!this.username) return this.reply(501, 'Must provide username')
if (this.server.options.anonymous === true && this.username === 'anonymous' ||
this.username === this.server.options.anonymous) {
if (
(this.server.options.anonymous === true && this.username === 'anonymous') ||
this.username === this.server.options.anonymous
) {
return this.login(this.username, '@anonymous')
.then(() => {
return this.reply(230);
})
.catch((err) => {
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
.then(() => {
return this.reply(230)
})
.catch((err) => {
log.error(err)
return this.reply(530, err.message || 'Authentication failed')
})
}
return this.reply(331);
return this.reply(331)
},
syntax: '{{cmd}} <username>',
description: 'Authentication username',
flags: {
no_auth: true
}
};
}

View File

@@ -1,50 +1,90 @@
import abor from './registration/abor.js'
import allo from './registration/allo.js'
import appe from './registration/appe.js'
import auth from './registration/auth.js'
import cdup from './registration/cdup.js'
import cwd from './registration/cwd.js'
import dele from './registration/dele.js'
import feat from './registration/feat.js'
import help from './registration/help.js'
import list from './registration/list.js'
import mdtm from './registration/mdtm.js'
import mkd from './registration/mkd.js'
import mode from './registration/mode.js'
import nlst from './registration/nlst.js'
import noop from './registration/noop.js'
import opts from './registration/opts.js'
import pass from './registration/pass.js'
import pasv from './registration/pasv.js'
import port from './registration/port.js'
import pwd from './registration/pwd.js'
import quit from './registration/quit.js'
import rest from './registration/rest.js'
import retr from './registration/retr.js'
import rmd from './registration/rmd.js'
import rnfr from './registration/rnfr.js'
import rnto from './registration/rnto.js'
import site from './registration/site/index.js'
import size from './registration/size.js'
import stat from './registration/stat.js'
import stor from './registration/stor.js'
import stou from './registration/stou.js'
import stru from './registration/stru.js'
import syst from './registration/syst.js'
import type from './registration/type.js'
import user from './registration/user.js'
import pbsz from './registration/pbsz.js'
import prot from './registration/prot.js'
import eprt from './registration/eprt.js'
import epsv from './registration/epsv.js'
/* eslint no-return-assign: 0 */
const commands = [
require('./registration/abor'),
require('./registration/allo'),
require('./registration/appe'),
require('./registration/auth'),
require('./registration/cdup'),
require('./registration/cwd'),
require('./registration/dele'),
require('./registration/feat'),
require('./registration/help'),
require('./registration/list'),
require('./registration/mdtm'),
require('./registration/mkd'),
require('./registration/mode'),
require('./registration/nlst'),
require('./registration/noop'),
require('./registration/opts'),
require('./registration/pass'),
require('./registration/pasv'),
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/rest'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),
require('./registration/rnto'),
require('./registration/site'),
require('./registration/size'),
require('./registration/stat'),
require('./registration/stor'),
require('./registration/stou'),
require('./registration/stru'),
require('./registration/syst'),
require('./registration/type'),
require('./registration/user'),
require('./registration/pbsz'),
require('./registration/prot'),
require('./registration/eprt'),
require('./registration/epsv')
];
abor,
allo,
appe,
auth,
cdup,
cwd,
dele,
feat,
help,
list,
mdtm,
mkd,
mode,
nlst,
noop,
opts,
pass,
pasv,
port,
pwd,
quit,
rest,
retr,
rmd,
rnfr,
rnto,
site,
size,
stat,
stor,
stou,
stru,
syst,
type,
user,
pbsz,
prot,
eprt,
epsv
]
const registry = commands.reduce((result, cmd) => {
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
aliases.forEach((alias) => result[alias] = cmd);
return result;
}, {});
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive]
aliases.forEach((alias) => (result[alias] = cmd))
return result
}, {})
module.exports = registry;
export default registry

View File

@@ -1,143 +1,162 @@
const _ = require('lodash');
const uuid = require('uuid');
const Promise = require('bluebird');
const EventEmitter = require('events');
import _ from 'lodash'
import uuid from 'uuid'
import Promise from 'bluebird'
import { EventEmitter } from 'node:events'
import BaseConnector from './connector/base.js'
import { FileSystem } from './fs.js'
import Commands from './commands/index.js'
import errors from './errors.js'
import DEFAULT_MESSAGE from './messages.js'
const BaseConnector = require('./connector/base');
const FileSystem = require('./fs');
const Commands = require('./commands');
const errors = require('./errors');
const DEFAULT_MESSAGE = require('./messages');
class FtpConnection extends EventEmitter {
export default class FtpConnection extends EventEmitter {
constructor(server, options) {
super();
this.server = server;
this.id = uuid.v4();
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
this.encoding = 'utf8';
this.bufferSize = false;
this._restByteCount = 0;
this._secure = false;
super()
this.server = server
this.id = uuid.v4()
this.commandSocket = options.socket
this.log = options.log.child({ id: this.id, ip: this.ip })
this.commands = new Commands(this)
this.transferType = 'binary'
this.encoding = 'utf8'
this.bufferSize = false
this._restByteCount = 0
this._secure = false
this.connector = new BaseConnector(this);
this.connector = new BaseConnector(this)
this.commandSocket = options.socket;
this.commandSocket.on('error', (err) => {
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {});
this.log.error(err, 'Client error')
this.server.emit('client-error', { connection: this, context: 'commandSocket', error: err })
})
this.commandSocket.on('data', this._handleData.bind(this))
this.commandSocket.on('timeout', () => {
this.log.trace('Client timeout')
this.close()
})
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
this.removeAllListeners();
});
if (this.connector) this.connector.end()
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy()
this.removeAllListeners()
})
}
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
const messages = _.compact(data.toString(this.encoding).split('\r\n'))
this.log.trace(messages)
return Promise.mapSeries(messages, (message) => this.commands.handle(message))
}
get ip() {
try {
return this.commandSocket ? this.commandSocket.remoteAddress : undefined;
return this.commandSocket ? this.commandSocket.remoteAddress : undefined
} catch (ex) {
return null;
return null
}
}
get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined;
return this._restByteCount > 0 ? this._restByteCount : undefined
}
set restByteCount(rbc) {
this._restByteCount = rbc;
this._restByteCount = rbc
}
get secure() {
return this.server.isTLS || this._secure;
return this.server.isTLS || this._secure
}
set secure(sec) {
this._secure = sec;
this._secure = sec
}
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.then((_code) => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
.then(() => this.commandSocket && this.commandSocket.destroy())
}
login(username, password) {
return Promise.try(() => {
const loginListeners = this.server.listeners('login');
const loginListeners = this.server.listeners('login')
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500)
} else {
return this.server.emitPromise('login', {connection: this, username, password});
return this.server.emitPromise('login', { connection: this, username, password })
}
}).then(({ root, cwd, fs, blacklist = [], whitelist = [] } = {}) => {
this.authenticated = true
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist)
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist)
this.fs = fs || new FileSystem(this, { root, cwd })
})
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
this.authenticated = true;
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
this.fs = fs || new FileSystem(this, {root, cwd});
});
}
reply(options = {}, ...letters) {
const satisfyParameters = () => {
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters];
if (!letters.length) letters = [{}];
if (typeof options === 'number') options = { code: options } // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters]
if (!letters.length) letters = [{}]
return Promise.map(letters, (promise, index) => {
return Promise.resolve(promise)
.then((letter) => {
if (!letter) letter = {};
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
return Promise.resolve(promise).then((letter) => {
if (!letter) letter = {}
else if (typeof letter === 'string') letter = { message: letter } // allow passing in message as first param
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket
if (!options.useEmptyMessage) {
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information'
if (!letter.encoding) letter.encoding = this.encoding
}
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then((message) => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
return letter;
});
});
});
};
.then((message) => {
if (!options.useEmptyMessage) {
const seperator = !options.hasOwnProperty('eol')
? letters.length - 1 === index
? ' '
: '-'
: options.eol
? ' '
: '-'
message = !letter.raw
? _.compact([letter.code || options.code, message]).join(seperator)
: message
letter.message = message
} else {
letter.message = ''
}
return letter
})
})
})
}
const processLetter = (letter) => {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => {
if (err) {
this.log.error(err);
return reject(err);
this.log.trace(
{ port: letter.socket.address().port, encoding: letter.encoding, message: letter.message },
'Reply'
)
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
if (error) {
this.log.error('[Process Letter] Socket Write Error', { error: error.message })
return reject(error)
}
resolve();
});
} else reject(new errors.SocketError('Socket not writable'));
});
};
resolve()
})
} else {
this.log.trace({ message: letter.message }, 'Could not write message')
reject(new errors.SocketError('Socket not writable'))
}
})
}
return satisfyParameters()
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch((err) => {
this.log.error(err);
});
.then((satisfiedLetters) =>
Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index)
})
)
.catch((error) => {
this.log.error('Satisfy Parameters Error', { error: error.message })
})
}
}
module.exports = FtpConnection;

View File

@@ -1,49 +1,57 @@
const {Socket} = require('net');
const tls = require('tls');
const Promise = require('bluebird');
const Connector = require('./base');
import { Socket } from 'node:net'
import tls from 'node:tls'
import ip from 'ip'
import Promise from 'bluebird'
import Connector from './base.js'
import errors from '../errors.js'
class Active extends Connector {
export default class Active extends Connector {
constructor(connection) {
super(connection);
this.type = 'active';
super(connection)
this.type = 'active'
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
waitForConnection({ timeout = 5000, delay = 250 } = {}) {
const checkSocket = () => {
if (this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket);
return Promise.resolve(this.dataSocket)
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return Promise.resolve()
.delay(delay)
.then(() => checkSocket())
}
return checkSocket().timeout(timeout);
return checkSocket().timeout(timeout)
}
setupConnection(host, port, family = 4) {
const closeExistingServer = () => Promise.resolve(
this.dataSocket ? this.dataSocket.destroy() : undefined);
const closeExistingServer = () => Promise.resolve(this.dataSocket ? this.dataSocket.destroy() : undefined)
return closeExistingServer()
.then(() => {
this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => {
this.dataSocket.pause();
return closeExistingServer().then(() => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
throw new errors.SocketError('The given address is not yours', 500)
}
this.dataSocket = new Socket()
this.dataSocket.on(
'error',
(err) =>
this.server &&
this.server.emit('client-error', { connection: this.connection, context: 'dataSocket', error: err })
)
this.dataSocket.connect({ host, port, family }, () => {
this.dataSocket.pause()
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server.options.tls);
const secureContext = tls.createSecureContext(this.server.options.tls)
const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
})
this.dataSocket = secureSocket
}
this.dataSocket.connected = true;
});
});
this.dataSocket.connected = true
})
})
}
}
module.exports = Active;

View File

@@ -1,53 +1,51 @@
const Promise = require('bluebird');
const errors = require('../errors');
import Promise from 'bluebird'
import errors from '../errors.js'
class Connector {
export default class Connector {
constructor(connection) {
this.connection = connection;
this.connection = connection
this.dataSocket = null;
this.dataServer = null;
this.type = false;
this.dataSocket = null
this.dataServer = null
this.type = false
}
get log() {
return this.connection.log;
return this.connection.log
}
get socket() {
return this.dataSocket;
return this.dataSocket
}
get server() {
return this.connection.server;
return this.connection.server
}
waitForConnection() {
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'))
}
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy());
this.dataSocket = null;
const socket = this.dataSocket
this.dataSocket.end(() => socket && socket.destroy())
this.dataSocket = null
}
}
closeServer() {
if (this.dataServer) {
this.dataServer.close();
this.dataServer = null;
this.dataServer.close()
this.dataServer = null
}
}
end() {
this.closeSocket();
this.closeServer();
this.closeSocket()
this.closeServer()
this.type = false;
this.connection.connector = new Connector(this);
this.type = false
this.connection.connector = new Connector(this)
}
}
module.exports = Connector;

View File

@@ -1,86 +1,121 @@
const net = require('net');
const tls = require('tls');
const ip = require('ip');
const Promise = require('bluebird');
import net from 'node:net'
import tls from 'node:tls'
import ip from 'ip'
import Promise from 'bluebird'
const Connector = require('./base');
const errors = require('../errors');
import Connector from './base.js'
import errors from '../errors.js'
class Passive extends Connector {
const CONNECT_TIMEOUT = 30 * 1000
export default class Passive extends Connector {
constructor(connection) {
super(connection);
this.type = 'passive';
super(connection)
this.type = 'passive'
}
waitForConnection({timeout = 5000, delay = 50} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
waitForConnection({ timeout = 5000, delay = 50 } = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'))
const checkSocket = () => {
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket);
return Promise.resolve(this.dataSocket)
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return Promise.resolve()
.delay(delay)
.then(() => checkSocket())
}
return checkSocket().timeout(timeout);
return checkSocket().timeout(timeout)
}
setupServer() {
this.closeServer();
return this.server.getNextPasvPort()
.then((port) => {
const connectionHandler = (socket) => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error({
pasv_connection: socket.remoteAddress,
cmd_connection: this.connection.commandSocket.remoteAddress
}, 'Connecting addresses do not match');
this.closeServer()
return this.server
.getNextPasvPort()
.then((port) => {
this.dataSocket = null
let idleServerTimeout
socket.destroy();
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
const connectionHandler = (socket) => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error(
{
pasv_connection: socket.remoteAddress,
cmd_connection: this.connection.commandSocket.remoteAddress
},
'Connecting addresses do not match'
)
this.dataSocket = socket;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
if (!this.connection.secure) {
this.dataSocket.connected = true;
}
};
this.dataSocket = null;
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.once('close', () => {
this.log.trace('Passive server closed');
this.end();
});
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err);
else {
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
socket.destroy()
return this.connection
.reply(550, 'Remote addresses do not match')
.then(() => this.connection.close())
}
});
});
});
}
clearTimeout(idleServerTimeout)
this.log.trace({ port, remoteAddress: socket.remoteAddress }, 'Passive connection fulfilled.')
this.dataSocket = socket
this.dataSocket.on(
'error',
(err) =>
this.server &&
this.server.emit('client-error', {
connection: this.connection,
context: 'dataSocket',
error: err
})
)
this.dataSocket.once('close', () => this.closeServer())
if (!this.connection.secure) {
this.dataSocket.connected = true
}
}
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {
pauseOnConnect: true
})
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler)
this.dataServer.maxConnections = 1
this.dataServer.on(
'error',
(err) =>
this.server &&
this.server.emit('client-error', {
connection: this.connection,
context: 'dataServer',
error: err
})
)
this.dataServer.once('close', () => {
this.log.trace('Passive server closed')
this.end()
})
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true
})
}
return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err)
else {
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT)
this.log.debug({ port }, 'Passive connection listening')
resolve(this.dataServer)
}
})
})
})
.catch((error) => {
this.log.trace(error.message)
throw error
})
}
}
module.exports = Passive;

View File

@@ -1,44 +1,38 @@
class GeneralError extends Error {
constructor(message, code = 400) {
super();
this.code = code;
this.name = 'GeneralError';
this.message = message;
super(message)
this.code = code
this.name = 'GeneralError'
}
}
class SocketError extends Error {
constructor(message, code = 500) {
super();
this.code = code;
this.name = 'SocketError';
this.message = message;
super(message)
this.code = code
this.name = 'SocketError'
}
}
class FileSystemError extends Error {
constructor(message, code = 400) {
super();
this.code = code;
this.name = 'FileSystemError';
this.message = message;
super(message)
this.code = code
this.name = 'FileSystemError'
}
}
class ConnectorError extends Error {
constructor(message, code = 400) {
super();
this.code = code;
this.name = 'ConnectorError';
this.message = message;
super(message)
this.code = code
this.name = 'ConnectorError'
}
}
module.exports = {
export default {
SocketError,
FileSystemError,
ConnectorError,
GeneralError
};
}

182
src/fs.js
View File

@@ -1,135 +1,145 @@
const _ = require('lodash');
const nodePath = require('path');
const uuid = require('uuid');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const errors = require('./errors');
import _ from 'lodash'
import nodePath from 'node:path'
import uuid from 'uuid'
import Promise from 'bluebird'
import { createReadStream, createWriteStream, constants } from 'node:fs'
import fsAsync from './helpers/fs-async.js'
import errors from './errors.js'
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
this._root = nodePath.resolve(root || process.cwd());
const UNIX_SEP_REGEX = /\//g
const WIN_SEP_REGEX = /\\/g
export default class FileSystem {
constructor(connection, { root, cwd } = {}) {
this.connection = connection
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'))
this._root = nodePath.resolve(root || process.cwd())
}
get root() {
return this._root;
return this._root
}
_resolvePath(path = '.') {
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
// Unix separators normalize nicer on both unix and win platforms
const resolvedPath = path.replace(WIN_SEP_REGEX, '/')
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${clientPath}`);
return nodePath.join(resolvedPath);
})();
// Join cwd with new path
const joinedPath = nodePath.isAbsolute(resolvedPath)
? nodePath.normalize(resolvedPath)
: nodePath.join('/', this.cwd, resolvedPath)
// Create local filesystem path using the platform separator
const fsPath = nodePath.resolve(
nodePath
.join(this.root, joinedPath)
.replace(UNIX_SEP_REGEX, nodePath.sep)
.replace(WIN_SEP_REGEX, nodePath.sep)
)
// Create FTP client path using unix separator
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/')
return {
clientPath,
fsPath
};
}
}
currentDirectory() {
return this.cwd;
return this.cwd
}
get(fileName) {
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.then((stat) => _.set(stat, 'name', fileName));
const { fsPath } = this._resolvePath(fileName)
return fsAsync.stat(fsPath).then((stat) => _.set(stat, 'name', fileName))
}
list(path = '.') {
const {fsPath} = this._resolvePath(path);
return fs.readdirAsync(fsPath)
.then((fileNames) => {
return Promise.map(fileNames, (fileName) => {
const filePath = nodePath.join(fsPath, fileName);
return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => {
return fs.statAsync(filePath)
.then((stat) => _.set(stat, 'name', fileName));
const { fsPath } = this._resolvePath(path)
return fsAsync
.readdir(fsPath)
.then((fileNames) => {
return Promise.map(fileNames, (fileName) => {
const filePath = nodePath.join(fsPath, fileName)
return fsAsync
.access(filePath, constants.F_OK)
.then(() => {
return fsAsync.stat(filePath).then((stat) => _.set(stat, 'name', fileName))
})
.catch(() => null)
})
.catch(() => null);
});
})
.then(_.compact);
})
.then(_.compact)
}
chdir(path = '.') {
const {fsPath, clientPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = clientPath;
return this.currentDirectory();
});
const { fsPath, clientPath } = this._resolvePath(path)
return fsAsync
.stat(fsPath)
.tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory')
})
.then(() => {
this.cwd = clientPath
return this.currentDirectory()
})
}
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
write(fileName, { append = false, start = undefined } = {}) {
const { fsPath, clientPath } = this._resolvePath(fileName)
const stream = createWriteStream(fsPath, { flags: !append ? 'w+' : 'a+', start })
stream.once('error', () => fsAsync.unlink(fsPath))
stream.once('close', () => stream.end())
return {
stream,
clientPath
};
}
}
read(fileName, {start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.tap((stat) => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return {
stream,
clientPath
};
});
read(fileName, { start = undefined } = {}) {
const { fsPath, clientPath } = this._resolvePath(fileName)
return fsAsync
.stat(fsPath)
.tap((stat) => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory')
})
.then(() => {
const stream = createReadStream(fsPath, { flags: 'r', start })
return {
stream,
clientPath
}
})
}
delete(path) {
const {fsPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.then((stat) => {
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlinkAsync(fsPath);
});
const { fsPath } = this._resolvePath(path)
return fsAsync.stat(fsPath).then((stat) => {
if (stat.isDirectory()) return fsAsync.rmdir(fsPath)
else return fsAsync.unlink(fsPath)
})
}
mkdir(path) {
const {fsPath} = this._resolvePath(path);
return fs.mkdirAsync(fsPath)
.then(() => fsPath);
const { fsPath } = this._resolvePath(path)
return fsAsync.mkdir(fsPath, { recursive: true }).then(() => fsPath)
}
rename(from, to) {
const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to);
return fs.renameAsync(fromPath, toPath);
const { fsPath: fromPath } = this._resolvePath(from)
const { fsPath: toPath } = this._resolvePath(to)
return fsAsync.rename(fromPath, toPath)
}
chmod(path, mode) {
const {fsPath} = this._resolvePath(path);
return fs.chmodAsync(fsPath, mode);
const { fsPath } = this._resolvePath(path)
return fsAsync.chmod(fsPath, mode)
}
getUniqueName() {
return uuid.v4().replace(/\W/g, '');
return uuid.v4().replace(/\W/g, '')
}
}
module.exports = FileSystem;
export { FileSystem }

View File

@@ -1,4 +1,3 @@
module.exports = function (path) {
return path
.replace(/"/g, '""');
};
export default function (path) {
return path.replace(/"/g, '""')
}

View File

@@ -1,46 +1,50 @@
const _ = require('lodash');
const moment = require('moment');
const errors = require('../errors');
import _ from 'lodash'
import moment from 'moment'
import errors from '../errors.js'
const FORMATS = {
ls,
ep
};
}
module.exports = function (fileStat, format = 'ls') {
if (typeof format === 'function') return format(fileStat);
export default function (fileStat, format = 'ls') {
if (typeof format === 'function') return format(fileStat)
if (!FORMATS.hasOwnProperty(format)) {
throw new errors.FileSystemError('Bad file stat formatter');
throw new errors.FileSystemError('Bad file stat formatter')
}
return FORMATS[format](fileStat);
};
return FORMATS[format](fileStat)
}
function ls(fileStat) {
const now = moment.utc();
const mtime = moment.utc(new Date(fileStat.mtime));
const timeDiff = now.diff(mtime, 'months');
const dateFormat = timeDiff < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
const now = moment.utc()
const mtime = moment.utc(new Date(fileStat.mtime))
const timeDiff = now.diff(mtime, 'months')
const dateFormat = timeDiff < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY'
return [
fileStat.mode ? [
fileStat.isDirectory() ? 'd' : '-',
fileStat.mode & 256 ? 'r' : '-',
fileStat.mode & 128 ? 'w' : '-',
fileStat.mode & 64 ? 'x' : '-',
fileStat.mode & 32 ? 'r' : '-',
fileStat.mode & 16 ? 'w' : '-',
fileStat.mode & 8 ? 'x' : '-',
fileStat.mode & 4 ? 'r' : '-',
fileStat.mode & 2 ? 'w' : '-',
fileStat.mode & 1 ? 'x' : '-'
].join('') : fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
fileStat.mode
? [
fileStat.isDirectory() ? 'd' : '-',
fileStat.mode & 256 ? 'r' : '-',
fileStat.mode & 128 ? 'w' : '-',
fileStat.mode & 64 ? 'x' : '-',
fileStat.mode & 32 ? 'r' : '-',
fileStat.mode & 16 ? 'w' : '-',
fileStat.mode & 8 ? 'x' : '-',
fileStat.mode & 4 ? 'r' : '-',
fileStat.mode & 2 ? 'w' : '-',
fileStat.mode & 1 ? 'x' : '-'
].join('')
: fileStat.isDirectory()
? 'drwxr-xr-x'
: '-rwxr-xr-x',
'1',
fileStat.uid || 1,
fileStat.gid || 1,
_.padStart(fileStat.size, 12),
_.padStart(mtime.format(dateFormat), 12),
fileStat.name
].join(' ');
].join(' ')
}
function ep(fileStat) {
@@ -50,6 +54,6 @@ function ep(fileStat) {
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
fileStat.mode ? `up${(fileStat.mode & 4095).toString(8)}` : null,
fileStat.isDirectory() ? '/' : 'r'
]).join(',');
return `+${facts}\t${fileStat.name}`;
]).join(',')
return `+${facts}\t${fileStat.name}`
}

View File

@@ -1,36 +1,59 @@
const net = require('net');
const Promise = require('bluebird');
const errors = require('../errors');
import net from 'node:net'
import errors from '../errors.js'
function* portNumberGenerator(min, max) {
let current = min;
const MAX_PORT = 65535
const MAX_PORT_CHECK_ATTEMPT = 5
function* portNumberGenerator(min, max = MAX_PORT) {
let current = min
while (true) {
if (current > 65535 || current > max) {
current = min;
if (current > MAX_PORT || current > max) {
current = min
}
yield current++;
yield current++
}
}
function getNextPortFactory(min, max = Infinity) {
const nextPortNumber = portNumberGenerator(min, max);
const portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
portCheckServer.listen(nextPortNumber.next().value);
});
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
const nextPortNumber = portNumberGenerator(portMin, portMax)
return () => new Promise((resolve) => {
portCheckServer.once('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => resolve(port));
});
portCheckServer.listen(nextPortNumber.next().value);
})
.catch(RangeError, (err) => Promise.reject(new errors.ConnectorError(err.message)));
return () =>
new Promise((resolve, reject) => {
const portCheckServer = net.createServer()
portCheckServer.maxConnections = 0
let attemptCount = 0
const tryGetPort = () => {
attemptCount++
if (attemptCount > maxAttempts) {
reject(new errors.ConnectorError('Unable to find valid port'))
return
}
const { value: port } = nextPortNumber.next()
portCheckServer.removeAllListeners()
portCheckServer.once('error', (err) => {
if (['EADDRINUSE'].includes(err.code)) {
tryGetPort()
} else {
reject(err)
}
})
portCheckServer.once('listening', () => {
portCheckServer.removeAllListeners()
portCheckServer.close(() => resolve(port))
})
try {
portCheckServer.listen(port, host)
} catch (err) {
reject(err)
}
}
tryGetPort()
})
}
module.exports = {
getNextPortFactory,
portNumberGenerator
};
export { getNextPortFactory, portNumberGenerator }

9
src/helpers/fs-async.js Normal file
View File

@@ -0,0 +1,9 @@
import fs from 'node:fs'
import Promise from 'bluebird'
const methods = ['stat', 'readdir', 'access', 'unlink', 'rmdir', 'mkdir', 'rename', 'chmod']
export default methods.reduce((obj, method) => {
obj[method] = Promise.promisify(fs[method])
return obj
}, {})

3
src/helpers/is-local.js Normal file
View File

@@ -0,0 +1,3 @@
export const isLocalIP = function (ip) {
return ip === '127.0.0.1' || ip == '::1'
}

View File

@@ -1,25 +0,0 @@
const http = require('http');
const Promise = require('bluebird');
const errors = require('../errors');
const IP_WEBSITE = 'http://api.ipify.org/';
module.exports = function (hostname) {
return new Promise((resolve, reject) => {
if (!hostname || hostname === '0.0.0.0') {
let ip = '';
http.get(IP_WEBSITE, (response) => {
if (response.statusCode !== 200) {
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
}
response.setEncoding('utf8');
response.on('data', (chunk) => {
ip += chunk;
});
response.on('end', () => {
resolve(ip);
});
});
} else resolve(hostname);
});
};

View File

@@ -1,148 +1,194 @@
const _ = require('lodash');
const Promise = require('bluebird');
const nodeUrl = require('url');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const EventEmitter = require('events');
import _ from 'lodash'
import Promise from 'bluebird'
import nodeUrl from 'node:url'
import buyan from 'bunyan'
import net from 'node:net'
import tls from 'node:tls'
import { EventEmitter } from 'node:events'
import Connection from './connection.js'
import { getNextPortFactory } from './helpers/find-port.js'
const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host');
const {getNextPortFactory} = require('./helpers/find-port');
class FtpServer extends EventEmitter {
export default class FtpServer extends EventEmitter {
constructor(options = {}) {
super();
this.options = Object.assign({
log: buyan.createLogger({name: 'ftp-srv'}),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false
}, options);
super()
this.options = Object.assign(
{
log: buyan.createLogger({ name: 'ftp-srv' }),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
random_pasv_port: false,
anonymous: false,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false,
timeout: 0
},
options
)
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._greeting = this.setupGreeting(this.options.greeting)
this._features = this.setupFeaturesMessage()
delete this.options.greeting;
delete this.options.greeting
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
this.connections = {}
this.log = this.options.log
this.url = nodeUrl.parse(this.options.url)
this.getNextPasvPort = this.getNextPortFactory()
const timeout = Number(this.options.timeout)
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout)
const serverConnectionHandler = (socket) => {
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
this.options.timeout > 0 && socket.setTimeout(this.options.timeout)
let connection = new Connection(this, { log: this.log, socket })
this.connections[connection.id] = connection
socket.on('close', () => this.disconnectClient(connection.id));
socket.on('close', () => this.disconnectClient(connection.id))
socket.once('close', () => {
this.emit('disconnect', {
connection,
id: connection.id,
newConnectionCount: Object.keys(this.connections).length
})
})
const greeting = this._greeting || [];
const features = this._features || 'Ready';
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
};
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
this.emit('connect', {
connection,
id: connection.id,
newConnectionCount: Object.keys(this.connections).length
})
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', (err) => this.log.error(err, '[Event] error'));
const greeting = this._greeting || []
const features = this._features || 'Ready'
return connection.reply(220, ...greeting, features).then(() => socket.resume())
}
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, { pauseOnConnect: true })
const quit = _.debounce(this.quit.bind(this), 100);
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler)
this.server.on('error', (err) => {
this.log.error(err, '[Event] error')
this.emit('server-error', { error: err })
})
process.on('SIGTERM', quit);
process.on('SIGINT', quit);
process.on('SIGQUIT', quit);
const quit = _.debounce(this.quit.bind(this), 100)
process.on('SIGTERM', quit)
process.on('SIGINT', quit)
process.on('SIGQUIT', quit)
}
getNextPortFactory() {
const nextPortFactory = getNextPortFactory(
_.get(this, 'url.hostname'),
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max')
)
const randomPortFactory = async () => 0
return _.get(this, 'options.random_pasv_port') ? randomPortFactory : nextPortFactory
}
get isTLS() {
return this.url.protocol === 'ftps:' && this.options.tls;
return this.url.protocol === 'ftps:' && this.options.tls
}
listen() {
return resolveHost(this.options.pasv_url || this.url.hostname)
.then((pasvUrl) => {
this.options.pasv_url = pasvUrl;
if (!this.options.pasv_url) {
this.log.warn('Passive URL not set. Passive connections not available.')
}
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
return new Promise((resolve, reject) => {
this.server.once('error', reject)
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject)
if (err) return reject(err)
this.log.info(
{
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve('Listening');
});
});
});
},
'Listening'
)
resolve('Listening')
})
})
}
emitPromise(action, ...data) {
return new Promise((resolve, reject) => {
const params = _.concat(data, [resolve, reject]);
this.emit.call(this, action, ...params);
});
const params = _.concat(data, [resolve, reject])
this.emit.call(this, action, ...params)
})
}
setupGreeting(greet) {
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
return greeting;
if (!greet) return []
const greeting = Array.isArray(greet) ? greet : greet.split('\n')
return greeting
}
setupFeaturesMessage() {
let features = [];
if (this.options.anonymous) features.push('a');
let features = []
if (this.options.anonymous) features.push('a')
if (features.length) {
features.unshift('Features:');
features.push('.');
features.unshift('Features:')
features.push('.')
}
return features.length ? features.join(' ') : 'Ready';
return features.length ? features.join(' ') : 'Ready'
}
disconnectClient(id) {
return new Promise((resolve) => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
return new Promise((resolve, reject) => {
const client = this.connections[id]
if (!client) return resolve()
delete this.connections[id]
setTimeout(() => {
reject(new Error('Timed out disconnecting the client'))
}, this.options.timeout || 1000)
try {
client.close(0);
client.close(0)
} catch (err) {
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve('Disconnected');
this.log.error(err, 'Error closing connection', { id })
}
});
resolve('Disconnected')
})
}
quit() {
return this.close()
.finally(() => process.exit(0));
return this.close().then(() => process.exit(0))
}
close() {
this.log.info('Server closing...');
this.server.maxConnections = 0;
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise((resolve) => {
this.server.close((err) => {
if (err) this.log.error(err, 'Error closing server');
resolve('Closed');
});
}))
.then(() => this.removeAllListeners());
}
this.server.maxConnections = 0
this.emit('closing')
this.log.info('Closing connections:', Object.keys(this.connections).length)
return Promise.all(Object.keys(this.connections).map((id) => this.disconnectClient(id)))
.then(
() =>
new Promise((resolve) => {
this.server.close((err) => {
this.log.info('Server closing...')
if (err) this.log.error(err, 'Error closing server')
resolve('Closed')
})
})
)
.then(() => {
this.log.debug('Removing event listeners...')
this.emit('closed', {})
this.removeAllListeners()
return
})
}
}
module.exports = FtpServer;

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
// 100 - 199 :: Remarks
100: 'The requested action is being initiated',
110: 'Restart marker reply',
@@ -23,7 +23,7 @@ module.exports = {
230: 'User logged in, proceed',
234: 'Honored',
250: 'Requested file action okay, completed',
257: '\'%s\' created',
257: "'%s' created",
/// 300 - 399 :: Positive Intermediate Replies
/// These types of replies indicate that the requested action was taken and that the server is awaiting further information to complete the request.
331: 'Username okay, awaiting password',
@@ -53,4 +53,4 @@ module.exports = {
551: 'Requested action aborted. Page type unknown',
552: 'Requested file action aborted. Exceeded storage allocation', // (for current directory or dataset).
553: 'Requested action not taken. File name not allowed'
};
}

View File

@@ -1,125 +1,121 @@
const {expect} = require('chai');
const Promise = require('bluebird');
const bunyan = require('bunyan');
const sinon = require('sinon');
const { expect } = require('chai')
const Promise = require('bluebird')
const bunyan = require('bunyan')
const sinon = require('sinon')
const FtpCommands = require('../../src/commands');
const FtpCommands = require('../../src/commands')
describe('FtpCommands', function () {
let sandbox;
let commands;
let sandbox
let commands
let mockConnection = {
authenticated: false,
log: bunyan.createLogger({name: 'FtpCommands'}),
log: bunyan.createLogger({ name: 'FtpCommands' }),
reply: () => Promise.resolve({}),
server: {
options: {
blacklist: ['allo']
}
}
};
}
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
commands = new FtpCommands(mockConnection);
commands = new FtpCommands(mockConnection)
sandbox.spy(mockConnection, 'reply');
sandbox.spy(commands, 'handle');
sandbox.spy(commands, 'parse');
});
sandbox.spy(mockConnection, 'reply')
sandbox.spy(commands, 'handle')
sandbox.spy(commands, 'parse')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
describe('parse', function () {
it('no args: test', () => {
const cmd = commands.parse('test');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal(null);
expect(cmd.raw).to.equal('test');
});
const cmd = commands.parse('test')
expect(cmd.directive).to.equal('TEST')
expect(cmd.arg).to.equal(null)
expect(cmd.raw).to.equal('test')
})
it('one arg: test arg', () => {
const cmd = commands.parse('test arg');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg');
expect(cmd.raw).to.equal('test arg');
});
const cmd = commands.parse('test arg')
expect(cmd.directive).to.equal('TEST')
expect(cmd.arg).to.equal('arg')
expect(cmd.raw).to.equal('test arg')
})
it('two args: test arg1 arg2', () => {
const cmd = commands.parse('test arg1 arg2');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg1 arg2');
expect(cmd.raw).to.equal('test arg1 arg2');
});
const cmd = commands.parse('test arg1 arg2')
expect(cmd.directive).to.equal('TEST')
expect(cmd.arg).to.equal('arg1 arg2')
expect(cmd.raw).to.equal('test arg1 arg2')
})
it('two args with quotes: test "hello world"', () => {
const cmd = commands.parse('test "hello world"');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('hello world');
expect(cmd.raw).to.equal('test "hello world"');
});
const cmd = commands.parse('test "hello world"')
expect(cmd.directive).to.equal('TEST')
expect(cmd.arg).to.equal('hello world')
expect(cmd.raw).to.equal('test "hello world"')
})
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg1 arg2 --zz88A');
expect(cmd.flags).to.deep.equal(['-l', '-A']);
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
});
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A')
expect(cmd.directive).to.equal('TEST')
expect(cmd.arg).to.equal('arg1 arg2 --zz88A')
expect(cmd.flags).to.deep.equal(['-l', '-A'])
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A')
})
it('one arg, with flags: list -l', () => {
const cmd = commands.parse('list -l');
expect(cmd.directive).to.equal('LIST');
expect(cmd.arg).to.equal(null);
expect(cmd.flags).to.deep.equal(['-l']);
expect(cmd.raw).to.equal('list -l');
});
const cmd = commands.parse('list -l')
expect(cmd.directive).to.equal('LIST')
expect(cmd.arg).to.equal(null)
expect(cmd.flags).to.deep.equal(['-l'])
expect(cmd.raw).to.equal('list -l')
})
it('does not check for option flags', () => {
const cmd = commands.parse('retr -test');
expect(cmd.directive).to.equal('RETR');
expect(cmd.arg).to.equal('-test');
expect(cmd.flags).to.deep.equal([]);
});
});
const cmd = commands.parse('retr -test')
expect(cmd.directive).to.equal('RETR')
expect(cmd.arg).to.equal('-test')
expect(cmd.flags).to.deep.equal([])
})
})
describe('handle', function () {
it('fails with unsupported command', () => {
return commands.handle('bad')
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(402);
});
});
return commands.handle('bad').then(() => {
expect(mockConnection.reply.callCount).to.equal(1)
expect(mockConnection.reply.args[0][0]).to.equal(502)
})
})
it('fails with blacklisted command', () => {
return commands.handle('allo')
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(502);
expect(mockConnection.reply.args[0][1]).to.match(/blacklisted/);
});
});
return commands.handle('allo').then(() => {
expect(mockConnection.reply.callCount).to.equal(1)
expect(mockConnection.reply.args[0][0]).to.equal(502)
expect(mockConnection.reply.args[0][1]).to.match(/blacklisted/)
})
})
it('fails with non whitelisted command', () => {
commands.whitelist.push('USER');
return commands.handle('auth')
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(502);
expect(mockConnection.reply.args[0][1]).to.match(/whitelisted/);
});
});
commands.whitelist.push('USER')
return commands.handle('auth').then(() => {
expect(mockConnection.reply.callCount).to.equal(1)
expect(mockConnection.reply.args[0][0]).to.equal(502)
expect(mockConnection.reply.args[0][1]).to.match(/whitelisted/)
})
})
it('fails due to being unauthenticated', () => {
return commands.handle('stor')
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(530);
expect(mockConnection.reply.args[0][1]).to.match(/authentication/);
});
});
});
});
return commands.handle('stor').then(() => {
expect(mockConnection.reply.callCount).to.equal(1)
expect(mockConnection.reply.args[0][0]).to.equal(530)
expect(mockConnection.reply.args[0][1]).to.match(/authentication/)
})
})
})
})

View File

@@ -1,49 +1,47 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'ABOR';
const CMD = 'ABOR'
describe.skip(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve(),
connector: {
waitForConnection: () => Promise.resolve(),
end: () => Promise.resolve()
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.connector, 'waitForConnection');
sandbox.spy(mockClient.connector, 'end');
});
sandbox.spy(mockClient, 'reply')
sandbox.spy(mockClient.connector, 'waitForConnection')
sandbox.spy(mockClient.connector, 'end')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// successful | no active connection', () => {
mockClient.connector.waitForConnection.restore();
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
mockClient.connector.waitForConnection.restore()
sandbox.stub(mockClient.connector, 'waitForConnection').rejects()
return cmdFn()
.then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(0);
expect(mockClient.reply.args[0][0]).to.equal(225);
});
});
return cmdFn().then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1)
expect(mockClient.connector.end.callCount).to.equal(0)
expect(mockClient.reply.args[0][0]).to.equal(225)
})
})
it('// successful | active connection', () => {
return cmdFn()
.then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(1);
expect(mockClient.reply.args[0][0]).to.equal(426);
expect(mockClient.reply.args[1][0]).to.equal(226);
});
});
});
return cmdFn().then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1)
expect(mockClient.connector.end.callCount).to.equal(1)
expect(mockClient.reply.args[0][0]).to.equal(426)
expect(mockClient.reply.args[1][0]).to.equal(226)
})
})
})

View File

@@ -1,28 +1,27 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'ALLO';
const CMD = 'ALLO'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
});
});
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202)
})
})
})

View File

@@ -1,10 +1,10 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'AUTH';
const CMD = 'AUTH'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve(),
server: {
@@ -12,37 +12,34 @@ describe(CMD, function () {
tls: {}
}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('TLS // supported', () => {
return cmdFn({command: {arg: 'TLS', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(234);
expect(mockClient.secure).to.equal(true);
});
});
return cmdFn({ command: { arg: 'TLS', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(234)
expect(mockClient.secure).to.equal(true)
})
})
it('SSL // not supported', () => {
return cmdFn({command: {arg: 'SSL', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
return cmdFn({ command: { arg: 'SSL', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
it('bad // bad', () => {
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
});
return cmdFn({ command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
})

View File

@@ -1,35 +1,34 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'CDUP';
const CMD = 'CDUP'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => Promise.resolve(),
fs: {
chdir: () => Promise.resolve()
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chdir');
});
sandbox.spy(mockClient, 'reply')
sandbox.spy(mockClient.fs, 'chdir')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('.. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');
});
});
});
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250)
expect(mockClient.fs.chdir.args[0][0]).to.equal('..')
})
})
})

View File

@@ -1,78 +1,77 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'CWD';
const CMD = 'CWD'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
fs: {chdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
fs: { chdir: () => {} }
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'chdir').resolves();
});
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'chdir').resolves()
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550)
})
})
it('fails on no fs chdir command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {}, fs: {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402)
})
})
})
it('test // successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
});
});
return cmdFn({ log, command: { arg: 'test', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250)
expect(mockClient.fs.chdir.args[0][0]).to.equal('test')
})
})
it('test // successful', () => {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
mockClient.fs.chdir.restore()
sandbox.stub(mockClient.fs, 'chdir').resolves('/test')
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
});
});
return cmdFn({ log, command: { arg: 'test', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250)
expect(mockClient.fs.chdir.args[0][0]).to.equal('test')
})
})
it('bad // unsuccessful', () => {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
mockClient.fs.chdir.restore()
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'))
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');
});
});
});
return cmdFn({ log, command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550)
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad')
})
})
})

View File

@@ -1,67 +1,67 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'DELE';
const CMD = 'DELE'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
fs: {delete: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
fs: { delete: () => {} }
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'delete').resolves();
});
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'delete').resolves()
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550)
})
})
it('fails on no fs delete command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {}, fs: {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402)
})
})
})
it('test // successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
});
});
return cmdFn({ log, command: { arg: 'test', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250)
expect(mockClient.fs.delete.args[0][0]).to.equal('test')
})
})
it('bad // unsuccessful', () => {
mockClient.fs.delete.restore();
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
mockClient.fs.delete.restore()
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'))
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');
});
});
});
return cmdFn({ log, command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550)
expect(mockClient.fs.delete.args[0][0]).to.equal('bad')
})
})
})

View File

@@ -1,60 +1,56 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const ActiveConnector = require('../../../src/connector/active');
const ActiveConnector = require('../../../src/connector/active')
const CMD = 'EPRT';
const CMD = 'EPRT'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
});
sandbox.spy(mockClient, 'reply')
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves()
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// unsuccessful | no argument', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
return cmdFn({}).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
it('// unsuccessful | invalid argument', () => {
return cmdFn({command: {arg: 'blah'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
return cmdFn({ command: { arg: 'blah' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
it('// successful IPv4', () => {
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('192.168.0.100');
expect(port).to.equal('35286');
expect(family).to.equal(4);
});
});
return cmdFn({ command: { arg: '|1|192.168.0.100|35286|' } }).then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0]
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(ip).to.equal('192.168.0.100')
expect(port).to.equal('35286')
expect(family).to.equal(4)
})
})
it('// successful IPv6', () => {
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23');
expect(port).to.equal('35286');
expect(family).to.equal(6);
});
});
});
return cmdFn({ command: { arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|' } }).then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0]
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23')
expect(port).to.equal('35286')
expect(family).to.equal(6)
})
})
})

View File

@@ -1,35 +1,34 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const PassiveConnector = require('../../../src/connector/passive');
const PassiveConnector = require('../../../src/connector/passive')
const CMD = 'EPSV';
const CMD = 'EPSV'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
address: () => ({port: 12345})
});
});
address: () => ({ port: 12345 })
})
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// successful IPv4', () => {
return cmdFn()
.then(() => {
const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229);
expect(message).to.equal('EPSV OK (|||12345|)');
});
});
});
return cmdFn({}).then(() => {
const [code, message] = mockClient.reply.args[0]
expect(code).to.equal(229)
expect(message).to.equal('EPSV OK (|||12345|)')
})
})
})

View File

@@ -0,0 +1,28 @@
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'FEAT'
describe(CMD, function () {
let sandbox
const mockClient = {
reply: () => Promise.resolve()
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore()
})
it('// successful', () => {
return cmdFn({ command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211)
expect(mockClient.reply.args[0][2].message).to.equal(' AUTH TLS')
})
})
})

View File

@@ -1,49 +1,45 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'HELP';
const CMD = 'HELP'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// successful', () => {
return cmdFn({command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
});
});
return cmdFn({ command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211)
})
})
it('help // successful', () => {
return cmdFn({command: {arg: 'help', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
});
});
return cmdFn({ command: { arg: 'help', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214)
})
})
it('allo // successful', () => {
return cmdFn({command: {arg: 'allo', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
});
});
return cmdFn({ command: { arg: 'allo', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214)
})
})
it('bad // unsuccessful', () => {
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});
});
});
return cmdFn({ command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502)
})
})
})

View File

@@ -1,12 +1,12 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'LIST';
const CMD = 'LIST'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
fs: {
@@ -21,13 +21,13 @@ describe(CMD, function () {
resume: () => {},
pause: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testdir',
dev: 2114,
@@ -45,85 +45,89 @@ describe(CMD, function () {
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
});
sandbox.stub(mockClient.fs, 'list').resolves([{
name: 'test1',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
}, {
name: 'test2',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
}]);
});
})
sandbox.stub(mockClient.fs, 'list').resolves([
{
name: 'test1',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
},
{
name: 'test2',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
}
])
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550)
})
})
it('fails on no fs list command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {}, fs: {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402)
})
})
})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(3);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
});
});
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150)
expect(mockClient.reply.args[1].length).to.equal(3)
expect(mockClient.reply.args[1][1]).to.have.property('raw')
expect(mockClient.reply.args[1][1]).to.have.property('message')
expect(mockClient.reply.args[1][1]).to.have.property('socket')
expect(mockClient.reply.args[2][0]).to.equal(226)
})
})
it('testfile.txt // successful', () => {
mockClient.fs.get.restore();
mockClient.fs.get.restore()
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testfile.txt',
dev: 2114,
@@ -141,35 +145,34 @@ describe(CMD, function () {
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
});
})
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(2);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
});
});
return cmdFn({ log, command: { directive: CMD, arg: 'testfile.txt' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150)
expect(mockClient.reply.args[1].length).to.equal(2)
expect(mockClient.reply.args[1][1]).to.have.property('raw')
expect(mockClient.reply.args[1][1]).to.have.property('message')
expect(mockClient.reply.args[1][1]).to.have.property('socket')
expect(mockClient.reply.args[2][0]).to.equal(226)
})
})
it('. // unsuccessful', () => {
mockClient.fs.list.restore();
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
mockClient.fs.list.restore()
sandbox.stub(mockClient.fs, 'list').rejects(new Error())
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(451);
});
});
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(451)
})
})
it('. // unsuccessful (timeout)', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').returns(Promise.reject(new Promise.TimeoutError()));
sandbox
.stub(mockClient.connector, 'waitForConnection')
.returns(Promise.reject(new Promise.TimeoutError()))
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
});
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425)
})
})
})

View File

@@ -1,66 +1,66 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'MDTM';
const CMD = 'MDTM'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
fs: {get: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
fs: { get: () => {} }
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
});
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'get').resolves({ mtime: 'Mon, 10 Oct 2011 23:24:11 GMT' })
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550)
})
})
it('fails on no fs get command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {}, fs: {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402)
})
})
})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213)
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
});
});
})
})
it('. // unsuccessful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
mockClient.fs.get.restore()
sandbox.stub(mockClient.fs, 'get').rejects(new Error())
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
});
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550)
})
})
})

View File

@@ -1,78 +1,77 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'MKD';
const CMD = 'MKD'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
fs: {mkdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
fs: { mkdir: () => {} }
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'mkdir').resolves();
});
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'mkdir').resolves()
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550)
})
})
it('fails on no fs mkdir command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
const badMockClient = { reply: () => {}, fs: {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
});
});
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402)
})
})
})
it('test // successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
});
});
return cmdFn({ log, command: { arg: 'test', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257)
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test')
})
})
it('test // successful', () => {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').resolves('test');
mockClient.fs.mkdir.restore()
sandbox.stub(mockClient.fs, 'mkdir').resolves('test')
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
});
});
return cmdFn({ log, command: { arg: 'test', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257)
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test')
})
})
it('bad // unsuccessful', () => {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'));
mockClient.fs.mkdir.restore()
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'))
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad');
});
});
});
return cmdFn({ log, command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550)
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad')
})
})
})

View File

@@ -1,35 +1,33 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'MODE';
const CMD = 'MODE'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('S // successful', () => {
return cmdFn({command: {arg: 'S'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
return cmdFn({ command: { arg: 'S' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
})
})
it('Q // unsuccessful', () => {
return cmdFn({command: {arg: 'Q'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
});
return cmdFn({ command: { arg: 'Q' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
})

View File

@@ -1,12 +1,12 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'NLST';
const CMD = 'NLST'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
fs: {
@@ -21,13 +21,13 @@ describe(CMD, function () {
resume: () => {},
pause: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testdir',
dev: 2114,
@@ -45,61 +45,63 @@ describe(CMD, function () {
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
});
sandbox.stub(mockClient.fs, 'list').resolves([{
name: 'test1',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
}, {
name: 'test2',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
}]);
});
})
sandbox.stub(mockClient.fs, 'list').resolves([
{
name: 'test1',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
},
{
name: 'test2',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
}
])
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(3);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
});
});
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150)
expect(mockClient.reply.args[1].length).to.equal(3)
expect(mockClient.reply.args[1][1]).to.have.property('raw')
expect(mockClient.reply.args[1][1]).to.have.property('message')
expect(mockClient.reply.args[1][1]).to.have.property('socket')
expect(mockClient.reply.args[2][0]).to.equal(226)
})
})
it('testfile.txt // successful', () => {
mockClient.fs.get.restore();
mockClient.fs.get.restore()
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testfile.txt',
dev: 2114,
@@ -117,16 +119,15 @@ describe(CMD, function () {
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
});
})
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(2);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
});
});
});
return cmdFn({ log, command: { directive: CMD, arg: 'testfile.txt' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150)
expect(mockClient.reply.args[1].length).to.equal(2)
expect(mockClient.reply.args[1][1]).to.have.property('raw')
expect(mockClient.reply.args[1][1]).to.have.property('message')
expect(mockClient.reply.args[1][1]).to.have.property('socket')
expect(mockClient.reply.args[2][0]).to.equal(226)
})
})
})

View File

@@ -1,28 +1,27 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'NOOP';
const CMD = 'NOOP'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
});
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
})
})
})

View File

@@ -1,58 +1,53 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'OPTS';
const CMD = 'OPTS'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501)
})
})
it('BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
});
});
return cmdFn({ command: { arg: 'BAD', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501)
})
})
it('UTF8 BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'UTF8 BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
return cmdFn({ command: { arg: 'UTF8 BAD', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501)
})
})
it('UTF8 OFF // successful', () => {
return cmdFn({command: {arg: 'UTF8 OFF', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('ascii');
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
return cmdFn({ command: { arg: 'UTF8 OFF', directive: CMD } }).then(() => {
expect(mockClient.encoding).to.equal('ascii')
expect(mockClient.reply.args[0][0]).to.equal(200)
})
})
it('UTF8 ON // successful', () => {
return cmdFn({command: {arg: 'UTF8 ON', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('utf8');
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
});
return cmdFn({ command: { arg: 'UTF8 ON', directive: CMD } }).then(() => {
expect(mockClient.encoding).to.equal('utf8')
expect(mockClient.reply.args[0][0]).to.equal(200)
})
})
})

View File

@@ -1,74 +1,69 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'PASS';
const CMD = 'PASS'
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let sandbox
let log = bunyan.createLogger({ name: CMD })
const mockClient = {
reply: () => {},
login: () => {},
server: {options: {anonymous: false}},
server: { options: { anonymous: false } },
username: 'anonymous'
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'login').resolves();
});
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient, 'login').resolves()
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('pass // successful', () => {
return cmdFn({log, command: {arg: 'pass', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.args[0]).to.eql(['anonymous', 'pass']);
});
});
return cmdFn({ log, command: { arg: 'pass', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230)
expect(mockClient.login.args[0]).to.eql(['anonymous', 'pass'])
})
})
it('// successful (already authenticated)', () => {
mockClient.server.options.anonymous = true;
mockClient.authenticated = true;
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
expect(mockClient.login.callCount).to.equal(0);
mockClient.server.options.anonymous = false;
mockClient.authenticated = false;
});
});
mockClient.server.options.anonymous = true
mockClient.authenticated = true
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202)
expect(mockClient.login.callCount).to.equal(0)
mockClient.server.options.anonymous = false
mockClient.authenticated = false
})
})
it('bad // unsuccessful', () => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects('bad');
mockClient.login.restore()
sandbox.stub(mockClient, 'login').rejects('bad')
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
});
});
return cmdFn({ log, command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530)
})
})
it('bad // unsuccessful', () => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects({});
mockClient.login.restore()
sandbox.stub(mockClient, 'login').rejects({})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
});
});
return cmdFn({ log, command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530)
})
})
it('bad // unsuccessful', () => {
delete mockClient.username;
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
});
});
});
delete mockClient.username
return cmdFn({ log, command: { arg: 'bad', directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503)
})
})
})

View File

@@ -1,51 +1,48 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'PBSZ';
const CMD = 'PBSZ'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve(),
server: {}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
});
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
});
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202)
})
})
it('// successful', () => {
mockClient.secure = true;
mockClient.server._tls = {};
mockClient.secure = true
mockClient.server._tls = {}
return cmdFn({command: {arg: '0'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.bufferSize).to.equal(0);
});
});
return cmdFn({ command: { arg: '0' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(mockClient.bufferSize).to.equal(0)
})
})
it('// successful', () => {
mockClient.secure = true;
mockClient.server._tls = {};
mockClient.secure = true
mockClient.server._tls = {}
return cmdFn({command: {arg: '10'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.bufferSize).to.equal(10);
});
});
});
return cmdFn({ command: { arg: '10' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(mockClient.bufferSize).to.equal(10)
})
})
})

View File

@@ -1,48 +1,45 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const ActiveConnector = require('../../../src/connector/active');
const ActiveConnector = require('../../../src/connector/active')
const CMD = 'PORT';
const CMD = 'PORT'
describe(CMD, function () {
let sandbox;
let sandbox
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
});
sandbox.spy(mockClient, 'reply')
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves()
})
afterEach(() => {
sandbox.restore();
});
sandbox.restore()
})
it('// unsuccessful | no argument', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425)
})
})
it('// unsuccessful | invalid argument', () => {
return cmdFn({command: {arg: '1,2,3,4,5'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
return cmdFn({ command: { arg: '1,2,3,4,5' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425)
})
})
it('// successful', () => {
return cmdFn({command: {arg: '192,168,0,100,137,214'}})
.then(() => {
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('192.168.0.100');
expect(port).to.equal(35286);
});
});
});
return cmdFn({ command: { arg: '192,168,0,100,137,214' } }).then(() => {
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0]
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(ip).to.equal('192.168.0.100')
expect(port).to.equal(35286)
})
})
})

Some files were not shown because too many files have changed in this diff Show More